17

I use C++ on ESP-32. When registering a timer I have to do this:

timer_args.callback = reinterpret_cast<esp_timer_cb_t>(&SoundMixer::soundCallback);
timer_args.arg = this;

Here the timer calls soundCallback.

And the same thing when registering a task:

xTaskCreate(reinterpret_cast<TaskFunction_t>(&SoundProviderTask::taskProviderCode), "SProvTask", stackSize, this, 10, &taskHandle);

So the method is started in a separated task.

GCC always warns me about these conversions, but it works just as planned.

Is it acceptable in production code? Is there a better way to do this?

2 Answers 2

47

A reinterpret_cast is always fishy unless you know exactly what you are doing. Here, your code happens to work only due to GCC's calling convention for C++ methods, but this smells heavily like undefined behaviour. In particular you should not assume that member functions are in any way compatible with normal function pointers.

The usual approach would be to instead define a C-compatible function with the appropriate signature, which internally calls the C++ method. For example:

extern "C" static void my_timer_callback(void* arg) {
  static_cast<SoundMixer*>(arg)->soundCallback();
}

This cast is fine because we are casting back from a void* to the type of the pointed-to object.

Details:

  • extern "C" specifies the language linkage of this function. Language linkage affects name mangling and the calling convention of the function. Member functions cannot have C language linkage. Language linkage is largely orthogonal to internal/external linkage.

  • For a callback the function may be “private”, i.e. have internal linkage. The C code never refers to the callback by name. The above code snippet specifies internal linkage through the static keyword (not a static method!). Alternatively, the function could have been placed into an anonymous namespace.

    I am not entirely sure about the interactions between extern "C" and static (internal linkage). E.g. [dcl.link] says that “All function types, function names with external linkage, and variable names with external linkage have a language linkage.” I interpret this so that the type of my_timer_callback has C language linkage, but that its function name does not.

  • A static_cast is appropriate here because we know the real type of the arg but cannot express it within the type system. In contrast a reinterpret_cast is appropriate when we want to reinterpret a bit pattern, e.g. a pointer to a numeric type.

  • Functions are not ordinary objects, and member functions even less so. You may reinterpret-cast between function pointer types as long as the function is only invoked through its real type (and analogously for member function pointers). Whether you can cast function pointers to other types (e.g. object pointers or void pointers) is implementation-defined (background). On POSIX casts between function pointers and void* are allowed so that dlsym() can work. Other casts involving (member) function pointers are undefined. In particular, casts between member functions and function pointers are not possible.

17
  • 1
    Doesn't std::bind also assume object pointer as first method argument? Aug 12, 2018 at 11:20
  • 5
    @val Yes, but that doesn't mean that member functions are compatible with ordinary functions, merely that bind() uses the INVOKE algorithm which handles member functions as a separate case from ordinary function objects incl. function pointers. Because std::bind() creates a functor it is not suitable for interfacing with C.
    – amon
    Aug 12, 2018 at 11:32
  • 1
    Another question: why do I need extern "C" here? Is C linkage important in this case? Aug 12, 2018 at 11:58
  • 5
    @val If you want to be able to call that function from C, it must use the C calling convention. This can be done by declaring that function with C language linkage, or by compiler-specific extensions (like __attribute__((cdecl)), but please don't do that). A C++ function is not guaranteed to have a C-compatible calling convention otherwise (though in GCC it typically works out fine).
    – amon
    Aug 12, 2018 at 12:19
  • 4
    @val For the details on why extern "C" is formally necessary, see [dcl.link] "Two function types with different language linkages are distinct types even if they are otherwise identical." and [expr.call] "Calling a function through an expression whose function type is different from the function type of the called function’s definition results in undefined behavior"
    – Ben Voigt
    Aug 12, 2018 at 17:30
-1

Personally, the most compatible, easy to implement and easy to understand approach that I've found is to just provide a "wrapper" function, compatible with the expected C interface, which internally calls the method (and, in case it's not static, instantiate or use an existing instance to do that). It could be seen as a sort of a variation of the Adapter Design Pattern.

3
  • 6
    Isn't that what amon answered?
    – Dronz
    Aug 12, 2018 at 21:31
  • 1
    @Dronz after a second read, yes, it's mostly that. As soon as I read static I saw it as a method and for some reason didn't realize it doesn't pass the this pointer as the first argument (and the following debate on the use of std::bind reinforced it). But yes, you're absolutely right! (Sorry for the double answer!) Aug 13, 2018 at 7:25
  • 3
    Yeah, static has at least three different, distinct meanings. And you'll mix them up if you are not careful. I would say, it's really helpful to understand the distinctions between the different uses of static, as each is a great tool in its own right. Aug 13, 2018 at 9:18

Not the answer you're looking for? Browse other questions tagged or ask your own question.