4

So I've read Developing C wrapper API for Object-Oriented C++ code and I like the approach, which I have taken with my library - opaque handles for each corresponding C++ class; avoiding using void*

But now, I'm faced with thinking about 'interfaces', and base classes. For example, I have a class hierarchy of "channel" classes - a base class for a "channel" and derived concrete classes for, for example, serial comms, in-memory buffers, sockets, etc.

So I have:

typedef struct serial_channel serial_channel;
typedef struct socket_channel socket_channel;
typedef struct memory_channel memory_channel;

serial_channel* create_serial_channel();
socket_channel* create_socket_channel();
memory_channel* create_memory_channel();

But I want to be able to pass any one of those into a function to associate it with a 'device' object:

void associate_device_with_channel(device*, channel*);

Easy in C++, since it understands base classes. How do I approach this in the C wrapper library - what type is channel in C?

The only thing I can think of is that I must resort to void* to represent a base class?

typedef void* channel;
void associate_device_with_channel(device*, channel*);

It works, but would let me pass any pointer?

On the other extreme, I can write a set of functions matching the derived channel classes:

void associate_device_with_serial_channel(device*, serial_channel*);
void associate_device_with_socket_channel(device*, socket_channel*);
void associate_device_with_memory_channel(device*, memory_channel*);

It's very verbose, and if I have to add new channel types, I have to add new functions to the interface as well.

Is there some kind of middle ground I've been missing? - like a single function, but not void*?

Community
  • 1
  • 1
Steve Folly
  • 7,437
  • 8
  • 44
  • 56
  • 1
    Instead of multiple channel types, use a single type. Include a type code if and only if you need to provide dedicated functions that are only applicable to specific types of channel. In that case the type checking has to be enforced at runtime. – David Heffernan Mar 04 '14 at 12:00
  • 1
    you can achieve inheritance effect in C when you include your base class (struct) as first element of inherited class (struct), taking advantage of the fact that pointer to a structure object, points to its initial member. Check this answer for an example http://stackoverflow.com/a/415536/2549281 – Dabo Mar 04 '14 at 12:24
  • 2
    @Dabo He isn't trying to do inheritance in C. He's trying to expose his C++ inheritance structure through opaque C handles. – Sneftel Mar 04 '14 at 14:13

3 Answers3

3

There isn't any perfect approach. You're trying to make your function take some opaque handles (the ones with the appropriate base class) but not any handle type (which void* would accept), and there just isn't a thing in C for that.

If you like, you can provide a function which takes serial_channel* and returns channel*, and another one for each other channel subclass. This gets you away from unsafe C casting, and doesn't require numfuncs*numderivedclasses different channel-taking functions.

Personally, I'd just void* it. They're using C, after all... clearly they don't care too much about their language keeping them safe.

Sneftel
  • 34,359
  • 11
  • 60
  • 94
2

First, I would set up my structures something like this:

typedef void base_class;
struct base_class_impl
{
    // base class member variables go here
}
struct derived_class
{
    // base class must come first in the derived struct
    struct base_class_impl base;
    // derived class member variables go here
}

Then, I would take pointers to base_class as arguments to my functions:

int base_class_get_count(base_class *b);

and I would always cast at the start of the function:

int base_class_get_count(base_class *b)
{
    struct base_class *base = (struct base_class *)b;
    // Operate on the object now
}

This enables base_class_get_count() to work even on objects of the derived type. The downside is that it doesn't allow the derived type to override a method - you would have to go a step further, implementing your own table of function pointers which the API calls (like base_class_get_count) dispatch out to, based on the entry in the table.

Chris McGrath
  • 1,876
  • 16
  • 16
  • Thanks Chris. Dispatching isn't a problem, since it's all C++ behind the scenes. Ultimately, your base class is still void*. I guess it's useful to show the intent and create a typedef representing the base class, even if it is void*. – Steve Folly Mar 04 '14 at 23:21
  • 1
    You're right, it's still a void*. However, if you do want a base class method to accept an object of type derived, I believe it's the only way in C. Otherwise, you'd have to have one "god" struct which contained all the member variables for all of the derived classes (if they contain additional member variables) and pass in that type - which somewhat defeats the purpose of OO to begin with. The void* approach then leaves you open to situations where you accidentally pass in the wrong object - the compiler won't help you there. I think it's an acceptable tradeoff. – Chris McGrath Mar 04 '14 at 23:29
  • 1
    Yes, to quote Sneftel in another answer - "They're using C, after all... clearly they don't care too much about their language keeping them safe." :-) – Steve Folly Mar 05 '14 at 16:56
1

If you only target GCC or Clang (I suspect that you wouldn't bother with C if you were targeting Visual Studio), one of your options is to create a union with the non-standard __transparent_union__ attribute to list the types that a function can accept. A function accepting a union parameter with the __transparent_union__ attribute will accept either that union, or any type contained in it.

union associable_channel
{
    channel* a;
    serial_channel* b;
    socket_channel* c;
    memory_channel* d; 
} __attribute__((__transparent_union__));

void associate_device_with_channel(union associable_channel chan);

serial_channel* serial;
socket_channel* socket;
memory_channel* mem;
associate_device_with_channel(serial);
associate_device_with_channel(socket);
associate_device_with_channel(mem);
zneak
  • 124,558
  • 39
  • 238
  • 307
  • 1
    That's a cool feature, but I'd be worried here. `__transparent_union__` is not going to perform any pointer adjustment when putting in a `serial_channel` and taking out a `channel`. So in the presence of multiple inheritance, this could silently screw things up. – Sneftel Mar 04 '14 at 14:27
  • Interesting feature, is that conceptually the same as having one non-explicit union constructor for each member? – Trillian Mar 04 '14 at 15:01
  • It only works with function parameters. You couldn't do `union associable_channel foo = serial`, for instance. (Well, we're talking about a non-standard feature, so clearly a vendor could make that work, but this behavior is not specified with GCC or Clang.) – zneak Mar 04 '14 at 15:39
  • As for multiple inheritance, the concern is real, but if OP can afford to C-cast pointers to `void*`, then transparent unions can be a replacement that's exactly as safe (read: that will silently break in the same situation) while keeping some additional type expressiveness. I personally never use multiple inheritance except for interfaces without fields and I know that I'm not alone working like this. – zneak Mar 04 '14 at 20:03
  • Unfortunately I'm using Visual Studio, so that won't work for me. But it's very useful to know. I wasn't aware of the multiple inheritance issue - I understand what you mean, but I'm not using MI so it won't be an issue for me. – Steve Folly Mar 05 '14 at 17:02