tags:

views:

2808

answers:

10

Can someone please share a set of nifty preprocessor hacks (ANSI C89/ISO C90 compatible please) which enable some kind of ugly (but usable) object-orientation in C? I am familiar with a few different object-oriented languages, so please don't respond with answers like "Learn C++!". I have read "Object-Oriented Programming With ANSI C" (beware: pdf) and several other interesting solutions, but I'm mostly interested in yours :-)!


See also Can you write object oriented code in C?

+9  A: 

(ripped from How to write production C code? [closed])

I once worked with a C library that was implemented in a way that struck me as quite elegant. They had written, in C, a way to define objects, then inherit from them so that they were as extensible as a C++ object. The basic idea was this:

  • Each object had its own file
  • Public functions and variables are defined in the .h file for an object
  • Private variables and functions were only located in the .c file
  • To "inherit" a new struct is created with the first member of the struct being the object to inherit from

Inheriting is difficult to describe, but basically it was this:

struct vehicle {
   int power;
   int weight;
}

Then in another file:

struct van {
   struct vehicle base;
   int cubic_size;
}

Then you could have a van created in memory, and being used by code that only knew about vehicles:

struct van my_van;
struct vehicle *something = &my_van;
vehicle_function( something );

It worked beautifully, and the .h files defined exactly what you should be able to do with each object.

Kieveli
I'd like to know more about how this worked :)
J Cooper
I really like this solution, except that all of the "object"'s internals are public.
Software Monkey
@Software Monkey: C has no access control. The only way to hide implementation details is to interact through opaque pointers, which can get pretty painful, since all fields would need to be accessed through accessor methods which probably can't be inlined.
Adam Rosenfield
@Adam: Compilers supporting link-time optimizations will inline them just fine...
Christoph
If you do this, you should also ensure that all the functions in the .c file that are not defined as public are defined as static so they don't end up as named functions in your object files. That ensures no one can find their names in the link phase.
jmucchiello
+3  A: 

ffmpeg (a toolkit for video processing) is written in straight C (and assembly language), but using an object-oriented style. It's full of structs with function pointers. There are a set of factory functions that initialize the structs with the appropriate "method" pointers.

Mr Fooz
I'll take a look at the source in a little while. I'll respond with my impressions of it either tonight or sometime tomorrow.
Anthony Cuozzo
i don't see any factory functions in it(ffmpeg), rather it doesnt seem to be using polymorphism/inheritance ( trivial way suggested above).
FL4SOF
avcodec_open is one factory function. It stuffs function pointers into a AVCodecContext struct (like draw_horiz_band). If you look at FF_COMMON_FRAME macro usage in avcodec.h, you'll see something akin to inheritance of data members. IMHO, ffmpeg proves to me that OOP is best done in C++, not C.
Mr Fooz
+2  A: 

If you think of methods called on objects as static methods that pass an implicit 'this' into the function it can make thinking OO in C easier.

For example:

String s = "hi";
System.out.println(s.length());

becomes:

string s = "hi";
printf(length(s)); // pass in s, as an implicit this

Or something like that.

jjnguy
...stating the obvious...
Artelius
@Artelius: Sure, but sometimes the obvious is not, until it's stated. +1 for this.
Software Monkey
+5  A: 

The GNOME desktop for Linux is written in object-oriented C, and it has an object model called "GObject" which supports properties, inheritance, polymorphism, as well as some other goodies like references, event handling (called "signals"), runtime typing, private data, etc.

It includes preprocessor hacks to do things like typecasting around in the class hierarchy, etc. Here's an example class I wrote for GNOME (things like gchar are typedefs):

Class Source

Class Header

Inside the GObject structure there's a GType integer which is used as a magic number for GLib's dynamic typing system (you can cast the entire struct to a "GType" to find it's type).

James Cape
unfortunately , the read me/tutorial file (wiki link) is not working and there is only reference manual for that(i am talking about GObject and not GTK). please provide some tutorial files for the same ...
FL4SOF
+28  A: 

I would advise against preprocessor (ab)use to try and make C syntax more like that of another more object-oriented language. At the most basic level, you just use plain structs as objects and pass them around by pointers:

struct monkey
{
    float age;
    bool is_male;
    int happiness;
};

void monkey_dance(struct monkey *monkey)
{
    /* do a little dance */
}

To get things like inheritance and polymorphism, you have to work a little harder. You can do manual inheritance by having the first member of a structure be an instance of the superclass, and then you can cast around pointers to base and derived classes freely:

struct base
{
    /* base class members */
};

struct derived
{
    struct base super;
    /* derived class members */
};

struct derived d;
struct base *base_ptr = (struct base *)&d;  // upcast
struct derived derived_ptr = (struct derived *)base_ptr;  // downcast

To get polymorphism (i.e. virtual functions), you use function pointers, and optionally function pointer tables, also known as virtual tables or vtables:

struct base;
struct base_vtable
{
    void (*dance)(struct base *);
    void (*jump)(struct base *, int how_high);
};

struct base
{
    struct base_vtable *vtable;
    /* base members */
};

void base_dance(struct base *b)
{
    b->vtable->dance(b);
}

void base_jump(struct base *b, int how_high)
{
    b->vtable->jump(b, how_high);
}

struct derived1
{
    struct base super;
    /* derived1 members */
};

void derived1_dance(struct derived1 *d)
{
    /* implementation of derived1's dance function */
}

void derived1_jump(struct derived1 *d, int how_high)
{
    /* implementation of derived 1's jump function */
}

/* global vtable for derived1 */
struct base_vtable derived1_vtable =
{
    &derived1_dance, /* you might get a warning here about incompatible pointer types */
    &derived1_jump   /* you can ignore it, or perform a cast to get rid of it */
};

void derived1_init(struct derived1 *d)
{
    d->super.vtable = &derived1_vtable;
    /* init base members d->super.foo */
    /* init derived1 members d->foo */
}

struct derived2
{
    struct base super;
    /* derived2 members */
};

void derived2_dance(struct derived2 *d)
{
    /* implementation of derived2's dance function */
}

void derived2_jump(struct derived2 *d, int how_high)
{
    /* implementation of derived2's jump function */
}

struct base_vtable derived2_vtable =
{
   &derived2_dance,
   &derived2_jump
};

void derived2_init(struct derived2 *d)
{
    d->super.vtable = &derived2_vtable;
    /* init base members d->super.foo */
    /* init derived1 members d->foo */
}

int main(void)
{
    /* OK!  We're done with our declarations, now we can finally do some
       polymorphism in C */
    struct derived1 d1;
    derived1_init(&d1);

    struct derived2 d2;
    derived2_init(&d2);

    struct base *b1_ptr = (struct base *)&d1;
    struct base *b2_ptr = (struct base *)&d2;

    base_dance(b1_ptr);  /* calls derived1_dance */
    base_dance(b2_ptr);  /* calls derived2_dance */

    base_jump(b1_ptr, 42);  /* calls derived1_jump */
    base_jump(b2_ptr, 42);  /* calls derived2_jump */

    return 0;
}

And that's how you do polymorphism in C. It ain't pretty, but it does the job. There are some sticky issues involving pointer casts between base and derived classes, which are safe as long as the base class is the first member of the derived class. Multiple inheritance is much harder - in that case, in order to case between base classes other than the first, you need to manually adjust your pointers based on the proper offsets, which is really tricky and error-prone.

Another (tricky) thing you can do is change the dynamic type of an object at runtime! You just reassign it a new vtable pointer. You can even selectively change some of the virtual functions while keeping others, creating new hybrid types. Just be careful to create a new vtable instead of modifying the global vtable, otherwise you'll accidentally affect all objects of a given type.

Adam Rosenfield
Adam, the fun of changing the global vtable of a type is to simulate duck-typing in C. :)
jmucchiello
+2  A: 

I used to do this kind of thing in C, before I knew what OOP was.

Following is an example, which implements a data-buffer which grows on demand, given a minimum size, increment and maximum size. This particular implementation was "element" based, which is to say it was designed to allow a list-like collection of any C type, not just a variable length byte-buffer.

The idea is that the object is instantiated using the xxx_crt() and deleted using xxx_dlt(). Each of the "member" methods takes a specifically typed pointer to operate on.

I implemented a linked list, cyclic buffer, and a number of other things in this manner.

I must confess, I have never given any thought on how to implement inheritance with this approach. I imagine that some blend of that offered by Kieveli might be a good path.

dtb.c:

#include <limits.h>
#include <string.h>
#include <stdlib.h>

static void dtb_xlt(void *dst, const void *src, vint len, const byte *tbl);

DTABUF *dtb_crt(vint minsiz,vint incsiz,vint maxsiz) {
    DTABUF          *dbp;

    if(!minsiz) { return NULL; }
    if(!incsiz)                  { incsiz=minsiz;        }
    if(!maxsiz || maxsiz<minsiz) { maxsiz=minsiz;        }
    if(minsiz+incsiz>maxsiz)     { incsiz=maxsiz-minsiz; }
    if((dbp=(DTABUF*)malloc(sizeof(*dbp))) == NULL) { return NULL; }
    memset(dbp,0,sizeof(*dbp));
    dbp->min=minsiz;
    dbp->inc=incsiz;
    dbp->max=maxsiz;
    dbp->siz=minsiz;
    dbp->cur=0;
    if((dbp->dta=(byte*)malloc((vuns)minsiz)) == NULL) { free(dbp); return NULL; }
    return dbp;
    }

DTABUF *dtb_dlt(DTABUF *dbp) {
    if(dbp) {
        free(dbp->dta);
        free(dbp);
        }
    return NULL;
    }

vint dtb_adddta(DTABUF *dbp,const byte *xlt256,const void *dtaptr,vint dtalen) {
    if(!dbp) { errno=EINVAL; return -1; }
    if(dtalen==-1) { dtalen=(vint)strlen((byte*)dtaptr); }
    if((dbp->cur + dtalen) > dbp->siz) {
        void        *newdta;
        vint        newsiz;

        if((dbp->siz+dbp->inc)>=(dbp->cur+dtalen)) { newsiz=dbp->siz+dbp->inc; }
        else                                       { newsiz=dbp->cur+dtalen;   }
        if(newsiz>dbp->max) { errno=ETRUNC; return -1; }
        if((newdta=realloc(dbp->dta,(vuns)newsiz))==NULL) { return -1; }
        dbp->dta=newdta; dbp->siz=newsiz;
        }
    if(dtalen) {
        if(xlt256) { dtb_xlt(((byte*)dbp->dta+dbp->cur),dtaptr,dtalen,xlt256); }
        else       { memcpy(((byte*)dbp->dta+dbp->cur),dtaptr,(vuns)dtalen);   }
        dbp->cur+=dtalen;
        }
    return 0;
    }

static void dtb_xlt(void *dst,const void *src,vint len,const byte *tbl) {
    byte            *sp,*dp;

    for(sp=(byte*)src,dp=(byte*)dst; len; len--,sp++,dp++) { *dp=tbl[*sp]; }
    }

vint dtb_addtxt(DTABUF *dbp,const byte *xlt256,const byte *format,...) {
    byte            textÝ501¨;
    va_list         ap;
    vint            len;

    va_start(ap,format); len=sprintf_len(format,ap)-1; va_end(ap);
    if(len<0 || len>=sizeof(text)) { sprintf_safe(text,sizeof(text),"STRTOOLNG: %s",format); len=(int)strlen(text); }
    else                           { va_start(ap,format); vsprintf(text,format,ap); va_end(ap);                     }
    return dtb_adddta(dbp,xlt256,text,len);
    }

vint dtb_rmvdta(DTABUF *dbp,vint len) {
    if(!dbp) { errno=EINVAL; return -1; }
    if(len > dbp->cur) { len=dbp->cur; }
    dbp->cur-=len;
    return 0;
    }

vint dtb_reset(DTABUF *dbp) {
    if(!dbp) { errno=EINVAL; return -1; }
    dbp->cur=0;
    if(dbp->siz > dbp->min) {
        byte *newdta;
        if((newdta=(byte*)realloc(dbp->dta,(vuns)dbp->min))==NULL) {
            free(dbp->dta); dbp->dta=null; dbp->siz=0;
            return -1;
            }
        dbp->dta=newdta; dbp->siz=dbp->min;
        }
    return 0;
    }

void *dtb_elmptr(DTABUF *dbp,vint elmidx,vint elmlen) {
    if(!elmlen || (elmidx*elmlen)>=dbp->cur) { return NULL; }
    return ((byte*)dbp->dta+(elmidx*elmlen));
    }

dtb.h

typedef _Packed struct {
    vint            min;                /* initial size                       */
    vint            inc;                /* increment size                     */
    vint            max;                /* maximum size                       */
    vint            siz;                /* current size                       */
    vint            cur;                /* current data length                */
    void            *dta;               /* data pointer                       */
    } DTABUF;

#define dtb_dtaptr(mDBP)                (mDBP->dta)
#define dtb_dtalen(mDBP)                (mDBP->cur)

DTABUF              *dtb_crt(vint minsiz,vint incsiz,vint maxsiz);
DTABUF              *dtb_dlt(DTABUF *dbp);
vint                dtb_adddta(DTABUF *dbp,const byte *xlt256,const void *dtaptr,vint dtalen);
vint                dtb_addtxt(DTABUF *dbp,const byte *xlt256,const byte *format,...);
vint                dtb_rmvdta(DTABUF *dbp,vint len);
vint                dtb_reset(DTABUF *dbp);
void                *dtb_elmptr(DTABUF *dbp,vint elmidx,vint elmlen);

PS: vint was simply a typedef of int - I used it to remind me that it's length was variable from platform to platform (for porting).

Software Monkey
holy moly, this could win an obfuscated C contest! i like it! :)
banister
+1  A: 

for me object orientation in C should have these features :

1) encapsulation and data hiding ( can be achieved using structs/opaque pointers)

2) inheritance and support for polymorphism ( single inheritance can be achieved using structs - make sure abstract base is not instantiable)

3) constructor and destructor functionality ( not easy to achieve)

4) type checking (at least for user defined types as C doesn't enforce any )

5) reference counting ( or some thing to implement RAAI)

6) limited support for exception handling (setjmp and longjmp )

on top of above it should rely on ANSI/ISO specifications and should not rely on compiler specific functionality.

FL4SOF
For number (5) - You can't implement RAII in a language without destructors (which means RAII is not a compiler-supported technique in C or Java).
Tom
constructors and destructors can be written for c based object - i guess GObject does it. and ofcourse RAAI ( it is not straight forward, may be ugly and need not be pragmatic at all) - all i was looking is to identify C based semantics to acheive the above.
FL4SOF
C doesn't support destructors. You have to type *something* in order to make them work. That means they don't clean up themselves. GObject doesn't change the language.
Tom
+4  A: 

C Object System (COS) sounds promising (it's still in alpha version). It tries to keep minimal the available concepts for the sake of simplicity and flexibility: uniform object oriented programming including open classes, metaclasses, property metaclasses, generics, multimethods, delegation, ownership, exceptions, contracts and closures. There is a draft paper (PDF) that describes it.

Exception in C is a C89 implementation of the TRY-CATCH-FINALLY found in other OO languages. It comes with a testsuite and some examples.

Both by Laurent Deniau, which is working a lot on OOP in C.

philippe
Very interesting!
Anthony Cuozzo
+1  A: 

If I were going to write OOP in C I would probably go with a pseudo-PIMPL design. Instead of passing pointers to structs, you end up passing pointers to pointers to structs. This makes the content opaque and facilitates polymorphism and inheritance. The real problem with OOP in C is what happens when variables exit scope. There's no compiler generated destructors and that can cause issues. MACROS can possibly help but it is always going to be ugly to look at.

jmucchiello
+2  A: 

Slightly off topic but the original C++ compiler, c-front, compiled C++ to C and then to assembler.
Preserved here

zebrabox
I've actually seen it before. I believe it was a nice piece of work.
Anthony Cuozzo
@Anthony Cuozzo : Stan Lippman wrote a great book called 'C++ - Inside the object model' where he related a lot of his experiences and design decisions in writing and maintaining c-front. It's still a good read and helped me immensely when transitioning from C to C++ many years back
zebrabox