0

I want to handover a class member as a callback function in C-style to an external DLL.

What I got so far:

void MyCallback(double a, unsigned char b, unsigned char* c)
{
 // do something
}

The DLL provides a definition:

extern "C" BOOL CALLBACK ExternalDLL_SetCallback(void(*pCallbackFunc)(double a, unsigned char b, unsigned char* c));
ExternalDLL_SetCallback(&MyCallback); // this works

Because I need access to class members of MyClass inside the callback function, I want to have MyCallback become a class member.

I tried to use std::bind() to capture this and pass it over with the callback:

using namespace std::placeholders;
typedef void (*cbf_t)(double, unsigned char, unsigned char*);
class MyClass {
public:
 void MyCallback(double a, unsigned char b, unsigned char* c) { this->doClassStuff(a); }; // callback function as a class member
 void doClassStuff(double a); // example function
 void setCallback() {
 m_cbfunc = std::bind(&MyClass::MyCallback, this, _1, _2, _3);
 ExternalDLL_SetCallback((cbf_t)&m_cbfunc);
 };
 std::function<void(double,unsigned char,unsigned char)> m_cbfunc;
}

This actually compiles and runs, but the program terminates as soon as a callback is actually called. Unfortunately, the program can only be executed on a remote machine where I have no options to use the debugger.

I also tried to use a lambda function to do the capture magic:

m_cbfunc = [this](double a, unsigned char b, unsigned char* c) { this->doClassStuff(a); }; // Lambda callback function

Most likely, in either case the C-style conversion (cbf_t) is the source of evil here. static_cast<cbf_t>(std::bind(...)) or static_cast<cbf_t>(std::function(...)) are marked by the compiler, so probably it doesn't even make sense to cast the result to a function pointer.

Is there a simple way to get around this pitfall without descending too far into assembler-style implementation?

In practice, I will be having a bunch of MyClass objects setting their callbacks and being called occasionally from the external DLL. If the callback is a member function, each call is automatically assigned to the right MyClass object.

But, as I can only get a static callback function so far, I don't see a good way to keep track of which object had registered the callback when it's coming in.

Can you give me advice on how this could be implemented in an easy way?

Update: I understood that lambdas with capture cannot be casted into a function pointer. It works only for plain lambdas. In my case: cbf_t func = +[](double a, unsigned char b, unsigned char* c) { std::cout << a << b << c; }; where the "+" replaces an explicit conversion to function pointer type. Now, I got the idea to create a map<std::function<void(double,unsigned char, unsigned char*)>, MyClass*> where I map generically created lambdas to my MyClass objects and than have the lambdas delegate the call to their respective MyClass object. But again, there is a show-stopper. I can easily get the address of the lambda of course when I create it, but inside the lambda body, I seem to have no chance to get it's actual address to compare it to the keys of my map. The only ways to get the address of the lambda are by introducing a [&fn] capture or adding a (this auto self, ...) function parameter to the lambda (only C++23) or using the y_combinator (which essentially does the same). But in either way, I'll be unable to pass them to my API DLL. Of course I could define a bunch of Callback functions like this

#include <vector>
#include <map>
#include <iostream>
typedef void (*cbf_t) (double, unsigned char, unsigned char*);
class MyClass
{
public:
 MyClass(unsigned int x) { m_x = x; };
 void doClassStuff(double a) { std::cout << a << "from MyClass obj" << m_x; };
 void setCallback(cbf_t &func) { /*ExternalDLL_SetCallback(func);*/ };
 inline static std::map<cbf_t, MyClass*> myclass_map;
protected:
 unsigned int m_x;
};
void MyCallback0(double a, unsigned char b, unsigned char *c) { MyClass::myclass_map[&MyCallback0]->doClassStuff(a); };
void MyCallback1(double a, unsigned char b, unsigned char *c) { MyClass::myclass_map[&MyCallback1]->doClassStuff(a); };
void MyCallback2(double a, unsigned char b, unsigned char *c) { MyClass::myclass_map[&MyCallback2]->doClassStuff(a); };
std::vector<cbf_t> cbs;
int main()
{
 cbs.push_back(&MyCallback0);
 cbs.push_back(&MyCallback1);
 cbs.push_back(&MyCallback2);
 
 for (auto& el : cbs)
 {
 MyClass *mc = new MyClass(std::distance(&cbs.front(), &el));
 mc->setCallback(el);
 MyClass::myclass_map[el] = mc;
 }
}

But in the end, I have to create the callback functions in a dynamic way at runtime and I haven't found a good approach how to achieve that without running into the restrictions that lambda functions have. But obviously, there must be an easy way.

asked Jan 23, 2025 at 11:18
9
  • 5
    It can't be done without changing the signature of ExternalDLL_SetCallback. Commented Jan 23, 2025 at 11:24
  • 4
    Anything with a state is not a C-style function pointer. Commented Jan 23, 2025 at 11:54
  • 1
    Only a few things will work, free functions (static member functions), or lambda's without captures (so no capturing of this). Commented Jan 23, 2025 at 12:14
  • 1
    If that's the real definition of cbf_t, the library seems to not be very mature. Sooner or later they will add a void* argument to the API which users can use to supply user defined data that will be included in the callback. Commented Jan 23, 2025 at 12:28
  • 1
    Fyi, never use C style casts in c++ , use static_cast and if the compiler refuses the cast then it is likely illegal, and will result in a crash when used Commented Jan 23, 2025 at 14:47

3 Answers 3

4
std::function<void(double,unsigned char,unsigned char)> m_cbfunc;
//...
ExternalDLL_SetCallback((cbf_t)&m_cbfunc);

This actually compiles and runs, but the program terminates as soon as a callback is actually called.

Not surprising. You've passed to ExternalDLL_SetCallback an address that is not an address of an object of cbf_t type. Anything can happen with code like this, crash is likely to happen.

Because I need access to class members of MyClass inside the callback function, I want to have MyCallback become a class member.

No you don't. An argument to ExternalDLL_SetCallback must be of the declared type which is not a member function type. So what do you really need to do? Depends on your specific use case.

If all calls to the callback in your program are supposed to operate on a single MyClass object, you can store a global struct and simply use it:

struct CallbackContext {
 MyClass* classObject = nullptr;
} g_theContext;
void MyCallback(double a, unsigned char b, unsigned char* c)
{
 g_theContext.classObject->doClassStuff(a);
 //...
}
//...
// set callback
assert(g_theContext.classObject == nullptr); //once
g_theContext.classObject = this;
ExternalDLL_SetCallback(&MyCallback);

If the library supports multiple callbacks and you want to register a number of callbacks with different MyClass objects that, too, can be arranged:

struct CallbackContext {
 std::list<MyClass*> processors;
} g_theContext;
void MyCallback(double a, unsigned char b, unsigned char* c)
{
 for(MyClass* obj : processors){
 obj->doClassStuff(a);
 //...
 }
}
//...
// set callback
if(g_theContext.processors.empty()){
 ExternalDLL_SetCallback(&MyCallback); //once
}
g_theContext.processors.push_back(this);

Since the library itself is a global object, you're essentially enhancing it with MyClass* storage for this specific callback, but do it on your side of the code.

(Notice that this is a single-thread code; if callbacks can be registered and|or called from multiple threads, you'll need thread safety measures for the shared object g_theContext.)

I will be having a bunch of MyClass objects setting their callbacks and being called occasionally from the external DLL. If the callback is a member function, each call is automatically assigned to the right MyClass object.

This may be requesting more than the library is willing to give, unfortunately. Given the interface, the library clearly has one callback or callback list and isn't supposed to differentiate whether to call this particular callback on that particular event.

If you want to add this functionality, the main question you need to ask yourself is, "what exactly differentiates one callback-triggering event from another"? For example, if an event can be identified by the c callback argument, you can add the corresponding logic to the CallbackContext:

struct CallbackContext {
 std::map<std::string, MyClass*> processors;
} g_theContext;
void MyCallback(double a, unsigned char b, unsigned char* c)
{
 if(auto p = g_theContext.processors.find(c); p != g_theContext.processors.end()){
 p->doClassStuff(a);
 }
}

If, instead, you can foresee which class you want to process the callback just as you initiate the event and the program is single-threaded, you can add that logic, too: have one pointer in g_theContext and set it to the proper value before calling the DLL.

But in the worst case, the problem is unsolveable. Suppose it's some kind of a network-interaction DLL. Suppose three different instances of your class "registered", through whatever means, three callbacks. Suppose three events have now happened. Their a, b and c values have no correlation with which object "wants" to process them. They all happen in some previously-unseen DLL-generated threads. They all are called from the same address of the DLL code. In that case, how exactly can the code decide which object event#2 "belongs" to?

answered Jan 23, 2025 at 11:54
Sign up to request clarification or add additional context in comments.

3 Comments

Thanks Abstraction, for your fruitful answer. You brought things some steps further and helped me understand the problem (hopefully) a lot better, yet. Still, I have the problem that the parameters a, b, c don't allow me to unambiguously decide which MyClass object has registered the callback with the API and should handle the call.
Moreover, you're right that in the (simplified) case it's not clear how the API decides which event will be delegated to the callbacks. In fact, there is more than one ExternalDLL_SetCallback(...). There rather are SetCallbackA, SetCallbackB and even a SetCallbackForEventType(EventType x, ...). However, the calling parameters of the cbf don't allow to decide, exactly what triggered the callback.
@rhb In that case, you can create MyCallbackA, MyCallbackB, MyCallbackC and so on (their amount is determined by the library header and thus finite). Each of those will check a different context on call (or a different part of the same context) and thus can potentially call doClassStuff for a different MyClass object. You can save yourself some typing with templates (template<int N> void callback(){g_ctx[N]->doClassStuff()}).
0

std::bind() and capturing lambdas are not compatible with C-style function pointers, so they will not work for this task.

Your callback requires a MyClass* object pointer in order to access the members of MyClass, so you need a way to pass your object's this pointer into MyCallback().

The signature of ExternalDLL_SetCallback() that you have shown does not allow for this. However, if the DLL provides some other way to pass a user-defined value into MyCallback(), then you can pass your object's this pointer that way, eg:

class MyClass {
public:
 static void MyCallback(double a, unsigned char b, unsigned char* c) {
 MyClass *pThis = (MyClass*) ExternalDLL_GetCallbackData();
 pThis->doClassStuff(a);
 };
 void doClassStuff(double a); // example function
 void setCallback() {
 ExternalDLL_SetCallback(&MyClass::MyCallback);
 ExternalDLL_SetCallbackData(this);
 };
};

But, if that is not the case, then you are stuck using either global/thread-local variables or manually-crafted thunks to pass the required MyClass* pointer around.

answered Jan 23, 2025 at 17:11

2 Comments

Unfortunately, it's a machine specific DLL, which is very unlikely to get a major change in the way, that it provides such a possibility to get a UserData handling. The only thing I could change about it, is the header file of the DLL interface. But I can't really think of a way to make use of this, because the callback calls are of course invoked inside the compiled DLL part
Can you share the DLL's header file. Or do you have any documentation for this DLL that you can share?
0

The "correct" answer to this sort of problem is to change the callback interface so that some (usually pointer-sized) user-provided parameter is also passed to the callback. That pattern allows associating user state with the callback.

If that is not possible, the next alternative is to use something like libffi closures to generate the callback at run-time. You create a closure that associates your needed data with the call, then provide that closure address as the callback. When the library invokes the closure, you unpack the bundled data and invoke the "real" call on the object.

answered Jan 23, 2025 at 20:34

Comments

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.