Passing C++ lambdas to C-function pointer callbacks
Table of Contents
1 Passing C++ lambdas to C callbacks
1.1 Overview
A recurring need in C++ development is passing lambdas or functors, also known as callable objects, to C functions that takes a function pointer callback. This article aims to explore how to pass function-objects (functors) and C++ lambdas to C function-pointer callbacks.
1.2 Sample code with C-function pointer callback
1.2.1 Source Code Listing
GIST with full sources:
- File: CMakeLists.txt
cmake_minimum_required(VERSION 3.9) project(cpp-lambda-test) #========== Global Configurations =============# #----------------------------------------------# set(CMAKE_CXX_STANDARD 17) set(CMAKE_VERBOSE_MAKEFILE ON) #========== Targets Configurations ============# add_library( clib SHARED clib.c) add_executable( consumer consumer.cpp) target_link_libraries( consumer clib)
- File: clib.c (Target: clib / shared library)
- C Shared library exporting functions with C-linkage dotimes_version1() and dotimes_version2() which take C function pointers callbacks.
#include <stdio.h> #include <stdlib.h> // Function without context pointer typedef void (*callback_t) (int n); typedef void (*callback_closure_t) (int n, void* context); /** Function with C-callback argument without context pointer. */ void dotimes_version1(int size, callback_t fun) { printf(" [TRACE] <ENTRY> Called function: %s \n", __FUNCTION__); for(int i = 0; i < size; i++){ fun(i); } printf(" [TRACE] <EXIT> Called function: %s \n", __FUNCTION__); } /** Function with C-callback argument with context pointer for passing function state to * to the function pointer. */ void dotimes_version2(int size, callback_closure_t fun, void* context) { printf(" [TRACE] <ENTRY> Called function: %s \n", __FUNCTION__); for(int i = 0; i < size; i++){ fun(i, context); } printf(" [TRACE] <EXIT> Called function: %s \n", __FUNCTION__); }
- File: consumer.cpp (Target: consumer / executable)
- C++ executable application which consumes the C shared library clib.so (Linux or BSD); clib.dylib (MacOSX) or clib.dll (Windows).
#include <iostream> #include <functional> // std::function container #include <cassert> using callback_t = void (*) (int n); using callback_closure_t = void (*) (int n, void* context); // Functions with C-linkage #define EXTERN_C extern "C" /** Provided by the shared library (aka shared object on Unix-like systems) */ EXTERN_C void dotimes_version1(int size, callback_t fun); EXTERN_C void dotimes_version2(int size, callback_closure_t fun, void* context); using FunctionCallback1 = std::function<void (int )>; /** Namespace contains workarounds for passing capturing lambdas * to function dotimes_version (C function without context void pointer) * * Note: This solution is not thread-safe. The most suitable workaround for * for passing capturing lambdas or C++ functors to C-callbakcs is to * redesign it for using a context void pointer, that allows passing state * to the C-callback. *----------------------------------------------------------------------*/ namespace Workaround1 { /* This function encapsulates callback global variable ** for avoiding the global-initialization fiasco. *-----------------------------------------------------*/ auto get_callback() -> FunctionCallback1& { // Global variable, its lifetimes corresponds to the program lifetime. static FunctionCallback1 callback; return callback; }; /** Set global variable callback */ auto set_callback(FunctionCallback1 func) -> void { auto& callback = get_callback(); callback = func; } void workaround1_callback_adapter(int n) { get_callback()(n); } /* Disadvantage: This solution is not thread-safe and requires locks * to protect the callback global state. Only one callback can be * passed per thread. */ void wrapper_to_dotimes_version1(int size, FunctionCallback1 func) { set_callback(func); dotimes_version1(size, &workaround1_callback_adapter); } } struct FunctionObject { int counter = 10; FunctionObject(){ } FunctionObject(int cnt): counter(cnt){} void operator()(int n) { std::printf(" [FUNCTOR ] n = %d / counter = %d \n", n, counter++); } }; void FunctionObject_adapter(int n, void* context) { assert(context != nullptr); // Note: C-style cast also works, but prefer C++-style cast. FunctionObject* pFunctor = static_cast<FunctionObject*>(context); // [Also works in this way =>>] pFunctor->operator()(n); (*pFunctor)(n); } int main(int argc, char** argv) { std::puts(" [INFO] Consumer started OK."); /* EXPERIMENT 1 - Passing non-capturing lambda to C-function pointer callbacks without * void context pointer. */ std::puts("\n ===== [EXPERIMENT 1] Passing non-capturing lambdas ===============\n"); { /** Non-capturing lambdas are converted to function pointers. */ dotimes_version1(5, [](int n){ std::printf(" [EXPERIMENT 1] n = %d \n", n); }); } /* EXPERIMENT 2 - Passing capturing lambda to C-function pointer callbacks without * void context pointer. * * This experiment fails as capturing lambdas cannot be converted to function-pointers. */ std::puts("\n ===== [EXPERIMENT 2] Passing Capturing lambdas <FAILURE> ========= \n"); { int counter = 10; auto lamb = [&counter](int n){ std::printf(" [EXPERIMENT 2] n = %d / counter = %d \n", n, counter++); }; /* COMPILE-TIME-ERROR: Non-capturing lambdas cannot be passed to * to function-pointers !!! * Remove the next comment ('//') in order to see the compile-time error: * * Error: * --------------------------------------------------------------- * * [build] cpp-lambda-c/consumer.cpp:30:3: error: no matching function for call to 'dotimes_version1' * [build] dotimes_version1(5, lamb); * [build] ^~~~~~~~~~~~~~~~ * [build] consumer.cpp:7:17: note: candidate function not viable: no known conversion from '(lambda at /home/mxpkf8/temp-projects/cpp-lambda-c/consumer.cpp:26:15)' to 'callback_t' (aka 'void (*)(int)') for 2nd argument * [build] extern "C" void dotimes_version1(int size, callback_t fun); * [build] ^ ****************************************************************/ // Change from '0' to '1' to enable the compile-time error. #if 0 // => Compile-time error! dotimes_version1(5, lamb); #endif } /* EXPERIMENT 3 - Passing capturing lambda to C-function pointer callbacks without * void context pointer using a global-state workaround. */ std::puts("\n ===== [EXPERIMENT 3] Passing Capturing lambdas - Workaround 1 =====\n"); { int counter = 10; auto lamb = [&counter](int n){ std::printf(" [EXPERIMENT 3] n = %d / counter = %d \n", n, counter++); }; std::puts("\n --->> Passing C++ capturing lambda "); Workaround1::wrapper_to_dotimes_version1(5, lamb); std::puts("\n --->> Passing function-object (aka C++ Functor) "); Workaround1::wrapper_to_dotimes_version1(5, FunctionObject(25)); } /* EXPERIMENT 4 - Passing capturing lambda to C-function pointer callbacks with * void context pointer. */ std::puts("\n ===== [EXPERIMENT 4] Passing Functors to capturing lambda ==\n"); { std::puts("\n --->> Passing function-object (aka C++ Functor) [APPROACH 1] "); FunctionObject obj1(26); dotimes_version2(5, &FunctionObject_adapter, &obj1); // Note: This lambda can only be passed due to it be non-capturing. auto adapter_for_FunctionObject = [](int n, void* context) { assert(context != nullptr && "Context pointer should not be nullptr."); FunctionObject* pFunctor = reinterpret_cast<FunctionObject*>(context); pFunctor->operator()(n); }; std::puts("\n --->> Passing function-object (aka C++ Functor) [APPROACH 2] "); FunctionObject obj2; dotimes_version2(5, adapter_for_FunctionObject, &obj2); } /* EXPERIMENT 5 - Passing capturing lambda to C-function pointer callbacks with * void context pointer. */ std::puts("\n ===== [EXPERIMENT 5] Passing Capturing lambdas <APPROACH 1> ==\n"); { using FunctionCallback2 = std::function<void (int size)>; auto adpter_for_lambda = [](int n, void* context) { assert(context != nullptr && "Context pointer (state) should not be null."); FunctionCallback2* pFunc = reinterpret_cast<FunctionCallback2*>(context); (*pFunc)(n); }; int counter = -100; auto callback_lambda = [&counter](int n){ std::printf(" [EXPERIMENT 5] n = %d / counter = %d \n", n, counter++); }; FunctionCallback2 callback_object = callback_lambda; std::puts("\n --->> Passing capturing lambda [APPROACH 1 - Type erasure] --- "); dotimes_version2(5, adpter_for_lambda, &callback_object); } std::puts("\n\n [INFO] System shutdown gracefully Ok."); return 0; }
1.2.2 Building and running
Donwload and build sources:
# Get sources $ >> cd /tmp && git clone https://gist.github.com/7db3de56dea0c502c6f749293b5013ef callback && cd callback # Build $ >> cmake --config Debug -H. -B_build $ >> cmake --build _build --target # Show _build directory $ >> ls _build/ CMakeCache.txt CMakeFiles/ cmake_install.cmake consumer* libclib.so* Makefile
Run executable: 'consumer'
$ >> _build/consumer [INFO] Consumer started OK. ===== [EXPERIMENT 1] Passing non-capturing lambdas =============== [TRACE] <ENTRY> Called function: dotimes_version1 [EXPERIMENT 1] n = 0 [EXPERIMENT 1] n = 1 [EXPERIMENT 1] n = 2 [EXPERIMENT 1] n = 3 [EXPERIMENT 1] n = 4 [TRACE] <EXIT> Called function: dotimes_version1 ===== [EXPERIMENT 2] Passing Capturing lambdas <FAILURE> ========= ===== [EXPERIMENT 3] Passing Capturing lambdas - Workaround 1 ===== --->> Passing C++ capturing lambda [TRACE] <ENTRY> Called function: dotimes_version1 [EXPERIMENT 3] n = 0 / counter = 10 [EXPERIMENT 3] n = 1 / counter = 11 [EXPERIMENT 3] n = 2 / counter = 12 [EXPERIMENT 3] n = 3 / counter = 13 [EXPERIMENT 3] n = 4 / counter = 14 [TRACE] <EXIT> Called function: dotimes_version1 --->> Passing function-object (aka C++ Functor) [TRACE] <ENTRY> Called function: dotimes_version1 [FUNCTOR ] n = 0 / counter = 25 [FUNCTOR ] n = 1 / counter = 26 [FUNCTOR ] n = 2 / counter = 27 [FUNCTOR ] n = 3 / counter = 28 [FUNCTOR ] n = 4 / counter = 29 [TRACE] <EXIT> Called function: dotimes_version1 ===== [EXPERIMENT 4] Passing Functors to capturing lambda == --->> Passing function-object (aka C++ Functor) [APPROACH 1] [TRACE] <ENTRY> Called function: dotimes_version2 [FUNCTOR ] n = 0 / counter = 26 [FUNCTOR ] n = 1 / counter = 27 [FUNCTOR ] n = 2 / counter = 28 [FUNCTOR ] n = 3 / counter = 29 [FUNCTOR ] n = 4 / counter = 30 [TRACE] <EXIT> Called function: dotimes_version2 --->> Passing function-object (aka C++ Functor) [APPROACH 2] [TRACE] <ENTRY> Called function: dotimes_version2 [FUNCTOR ] n = 0 / counter = 10 [FUNCTOR ] n = 1 / counter = 11 [FUNCTOR ] n = 2 / counter = 12 [FUNCTOR ] n = 3 / counter = 13 [FUNCTOR ] n = 4 / counter = 14 [TRACE] <EXIT> Called function: dotimes_version2 ===== [EXPERIMENT 5] Passing Capturing lambdas <APPROACH 1> == --->> Passing capturing lambda [APPROACH 1 - Type erasure] --- [TRACE] <ENTRY> Called function: dotimes_version2 [EXPERIMENT 5] n = 0 / counter = -100 [EXPERIMENT 5] n = 1 / counter = -99 [EXPERIMENT 5] n = 2 / counter = -98 [EXPERIMENT 5] n = 3 / counter = -97 [EXPERIMENT 5] n = 4 / counter = -96 [TRACE] <EXIT> Called function: dotimes_version2 [INFO] System shutdown gracefully Ok.
1.2.3 Analysis
The goal of the application (consumer.cpp) is to call pass C++ lambdas and C++ callable objects (functors) as he following C-functions which takes C-function pointer callbacks:
using callback_t = void (*) (int n); using callback_closure_t = void (*) (int n, void* context); // Functions with C-linkage #define EXTERN_C extern "C" /** Provided by the shared library (aka shared object on Unix-like systems) */ EXTERN_C void dotimes_version1(int size, callback_t fun); EXTERN_C void dotimes_version2(int size, callback_closure_t fun, void* context);
Experiment 1:
- Non-capturing lambdas are converted to function-pointers when passed to C-function pointer callbacks.
/* EXPERIMENT 1 - Passing non-capturing lambda to C-function pointer callbacks without * void context pointer. */ std::puts("\n ===== [EXPERIMENT 1] Passing non-capturing lambdas ===============\n"); { /** Non-capturing lambdas are converted to function pointers. */ dotimes_version1(5, [](int n){ std::printf(" [EXPERIMENT 1] n = %d \n", n); }); }
Program output:
[INFO] Consumer started OK. ===== [EXPERIMENT 1] Passing non-capturing lambdas =============== [TRACE] <ENTRY> Called function: dotimes_version1 [EXPERIMENT 1] n = 0 [EXPERIMENT 1] n = 1 [EXPERIMENT 1] n = 2 [EXPERIMENT 1] n = 3 [EXPERIMENT 1] n = 4 [TRACE] <EXIT> Called function: dotimes_version1
Experiment 2: [COMPILE-TIME ERROR]
- A a capturing lambdas cannot be converted into a function pointer, the following code causes a compile-time error if the "#if" is enabled as "#if 1".
std::puts("\n ===== [EXPERIMENT 2] Passing Capturing lambdas <FAILURE> ========= \n"); { int counter = 10; auto lamb = [&counter](int n){ std::printf(" [EXPERIMENT 2] n = %d / counter = %d \n", n, counter++); }; // Change from '0' to '1' to enable the compile-time error. #if 0 // => Compile-time error! dotimes_version1(5, lamb); #endif }
Compile-time error:
[build] cpp-lambda-c/consumer.cpp:30:3: error: no matching function for call to 'dotimes_version1' [build] dotimes_version1(5, lamb); [build] ^~~~~~~~~~~~~~~~ [build] consumer.cpp:7:17: note: candidate function not viable: no known conversion from ' (lambda at /consumer.cpp:26:15)' to 'callback_t' (aka 'void (*)(int)') for 2nd argument [build] extern "C" void dotimes_version1(int size, callback_t fun); [build] ^
Experiment 3: Workaround for passing capturing-lambdas to C-function pointer callbacks.
- Passing a capturing lambda to a C-function that takes a C function pointer callback, requires a workaround using global state. The shortcoming of this method is the lack of thread-safety due to the usage of global state. A thread-safe version of this workaround requires using locks, such as mutex, in order to avoid race condition undefined-behaviors. The most suitable solution to this issue is to redesign the function dotimes_version1 by making it take an extra void pointer for passing the C function pointer state.
std::puts("\n ===== [EXPERIMENT 3] Passing Capturing lambdas - Workaround 1 =====\n"); { int counter = 10; auto lamb = [&counter](int n){ std::printf(" [EXPERIMENT 3] n = %d / counter = %d \n", n, counter++); }; std::puts("\n --->> Passing C++ capturing lambda "); Workaround1::wrapper_to_dotimes_version1(5, lamb); std::puts("\n --->> Passing function-object (aka C++ Functor) "); Workaround1::wrapper_to_dotimes_version1(5, FunctionObject(25)); }
Output:
===== [EXPERIMENT 3] Passing Capturing lambdas - Workaround 1 ===== --->> Passing C++ capturing lambda [TRACE] <ENTRY> Called function: dotimes_version1 [EXPERIMENT 3] n = 0 / counter = 10 [EXPERIMENT 3] n = 1 / counter = 11 [EXPERIMENT 3] n = 2 / counter = 12 [EXPERIMENT 3] n = 3 / counter = 13 [EXPERIMENT 3] n = 4 / counter = 14 [TRACE] <EXIT> Called function: dotimes_version1 --->> Passing function-object (aka C++ Functor) [TRACE] <ENTRY> Called function: dotimes_version1 [FUNCTOR ] n = 0 / counter = 25 [FUNCTOR ] n = 1 / counter = 26 [FUNCTOR ] n = 2 / counter = 27 [FUNCTOR ] n = 3 / counter = 28 [FUNCTOR ] n = 4 / counter = 29 [TRACE] <EXIT> Called function: dotimes_version1
Workaround code at to of file consumer.cpp:
- The function get__callback returns a reference to the global state or global variable named callback whose lifetime is the same as the application lifetime. The global variable is encapsulated in this function for avoiding global-initialization-fiasco undefined behavior.
- The function set_callback sets the global variable (global state) encapsulated by the function set_callback.
- The function wrapper_to_dotimes_version1 sets the callback global variable and calls the C-function dotimes_version1.
using FunctionCallback1 = std::function<void (int )>; namespace Workaround1 { /* This function encapsulates callback global variable ** for avoiding the global-initialization fiasco. *-----------------------------------------------------*/ auto get_callback() -> FunctionCallback1& { // Global variable, its lifetimes corresponds to the program lifetime. static FunctionCallback1 callback; return callback; }; /** Set global variable callback */ auto set_callback(FunctionCallback1 func) -> void { auto& callback = get_callback(); callback = func; } void workaround1_callback_adapter(int n) { get_callback()(n); } /* Disadvantage: This solution is not thread-safe and requires locks * to protect the callback global state. Only one callback can be * passed per thread. */ void wrapper_to_dotimes_version1(int size, FunctionCallback1 func) { set_callback(func); dotimes_version1(size, &workaround1_callback_adapter); } }
Experiment 4: Passing functors to function-pointer arguments.
- Selected code before main function.
using callback_closure_t = void (*) (int n, void* context); // Provided by the shared library (clib) EXTERN_C void dotimes_version2( int size // C-function pointer argument , callback_closure_t fun // void-pointer (context pointer) // Extra argument for passing state // to the callback function-pointer. , void* context ); struct FunctionObject { int counter = 10; FunctionObject(){ } FunctionObject(int cnt): counter(cnt){} void operator()(int n) { std::printf(" [FUNCTOR ] n = %d / counter = %d \n", n, counter++); } }; void FunctionObject_adapter(int n, void* context) { assert(context != nullptr); // Note: C-style cast also works, but prefer C++-style cast. FunctionObject* pFunctor = static_cast<FunctionObject*>(context); // [Also works in this way =>>] pFunctor->operator()(n); (*pFunctor)(n); }
- Often C-functions that takes a function pointer callback provide an extra void-pointer parameter for passing a pointer to the function-pointer context or state. If a function takes this extra parameter, passing lambdas or functors becomes easier. The following code takes advantage of this extra void pointer for passing the function FunctionObject to the C function dotimes_version2.
std::puts("\n ===== [EXPERIMENT 4] Passing Functors to capturing lambda ==\n"); { std::puts("\n --->> Passing function-object (aka C++ Functor) [APPROACH 1] "); FunctionObject obj1(26); dotimes_version2(5, &FunctionObject_adapter, &obj1); // Note: This lambda can only be passed due to it be non-capturing. auto adapter_for_FunctionObject = [](int n, void* context) { assert(context != nullptr && "Context pointer should not be nullptr."); FunctionObject* pFunctor = reinterpret_cast<FunctionObject*>(context); pFunctor->operator()(n); }; std::puts("\n --->> Passing function-object (aka C++ Functor) [APPROACH 2] "); FunctionObject obj2; dotimes_version2(5, adapter_for_FunctionObject, &obj2); }
Output:
===== [EXPERIMENT 4] Passing Functors to capturing lambda == --->> Passing function-object (aka C++ Functor) [APPROACH 1] [TRACE] <ENTRY> Called function: dotimes_version2 [FUNCTOR ] n = 0 / counter = 26 [FUNCTOR ] n = 1 / counter = 27 [FUNCTOR ] n = 2 / counter = 28 [FUNCTOR ] n = 3 / counter = 29 [FUNCTOR ] n = 4 / counter = 30 [TRACE] <EXIT> Called function: dotimes_version2 --->> Passing function-object (aka C++ Functor) [APPROACH 2] [TRACE] <ENTRY> Called function: dotimes_version2 [FUNCTOR ] n = 0 / counter = 10 [FUNCTOR ] n = 1 / counter = 11 [FUNCTOR ] n = 2 / counter = 12 [FUNCTOR ] n = 3 / counter = 13 [FUNCTOR ] n = 4 / counter = 14 [TRACE] <EXIT> Called function: dotimes_version2
Experiment 5: Passing capturing-lambdas to function-pointer arguments.
- This code is similar to the one from experiment 4. It passes capturing lambda to the C function dotimes_version2 using the extra void pointer (context) of this function.
/* EXPERIMENT 5 - Passing capturing lambda to C-function pointer callbacks with * void context pointer. */ std::puts("\n ===== [EXPERIMENT 5] Passing Capturing lambdas <APPROACH 1> ==\n"); { using FunctionCallback2 = std::function<void (int size)>; auto adpter_for_lambda = [](int n, void* context) { assert(context != nullptr && "Context pointer (state) should not be null."); FunctionCallback2* pFunc = reinterpret_cast<FunctionCallback2*>(context); (*pFunc)(n); }; int counter = -100; auto callback_lambda = [&counter](int n){ std::printf(" [EXPERIMENT 5] n = %d / counter = %d \n", n, counter++); }; FunctionCallback2 callback_object = callback_lambda; std::puts("\n --->> Passing capturing lambda [APPROACH 1 - Type erasure] --- "); dotimes_version2(5, adpter_for_lambda, &callback_object); }
Output:
===== [EXPERIMENT 5] Passing Capturing lambdas <APPROACH 1> == --->> Passing capturing lambda [APPROACH 1 - Type erasure] --- [TRACE] <ENTRY> Called function: dotimes_version2 [EXPERIMENT 5] n = 0 / counter = -100 [EXPERIMENT 5] n = 1 / counter = -99 [EXPERIMENT 5] n = 2 / counter = -98 [EXPERIMENT 5] n = 3 / counter = -97 [EXPERIMENT 5] n = 4 / counter = -96 [TRACE] <EXIT> Called function: dotimes_version2 [INFO] System shutdown gracefully Ok.