2

I have an extern "C" interface to some dynamically loaded code. For that code some objects are opaque handles, expressed as void*. The interface looks like this:

extern "C" {
  void* get_foo() { Foo* foo =  /* create and remember object */; return foo; }
  int foo_get_bar(void* Foo) { return ((Foo*)foo)->bar(); }
}

Such pattern are pretty much the standard way to interact with C++ objects across a C-API, AFAIK. But I wonder if I can omit that pointer cast?

The caller of the code is generated and only linked against the interface above (it has its own function declarations). So it effectively generates code like this:

void foo_get_bar(void*);
void* get_foo();

int do_something() { return foo_get_bar(get_foo()) };

We can assume that the caller uses the code correctly (i.e., it does not pass wrong pointers). But of course it has no notion of a Foo pointer.

Now could I change the interface to the (simpler) variant:

extern "C" {
  int foo_get_bar(Foo* Foo) { return foo->bar(); }
}

Or is there a subtle difference between void* and Foo* at the linker level (could e.g., the sizes not match up?).

edit: I might have caused some confusion with the caller code in C. There is no C code. There is no C compiler and thus no C type checker involved. The caller code is generated and it uses the C-API. So to use an opaque struct I would need to know how the C-API represents that pointer after compilation.

choeger
  • 3,562
  • 20
  • 33
  • No; you can't omit the cast in `int foo_get_bar(void* Foo) { return ((Foo*)foo)->bar(); }`. This is one of the reasons you should avoid using `void *`, even in C, for opaque types. You'd be much better off with `typedef struct Foo Foo;` (without necessarily exposing the details of what's in a `Foo`) and then using `Foo *` (or `struct Foo *`) in the interfaces. – Jonathan Leffler Jul 06 '18 at 07:18
  • That might be a good idea for a C-API for humans, but the caller code is generated (and not C). So your comment just rephrases the question to: What is the difference between a pointer to your opaque struct and a void pointer in C's linker model? – choeger Jul 06 '18 at 07:21
  • The advantage of the opaque types is type safety. You can pass any pointer of any type to a function that takes a `void *` argument. You have to get the pointer type correct with the opaque types. – Jonathan Leffler Jul 06 '18 at 07:25
  • Type safety is not a concern. There is no C compiler involved. – choeger Jul 06 '18 at 07:48
  • Is `Foo` a (C++) class? But this is safe enough (although you can perhaps do better) on any sane platform. – Paul Sanders Jul 06 '18 at 09:22

2 Answers2

2

I found the question a little confusing, but if I understand you correctly this is fine. The size of a pointer to anything is the same on any modern platform (we have, thankfully, left behind us the insanity of __near, __far, and __middling).

So I might introduce some notion of type-safety like this in the header file declaring get_foo():

// Foo.h
struct Foo;

extern "C"
{
    Foo* get_foo ();
    int foo_get_bar (Foo* Foo);
}

And in the implementation of class Foo, which is presumably C++, we might have:

// foo.cpp
struct Foo
{
    int get_bar () { return 42; }
};

Foo* get_foo () { return new Foo (); }
int foo_get_bar (Foo *foo) { return foo->get_bar (); }

Sneaky, but legal. You just have to declare Foo as a struct rather than a class in foo.cpp, which means that if you want things private or protected you have to explicitly say so.

If that's not palatable, then you can do this instead in foo.h:

#ifdef __cplusplus
    class Foo;
#else
    struct Foo;
#endif

And then declare Foo as a class rather than a struct in foo.c. Choose your poison.

Live demo.

Paul Sanders
  • 24,133
  • 4
  • 26
  • 48
1

In practice the pointer sizes/representations shouldn't change on common platforms x86 and ARM, so you may get away with using void *.


In principle however, the standard doesn't guarantee that 2 pointers of different referenced types will have the same pointer representation and even width, except for the cases mentioned in C11 6.2.5p28:

  • that a pointer to void will have the same representation and alignment requirement as a pointer to a character type
  • pointers to compatible types will have the same representation and alignment requirements despite the alignment
  • all pointers to structures will have the same representation and alignment requirements among themselves
  • all pointers to unions will have the same representation and alignment requirements among themselves

Therefore the cleanest way to represent a pointer to a C++ class in C is to use a pointer to an incomplete struct:

typedef struct Foo Foo;
void foo_get_bar(Foo *);
Foo *get_foo(void);

Another advantage is that the compiler will complain if you try to use a Bar * by mistake.


P.S.: do note that get_foo() does not mean the same as get_foo(void) in C.