2
\$\begingroup\$

I come from an OOP background, and have recently delved into C and embedded systems.

As an exercise I wanted to see if I could wrap some OOP concepts into a macro header.

It works as I intended, is fairly simple to use and get a grip on if you know OOP terminology / concepts.

Would love any feedback to see if I'm doing something stupid.

At the moment it's only heap allocation. I will update in future for stack based struct objects, but it only requires the coder write 1 additional function to implement.

It's >C/GNU99 and requires P99 for the variadic macros (I've yet to copy those little bits over).

eOOPc.h

#ifndef OOP_MAIN_H
#define OOP_MAIN_H
    
    #ifndef _STDLIB_H
        #include <stdlib.h>
    #endif
    
    #ifndef P99_IF_H_
        #include "p99/p99_if.h"
    #endif

    /**
    * grab an interface definition by calling it's macro.
    * @param i Interface name
    */
        #define eIMPLEMENTS(i) eINTERFACE_##i()

    /**
    * add a parent class struct to gain access to it's public methods
    * @param p Struct name
    * @param n Property name
    */
        #define eEXTENDS(p,n) struct p n

    /**
    * helper macro to denote that this parent is upcastable (this macro must be first element of containing
    * struct for this to be true)
    * @param p Struct name
    * @param n Property name
    */
        #define eDIR_EXTENDS(p,n) eEXTENDS(p, n)
    
    /**
    * Instantiate an object 'o*' of type 'c' by using function 'c_instatiate()'
    * @param <classtype_t> c
    * @param var o Object variable name
    * @param ... any further arguments
    */
        #define eNEW_INS(c,o, ...) P99_IF_EMPTY(__VA_ARGS__) (c##_instantiate(o)) (c##_instantiate(o, __VA_ARGS__))
    
    /**
    * Call allocation method and imediately fire instatiation function for heap object
    * @param <classtype_t> c
    * @param var o Object variable name
    * @param ... any further arguments
    */
        #define eNEW(c,o, ...) struct c*o = (struct c *)malloc(sizeof(struct c)); eNEW_INS(c,o, __VA_ARGS__)
    
    /**
    * public property DECLARATION
    * @param t Type
    * @param p Property name
    */
        #define ePROP_DEC(t, p) t p
    
    /**
    * public property DEFINITION
    * @param p Property name
    * @param v Value
    */
        #define ePROP_DEF(p, v) self->p = v
    
    /**
    * private property PUBLIC function-pointer DECLARATIONS for public struct
    * @param t Type
    * @param p Property name
    * @param m get/set/getset
    */
        #define ePRIV_PROP_DEC_get(t,p) t (*get_##p)(void * eOBJ)
        #define ePRIV_PROP_DEC_set(t,p) void (*set_##p)(void * eOBJ, t v)
        #define ePRIV_PROP_DEC_getset(t,p) ePRIV_PROP_DEC_get(t,p); ePRIV_PROP_DEC_set(t,p)
        #define ePRIV_PROP_DEC_PUB(t, p, m) ePRIV_PROP_DEC_##m(t,p)
    
    /**
    * private property PRIVATE function-pointer DECLARATIONS for private struct
    * @param t Type
    * @param p Property name
    * @param m get/set/getset
    */
        #define ePRIV_PROP_DEC_PRIV(t, p, m) ePROP_DEC(t,p); ePRIV_PROP_DEC_##m(t,p)
    
    /**
    * private property PUBLIC function DECLARATIONS
    * @param c Type
    * @param t Function return type
    * @param p Property name
    * @param m get/set/getset
    */
        #define ePRIV_PROP_FUNC_DEC_get(c, t, p) t c##_get_##p(void * eOBJ);
        #define ePRIV_PROP_FUNC_DEC_set(c, t, p) void c##_set_##p(void * eOBJ, t v );
        #define ePRIV_PROP_FUNC_DEC_getset(c, t, p) ePRIV_PROP_FUNC_DEC_get(c, t, p) ePRIV_PROP_FUNC_DEC_set(c, t, p)
        #define ePRIV_PROP_FUNC_DEC(c, t, p, m) ePRIV_PROP_FUNC_DEC_##m(c, t, p)
    
    /**
    * private property function ALLOCATIONS for object function pointers in instantiate
    * @param c Type
    * @param p Property name
    * @param v Property value
    * @param m get/set/getset
    */
        #define ePRIV_PROP_DEF_get(c, p) self->get_##p = &c##_get_##p
        #define ePRIV_PROP_DEF_set(c, p) self->set_##p = &c##_set_##p
        #define ePRIV_PROP_DEF_getset(c, p) ePRIV_PROP_DEF_get(c, p); ePRIV_PROP_DEF_set(c, p)
        #define ePRIV_PROP_DEF(c, p, v, m) self->p = v; ePRIV_PROP_DEF_##m(c, p)
    
    /**
    * private property PUBLIC get/set function DEFINITIONS
    * @param c Type
    * @param t Function return type
    * @param p Property name
    * @param m get/set/getset
    */
        #define ePRIV_PROP_FUNC_DEF_get(c, t, p) t c##_get_##p(void * eOBJ){ eSELF(c); return self->p; }
        #define ePRIV_PROP_FUNC_DEF_set(c, t, p) void c##_set_##p(void * eOBJ, t v ){ eSELF(c); self->p = v; }
        #define ePRIV_PROP_FUNC_DEF_getset(c, t, p) ePRIV_PROP_FUNC_DEF_get(c, t, p) ePRIV_PROP_FUNC_DEF_set(c, t, p)
        #define ePRIV_PROP_FUNC_DEF(c, t, p, m) ePRIV_PROP_FUNC_DEF_##m(c, t, p)
    
    /**
    * Cast "self" back into the class type
    * @param <classtype_t> c
    */
        #define eSELF(c) c * self = (c*)eOBJ
    
    /**
    * Get the value of a protected variable 'p' within object 'o'
    * Prints to stderr if property is private
    * @param var o Object
    * @param var p Object property
    */
        #define eGET(o, p) o->get_##p(o)
    
    /**
    * Set the value of a protected variable 'x' within object 'o'
    * Prints to stderr if property is private
    * @param var o Object
    * @param var p Object property
    * @param var v The new value
    */
        #define eSET(o, p, v) o->set_##p(o, v)
    
    /**
    * Method call wrapper that passes object as first argument for use of eSELF()
    * @param var o Object
    * @param var m The method
    * @param ... Other args
    */
        #define eMETH(o, m, ...) P99_IF_EMPTY(__VA_ARGS__) ((*o->m)(o)) ((*o->m)(o, __VA_ARGS__))
        
    /**
    * Free memory on heap for object
    * @param var o Object variable name
    */
        #define eDESTROY(o) free(o); o = ((void*)0)
    
    /**
    * Free memory on heap for object by calling defined function to allow further actions
    * such as destroying string / struct members within object
    * @param <classtype_t> c
    * @param var o Object variable name
    */
        #define eDESTROY_M(c, o) c##_heap_destruct(o); o = ((void*)0)
        
#endif //OOP_MAIN_H

With some example files:

class.h Public header file

#ifndef OOP_CLASS_H
#define OOP_CLASS_H

//interface definitions can contain expressly written variables,
    //other e@ macros etc as needed.
        #define eINTERFACE_interface() \
            int poop; \
            int shmoop
    
        struct parent{
            int pprop1;
            int pprop2;
        };

//PUBLIC DEFINITION AND METHODS
    
    struct Class_t{
    
        //parent for upcasts
            eDIR_EXTENDS(parent, parent);
    
        //interface
            eIMPLEMENTS(interface);
    
        //define a public property
            ePROP_DEC(int, prop1);
            
        //define function pointers for get, set or both for private property
            ePRIV_PROP_DEC_PUB(int, prop2, get);
        
        //public method function pointer
            int (*method1)(void * eOBJ);
        
    };
    
//public class function declarations

    //get and/or set PUBLIC function declarations
        ePRIV_PROP_FUNC_DEC(Class_t, int, prop2, get)
    
    //other defined public methods declarations
        int Class_t_method1(void * eOBJ);

    //always need an instantiate. Void pointer so function can cast back to struct type pointer
    //this means we don't get conflicting type errors
        void Class_t_instantiate(void * eOBJ);

#endif //OOP_CLASS_H

class.c Private Source & Definitions

//need OOP macros
    #include "eOOPc.h"

//include public declaration
    #include "class.h"

//PRIVATE DECLARAION
    typedef struct{
    
        eDIR_EXTENDS(parent, parent);
    
        //interface
            eIMPLEMENTS(interface);
    
        //matching public prop declaration
            ePROP_DEC(int, prop1);
        
        //private property declaration + PUBLIC function pointer declarations
            ePRIV_PROP_DEC_PRIV(int, prop2, get);
    
        //public method function pointer
            int (*method1)(void * eOBJ);
        
        //private method function pointer
            int (*method2)(void * eOBJ);
    
    } Class_t;

//PRIVATE METHODS
    int Class_t_method2(void * eOBJ){
    
        eSELF(Class_t);
        
        return self->prop1;
    
    }

//PUBLIC METHOD DEFINITIONS TO OVERRIDE DECLARATIONS

    //private property PUBLIC get/set method definitions
        ePRIV_PROP_FUNC_DEF(Class_t, int, prop2, get)
    
    //other public method definitions
        int Class_t_method1(void * eOBJ){
        
            eSELF(Class_t);
            
            return 4;
        
        }

    void Class_t_instantiate(void * eOBJ){
    
        eSELF(Class_t);
        
        ePROP_DEF(prop1, 3);
        
        ePRIV_PROP_DEF(Class_t, prop2, 4, get);
        
        self->method1 = &Class_t_method1;
        self->method2 = &Class_t_method2;
    
    }
    

main.c Example

Note it only has access to the class.h file - the further methods and properties defined in class.c are private and inaccessible.

#include "eOOPc.h"
#include "class.h"

int main (void){

    //instantiate object (i.e. new)
        eNEW(Class_t, object);
        
        int i = eMETH(object, method1);
        
    return 0;

}

\$\endgroup\$
5
  • 2
    \$\begingroup\$ Have you tested this beyond what you have in the main.c example? At the moment this looks very hypothetical which would make it off-topic for Code Review. Possible problems I see in the implementation: Don't include stdio.h. All members of a struct are public so a private member variable is impossible to implement. The code is hiding things which would make it difficult for a new programmer to maintain. \$\endgroup\$ – pacmaninbw Apr 6 at 11:51
  • 2
    \$\begingroup\$ What problem does this code solve? It's an exercise, we get that, but even exercises tend to target a fake problem to solve. \$\endgroup\$ – Mast Apr 6 at 12:55
  • \$\begingroup\$ @pacmaninbw Actually no - private members are entirely possible - if the calling source file only includes the public class header, it does not have access to the additional members in the struct as defined in the class source file. They have to be in a certain order for the memory mapping to work (as I understand it). This was taken from the book: amazon.co.uk/… \$\endgroup\$ – E R Barratt Apr 6 at 13:44
  • \$\begingroup\$ @ Mast - It enables me to write program using OOP using an easy to read script. One example I have that I'm writing with it now - github.com/erbarratt/4c04oop - is a cpu emulator, where each "device" in the system acts as an object. Therefor, it's easy to write multiple RAM blocks for example, or add other devices to a bus object etc. This makes more sense to me in this specific case. Or games. \$\endgroup\$ – E R Barratt Apr 6 at 13:48
  • 2
    \$\begingroup\$ One person's "easy-to-read script" is another person's "oh god no, now I need to read the library to understand what this is doing" \$\endgroup\$ – Reinderien Apr 6 at 16:06
3
\$\begingroup\$

The appeal of C is that it's simple, standard, and performant. Whereas your approach might (?) not impact performance all that much, it soundly destroys the first two concepts.

For reference, you're of course not the first to have tried crow-barring OOP into C. I somewhat recoil at the idea of dense, complex macro magic at all, much less to implement OOP. My approach - that I think you could benefit from, although your question has fallen short of demonstrating any concrete application - is

  • Yes, use OOP; it is a useful way to think about code
  • No, do not attempt to be clever and abstract it, using macros or otherwise

OOP is entirely possible using "stock C", via

  • Context structures to encapsulate member variables
  • Free functions accepting context structure pointers to represent object methods
  • Opaque structure definition in header files to enforce separation of concerns

This pattern is very common in C, very legible, and will produce clean code. Handing code written in this way to someone who has a fundamental understanding of C will succeed in them being able to follow and contribute to the code. Handing code written in the way presented in the question to the same person will not produce the same effect. The pattern presented in the question also has more surface area for bugs.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.