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:
-
'static
Can the environment be held arbitrarily long, or is there some call frame / scope / lifetime it cannot outlive?
-
Send
Can the environment be accessed (non-concurrently) from another thread?
- For the sake of sanity, non-
Send
closures are not supported.
- For the sake of sanity, non-
-
Fn
vs.FnMut
Both involve a callable API, but
FnMut
involves non-concurrent access whereasFn
allows 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 simplydrop
s its input); -
and finally, when dealing with
ArcDynFnN<...>
, it also implementsClone
, although it willpanic!
if the.retain
function 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
.