"Object-based" C Architecture
Quick summary
Assuming you have this .c file:
// -------------
// my_module.c
// -------------
#include "my_module.h"
// Definition of the opaque struct "object" of C-style "class" "my_module".
struct my_module_s
{
int int1;
int int2;
float f1;
// etc. etc--add more "private" member variables as you see fit
};
These are two beautiful and popular opaque pointer styles:
Recommended technique 1: _h
typedef to a pointer to a struct
// -------------
// my_module.h
// -------------
#pragma once
typedef struct my_module_s *my_module_h;
typedef const struct my_module_s *const_my_module_h;
// This function `malloc`s memory for your struct, and assigns the address
// of that memory to the passed-in pointer.
void my_module_open(my_module_h * my_module_h_p);
// This function can only read the contents of the underlying struct.
void my_module_do_stuff1(const_my_module_h my_module);
// This function can read and modify the contents of the underlying struct.
void my_module_do_stuff2(my_module_h my_module);
// Free the memory allocated by `my_module_open()`.
void my_module_close(my_module_h my_module);
Recommended technique 2: _t
typedef to a struct
// -------------
// my_module.h
// -------------
#pragma once
typedef struct my_module_s my_module_t;
// This function `malloc`s memory for your struct, and assigns the address
// of that memory to the passed-in pointer.
void my_module_open(my_module_t ** my_module_p);
// This function can only read the contents of the underlying struct.
void my_module_do_stuff1(const my_module_t* my_module);
// This function can read and modify the contents of the underlying struct.
void my_module_do_stuff2(my_module_t* my_module);
// Free the memory allocated by `my_module_open()`.
void my_module_close(my_module_t* my_module);
Technically, you can even mix the two if you want, but for consistency, I recommend against that. Choose one and go with it.
Details
I am accustomed to using Option 1 from the question, except where you name your reference with _h
(ex: my_module_h
) to signify it is a "handle" to a C-style "object" of this given C "class".
You can use a special const
form of the handle (ex: const_my_module_h
) wherever you want the handle object to be a non-modifiable, read-only input to a function.
So, do this style:
// -------------
// my_module.h
// -------------
#pragma once
// An opaque pointer (handle) to a C-style "object" of "class" type "my_module"
typedef struct my_module_s *my_module_h;
// Same as above, but an opaque pointer to a const struct instead, so you can't
// modify the contents of the struct.
typedef const struct my_module_s *const_my_module_h;
// This function can only read the contents of the underlying struct.
void my_module_do_stuff1(const_my_module_h my_module);
// This function can read and modify the contents of the underlying struct.
void my_module_do_stuff2(my_module_h my_module);
// -------------
// my_module.c
// -------------
// Definition of the opaque struct "object" of C-style "class" "my_module".
struct my_module_s
{
int int1;
int int2;
float f1;
// etc. etc--add more "private" member variables as you see fit
};
Note that the main answer says:
But whatever you do, never use typedef
to define names for pointer types.
I only partially agree with this. I would say: never use typedef
to define names for pointer types when you use the same name as the struct or _t
, such as my_module
or my_module_t
. For one, my_module
should never be a typedef name, in my opinion, because I'd like to write my_type my_variable
as my_module_t my_module
, so my_module
mustn't be the typedef
name. And my_module_t
should only be a typedef
ed name to a struct, not to a pointer to a struct, because, like @R.. GitHub STOP HELPING ICE implied, it confusingly hides the fact that it's a pointer.
However, when you use _h
instead of _t
at the end, this is a common pattern that can mean "handle", where a handle can be a pointer to an opaque struct. So, this is just fine in my opinion, because again, _h
is a reasonable exception that may imply pointer usage for this design pattern.
However, I'm not a die-hard _h
handle fan. I am perfectly fine with solutions where you see the pointer *
too. Since not having to type struct
is one of the best features of C++ 😛, however, I really don't want to have to type struct
. So, if you really want to see the *
to know it's a pointer, do this instead:
// -------------
// my_module.h
// -------------
#pragma once
typedef struct my_module_s my_module_t;
// This function can only read the contents of the underlying struct.
void my_module_do_stuff1(const my_module_t* my_module);
// This function can read and modify the contents of the underlying struct.
void my_module_do_stuff2(my_module_t* my_module);
// -------------
// my_module.c
// -------------
// Definition of the opaque struct "object" of C-style "class" "my_module".
struct my_module_s
{
int int1;
int int2;
float f1;
// etc. etc--add more "private" member variables as you see fit
};
Full opaque pointer module example
Here's a full example using opaque pointers in C to create objects. The following architecture might be called "object-based C". You can see and run a full example of this from my eRCaGuy_hello_world repo in these 3 files here:
opaque_pointer_demo_main.c
opaque_pointer_demo_module.c
opaque_pointer_demo_module.h
Run commands:
# As C
./opaque_pointer_demo_main.c
# OR (same thing)
gcc -Wall -Wextra -Werror -O3 -std=gnu17 opaque_pointer_demo_main.c opaque_pointer_demo_module.c -o bin/a -lm && bin/a
# As C++
g++ -Wall -Wextra -Werror -O3 -std=gnu++17 opaque_pointer_demo_main.c opaque_pointer_demo_module.c -o bin/a && bin/a
Example run and output:
eRCaGuy_hello_world$ c/opaque_pointer_demo_main.c
Hello World
my_module_open() done
my_module->my_private_int1 = 0
my_module_do_stuff1() done
my_module_do_stuff2() done
my_module->my_private_int1 = 7
my_module_do_stuff1() done
my_module_do_stuff2() done
my_module_close() done
Source code:
//==============================================================================
// my_module.h
//==============================================================================
#pragma once
// See my answer: https://stackoverflow.com/a/54488289/4561887
// An opaque pointer (handle) to a C-style "object" of "class" type "my_module"
typedef struct my_module_s *my_module_h;
// Same as above, but an opaque pointer to a const struct instead, so you can't
// modify the contents of the struct.
typedef const struct my_module_s *const_my_module_h;
// Create a new "object" of "class" "my_module": A function that takes a
// **pointer to** an "object" handle, `malloc`s memory for a new copy of the
// opaque `struct my_module_s`, then points the user's input handle (via its
// passed-in pointer) to this newly-created "object" of "class" "my_module".
void my_module_open(my_module_h * my_module_h_p);
// A function that takes this "object" (via its handle) as a read-only object.
void my_module_do_stuff1(const_my_module_h my_module);
// A function that takes this "object" (via its handle) as a read-write object.
void my_module_do_stuff2(my_module_h my_module);
// Destroy the passed-in "object" of "class" type "my_module": A function that
// can close this object by stopping all operations, as required, and `free`ing
// its memory.
void my_module_close(my_module_h my_module);
//==============================================================================
// my_module.c
//==============================================================================
#include "opaque_pointer_demo_module.h"
#include <stdio.h> // For `printf()`
#include <stdlib.h> // For malloc() and free()
#include <string.h> // For memset()
// Definition of the opaque struct "object" of C-style "class" "my_module".
// - NB: Since this is an opaque struct (declared in the header but not defined
// until the source file), it has the following 2 important properties:
// 1) It permits data hiding, wherein you end up with the equivalent of a C++
// "class" with only *private* member variables.
// 2) Objects of this "class" can only be dynamically allocated. No static
// allocation is possible since any module including the header file does not
// know the contents of **nor the size of** (this is the critical part) this
// "class" (ie: C struct).
struct my_module_s
{
int my_private_int1;
int my_private_int2;
float my_private_float;
// etc. etc--add more "private" member variables as you see fit
};
void my_module_open(my_module_h * my_module_h_p)
{
// Ensure the passed-in pointer is not NULL (since it is a core
// dump/segmentation fault to try to dereference a NULL pointer)
if (!my_module_h_p)
{
// Print some error or store some error code here, and return it at the
// end of the function instead of returning void.
goto done;
}
// Now allocate the actual memory for a new my_module C object from the
// heap, thereby dynamically creating this C-style "object".
my_module_h my_module; // Create a local object handle (pointer to a struct)
// Dynamically allocate memory for the full contents of the struct "object"
my_module = (my_module_h)malloc(sizeof(*my_module));
if (!my_module)
{
// Malloc failed due to out-of-memory. Print some error or store some
// error code here, and return it at the end of the function instead of
// returning void.
goto done;
}
// Initialize all memory to zero (OR just use `calloc()` instead of
// `malloc()` above!)
memset(my_module, 0, sizeof(*my_module));
// Now pass out this object to the user, and exit.
*my_module_h_p = my_module;
done:
printf("my_module_open() done\n"); // for demo purposes only
}
void my_module_do_stuff1(const_my_module_h my_module)
{
// Ensure my_module is not a NULL pointer.
if (!my_module)
{
goto done;
}
// Do stuff where you use my_module private "member" variables. Ex: use
// `my_module->my_private_int1` here, or `my_module->my_private_float`, etc.
printf("my_module->my_private_int1 = %i\n", my_module->my_private_int1);
// Now try to modify the variable. This results in the following compile-time
// error since this function takes `const_my_module_h`:
//
// In C:
//
// eRCaGuy_hello_world/c$ ./opaque_pointer_demo_main.c
// ./opaque_pointer_demo_module.c: In function ‘my_module_do_stuff1’:
// ./opaque_pointer_demo_module.c:99:32: error: assignment of member ‘my_private_int1’ in read-only object
// 99 | my_module->my_private_int1 = 8;
// | ^
//
// In C++:
//
// eRCaGuy_hello_world/c$ g++ -Wall -Wextra -Werror -O3 -std=gnu++17 opaque_pointer_demo_main.c opaque_pointer_demo_module.c -o bin/a && bin/a
// opaque_pointer_demo_module.c: In function ‘void my_module_do_stuff1(const_my_module_h)’:
// opaque_pointer_demo_module.c:99:32: error: assignment of member ‘my_module_s::my_private_int1’ in read-only object
// 99 | my_module->my_private_int1 = 8;
// | ~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~
//
//
// my_module->my_private_int1 = 8;
done:
printf("my_module_do_stuff1() done\n"); // for demo purposes only
}
void my_module_do_stuff2(my_module_h my_module)
{
// Ensure my_module is not a NULL pointer.
if (!my_module)
{
goto done;
}
// Do stuff where you use AND UPDATE my_module private "member" variables.
// Ex:
my_module->my_private_int1 = 7;
my_module->my_private_float = 3.14159;
// Etc.
done:
printf("my_module_do_stuff2() done\n"); // for demo purposes only
}
void my_module_close(my_module_h my_module)
{
// Ensure my_module is not a NULL pointer.
if (!my_module)
{
goto done;
}
free(my_module);
done:
printf("my_module_close() done\n"); // for demo purposes only
}
Simplified example usage:
///usr/bin/env ccache gcc -Wall -Wextra -Werror -O3 -std=gnu17 "$0" "$(dirname "$0")/opaque_pointer_demo_module.c" -o /tmp/a -lm && /tmp/a "$@"; exit
// For the line just above, see my answer here: https://stackoverflow.com/a/75491834/4561887
// local includes
#include "opaque_pointer_demo_module.h"
// 3rd party includes
// NA
// C library includes
#include <stdbool.h> // For `true` (`1`) and `false` (`0`) macros in C
#include <stdint.h> // For `uint8_t`, `int8_t`, etc.
#include <stdio.h> // For `printf()`
// int main(int argc, char *argv[]) // alternative prototype
int main()
{
printf("Hello World\n");
bool exit_now = false;
// setup/initialization
my_module_h my_module = NULL;
// For safety-critical and real-time embedded systems, it is **critical**
// that you ONLY call the `_open()` functions during **initialization**, but
// NOT during normal run-time, so that once the system is initialized and
// up-and-running, you can safely know that no more dynamic-memory
// allocation, which is non-deterministic and can lead to crashes, will
// occur.
my_module_open(&my_module);
// Ensure initialization was successful and `my_module` is no longer NULL.
if (!my_module)
{
// await connection of debugger, or automatic system power reset by
// watchdog
printf("ERROR: my_module_open() failed!\n"); // for demo purposes only
// log_errors_and_enter_infinite_loop();
}
// run the program in this infinite main loop
while (exit_now == false)
{
static size_t counter = 0;
my_module_do_stuff1(my_module);
my_module_do_stuff2(my_module);
// exit after 2 iterations, for demo purposes only
counter++;
if (counter >= 2)
{
exit_now = true;
}
}
// program clean-up; will only be reached in this case in the event of a
// major system problem, which triggers the infinite main loop above to
// `break` or exit via the `exit_now` variable
my_module_close(my_module);
// for microcontrollers or other low-level embedded systems, we can never
// return, so enter infinite loop instead
//
// while (true) {}; // await reset by watchdog
return 0;
}
Improvements: using enum
error handling, and a configuration struct
The only improvements beyond this would be to:
Implement full error handling and return the error instead of void
. Ex:
/// @brief my_module error codes
typedef enum my_module_error_e
{
/// No error
MY_MODULE_ERROR_OK = 0,
/// Invalid Arguments (ex: NULL pointer passed in where a valid
/// pointer is required)
MY_MODULE_ERROR_INVARG,
/// Out of memory
MY_MODULE_ERROR_NOMEM,
/// etc. etc.
MY_MODULE_ERROR_PROBLEM1,
} my_module_error_t;
Now, instead of returning a void
type in all of the functions above and below, return a my_module_error_t
error type instead!
Add a configuration struct called my_module_config_t
to the .h
file, and pass it in to the open
function to update internal variables when you create a new object. This helps encapsulate all configuration variables in a single struct for cleanliness when calling _open()
.
Example:
//--------------------
// my_module.h
//--------------------
// my_module configuration struct
typedef struct my_module_config_s
{
int my_config_param_int;
float my_config_param_float;
} my_module_config_t;
my_module_error_t my_module_open(my_module_h * my_module_h_p,
const my_module_config_t *config);
//--------------------
// my_module.c
//--------------------
my_module_error_t my_module_open(my_module_h * my_module_h_p,
const my_module_config_t *config)
{
my_module_error_t err = MY_MODULE_ERROR_OK;
// Ensure the passed-in pointer is not NULL (since it is a core
// dump/segmentation fault to try to dereference a NULL pointer)
if (!my_module_h_p)
{
// Print some error or store some error code here, and return it
// at the end of the function instead of returning void. Ex:
err = MY_MODULE_ERROR_INVARG;
goto done;
}
// Now allocate the actual memory for a new my_module C object from
// the heap, thereby dynamically creating this C-style "object".
my_module_h my_module; // Create a local object handle (pointer to a
// struct)
// Dynamically allocate memory for the full contents of the struct
// "object"
my_module = (my_module_h)malloc(sizeof(*my_module));
if (!my_module)
{
// Malloc failed due to out-of-memory. Print some error or store
// some error code here, and return it at the end of the function
// instead of returning void. Ex:
err = MY_MODULE_ERROR_NOMEM;
goto done;
}
// Initialize all memory to zero (OR just use `calloc()` instead of
// `malloc()` above!)
memset(my_module, 0, sizeof(*my_module));
// Now initialize the object with values per the config struct passed
// in. Set these private variables inside `my_module` to whatever they
// need to be. You get the idea...
my_module->my_private_int1 = config->my_config_param_int;
my_module->my_private_int2 = config->my_config_param_int*3/2;
my_module->my_private_float = config->my_config_param_float;
// etc etc
// Now pass out this object handle to the user, and exit.
*my_module_h_p = my_module;
done:
return err;
}
And usage:
my_module_error_t err = MY_MODULE_ERROR_OK;
my_module_h my_module = NULL;
my_module_config_t my_module_config =
{
.my_config_param_int = 7,
.my_config_param_float = 13.1278,
};
err = my_module_open(&my_module, &my_module_config);
if (err != MY_MODULE_ERROR_OK)
{
switch (err)
{
case MY_MODULE_ERROR_INVARG:
printf("MY_MODULE_ERROR_INVARG\n");
break;
case MY_MODULE_ERROR_NOMEM:
printf("MY_MODULE_ERROR_NOMEM\n");
break;
case MY_MODULE_ERROR_PROBLEM1:
printf("MY_MODULE_ERROR_PROBLEM1\n");
break;
case MY_MODULE_ERROR_OK:
// not reachable, but included so that when you compile with
// `-Wall -Wextra -Werror`, the compiler will fail to build if you
// forget to handle any of the error codes in this switch statement.
break;
}
// Do whatever else you need to in the event of an error, here. Ex:
// await connection of debugger, or automatic system power reset by
// watchdog
while (true) {};
}
// ...continue other module initialization, and enter main loop
See also:
- [another answer of mine which references my answer above] Architectural considerations and approaches to opaque structs and data hiding in C
- My answer: Error handling in C code - I demonstrate using
enum
s for error handling in C or C++.
Additional reading on object-based C architecture:
- Providing helper functions when rolling out own structures
Additional reading and justification for valid usage of goto
in error handling for professional code:
- An argument in favor of the use of
goto
in C for error handling: https://github.com/ElectricRCAircraftGuy/eRCaGuy_dotfiles/blob/master/Research_General/goto_for_error_handling_in_C/readme.md
- *****EXCELLENT ARTICLE showing the virtues of using
goto
in error handling in C: "Using goto for error handling in C" - https://eli.thegreenplace.net/2009/04/27/using-goto-for-error-handling-in-c
- Valid use of goto for error management in C?
- My answer: Error handling in C code
- My answer: Why are goto, break, continue and multiple return statements considered bad practice
Search terms to make more googlable: opaque pointer in C, opaque struct in C, typedef enum in C, error handling in C, c architecture, object-based c architecture, dynamic memory allocation at initialization architecture in c
Option 1.5
, here: stackoverflow.com/a/54488289/4561887.