Saturday, July 8, 2017

C Closures

I've started writing this article about C Closures while doing research for a project that needed them, eventually I've failed to provide a working code on Visual Studio and decided to keep the article as a lesson learned rather than a how to.


Callbacks are great language construct, no matter which language, but adding data to each callback is sometimes necessary. While in C++ you can provide a std::function callback which can include user data and even use lambda with captured variables, in C its a bit different.

#include <functional>

int test_function(int n1)
{
 printf("value %d", n1);
 return n1;
}

int main()
{
 auto f1 = std::bind(&test_function, 42 );
 f1();

 int n1 = 42;

 auto f2 = [=]() {return n1; };

 f2();

    return 0;
}


Since you can't create a function at runtime, Many C APIs provide a way to include data pointer and this pointer is passed to the callback.

typedef void (fn)(int value, void* data);
int function(fn* f, void* v) {
 f(1, v);
 return 0;
}

APIs that do not provide userdata void * pointer are a bit trickier to call with user data. To overcome this problem, the developer can use something called Closures. C closures are not part of the language but are still possible. Two of the common libraries that provide this functionality are libffcall and libffi, both of these libraries generate a function on the fly and provide the new function's address.

To actually generate these functions, these libraries needs to know the CPU architecture and compiler used because they need to implement a compatible call.

Lets start with libffcall, to compile it you can start by cloning https://github.com/libffcall/libffcall
You can find the documentation here: https://www.gnu.org/software/libffcall/

If you're compiling for linux, you should read the readme file, if you're compiling for windows, you should read readme.win32.

For some reason, the compilation failed on my machine and I've had to add _WIN32 and _WIN64 to the #ifdef __i386__ and #ifdef __x86_64__ like so:

#if defined(__i386__ ) || defined(_WIN32)
#define TRAMP_LENGTH 15
#define TRAMP_ALIGN 16  /* 4 for a i386, 16 for a i486 */
#endif

and

#if defined(__x86_64__) || defined(_WIN64)
#define TRAMP_LENGTH 32
#define TRAMP_ALIGN 16
#endif

In the end, I couldn't get the project working on Visual Studio, I've then proceeded to try libffi with Visual Studio as well and after fixing and workarounding more than a dozen errors I gave up.

I have no doubt that these two projects work in more than one environment, but perhaps because its not very simple to build and use might point to a weak link, it works by creating assembly code that encapsulates the userdata and the function pointer, completely ignoring the compiler (though it should use the same calling conventions though a provided generator). On top of that, because its not using the compiler directly, its cross-platform-ness is not as robust across compilers and CPU architectures as portable C should be. To strengthen my point, libffi for example, supports only 64bit visual c++ builds according to the build scripts.

I'm not sure if its my own fault for not being able to compile these libraries successfully on Windows/Visual Studio but in any case I see it as an important lesson about C API Design, no matter how ridiculous it might look at first, if you're expecting a callback, a void * user data should be provided as well.

I'm including my build batch for libffi/Windows, if anyone is successful using any of these libraries, share your knowledge, if you find a different method to implement closures in a cross-platform way, even better.

set CYG_ROOT=%CD%/cygwin
set CYG_CACHE=%CD%/cygwin/var/cache/setup
set CYG_MIRROR=http://mirrors.kernel.org/sourceware/cygwin/

rem libffi is not supported on x64/visual c++
rem set VCVARS_PLATFORM=x86
rem set BUILD=x86-pc-cygwin
rem set HOST=x86-pc-winnt

set VCVARS_PLATFORM=amd64
set BUILD=x86_64-pc-cygwin
set HOST=x86_64-pc-winnt

curl -O http://cygwin.com/setup-x86.exe

setup-x86.exe -qnNdO -R "%CYG_ROOT%" -s "%CYG_MIRROR%" -l "%CYG_CACHE%" -P dejagnu
setup-x86.exe -qnNdO -R "%CYG_ROOT%" -s "%CYG_MIRROR%" -l "%CYG_CACHE%" -P Devel,autoconf,automake,make,libtool

%CYG_ROOT%/bin/bash -lc "cygcheck -dc cygwin"

rem %comspec% /k ""C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall.bat"" x86
%comspec% /k ""C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall.bat"" amd64

%CYG_ROOT%\bin\sh -lc "(cd $OLDPWD; ./autogen.sh;)"
%CYG_ROOT%\bin\sh -lc "(cd $OLDPWD; ./configure CC=''$PWD'/msvcc.sh' CXX=''$PWD'/msvcc.sh' CXXCPP=''$PWD'/msvcc.sh' LD=link CPP='cl -nologo -EP' --build=$BUILD --host=$HOST; cp src/x86/ffitarget.h include; make;)"

rem %CYG_ROOT%\bin\sh -lc "(cd $OLDPWD; ./configure CC='./msvcc.sh -m64' CXX='./msvcc.sh -m64' LD=link CPP='cl -nologo -EP' --build=$BUILD --host=$HOST; cp src/x86/ffitarget.h include; make;)"

No comments:

Post a Comment