43

I'm trying to register a callback in a C-API that uses the standard function-pointer+context paradigm. Here's what the api looks like:

void register_callback(void(*callback)(void *), void * context);

What I'd really like to do is be able to register a C++ lambda as the callback. Additionally, I want the lambda to be one that has captured variables (ie. can't be converted to a straight stateless std::function)

What kind of adapter code would I need to write to be able to register a lambda as the callback?

Michael Bishop
  • 4,240
  • 3
  • 37
  • 41
  • 3
    while std::function is a type erasure mechanism capable of holding a lambda expression, it's not a lambda – ScarletAmaranth Dec 11 '13 at 17:31
  • 1
    That's fair. Did the dicrepancy prevent you from understanding the question? Is there there a way I can rewrite it? – Michael Bishop Dec 11 '13 at 17:36
  • it could be, I haven't given the question a further thought to be entirely honest with you, but you could rewrite saying you need to pull the function pointer out of std::function<> rather than out of a lambda – ScarletAmaranth Dec 11 '13 at 17:38
  • That sounds great. Is there a way to pull a context out of it? Some sort of state that I could pass as the context pointer to the C-api? – Michael Bishop Dec 11 '13 at 17:39

4 Answers4

22

The simple approach is to stick the lambda into a std::function<void()> which is kept somewhere. Potentially it is allocated on the heap and merely referenced by the void* registered with the entity taking the callback. The callback would then simply be a function like this:

extern "C" void invoke_function(void* ptr) {
    (*static_cast<std::function<void()>*>(ptr))();
}

Note that std::function<S> can hold function objects with state, e.g., lambda functions with a non-empty capture. You could register a callback like this:

register_callback(&invoke_function,
  new std::function<void()>([=](){ ... }));
Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
Dietmar Kühl
  • 150,225
  • 13
  • 225
  • 380
21

The most efficient way is to voidify the lambda directly.

#include <iostream>
#include <tuple>
#include <memory>

template<class...Args>
struct callback {
  void(*function)(void*, Args...)=nullptr;
  std::unique_ptr<void, void(*)(void*)> state;
};
template<typename... Args, typename Lambda>
callback<Args...> voidify( Lambda&& l ) {
  using Func = typename std::decay<Lambda>::type;
  std::unique_ptr<void, void(*)(void*)> data(
    new Func(std::forward<Lambda>(l)),
    +[](void* ptr){ delete (Func*)ptr; }
  );
  return {
    +[](void* v, Args... args)->void {
      Func* f = static_cast< Func* >(v);
      (*f)(std::forward<Args>(args)...);
    },
    std::move(data)
  };
}

void register_callback( void(*function)(void*), void * p ) {
  function(p); // to test
}
void test() {
  int x = 0;
  auto closure = [&]()->void { ++x; };
  auto voidified = voidify(closure);
  register_callback( voidified.function, voidified.state.get() );
  register_callback( voidified.function, voidified.state.get() );
  std::cout << x << "\n";
}
int main() {
  test();
}

here voidify takes a lambda and (optionally) a list of arguments, and generates a traditional C-style callback-void* pair. The void* is owned by a unique_ptr with a special deleter so its resources are properly cleaned up.

The advantage of this over a std::function solution is efficiency -- I eliminated one level of run-time indirection. The lifetime that the callback is valid is also clear, in that it is in the std::unique_ptr<void, void(*)(void*)> returned by voidify.

unique_ptr<T,D>s can be moved into shared_ptr<T> if you want a more complex lifetime.


The above mixes lifetime with data, and type erasure with utility. We can split it:

template<class Lambda, class...Args>
struct callback {
  void(*function)(void*, Args...)=nullptr;
  Lambda state;
};

template<typename... Args, typename Lambda>
callback<typename std::decay<Lambda>::type, Args...> voidify( Lambda&& l ) {
  using Func = typename std::decay<Lambda>::type;
  return {
    +[](void* v, Args... args)->void {
      Func* f = static_cast< Func* >(v);
      (*f)(std::forward<Args>(args)...);
    },
    std::forward<Lambda>(l)
  };
}

Now voidify does not allocate. Simply store your voidify for the lifetime of the callback, passing a pointer-to-second as your void* along side the first function pointer.

If you need to store this construct off the stack, converting the lambda to a std::function may help. Or use the first variant above.

void test() {
  int x = 0;
  auto closure = [&]()->void { ++x; };
  auto voidified = voidify(closure);
  register_callback( voidified.function, &voidified.state );
  register_callback( voidified.function, &voidified.state );
  std::cout << x << "\n";
}
Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
  • 9
    I'm not sure if I'm impressed or horrified by the word "voidified" – Caleth Jan 18 '18 at 09:27
  • Has anybody had success making `voidify` work for a callback signature that has multiple arguments? When I add arguments to the closure and arguments to the callback function signature, the type system always thinks the type of `voidified.first` is `void (*)(void*)` – Chris Cleeland Jan 30 '20 at 20:33
  • @ChrisCleeland You have to pass in `` explicitly, it cannot be deduced from the lambda. Is that your issue? – Yakk - Adam Nevraumont Jan 30 '20 at 20:34
  • Ahhh. So if the callback signature is `void(*)(int, long long)` then one needs to use `voidify(closure)` along with `auto closure = [=](int, long long) { ... }`. – Chris Cleeland Jan 30 '20 at 20:49
  • 1
    @ChrisCleeland For callback `void(*)(void*, int, long long)` you call `auto cb = voidify([&](int x, long long l){ /* code */ });`. For lambdas be useful as callbacks, the callback must be a `void*` userdata and a function-taking `void*` couple. You also have to modify `voidify` if the C-style callback takes the "userdata" `void*` other than as the 1st argument. – Yakk - Adam Nevraumont Jan 30 '20 at 21:18
  • *"The lifetime that the callback is valid is also clear,"* Well maybe the lifetime is clear, but it's different from the points in the code where using it doesn't produce UB, due to the captures of the lambda. – dyp May 18 '21 at 14:04
  • @dyp Sure, but at least the lambda isn't being copied around indefinitely by errant std functions. ;) – Yakk - Adam Nevraumont May 18 '21 at 14:06
  • Hm. Maybe I just don't like the example... most C callback registries I know actually store the function pointer somewhere to be called later, ignoring the current scope. Passing in a lambda with ref-capture then becomes a code smell, even if it's fine here. – dyp May 18 '21 at 14:14
  • @dyp In that case, that API had better have an unregister mechanism. If it does, you have to store the `voidify` result (the `callback>` struct) until that unregister occurs. It is just bog standard resource management? – Yakk - Adam Nevraumont Jun 15 '21 at 14:34
  • My issue is with the ref-capture, not the `voidify` object. You provide an elegant mechanism to manage the required state for type erasure / providing a `void(void*)` adapter, but if someone puts int a lambda with ref-capture there's a not quite obvious lifetime issue. Essentially the thing that `std::thread` tries to prevent by copying everything. – dyp Jun 16 '21 at 08:48
  • @dyp Sure, you do have to know that `[&]` is only something you do when your lambda and its copies have limited lifetime. This isn't unique to C style callbacks however? If you had a std function callback, the same problem occurs. – Yakk - Adam Nevraumont Jun 16 '21 at 13:49
  • Yes this is not unique to C-style callbacks. I've seen a minor improvement by forcing `voidify(&closure)` to show that closure it itself responsible for its lifetime (and those of its captures). We don't have a borrow checker so I don't see how it would be possible to prevent this mistake. I wanted to highlight it for someone who reads just the code, this is something that I think can easily happen, especially if you consider the lifetime issue to be solved (because it is solved for `callback<>`). – dyp Jun 16 '21 at 16:15
6

A lambda function is compatible with C-callback function as long as it doesn't have capture variables.
Force to put something new to old one with new way doesn't make sense.
How about following old-fashioned way?

typedef struct
{
  int cap_num;
} Context_t;

int cap_num = 7;

Context_t* param = new Context_t;
param->cap_num = cap_num;   // pass capture variable
register_callback([](void* context) -> void {
    Context_t* param = (Context_t*)context;
    std::cout << "cap_num=" << param->cap_num << std::endl;
}, param);
Hill
  • 3,391
  • 3
  • 24
  • 28
2

A very simple way to get use a lamda function as a C funtion pointer is this:

auto fun=+[](){ //add code here }

see this answer for a example. https://stackoverflow.com/a/56145419/513481

janCoffee
  • 375
  • 3
  • 5