I have two pieces of code: The first, inside a C++ program, is where I load and call a function from an external test_lib.so
:
typedef void *(*init_t)(); // init_t is ptr to fcn returning a void*
typedef void (*work_t)(void *); // work_t is ptr to fcn taking a void*
void *lib = dlopen("test_lib.so", RTLD_NOW);
init_t init_fcn = dlsym(lib, "test_fcn");
work_t work_fcn = dlsym(lib, "work_fcn");
void *data = init_fcn();
work_fcn(data);
The second piece of code is the one that compiles to test_lib.so
:
struct Data {
// ...
};
extern "C" {
void *init_fcn() {
Data *data = new Data; // generate a new Data*...
return data; // ...and return it as void*
}
void work_fcn(void *data) { // take a void*...
static_cast<Data *>(data)->blabla(); // ...and treat it as Data*
static_cast<Data *>(data)->bleble();
}
}
Now, the first piece of code doesn't need to know what Data
is, it just passes the pointer around, so it's a void*
. But the library, which works directly with data
's methods and members, needs to know, so it must convert the void*
s to Data*
s.
But the interface between the two pieces of code is just some functions with pointer arguments and/or return types. I could just keep the void*
in the client, and change every instance of void*
in the library to Data*
. I did that, and everything works fine (my system is Linux/GCC 6.2.1).
My question is: was I lucky, or is this guaranteed to work everywhere? If I'm not mistaken, the result of calling some f(Data*)
with a void*
argument is just as if called reinterpret_cast<Data*>
on the void*
--- and that couldn't possibly be dangerous. Right?
EDIT: No, simply making the Data
type transparent to the client code won't work. The client code calls many libraries through the same API, but each library might have its own implementation. For the client, Data
could be anything.
While this is likely to work in practice, C doesn't guarantee this behavior.
There are two problems:
Different pointer types can have different sizes and representations. On such an implementation going to void *
and back involves an actual conversion at runtime, not just a cast to make the compiler happy. See http://c-faq.com/null/machexamp.html for a list of examples, e.g. "The old HP 3000 series uses a different addressing scheme for byte addresses than for word addresses; like several of the machines above it therefore uses different representations for char *
and void *
pointers than for other pointers."
Different pointer types can use different calling conventions. For example, an implementation might pass void *
on the stack but other pointers in registers. C doesn't define an ABI, so this is legal.
That said, you're using dlsym
, which is a POSIX function. I don't know if POSIX imposes additional requirements that make this code portable (to all POSIX systems).
On the other hand, why don't you use Data *
everywhere? On the client side you can just do
struct Data;
to leave the type opaque. This fulfills your original requirements (the client can't mess with the internals of Data
because it doesn't know what it is, it can only pass pointers around), but also makes the interface a bit safer: You can't accidentally pass the wrong pointer type to it, which would be silently accepted by something taking void *
.