libcclosure is a library which adds thread-safe closures as first-class functions to the C language.
This library is heavily inspired by and intended as a more permissively-licensed alternative to libffcall's callback module. If your project's license permits the use of GPL 3.0-licensed software, you should probably use libffcall instead; it has had more rigorous bug testing and supports a wider range of systems and architectures.
- Linux
This library uses CMake to generate its build system.
The first step is to configure the build system by running the following command:
$ CC=gcc cmake -S . -B build \
-D CMAKE_BUILD_TYPE=Release \
-D BUILD_TESTING=OFF \
-D BUILD_THREADING=ON \
-D BUILD_ARCH=x86_64
Setting the CC
environment variable is optional and likely unnecessary unless you want to use a compiler other than your user default. The supported compilers are GCC, Clang, and TCC.
Unless you you plan to modify libcclosure, itself, you'll likely want to use Release
for CMAKE_BUILD_TYPE
and OFF
for BUILD_TESTING
.
While thread-safety is one of the primary goals of this library, it also involves non-negligible overhead. If you'll be using libcclosure in a single-threaded environment, you can gain a little extra performance by using OFF
for BUILD_THREADING
to prevent the inclusion of thread-safety-related system calls.
Finally, choose a target architecture to build the library for by passing it as BUILD_ARCH
. The supported architectures are x86
and x86_64
.
To build the library, run:
$ cmake --build build/
This creates both a static (libcclosure.a
) and shared (libcclosure.so
) library.
# cmake --build build/ --target install
By default, this will install the library to /usr/local
. You can change the installation directory by instead running:
$ cmake -S . -B build -D CMAKE_INSTALL_PREFIX=$HOME/.local
$ cmake --build build/ --target install
The header file cclosure.h
will be installed to ${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_INCLUDEDIR}
.
The library files libcclosure.a
and libcclosure.so
will be installed to ${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_LIBDIR}
.
Importable cmake scripts which define the targets CClosure::cclosure_static
and CClosure::cclosure_shared
will be installed to the directory ${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_CMAKEDIR}/CClosure
.
These closures are first-class C functions in the sense that they can accept arbitrary arguments (including variadic) and have an arbitrary return type. To create one, first define a callback function that accepts a special closure "context" as its first argument followed by the other desired arguments:
int Callback(CClosureCtx ctx, double filter, size_t numVArgs, ...) {
/* "ctx.env" is a pointer to the closure's environment. */
/* ... */
}
To create a closure, you must bind an environment to the callback function (pass true
as the third argument to CClosureNew
if the callback returns an aggregate type rather than a scalar):
int (*closure)(double, size_t, ...) = CClosureNew(Callback, &someEnv, false);
CClosureNew
is completely thread-safe assuming that libcclosure was compiled with multi-threading support.
closure
can now be called like any other C function, and its bound environment will be implicitly passed to it before the arguments it was called with:
int val0 = closure(15.0, 2, "some", "string");
int val1 = closure(8.0, 0);
The bound closure is thread-safe in the sense that multiple threads may safely call it in parallel and read from its environment. If the closure's callback modifies its environment, however, you must ensure that it does so in a thread-safe manner (like by using a mutex).
Use CClosureCheck
to determine whether or not a given reference is to a bound closure:
bool isClosure = CClosureCheck(closure);
Retrieve the environment bound to a closure using CClosureGetEnv
:
void *env = CClosureGetEnv(closure);
and retrieve the callback function bound to it using CClosureGetFcn
:
void *fcn = CClosureGetFcn(closure);
Use CClosureFree
to de-allocate a bound closure:
void *env = CClosureFree(closure);
Note that CClosureFree
returns the previously-bound environment.
CClosureFree
is thread-safe in the sense that multiple threads may safely call it (along with CClosureNew
) in parallel. It is also safe for a closure to free itself and still return as normal. However, there are
situations in which calling this function along with others (such as CClosureGetEnv
and CClosureGetFcn
)
in parallel may result in undefined behavior.
Test whether or not libcclosure was compiled with multi-threading support using the CCLOSURE_THREAD_TYPE
global:
switch (CCLOSURE_THREAD_TYPE) {
/* Compiled with multi-threading support using POSIX Threads. */
case CCLOSURE_THREAD_PTHREADS:
break;
/* Not compiled with any multi-threading support. */
case CCLOSURE_THREAD_NONE:
break;
}
Suppose an external API provides some function that accepts a callback function:
/* list.h */
#include <stdbool.h>
#include <stddef.h>
typedef void List;
List *ListCreate(size_t num, ...);
void ListForEach(List *list, bool (*callback)(int *element));
Functions like this typically accept a "data" parameter to pass to callback in addition to element
, but imagine that isn't the case here. That functionality can be recreated using a closure:
/* main.c */
#include <stdio.h>
#include <stdlib.h>
#include "cclosure.h"
#include "lists.h"
/* Type of closure environment. */
struct SumGreaterThanEnv {
int sum;
int threshold;
};
/* Function that accepts closure context as first parameter. */
static bool SumGreaterThanCallback(CClosureCtx ctx, int *element) {
/* Closure context contains the bound environment. */
struct SumGreaterThanEnv *env = ctx.env;
if (*element > env->threshold)
env->sum += *element;
return true;
}
int main(void) {
List *list = ListCreate(5, 3, -10, 77, 42, 15);
/* Instantiate an environment for the closure. */
struct SumGreaterThanEnv *env = malloc(sizeof(struct SumGreaterThanEnv));
*env = (SumGreaterThanEnv){
.sum = 0,
.threshold = 10
};
/* Create a closure by binding environment to callback. */
bool (*callback)(int *) = CClosureNew(SumGreaterThanCallback, env, false);
/* Callback is now a first-class function that will be passed environment
implicitly as its first parameter. */
ListForEach(list, callback);
/* Would print "134". */
printf("%i", env->sum);
/* "CClosureFree" returns the closure's environment. */
free(CClosureFree(callback));
return 0;
}