Expand description
Provides wrappers around closures which allows them to be called through context-free unsafe bare functions. For example:
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.
§Example
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}");
}§Supported Configurations
§Targets
closure-ffi currently supports the following platforms (with some features being restricted to others). no_std is supported, but a global allocator must be registered with the alloc crate.
| Target | safe_jit | default_jit_alloc | CI tested? |
|---|---|---|---|
x86_64 (unix) | ✅ | ✅ | ✅ (linux) |
x86_64 (windows) | ✅ | ✅ | ✅ (msvc) |
x86_64 (none) | ✅ | ❌ | ❌ |
i686 (unix) | required (*) | ✅ | ❌ |
i686 (windows) | ✅ | ✅ | ✅ (msvc) |
i686 (none) | ✅ | ❌ | ❌ |
aarch64-apple-darwin | ✅ (**) | ✅ | ✅ |
aarch64 (unix) | ✅ (**) | ✅ | ✅ (linux) |
aarch64 (windows) | ✅ (**) | ✅ | ❌ |
aarch64 (none) | ✅ (**) | ❌ | ❌ |
arm (linux) | ✅ (**) | ✅ | ❌ |
arm (none) | ✅ (**) | ❌ | ❌ |
thumbv7 (linux) | ✅ (**) | ✅ | ❌ |
thumbv7 (none) | ✅ (**) | ❌ | ❌ |
(*): The feature is required as disabling it would lead to incorrect code being emitted.
(**): Depends on Capstone, which requires a C toolchain and some libc headers to build.
§Executable Memory Allocators
closure-ffi abstracts away executable memory allocators via the JitAlloc trait. BareFn types can be constructed using an arbitrary JitAlloc implementation using the constructor methods with a _in suffix.
The default_jit_alloc feature (enabled by default) provides the built-in global JitAlloc implementation. You can override the global implementation by disabling it and enabling global_jit_alloc instead.
§Calling conventions
The following calling conventions (and all -unwind variants) are supported. Calling convention marker types can be found in the cc module.
- All “standard” foreign calling conventions like
C,systemandefiapi. - The
Rustcalling convention. Note that this calling convention is unstable and ABI compatibility is only guaranteed within a particular binary! - On x64 Windows, the
win64calling convention. - On x86 Windows, the
stdcall,cdecl,fastcallandthiscallcalling conventions. - On non-Windows x64, the
sysv64calling convention. - On ARM (not Aarch64), the
aapcscalling convention.
§Signatures
The following function signatures are supported:
-
Functions of up to 12 arguments with arbitrary argument types. This means that all ffi-safe types can be used in the function signature: thin references,
#[repr(C)]types,Option<&T>,NonNull, thinCStrrefs, etc. Note that you will not get a warning if using a non ffi-safe type in the function signature. -
Lifetime-generic (a.k.a. higher-kinded) bare functions, e.g.
for<'a, 'b> unsafe extern "C" fn(&'a CStr, &'b CStr) -> &'a CStrthrough thebare_hrtb!macro (requires theproc_macrosfeature). -
Variadic C functions e.g.
unsafe extern "C" printf(*const c_char, ...)are supported when thec_variadiccrate and nightly feature are enabled.
§Features Flags
The crate comes with the following feature flags:
§Stable
-
std(default): Usestdfeatures. When this is turned off, the crate is compatible withno_std, although a global allocator must be defined. -
global_jit_alloc(default): Provides theGlobalJitAllocZST which defers to a global JIT allocator implementation provided either throughdefault_jit_allocfeature or theglobal_jit_alloc!macro. This is necessary to constructBareFntypes without explicitly passing an allocator. -
default_jit_alloc(default): Provides a global JIT allocator implementation through thejit-allocator2crate. Note that said crate relies on operating system APIs, so not all configurations are supported. See the Targets section for details. -
proc_macros: Provides thebare_hrtbproc macro which is necessary for creating bare functions with signatures that involve higher-kinded lifetimes (i.e.for<'a, ...>statements). -
safe_jit(default): Implements disassembler-aided relocation of the thunk template prologue. This is not so much a feature as it is an integral part of the crate.Without it, the crate makes the (unsafe) assumption that the thunk prologues are trivially relocatable, and blocks certain compiler optimizations to try to uphold this. However, this is not guaranteed and UB is a real possibility. While this feature can be disabled to improve compatibility with targets for which the dependency on the Capstone disassembler (a C library) cannot be built, I would strongly suggest not doing so.
-
no_safe_jit: Since not havingsafe_jitenabled is inherently unsafe, the crate will refuse to build unless this feature is enabled to prevent accidentally forgettingsafe_jiton--no-default-featurebuilds.
§Unstable (require a nightly compiler)
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 acore::marker::Tuplebound onFnPtr::Args. This allows downstream crates to easily integrate the library with closure-related nightly features such asunboxed_closuresandfn_traits.c_variadic: Adds partial (no invocation throughcall)FnPtrandFn*Thunkimplementations for variadic functions.coverage: Enables support for the-C instrument-coveragecompiler flag.
§How it Works
Unlike libffi and similar libraries, this crate leverages the Rust compiler itself to monomorphize optimized bare function thunk templates for each function signature. For example, given F: Fn(usize) -> usize, a thunk template for the “C” calling convention on x86_64 would look like this:
unsafe extern "C" fn thunk(arg0: usize) -> usize {
let closure_ptr: *const F;
core::arch::asm!(
"mov {cl_addr}, [rip + 2f]",
"jmp [rip + 2f+$8]",
".balign 8, 0xCC",
"2:",
".8byte {cl_magic_0}", // closure pointer
".8byte {cl_magic_1}", // thunk exit addrsss
cl_magic_0 = const { CL_MAGIC[0] },
cl_magic_1 = const { CL_MAGIC[1] },
cl_addr = out(reg) $closure_ptr,
options(nostack)
);
(&*closure_ptr)(arg0)
}where CL_MAGIC is a sequence of invalid or reserved undefined (UDF) instructions that will not be found in a compiler-generated function prologue.
When instantiated for a particular instance of the closure, the magic constant is searched to find where to write a pointer to it as well as the address of the next instruction past the asm! block. A disassembler is then used to relocate the code up to the end of the asm! block to dynamically allocated executable memory.
This is very fast at runtime, since most work is done at compile time and the crate does not need to inspect argument types and manually emit instructions depending on the architecture and calling convention. The compiler can also inline the closure’s code into the thunk template, optimizing the prologue and avoiding further branches or stack spilling.
§Non-capturing closures
If the Fn impl is a zero-sized type, such as a non-capturing closure or a function item, it is possible to “conjure” a valid reference to the type from a dangling pointer. Hence a thunk template like this is valid for all instances of the closure:
unsafe extern "C" fn thunk(arg0: usize) -> usize {
let closure_ptr: *const F = core::ptr::dangling();
(&*closure_ptr)(arg0)
}This optimization lets closure-ffi thunk this kind of closure without allocating or emitting any code at runtime, making BareFn a quasi zero-cost abstraction. For example, consider 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);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());§Credits
§Changelog
§[v5.0.1] - 2025-10-28
§Fixed
- Use forked
iced-x86crate to avoid conflicts with dependents using it with thestdfeature. This is temporary until a new iced version is released to allowstdandno_stdfeatures to be enabled at the same time.
§[v5.0.0] - 2025-10-19
§Breaking Changes
- Compiling with a Thumb JSON target file will now require Nightly Rust.
- With the addition of the
safe_jitfeature, compiling with--no-default-featureswill now error unless theno_safe_jitfeature is explicitly enabled to prevent accidentally forgetting to enablesafe_jit. - Strengthened trait bounds on
FnPtr::CCto make some APIs more ergonomic. This is technically a breaking change but is realistically harmless, asFnPtrshould not be implemented by the end user. - Changed the
JitAllocblanket impl from&Jfor allJ: JitAllocto any type implementingDeref<Target = J>. This is more general and avoids having to write forwarding impls when putting aJitAllocin aLazyLock, for example, but may break some downstreamJitAllocwrappers.
§Added
-
Thunk generation is now fully safe thanks to the
safe_jitfeature, which uses a disassembler to properly relocate the prologue code instead of assuming it is trivially relocatable. This brings an end to this crate’s UB issues. -
Support for the
efiapiandRustcalling conventions.
§Fixed
global_jit_allocmacro ambiguous parsing for the unsafe block variant.- Incorrect relocation of thunk prologues on
i686-unknown-linux-gnu.
§Removed
- i686-specific Windows calling conventions from x64 Windows targets.
§[v4.1.0] - 2025-09-23
§Changed
- Thunk generation modified to be a zero-cost abstraction: For functions items and non-capturing closures, constructing a
BareFn*type will not allocate or emit code. Instead, it will use a compile-time template that conjures an instance of the ZST to invoke it. - Added changelog to the documentation.
- Added UB warning to the documentation.
§[v4.0.0] - 2025-09-22
This update adds the scaffolding required to implement “higher order” transformations on bare function thunks. For example, it is now possible to write a function that synchronizes a generic FnMutThunk implementation while printing its return value:
use closure_ffi::{thunk_factory, cc, traits::{FnPtr, FnThunk, FnMutThunk}};
#[cfg(feature = "std")]
fn lock_and_debug<B: FnPtr, F: Send>(fun: F) -> impl FnThunk<B> + Sync
where
for<'a, 'b, 'c> B::Ret<'a, 'b, 'c>: std::fmt::Debug,
(cc::C, F): FnMutThunk<B>,
{
let locked = std::sync::Mutex::new((cc::C, fun));
thunk_factory::make_sync(move |args| unsafe {
let ret = locked.lock().unwrap().call_mut(args);
println!("value: {ret:?}");
ret
})
}This is particularly useful for hooking libraries.
§Breaking Changes
- Removed
where Self: 'a + 'b + 'cbounds onFnPtr::ArgsandFnPtr::Ret - Regression in the expressivity of
bare_hrtb!(): Now requires a'staticbound on certain generic parameters - removed zero-variant enum from
FnPtr::Argsfor extern variaric functions to be able to implement the new trait functions.FnPtr::callnow const panics instead of being impossible to call for them.
§Added
FnPtr::make_*_thunkfunctions that can create aFn*Thunkimplementation from a closure with tuple-packed arguments.FnOnceThunk::call_once,FnMutThunk::call_mutandFnThunk::callfor invoking the underlying closure with tuple-packed arguments.thunk_factorymodule for creatingFn*Thunkimplementations that satisfy combinations ofSendandSyncbounds.
§Fixed
libcdependency not compatible withno_stdon Linux ARM targets
§[v3.0.1] - 2025-06-21
§Fixed
- docs.rs build
§[v3.0.0] - 2025-06-20
§Breaking Changes
ToBoxedUnsizehas been renamed toToBoxedDynis now an unsafe trait. See the documentation for the new invariants.SendandSyncimpl bounds onBareFnare now stricter to catch more unsafety.- Major overhaul of feature flags. See README to view the changes.
§Added
UntypedBareFn*types that erase the bare function type entirely. Can be used to storeBareFn*wrappers of different types in a data structure.coverageunstable feature to support the-C instrument-coveragerustc flag.
§Changed
- Change thunk assembly magic numbers/sentinel values to sequences that are guaranteed to not be emitted by the compiler. Thanks to @Dasaav-dsv for the help.
- Move the arch/feature compile_error checks into the build script for better errors.
- Dual license under Apache-2.0 and MIT.
§[v2.4.0] - 2025-06-08
§Added
c_variadicfeature to add partial support for C variadic functions.
§[v2.3.0] - 2025-05-30
§Added
tuple_traitfeature to add acore::marker::Tuplebound toFnPtr::Args, allowing better interoperability with other Nightly features such asfn_traitsandunboxed_closures.
§Changed
- use
dep:crateoptional dependency toggles to prevent implicit dependency named features. This is technically a breaking change, but as these features are not documented I have decided to not bump the major version.
§[v2.2.0] - 2025-05-30
§Fixed
bundled_jit_allocshould now work oni686-pc-windows-msvcwithout linker errors
§Changed
- Bundled JIT allocator now uses
jit-allocator2, a maintained fork ofjit-allocatorwhich fixes a linker issue oni686-pc-windows-msvc.
§[v2.1.0] - 2025-05-29
§Added
from_ptrandto_ptrmethods toFnPtrtrait, to avoid relying ontransmute_copySendandSyncsupertraits onFnPtr- Support for
C-unwindand other-unwindcalling conventions
§[v2.0.1] - 2025-05-29
§Fixed
- Typos in documentation
§[v2.0.0] - 2025-05-29
First stable release. 1.0.0 was skipped as significant changes to the API were made since the last
release.
§Breaking changes
-
Changes to the trait system: bare function parameters now implement the
FnPtrtrait, which was carefully re-designed after attempting to build a function hooking library aroundclosure-ffi. This required changes to the way higher-kinded bare functions are supported; see the doc for the newbare_hrtb!proc macro to learn more. -
Moved traits to the
traitsmodule. All traits used are now fully documented, including theFn*Thunktraits used to generate the bare function thunks. This allows building a function-generic API that makes use of closure-ffi internally. -
Sweeping changes to the
BareFn*generic parameters. TheBareFn*types now type erase the closure, removing the need for thebare_dynmacro (which was not ideal as it would add an unnecessary layer of indirection). The DST used for type erasure is customizable via the type parameterSofBareFn*Any, withBareFn*andBareFn*Sendnow being type aliases for the common cases of no additional bounds and aSendbound on the closure, respectively. -
Removed the
bare_dyn!macro as it is no longer needed now thatBareFn*type-erases the closure. -
Replaced the
cc::hrtb!by thebare_hrtb!macro, which now works differently. See the doc for more info.
§Added
unstablefeature enabling support for functionality locked behind unstable Rust features.
§[v0.5.0] - 2025-04-29
§Breaking Changes
- Changes to the
JitAllocAPI:flush_instruction_cacheandprotect_jit_memorynow take&selfas an argument. This was necessary to makeJitAllocdyn-compatible as part of thecustom_jit_allocfeature.
§Added
- This changelog
- GH actions to automate publishing of the crate
custom_jit_allocfeature allowing downstream crates to implement their ownGlobalJitAlloc
§Fixed
- Stop using .text relocations in asm thunks for compatibility with platforms where they are not allowed
(e.g. MacOS). Relocations are still used when
target_arch = "x86". (Fixes #5)
§[v0.4.0] - 2025-04-27
§Added
- CI checks
- Implementations of
JitAlloconLazyLock(andspin::Lazyonno_std) for easy use with statics
§Fixed
- ARM/Thumb support
Modules§
- bare_
closure - Provides the
BareFnOnce,BareFnMutandBareFnwrapper types which allow closures to be called through context-free unsafe bare functions. - cc
- Defines calling convention marker types for use with
BareFnvariants. - jit_
alloc - Abstractions around allocators that provide dual-mapped memory with XOR protection rules (one RW view and one RX view) suitable for emitting code at runtime.
- prelude
- Common imports required to use
closure-ffi. - thunk_
factory - Provides factory functions for creating
FnThunkimplementations from a closure while preserving its Sync/Sendness. - traits
- Traits that
closure-ffiuses to power its functionality.
Macros§
- bare_
hrtb proc_macros - Declares a wrapper type around a higher-ranked bare function which can be used with BareFn and friends.
- global_
jit_ alloc global_jit_allocand non-default_jit_alloc - Defines a global
JitAllocimplementation whichGlobalJitAllocwill defer to.
Structs§
- Bare
FnAny - Wrapper around a
Fnclosure which exposes a bare function thunk that can invoke it without additional arguments. - Bare
FnMut Any - Wrapper around a
FnMutclosure which exposes a bare function thunk that can invoke it without additional arguments. - Bare
FnOnce Any - Wrapper around a
FnOnceclosure which exposes a bare function thunk that can invoke it without additional arguments. - JitAlloc
Error - Anonymous error that may be returned by
JitAllocimplementations whenJitAlloc::allocorJitAlloc::releasefail. - Untyped
Bare Fn - Type-erased wrapper around a
Fnclosure which exposes a pointer to a bare function thunk. - Untyped
Bare FnMut - Type-erased wrapper around a
FnMutclosure which exposes a pointer to a bare function thunk. - Untyped
Bare FnOnce - Type-erased wrapper around a
FnOnceclosure which exposes a pointer to a bare function thunk.
Traits§
- JitAlloc
- Generic allocator providing virtual memory suitable for emitting code at runtime.
Type Aliases§
- BareFn
- Wrapper around a
Fnclosure which exposes a bare function thunk that can invoke it without additional arguments. - Bare
FnMut - Wrapper around a
FnMutclosure which exposes a bare function thunk that can invoke it without additional arguments. - Bare
FnMut Sync - Wrapper around a
FnMutclosure which exposes a bare function thunk that can invoke it without additional arguments. - Bare
FnOnce - Wrapper around a
FnOnceclosure which exposes a bare function thunk that can invoke it without additional arguments. - Bare
FnOnce Sync - Wrapper around a
FnOnceclosure which exposes a bare function thunk that can invoke it without additional arguments. - Bare
FnSync - Wrapper around a
Fnclosure which exposes a bare function thunk that can invoke it without additional arguments.