0

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.

fonini
  • 2,989
  • 3
  • 21
  • 39
  • why use c++ tag? – Kostas May 04 '17 at 05:02
  • 1
    @GillBates because I'm using C++ on both the client and the library. The c tag is because dlopen/dlsym use the C ABI – fonini May 04 '17 at 05:04
  • @fonini There is no such thing as `static_cast`, `operator new` in `C`. Adjust your tags. – PaulMcKenzie May 04 '17 at 05:08
  • @PaulMcKenzie In the first line of the body of my question, I mention that it's C++ – fonini May 04 '17 at 05:09
  • The `extern "C"` defines function name mangling behavior, allowing the function to be called from other source code (such as C, Java, whatever) - it shouldn't effect the rules of the source code in which you code (i.e., C++ in your case), so no... – Myst May 04 '17 at 05:11
  • 1
    @Myst "so no" is the answer to which question? I'm confused – fonini May 04 '17 at 05:12
  • C++ code calling `C` functions does not make it a `C` program or worthy of a `C` tag, especially if the `C++` is using `C++` features not present in the `C` language. – PaulMcKenzie May 04 '17 at 05:15
  • @PaulMcKenzie the question is about the C calling convention. If I had to choose between a C tag and a C++ tag, I'd go with the C tag. – fonini May 04 '17 at 05:16
  • Not when you introduce terms such as `reinterpret_cast` and `static_cast`. Those are `C++` features not known in `C`, and are germane to your question. Also, calling convention has nothing to do with `C` language. All the calling convention defines is how parameters are passed and returned on the stack, regardless of the language. – PaulMcKenzie May 04 '17 at 05:18
  • 1
    @PaulMcKenzie: I think you're being overly pedantic and protective of the C tag here. This question clearly involves the C ABI at its core, and is critical to the question itself. In fact, this question is more about the C ABI than it is about C++. – Cornstalks May 04 '17 at 05:20
  • 3
    @fonini: I don't quite see the need for a void pointer here. What is preventing you from opaquely forward declaring the structure Data on the client side? – doynax May 04 '17 at 05:38
  • No, the `extern` directive doesn't (shouldn't) effect the way the language behaves. The types themselves were defined as void pointers. C has nothing to do with your experience. – Myst May 04 '17 at 05:39
  • @doynax The client calls many libs with the same code, it can't know the `Data` types for all libs; I'll update the question saying this – fonini May 04 '17 at 05:46
  • 1
    The code you posted is fine. But then near the end you seem to say that your real question is about code you didn't post. It would be much better to post the code you are actually asking about. – M.M May 04 '17 at 05:48
  • 1
    @Myst: The `extern "C"` language linkage doesn't invoke the rules of C, exactly. But it does affect (and have an effect) on the behavior of C++. – Ben Voigt May 04 '17 at 05:53
  • "If I had to choose between a C tag and a C++ tag, I'd go with the C tag." You would be wrong then. The C tag is a language tag, it tags questions about the C language. It is not an ABI tag or a calling conventions tag or any such thing. The only language involved here is C++, so the only relevant language tag is C++. Please remove the C tag. – n. m. could be an AI May 04 '17 at 07:51
  • Ok, tag removed. – fonini May 04 '17 at 14:36

3 Answers3

3

Calling any function through the wrong function type is automatically undefined behavior. From C++ Standard draft n4604 (roughly C++17) [expr.reinterpret.cast]:

A function pointer can be explicitly converted to a function pointer of a different type. The effect of calling a function through a pointer to a function type that is not the same as the type used in the definition of the function is undefined. Except that converting a prvalue of type "pointer to T1" to the type "pointer to T2" (where T1 and T2 are function types) and back to its original type yields the original pointer value, the result of such a pointer conversion is unspecified.

Calling any function through a function pointer type with the wrong linkage is also undefined behavior. Your typedefs don't use "C" linkage, ergo UB. From draft n4604 section [expr.call]:

Calling a function through an expression whose function type has a language linkage that is different from the language linkage of the function type of the called function’s definition is undefined.

Besides that point, different pointer types are not required to have the same representation. (cv-qualified) void* can hold any object pointer, but its alignment restrictions are the same as char* (that is, no restriction) and as a result, it's not necessarily representation compatible with other object pointer types and may not even be the same size. (And most definitely, object pointers, function pointers, and the variations on pointer-to-member are frequently different sizes on real-world systems.)

Community
  • 1
  • 1
Ben Voigt
  • 277,958
  • 43
  • 419
  • 720
  • The typedefs are inside a class: `class A { typedef void *(*init_t)(); typedef void (*work_t)(void *); /* ... */ }` Do I really need to `extern "C"` the typedefs? – fonini May 04 '17 at 06:17
  • 1
    @fonini: To be portable, they can't be inside a class, they have to use a linkage specification when forming the function type, and linkage specifications are only allowed at namespace scope. After defining the typedef at namespace scope, you can then give it a name in member scope of the class using a second typedef. To avoid undefined behavior you really do need to `extern "C"` the typedefs, yes. – Ben Voigt May 04 '17 at 06:22
2

While this is likely to work in practice, C doesn't guarantee this behavior.

There are two problems:

  1. 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."

  2. 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 *.

melpomene
  • 84,125
  • 8
  • 85
  • 148
  • Even if different pointer types follow different conventions for passing around, it would be fine since both the caller and the callee have the type defined as `void*`. The cast is done later. – Ajay Brahmakshatriya May 04 '17 at 05:41
  • @AjayBrahmakshatriya No, the question is "I could just keep the `void*` in the client, and change every instance of `void*` in the library to `Data*`. ... was I lucky, or is this guaranteed to work everywhere?" – melpomene May 04 '17 at 05:44
  • Your issues 1 and 2 don't impact this code; whatever effect those conversions might have is contained within the `init_fcn` and `work_fcn` functions – M.M May 04 '17 at 05:46
  • @Melpomene Ah, I read the last part of the question wrong. I will remove my answer. – Ajay Brahmakshatriya May 04 '17 at 05:46
  • @melpomene If, in the client, I just declare `struct Data;` and change all instances of `void*` to `Data*`, it will be fine? I don't see why this new `Data*` would be different from the old `void*`. Aren't they all just "pointers to unknown type"? – fonini May 04 '17 at 06:00
  • 3
    @fonini Yes, in that they're all pointers to an incomplete type. No, because different incomplete types are still different. In particular, it may help to think of `void *` as a common pointer serialization format. Converting from some `T *` to `void *` serializes it, and casting to `T *` deserializes it back. (And different T's can use different serialization formats.) – melpomene May 04 '17 at 06:04
  • Just to be clear: I can pass a pointer to an incomplete type to a function that's execting a pointer to a specific type, but I don't need the actual type names to be equal, right? (the caller and called are in different executables) – fonini May 04 '17 at 06:30
  • @fonini If they're both structs, yes (if I recall correctly). I think the standard requires all pointer-to-struct types to have the same size/representation. – melpomene May 04 '17 at 06:31
  • Now I'm surprised. The name of the class is hard-coded in the executable? One function will never see the source files of the other. – fonini May 04 '17 at 06:36
  • 1
    @fonini Why are you surprised? You asked me to confirm something and I did. – melpomene May 04 '17 at 06:39
  • 1
    @fonini If different parts of your code have different definitions of `struct Data`, that's a violation of the One Definition Rule and constitutes undefined behaviour. – n. m. could be an AI May 04 '17 at 07:54
0

You can make it cleaner by using opaque structure definitions. See the second half of the accepted answer here:

Why should we typedef a struct so often in C?

Thus the caller is handling pointers to a defined type, but cannot see inside what is being pointed at. The implementation has the actual struct definition, and can work with it. No more casting is required.

Community
  • 1
  • 1
bazza
  • 7,580
  • 15
  • 22