Crates.io | closure-ffi |
lib.rs | closure-ffi |
version | 4.1.0 |
created_at | 2025-01-13 01:59:24.249359+00 |
updated_at | 2025-09-24 10:47:01.914695+00 |
description | FFI utility for creating bare function pointers that invoke a closure |
homepage | |
repository | https://github.com/tremwil/closure-ffi |
max_upload_size | |
id | 1513990 |
size | 177,773 |
Provides wrappers around closures which allows them to be called through context-free unsafe bare functions.
Context-free bare functions are not needed very often, as properly designed C APIs typically allow the user to specify an opaque pointer to a context object which will be provided to the function pointer. However, this is not always the case, and may be impossible in less common scenarios, e.g. function hooking for game modding/hacking.
Currently supports the following platforms:
The technique used by this crate to generate bare function thunks that invoke a closure leverages the compiler itself to emit the argument forwarding code at compile-time and is thus much simpler and faster than full-JIT approaches like libffi. The major downside is that it relies on assumptions about the structure of the machine code emitted by the compiler, namely that the thunk's prologue can be trivially relocated. While reasonable for the supported architectures, these assumptions are not guarantees --- the crate relies on a form1 of undefined behavior.
Extensive testing with different bare function argument types (including non-FFI safe types!) has been conducted to ensure that the conditions for creating a broken thunk don't occur in practice, and any such breakage is extremely unlikely to manifest as anything more subtle than an immediate SIGSEGV
on call. But in the end, UB is still UB.
If you are building production-grade or mission-critical software, please do not use this crate and rely on tried and tested solutions like libffi instead. It is mainly intended for game modding, process hacking and function hooks where strict soundness guarantees are not as important.
If the above warning hasn't dissuaded you from using this crate, here are the pros of closure-ffi:
Supports a wide variety of functions types and calling conventions:
#[repr(C)]
types, Option<&T>
, NonNull
, thin CStr
refs, etc.for<'a, 'b> unsafe extern "C" fn(&'a CStr, &'b CStr) -> &'a CStr
using the bare_hrtb!
macro (requires the proc_macros
feature)unsafe extern "C" printf(*const c_char, ...)
if using the c_variadic
crate and nightly feature.Highly flexible API:
JitAlloc
trait#![no_std]
support (though alloc
is still required)BareFn
types for when you need to store closures of different signatures in a collectionSend
/Sync
) and lifetime bounds on the type-erased closureOptimized codegen: The crate leverages the compiler itself to monomorphize optimized bare function thunk templates for each function signature.
Fast thunk generation: Most of the work is done at compile time, so the crate does not need to inspect argument types and manually emit instructions depending on the architecture and calling convention.
Quasi zero-cost abstraction for function items and non-capturing closures: Since they are zero-sized types, the compiler-generated thunk template is universally applicable and no code needs to be emitted at runtime. For example, take the following code:
extern "C" fn takes_fn(cb: unsafe extern "C" fn(u32) -> u32) {
// do something ...
}
extern "C" fn times_two(x: u32) -> u32 {
2 * x
}
takes_fn(times_two);
As of Rust 1.90, writing takes_fn(|x| 2 * x)
does not compile since non-capturing closures can only coerce to "Rust"
calling convention bare functions. Using closure-ffi in this situation is possible and essentially equivalent to the above: No memory is allocated and the few extra branches on the size of the closure will likely be optimized away.
let bare_fn = closure_ffi::BareFn::new_c(|x: u32| 2 * x);
takes_fn(bare_fn.bare());
Passing a closure to a C API taking a contextless function pointer:
use closure_ffi::{BareFnMut};
// Imagine we have an foreign C API for registering and unregistering some callback function.
// Notably, the API does not let the user provide a context object to the callback.
unsafe extern "C" fn ffi_register_callback(cb: unsafe extern "C" fn(u32)) {
// ...
}
unsafe extern "C" fn ffi_unregister_callback(cb: unsafe extern "C" fn(u32)) {
// ...
}
#[cfg(feature = "default_jit_alloc")]
{
// We want to keep track of sum of callback arguments without using
// statics. This is where closure-ffi comes in:
let mut sum = 0; // Non-'static closures work too!
let wrapped = BareFnMut::new_c(|x: u32| {
sum += x;
});
// Safety: Here, we assert that the foreign API won't use the callback
// in ways that break Rust's safety rules. Namely:
// - The exclusivity of the FnMut's borrow is respected.
// - If the calls are made from a different thread, the closure is Sync.
// - We unregister the callback before the BareFnMut is dropped.
unsafe {
ffi_register_callback(wrapped.bare());
// Do something that triggers the callback...
ffi_unregister_callback(wrapped.bare());
}
drop(wrapped);
println!("{sum}");
}
The crate comes with the following feature flags:
std
(default): Use std
features. When this is turned off, the crate is compatible with no_std
,
although a global allocator must be defined.global_jit_alloc
(default): Provides the GlobalJitAlloc
ZST which defers to a global JIT allocator implementation
provided either through default_jit_alloc
feature or the global_jit_alloc!
macro.default_jit_alloc
(default): Provides a global JIT allocator implementation through the
jit-allocator2
crate. Note that said crate relies on operating system APIs,
so while some no_std
configurations are supported, bare metal ones will not be able to use this feature.proc_macros
: Provides the bare_hrtb
proc macro which is necessary for creating bare
functions with signatures that involve higher-kinded lifetimes (i.e. for<'a, ...>
statements).unstable
: Enable the use of unstable Rust features for aspects of the crate that benefit from
them without causing any API breaks. Unstable features that can cause breaking changes when enabled
are gated separately, and also enable this feature.
tuple_trait
: Adds a core::marker::Tuple
bound on FnPtr::Args
. This allows downstream crates to easily integrate the library with closure-related
nightly features such as unboxed_closures
and fn_traits
.
c_variadic
: Adds partial (no invocation through call
) FnPtr
and Fn*Thunk
implementations for variadic functions.
coverage
: Enables support for the -C instrument-coverage
compiler flag.
The kind of undefined behavior relied on by closure-ffi is not as bad as UB within the Rust abstract machine, which makes your entire program meaningless. The potentially incorrect codegen is restricted to JIT'ed thunks, and can only occur when one is actually called. ↩