Introduction
Although we have tons of high-level and script based languages and platforms, C is still a very important player, like in the embedded world, kernel, or even application code.
If you want to implement polymorphism in C, it's usually a complex thing. At the language level, function pointers allow polymorph code. But you need to do all the "dirty stuff" by yourself.
Even if you manage to implement your interface as a struct
of function-pointers, you need to call it from the pointer, and also pass some state to it. Calling function pointers, including passing state, are not very readable, and often need manual casts. So you have no compile time checking.
This is the place for cobj
: It's a simple pattern for interface base inheritance, and a generator which generates all the required boilerplate code by using just the preprocessor.
Please note that cobj
is no runtime. In fact, it doesn't even include a single .c file in its distribution. So cobj
doesn't care about lifetime, allocation, serialization, or anything like that. It only cares about calling methods and passing state. It doesn't allocate, so there is no dependency on malloc
, and you may initialize objects everywhere, also on the stack, or inline in other structures.
It's open source, under a very permitting license (MIT license), hosted on github (https://github.com/gprossliner/cobj). If you have any problems, also regarding the documentation or the demo project, please file an issue on github!
The Entities
There are these basic entities in cobj
:
- Interfaces
- declares a set of methods
- Classes
- implements one or more interfaces
- may declare
private
variables - may declare initialize arguments
- Objects
- A block memory initialized for a specific class, hold the
private
variables, and the descriptor
- References
- A combination of object and interface
- Is the result of a "
queryinterface
" call - Interface methods can only be called on a reference
- Descriptors
- A descriptor is a global variable to represent and describe an interface of class
- Normally only needed internally in the generated, not in your own code
- Generated as constants, so normally they will go to program memory, and consume no ram.
The Demo
I don't want to write thousands of words, before presenting you the code, so I'll show single snippets. The overall scenario in the demo is the following:
There is an interface named "gpio_pin
", which has four methods defined:
bool get_value()
void set_value(bool value)
void set_options(gpio_options options)
void toggle()
There are two classes defined, implementing this interface. The first is the "hw_gpio_pin
", which accesses some (virtual) hardware on peripheral registers, and a "gpio_pin_inverter
", which takes another gpio_pin
, to invert its logical state. Please see additional example interfaces and classes in the github demo.
Object Initialization
In most cases, classes have some state assigned to them. So cobj
allows easy initialization of objects, because you can define zero or more arguments for an "initialize
" method, which every class has automatically.
For the demo, we have the following parameters:
hw_gpio_pin_initialize(int pin_nr)
gpio_pin_inverter_initialize(gpio_pin * pin)
Please note that the initialize method returns bool
. This value is not used by cobj
itself. You may use it to represent the result of the initialization. When you know that the initialization of an object will not fail, you don't need to check the returned value.
Private State
Most classes need some private
state to implement their functionality. If you have singletons, you may use ordinary global static
variables, but if you may have multiple instances of a class (on object), this state must be specified somewhere else.
In cobj
, you can define private
variables. The generator forms a struct
out of them. The size of the struct
is known to the compiler, so you can allocate objects everywhere (stack, heap, inline in struct
s).
Because C has no "this-call Calling Convention", the this
parameter (named "self
" in cobj
, to avoid using a C++ keyword), has to be passed manually as the first argument. Please note that cobj
generates type safe code be default, so the compiler checks if you don't mix classes!
Consumer Code
Now it's really time to show you some code. This is the code that can be used by any .c file, which includes the .h files where the interface is defined (gpio_pin.h, which I'll show you later).
This file is the application's logic, which operates transparently on every implementation of an gpio_pin
.
#include "gpio_pin.h"
void logic(gpio_pin * input_pin, gpio_pin * output_pin)
{
bool value = gpio_pin_get_value(input_pin);
gpio_pin_set_value(output_pin, value);
}
The logic is not highly complex, and there are indeed easier ways to archive this goal, but I don't want to spend your time with complex application logic, cobj
doesn't care about your implementation. ;-)
Please note the naming and signature, which is always:
interfacename_methodname(interfacename * self [, arguments...]);
The arguments "input_pin
" and "output_pin
" are references to the interface. A reference represents a specific implementation of an interface. It has two pointer-sized fields. It's the result of a "queryinterface
" method, I'll show you later. You can call methods only on a reference, never on an object directly. Normally, you only pass references around, not objects. So a reference will be the most common entity used for an interface at runtime when using cobj
. It's the "natural" representation of the interface. So I choose the name of the interface as the name of the typedef
'ed reference struct
. Always when you see "interfacename
" in the signature, it represents a reference.
The next code is the driver, which declares and initializes instances of classes for the logic code:
#include "gpio_pin.h"
#include "hw_gpio_pin.h"
#include "gpio_pin_inverter.h"
#include "logic.h"
static hw_gpio_pin input_pin_hw;
static hw_gpio_pin output_pin_hw;
void main()
{
hw_gpio_pin_initialize(&input_pin_hw, 13);
hw_gpio_pin_initialize(&output_pin_hw, 12);
gpio_pin input_pin_ref;
gpio_pin output_pin_ref;
gpio_pin_queryinterface(&input_pin_hw.object, &input_pin_ref);
gpio_pin_queryinterface(&output_pin_hw.object, &output_pin_ref);
logic(&input_pin_ref, &output_pin_ref);
}
Please note the following conventions:
classname_initialize(classname * self [, arguments...])
interfacename_queryinterface(objectvariable.object, &interfacename)
Some notes:
- You always have to call the
initialize
method, even if you don't define own variables, because the object_descriptor
field has to be initialized, which is done in the generated code initialize
returns bool
, that you may use to signal success in the application. The returned value has no significance in cobj
, it just returns it from the method, and it will never return false
by itself. So if you know that a method will not fail, you may not need to test it. queryinterface
returns false
in case the interface is not implemented by the given class. In this case, the reference is not initialized. If you are not sure whether a class implements the specific interface, you have to check / assert the return value, otherwise you'll be using an uninitialized method, which behavior is undefined.
Declaring Metadata
cobj
is based on the preprocessor. To represent lists (like list of methods, variables, ...), it uses the x-macro technique. Let's give a short overview of it, maybe I can do an own article on that, just ask!
The x-macro Technique
The general pattern is, that you #define
a symbol (like "METHODS
") by using a list of other symbols (like "METHOD
"). When the preprocessor processes the "METHODS
" afterwards, it applies the "METHOD
" macro in its current definition. After this, the "METHODS
" is still defined, so by redefining "METHOD
", you can generate different code by a single definition.
Example
This is a generic example for x-macros, just to show how to use them in both, the interface and class headers.
#define METHODS METHOD(foo) METHOD(bar)
#define METHOD(name) void name(void);
METHODS
#define METHOD(name) void name(){}
METHODS
This file generates the following code:
void foo();
void bar();
void foo{}
void bar{}
The \ character to make the improve readability of definitions
C allows preprocessor definitions to span multiple lines, while the backslash character is used as a line-continuation marker (https://gcc.gnu.org/onlinedocs/gcc-3.0.1/cpp_3.html)
cobj
makes use of it, and you may also use it for your definitions, because it's more readable. Otherwise for example, you would have to put all method definitions into a single line.
So the previous example could be written as:
#define METHODS \
METHOD(foo) \
METHOD(bar)
Comment in multiline macros
Please note that you can't use C++ style comments (// comment
) in the middle of multiline macros, because the compiler would not "see" the \
within the comment. So:
#define METHODS \
METHOD(foo)
#define METHODS \
METHOD(foo) \
METHOD(bar)
How to Define Interfaces
Each interface must be defined in its own .h file. It's based on the general cobj
pattern:
- define attributes
- call generator
#ifndef GPIO_PIN_H_ // include guard
#define GPIO_PIN_H_
#include "stdbool.h"
#define COBJ_INTERFACE_NAME gpio_pin
#define COBJ_INTERFACE_METHODS \
COBJ_INTERFACE_METHOD(bool, get_value) \
COBJ_INTERFACE_METHOD(void, set_value, bool, value)
#include "cobj-interface-generator.h"
#endif
Please note the following:
- The generator must always be included after the defines, not at the beginning of the file!
- The comma between the type of an argument, and its name is mandatory! It would be nicer without, but there is no method I can think of splitting by whitespace in the preprocessor
- You may use any modifier for types, to the expression
COBJ_INTERFACE_METHOD(unsigned int, foo, const void *, address, struct data data)
is valid - Because every number of arguments have to be handled explicitly internally in
cobj
, there is a limit in the number of arguments. This limit is currently set to 16
. You'll get an error if you define a method with more than 16 arguments. If this is not enough for you, you may file an issue on github.
How to Define Classes
Classes consists of two files. One header for the file (hw_gpio_pin.h). This classheader has be included by every file, which either initializes a class, or allocates a class, but not just to call methods of any interface reference (in this case, only the interface.h file must be included).
The class.h file
First, I'll show you how to create the class.h file. There are inline comments to explain the structure of the file.
#ifndef HW_GPIO_PIN_H_ // include guard
#define HW_GPIO_PIN_H_
#define COBJ_CLASS_NAME hw_gpio_pin
#define COBJ_CLASS_PARAMETERS \
COBJ_CLASS_PARAMETER(int, pin_nr)
#define COBJ_CLASS_VARIABLES \
COBJ_CLASS_VARIABLE(int, port_address) \
COBJ_CLASS_VARIABLE(int, port_mask)
#define COBJ_CLASS_INTERFACES \
COBJ_CLASS_INTERFACE(gpio_pin)
#define COBJ_INTERFACE_IMPLEMENTATION_MODE
#include "gpio_pin.h"
#undef COBJ_INTERFACE_IMPLEMENTATION_MODE
#include "cobj-classheader-generator.h"
#endif
Please note the following:
- It's recommended that the name of the .h and the .c file matches the name provided by the
COBJ_CLASS_NAME
symbol - At the moment, you include your the .h files of implemented interfaces, you have to define the
COBJ_INTERFACE_IMPLEMENTATION_MODE
symbol. This causes the generator the code necessary to implement the interface in this class. - Either the
COBJ_CLASS_INTERFACES
, or the COBJ_INTERFACE_IMPLEMENTATION_MODE
is redundant, I have not found a way to substitute one for another (like don't require the COBJ_INTERFACE_IMPLEMENTATION_MODE
, but see if the included .h is within the COBJ_CLASS_INTERFACES
).
The class.c File
The next step is to implement the method implementations in the .c file. Please note the following:
- Every implementation method has the suffix
_impl
- Every implementation method has to be
static
- You don't need to declare the implementation methods, they are declared by the generator
- The self argument uses another
typedef
than on the consumer side, which allows to access the private
variables with ease. It also uses the _impl
suffix, and is always a pointer. - The "
COBJ_IMPLEMENTATION_FILE
" symbol has be be define in the .c file, before the class-header is included. - The signature of the implementation methods is:
static return_type interfacename_methodname_impl(classname_impl * self [, arguments])
- The signature of the initialize method is:
static bool classname_initialize_impl(classname_impl * self [, init_arguments])
Example (hw_gpio_pin.c)
#define COBJ_IMPLEMENTATION_FILE
#include "hw_gpio_pin.h"
static bool initialize_impl(hw_gpio_pin_impl * self, int pin_nr)
{
self->port_address = 0;
self->port_mask = 0;
}
static bool gpio_pin_get_value_impl(hw_gpio_pin_impl * self)
{
}
static void gpio_pin_set_value_impl(hw_gpio_pin_impl * self, bool value)
{
}
static void gpio_pin_set_options_impl(hw_gpio_pin_impl * self, gpio_pin_options options)
{
}
static void gpio_pin_toggle_impl(hw_gpio_pin_impl * self)
{
}
The Interface Registry
Because cobj
needs to generate some code for an interface (the descriptor or the callable methods), this code has to be generated in a .c file somewhere.
You may put each interface in a single .c file, but because you normally don't need any custom code here, it's recommended to use a single .c file, named interface_registry.c in your project, which contains all the global code of the interfaces. If you split the files, you have to make sure that every interface is contained in exactly one registry. Otherwise, you'll get linker error for symbols not defined, or defined more than once.
The interface_registry.c file just defines the COBJ_INTERFACE_REGISTRY_MODE
symbol, and then includes all the .h files of the interfaces. In our example, we have just one, so the file is:
#define COBJ_INTERFACE_REGISTRY_MODE
#include "gpio_pin.h"
A Word About Runtime Overhead
The overhead cobj
introduces is in fact very minimal. It can be isolated to the following operations:
- Allocation:
cobj
don't allocate, so you have total control about your memory. This also means zero overhead in allocation. The initialize are two call instructions, where one is normally inlined, storing a pointer, and any custom code in initialize_impl
. - Getting a reference: When you call
queryreference
, an indirect call to a generated method on the class_definition
field of the object is executed. The implementation checks for the requested interlaces in a series of if
statements, in declaration order. So you have O(n), where n is the number of interfaces that are implemented by the given class. - Calling interface methods on a reference: Calling an interface method consists of a
static
call to the entry method, which performs an indirect call to the implementation method. To ensure the no warnings, in fact we have two calls (method_thunk
calls method_impl
), but they are both static
, so the latter call is normally inlined.
How to Use the Code
In the download, I just provided the sources, without a project or makefile. There are several different IDEs and compilers, so you may choose any one you like.
Environment and Portability
The code has been tested against GCC v4.4.7, and it compiles to a code with zero warnings, even when settings -wall (all warnings). This was a design criteria for cobj
, and much code is generated, just to make sure you don't get warnings from the generated code.
If you setup a project or makefile, please note that the /cobj directory needs to be in the include path.
To obfuscate the names of the private
variables, cobj
currently uses the __COUNTER__
macro, which is a gcc extension. Alternatives are welcome! If you use another compiler, please file an issue.
cobj
has no direct dependency to the OS used, or if an OS is used at all. It has also no dependency on a specific architecture. It should work well even in the 8 bit world. It's currently tested on 32 bit only, so if you find any portability issues, please file an issue.
Intellisense and Code-Completion
Different tools can handle complex .h files (like the cobj
generators) differently. I tested against Visual Assist (http://www.wholetomato.com/), as this is integrated into Atmel Studio, and it was not able to process the files in a way that you have a good experience within your IDE. This is of cause not an optimal situation. But it's not the end of the world, because there is a good workaround, that all works well:
- You create a .c file (e.g. vax-helper.c, which is not compiled in the project, it's just for the Intellisense)
- You add a
#include
to every interface and class header used. - You run the gcc compiler to only preprocess the file, and not include the world:
-I"demo" -I"cobj" -E -P -o "vax-helper\vax-helper.h" "vax-helper\vax-helper.tmp.c"
-I"cobj"
: include the cobj directory in the include path
-E
: preprocess only
-P
: Inhibit generation of line markers in the output from the preprocessor (readability)
-o
: output file - After running this command, you have all needed declarations in a .h file, that Visual Assist uses in your project, even if you don't include it in the .c file.
- You may automate this process by a build event.
Please note that I would appreciate any tests on different IDEs and Tools! Just get in contact by using the Discussions, or file an issue on github!
How Can I Find Errors Within my metadata?
We all know that C compilers are not the best in reporting errors. Often, you just miss a single semicolon, and you get hundreds of errors. If you use a meta programming framework like cobj
, the situation doesn't get better.
I really want to allow the user to know what's wrong in case of an error, but controlling the compiler output is very limited, as you can't embed preprocessor statements like #error
in macro definitions.
So I choose the following strategy in errors:
- If you have a syntactical error within one of your files, go to the location of the error, and check with the documentation, or the examples, what is wrong.
- If you have a semantic error within one of your files, like missing the comma between the name and the type of a definition, the location of the error is normally within one of the cobj files. In this case, there are comments within the source code, to guide you what's wrong, like:
Tip: On errors, don't just use the Error-List View in the IDE. GCC outputs a lot more in the log, than IDE normally displays, and the errors are sorted (always start with the first error or warning!). If you forgotten to implement a _impl
method in a .c file, for example, the Output Windows just shows "unresolved symbol gpio_pin_get_value
", while the Output Windows shows also the name of the .o file it compiles.
Tip: To further diagnose issues, it's helpful to check the file after preprocessing. In GCC, you may use the -E
, or the -save-temps
switch.
Points of Interest
It was really funny to build cobj
. And it really works will in its defined scenario. If you have any ideas for improvement, if you find any bugs, if you want to participate, if you did some tests you want to share, ....
Use the discussions here on CodeProject!
File an issue on github (https://github.com/gprossliner/cobj/issues)
You may file an issue, not only for bug in the code, but also if you need more demos, have problems with the integration in your project, or if you want to discuss an improvement.
I also would really appreciate any "case study" if someone chooses to use cobj
in a project. What is good? What are the most painful lessons learned? I also would love someone to do a performance test for cobj
.
History
This is the first version of cobj
. It's still work in process, so some things may change, or there will be some improvements. Please also check out the github page, where you'll find additional samples. I'll try to update this article if something changes, but the github source tree will always be more current than the article.