Click here to Skip to main content
15,867,568 members
Articles / Internet of Things / Raspberry-Pi

cobj: Polymorphism in Plain C

Rate me:
Please Sign up or sign in to vote.
5.00/5 (11 votes)
7 Jun 2016MIT15 min read 16K   226   15   3
cobj is a preprocessor based generator for interface based polymorphism

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 structs).

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.

C++
// file: logic.c

#include "gpio_pin.h"

void logic(gpio_pin * input_pin, gpio_pin * output_pin)
{
    // read the value of the input_pin
    bool value = gpio_pin_get_value(input_pin);
    
    // the output must have the same state as the input
    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:

C++
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:

C++
// file: main.c

// include interface headers
#include "gpio_pin.h"

// include class headers
#include "hw_gpio_pin.h"
#include "gpio_pin_inverter.h"

// additional includes
#include "logic.h"

// declare the objects needed
static hw_gpio_pin input_pin_hw;
static hw_gpio_pin output_pin_hw;

void main()
{
    // we use pin #13 for the input
    hw_gpio_pin_initialize(&input_pin_hw, 13);
    
    // and pin #12 for the output
    hw_gpio_pin_initialize(&output_pin_hw, 12);
    
    // to call any functions, and to pass it to the logic, we need to query for interfaces:
    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);
    
    // start the logic
    logic(&input_pin_ref, &output_pin_ref);    
}

Please note the following conventions:

C++
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.

C++
#define METHODS METHOD(foo) METHOD(bar)

// all of the following code can be place in a different .h file
// for example a "generator.h" and included by #include "generator.h"

// define how to generate METHOD(name)
// in this case some forward-declaration
#define METHOD(name) void name(void);

// generate the code by let METHODS expand
METHODS

// METHODS is still defined, and processed.
// so we can redefine "METHOD" now, to generate some other code
// let's do the implementation
#define METHOD(name) void name(){}

// and generate it
METHODS

This file generates the following code:

C++
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:

C++
#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:

C++
#define METHODS \
    METHOD(foo) // not ok \
    METHOD(bar)

#define METHODS \
    METHOD(foo) /* ok */ \
    METHOD(bar)

How to Define Interfaces

Each interface must be defined in its own .h file. It's based on the general cobj pattern:

  1. define attributes
  2. call generator
C++
// file "gpio_pin.h"
#ifndef GPIO_PIN_H_   // include guard
#define GPIO_PIN_H_

// include needed headers, maybe other interface headers:
#include "stdbool.h"

// define the name of the interface, should match the name of the .h file
#define COBJ_INTERFACE_NAME gpio_pin

// define the methods of the interface
// by using the COBJ_INTERFACE_METHOD->COBJ_INTERFACE_METHOD x-macro
#define COBJ_INTERFACE_METHODS \
    COBJ_INTERFACE_METHOD(bool, get_value)   \
    COBJ_INTERFACE_METHOD(void, set_value, bool, value)
    
// run the generator, note that the cobj directory needs to be in your include path!
#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.

C++
// file hw_gpio_pin.h
#ifndef HW_GPIO_PIN_H_   // include guard
#define HW_GPIO_PIN_H_

// define the name of the class
#define COBJ_CLASS_NAME	hw_gpio_pin

// define the parameters for the initialize method
#define COBJ_CLASS_PARAMETERS	\
	COBJ_CLASS_PARAMETER(int, pin_nr)

// define the private variables
#define COBJ_CLASS_VARIABLES	\
	COBJ_CLASS_VARIABLE(int, port_address)	\
	COBJ_CLASS_VARIABLE(int, port_mask)

// define the implemented interfaces
#define COBJ_CLASS_INTERFACES	\
	COBJ_CLASS_INTERFACE(gpio_pin)

// include the .h file of the implemented interfaces,
// while define the "COBJ_INTERFACE_IMPLEMENTATION_MODE" symbol
// this is mandatory, so that generator implements the entities needed!
#define COBJ_INTERFACE_IMPLEMENTATION_MODE
#include "gpio_pin.h"
#undef  COBJ_INTERFACE_IMPLEMENTATION_MODE

// call the generator
#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)

C++
// file "hw_gpio_pin.c"

// define the COBJ_IMPLEMENTATION_FILE symbol, to use the correct generator mode
#define COBJ_IMPLEMENTATION_FILE

// include the class-header
// this also includes the interface-headers indirectly, so you don't need to do it by yourself!
// It's generally recommended to use include-guards in include files,
// like shown in the .h file examples
#include "hw_gpio_pin.h"

// the initialize methods
static bool initialize_impl(hw_gpio_pin_impl * self, int pin_nr)
{

  self->port_address = 0;
  self->port_mask = 0;

  // implementation, see attached file
}

static bool gpio_pin_get_value_impl(hw_gpio_pin_impl * self)
{
	// implementation, see attached file
}

static void gpio_pin_set_value_impl(hw_gpio_pin_impl * self, bool value)
{
  // implementation, see attached file
}

static void gpio_pin_set_options_impl(hw_gpio_pin_impl * self, gpio_pin_options options)
{
  // implementation, see attached file
}

static void gpio_pin_toggle_impl(hw_gpio_pin_impl * self)
{
  // implementation, see attached file
}

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:

C++
// file: interface_registry.c
#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:

  1. You create a .c file (e.g. vax-helper.c, which is not compiled in the project, it's just for the Intellisense)
  2. You add a #include to every interface and class header used.
  3. 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
  4. 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.
  5. 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:
C++
        /*
Common Error:
expected ';', ',' or ')' before 'COBJPVT_GEN_METHOD_ARGS_SEPERATOR_1'
or any odd number:

Cause:
The syntax of the COBJ_INTERFACE_METHOD is invalid, because the parameter
list must always contain pairs values.
You may be missing the comma between the type and the name.

Resolution:
Insert a comma between the type and the name, like:
Wrong: COBJ_INTERFACE_METHOD(void, foo, int i)
Right: COBJ_INTERFACE_METHOD(void, foo, int, i)
        */

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.

License

This article, along with any associated source code and files, is licensed under The MIT License


Written By
Architect
Austria Austria
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
PraiseGreat contribution... Pin
Ashok Kumar RV14-Jun-16 2:43
Ashok Kumar RV14-Jun-16 2:43 
GeneralRe: Great contribution... Pin
GProssliner15-Jun-16 3:32
GProssliner15-Jun-16 3:32 
GeneralMessage Closed Pin
7-Jul-16 1:48
Member 126232047-Jul-16 1:48 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.