Adding state: Closures
Since bare function pointers cannot carry any non-global instance-specific state, their usability is quite limited. For a callback-based API to be good, it must be able to support some associated state.
Stateful callbacks in C
In C, the idiomatic way to achieve this is to carry an extra void * parameter
(traditionally called data, ctx, env or payload), and have the function pointer
receive it as one of its parameters:
Example
#include <assert.h>
#include <stdlib.h>
void call_n_times (
size_t repeat_count,
void (*cb)(void * cb_ctx),
void * cb_ctx)
{
for (size_t i = 0; i < repeat_count; ++i) {
(*cb)(cb_ctx);
}
}
void my_cb (
void * cb_ctx);
int main (void)
{
int counter = 0; // state to be shared
int * at_counter = &counter; // pointer to the state
void * cb_ctx = (void *) at_counter; // type-erased
call_n_times(
42,
my_cb,
cb_ctx)
;
assert(counter == 42);
return EXIT_SUCCESS;
}
// where
void my_cb (
void * cb_ctx)
{
int * at_counter = (int *) cb_ctx; // undo type erasure
*at_counter += 1; // access state through dereference
}
This pattern is so pervasive that the natural thing to do is to bundle those
two fields (data pointer, and function pointer) within a struct:
typedef struct MyCallback {
void (*cb)(void * ctx);
void * ctx;
} MyCallback_t;
Example
#include <assert.h>
#include <stdlib.h>
typedef struct MyCallback {
void * ctx;
void (*fptr)(void * ctx);
} MyCallback_t;
void call_n_times (
size_t repeat_count,
MyCallback_t cb)
{
for (size_t i = 0; i < repeat_count; ++i) {
(*cb.fptr)(cb.ctx);
}
}
void my_cb (
void * cb_ctx);
int main (void)
{
int counter = 0;
call_n_times(
42,
(MyCallback_t) {
.fptr = my_cb,
.ctx = (void *) &counter,
})
;
assert(counter == 42);
return EXIT_SUCCESS;
}
// where
void my_cb (
void * cb_ctx)
{
int * at_counter = (int *) cb_ctx;
*at_counter += 1;
}
Back to Rust
In Rust, the situation is quite more subtle, since the properties of the closure are not wave-handed like they are in C. Instead, there are very rigurous things to take into account:
-
'staticCan the environment be held arbitrarily long, or is there some call frame / scope / lifetime it cannot outlive?
-
SendCan the environment be accessed (non-concurrently) from another thread?
- For the sake of sanity, non-
Sendclosures are not supported.
- For the sake of sanity, non-
-
Fnvs.FnMutBoth involve a callable API, but
FnMutinvolves non-concurrent access whereasFnallows concurrent access (e.g., closure then has to be reentrant-safe and, whenSync, thread-safe too). -
Sync(+Fn)Is the closure thread-safe / can it be called in parallel?
To get a better understanding of the
Fn*traits and themove? |...| ...closure sugar in Rust I highly recommend reading the Closures: Magic functions blog post.
Such struct definitions are available, in a generic fashion, in ::safer_ffi,
under the ::safer_ffi::closure module.
Disclaimer about callbacks using lifetimes
Function signatures involving lifetimes are not supported yet (and will
probably never be, due to a limitation of Rust's typesystem). Using a
transparent newtype around concrete closure signatures would circumvent that
genericity limitation, and the crate's author intends to release a macro that
would automate that step. In the meantime, you will have to use raw pointers or
the Raw variants of the provided C types (e.g.,
c_slice::Raw, char_p::Raw).
For instance, MyCallback_t above is equivalent to using, within Rust, the
RefDynFnMut0<'_, ()> type, a ReprC version of
&'_ mut (dyn Send + FnMut()):
C layout
typedef struct {
// Cannot be NULL
void * env_ptr;
// Cannot be NULL
void (*call)(void * env_ptr);
} RefDynFnMut0_void_t;
Borrowed closures
More generally, when having to deal with a
borrowed?
stateful callback
having N inputs of type A1, A2, ..., An, and a return type of Ret, i.e.,
a &'_ mut (dyn Send + FnMut(A1, ..., An) -> Ret), then the ReprC
equivalent to use is:
RefDynFnMutN<'_, Ret, A1, ..., An>
-
C layout:
typedef struct { // Cannot be NULL void * env_ptr; // &'_ mut TypeErased // Cannot be NULL Ret_t (*call)(void * env_ptr, A1_t arg_1, A2_t arg2, ..., An_t arg_n); } RefDynFnMutN_Ret_A1_A2_..._An_t;
Example: call_n_times in Rust
The previously shown API:
typedef struct MyCallback {
void * ctx;
void (*fptr)(void * ctx);
} MyCallback_t;
void call_n_times (
size_t repeat_count,
MyCallback_t cb)
{
for (size_t i = 0; i < repeat_count; ++i) {
(*cb.fptr)(cb.ctx);
}
}
can be trivially implemented in Rust with the following code:
use ::safer_ffi::prelude::*;
#[ffi_export]
fn call_n_times (
repeat_count: usize,
cb: RefDynFnMut0<'_, ()>,
)
{
// A current limitation of the `#[ffi_export]` is that it does not support
// any non-identifier patterns such as `mut cb`.
// We thus need to rebind it at the beginning of the function's body.
// This ought to be fixed very soon.
let mut cb = cb;
for _ in 0 .. repeat_count {
cb.call();
}
}
Bonus: calling it from Rust
Although most FFI functions are only to be called by C, sometimes we wish to
call them from Rust too (e.g., when wanting to test them). In that case,
know that the ...DynFn...N<...> family of ReprC closures all come with:
-
constructors supporting the equivalent Rust types (before type erasure!);
-
as well as as
.call(...)method as showcased just above; -
when dealing with owned variants, the Rust types implement
Drop(so that offering a function to free a closure is as simple as exporting a function that simplydrops its input); -
and finally, when dealing with
ArcDynFnN<...>, it also implementsClone, although it willpanic!if the.retainfunction pointer happens to beNULL.
let mut count = 0;
call_n_times(42, RefDynFnMut0::new(&mut || { count += 1; }));
assert_eq!(count, 42);
Owned closures
When, instead, the closure may be held arbitrarily long (e.g., in another thread), and may have some destructor logic, i.e., when dealing with a heap-allocation-agnostic generalization of:
Box<dyn 'static + Send + FnMut(A1, ..., An) -> Ret>
then, the ReprC equivalent type to use is:
BoxDynFnMutN<Ret, A1, ..., An>
C layout
typedef struct {
// Cannot be NULL
void * env_ptr; // Box<TypeErased>
// Cannot be NULL
Ret_t (*call)(void * env_ptr,
A1_t arg_1,
A2_t arg2,
...,
An_t arg_n);
// Cannot be NULL
void (*free)(void * env_ptr);
} BoxDynFnMutN_Ret_A1_A2_..._An_t;
Ref-counted thread-safe closures
And, finally, when, on top of the previous considerations, the closure may have
multiple owners (requiring ref-counting) and/or may be called by concurrent
(Fn instead of FnMut) and even parallel (added Sync bound) code, i.e.,
when dealing with a heap-allocation-agnostic generalization of:
Arc<dyn 'static + Send + Sync + Fn(A1, ..., An) -> Ret>
then, the ReprC equivalent type to use is:
ArcDynFnN<Ret, A1, ..., An>
C layout
typedef struct {
// Cannot be NULL
void * env_ptr; // Arc<TypeErased>
// Cannot be NULL
Ret_t (*call)(void * env_ptr,
A1_t arg_1,
A2_t arg2,
...,
An_t arg_n);
// Cannot be NULL
void (*release)(void * env_ptr);
// May be NULL
void (*retain)(void * env_ptr);
} ArcDynFnN_Ret_A1_A2_..._An_t;
- Note how an
ArcDynFnN... *can be casted to aBoxDynFnMutN... *(same prefix), and how the latter can be converted to the former by having.retain = NULL.