Crate closure_ffi

Crate closure_ffi 

Source
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.

Targetsafe_jitdefault_jit_allocCI 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, system and efiapi.
  • The Rust calling convention. Note that this calling convention is unstable and ABI compatibility is only guaranteed within a particular binary!
  • On x64 Windows, the win64 calling convention.
  • On x86 Windows, the stdcall, cdecl, fastcall and thiscall calling conventions.
  • On non-Windows x64, the sysv64 calling convention.
  • On ARM (not Aarch64), the aapcs calling 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, thin CStr refs, 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 CStr through the bare_hrtb! macro (requires the proc_macros feature).

  • Variadic C functions e.g. unsafe extern "C" printf(*const c_char, ...) are supported when the c_variadic crate and nightly feature are enabled.

§Features Flags

The crate comes with the following feature flags:

§Stable

  • 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. This is necessary to construct BareFn types without explicitly passing an allocator.

  • 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 not all configurations are supported. See the Targets section for details.

  • 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).

  • 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 having safe_jit enabled is inherently unsafe, the crate will refuse to build unless this feature is enabled to prevent accidentally forgetting safe_jit on --no-default-feature builds.

§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 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.

§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

  • tremwil: Library author and maintainer
  • Dasaav: lock (x14) push eax x86 magic byte sequence idea

§Changelog

§[v5.0.1] - 2025-10-28

§Fixed

  • Use forked iced-x86 crate to avoid conflicts with dependents using it with the std feature. This is temporary until a new iced version is released to allow std and no_std features 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_jit feature, compiling with --no-default-features will now error unless the no_safe_jit feature is explicitly enabled to prevent accidentally forgetting to enable safe_jit.
  • Strengthened trait bounds on FnPtr::CC to make some APIs more ergonomic. This is technically a breaking change but is realistically harmless, as FnPtr should not be implemented by the end user.
  • Changed the JitAlloc blanket impl from &J for all J: JitAlloc to any type implementing Deref<Target = J>. This is more general and avoids having to write forwarding impls when putting a JitAlloc in a LazyLock, for example, but may break some downstream JitAlloc wrappers.

§Added

  • Thunk generation is now fully safe thanks to the safe_jit feature, 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 efiapi and Rust calling conventions.

§Fixed

  • global_jit_alloc macro 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 + 'c bounds on FnPtr::Args and FnPtr::Ret
  • Regression in the expressivity of bare_hrtb!(): Now requires a 'static bound on certain generic parameters
  • removed zero-variant enum from FnPtr::Args for extern variaric functions to be able to implement the new trait functions. FnPtr::call now const panics instead of being impossible to call for them.

§Added

  • FnPtr::make_*_thunk functions that can create a Fn*Thunk implementation from a closure with tuple-packed arguments.
  • FnOnceThunk::call_once, FnMutThunk::call_mut and FnThunk::call for invoking the underlying closure with tuple-packed arguments.
  • thunk_factory module for creating Fn*Thunk implementations that satisfy combinations of Send and Sync bounds.

§Fixed

  • libc dependency not compatible with no_std on Linux ARM targets

§[v3.0.1] - 2025-06-21

§Fixed

  • docs.rs build

§[v3.0.0] - 2025-06-20

§Breaking Changes

  • ToBoxedUnsize has been renamed to ToBoxedDyn is now an unsafe trait. See the documentation for the new invariants.
  • Send and Sync impl bounds on BareFn are 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 store BareFn* wrappers of different types in a data structure.
  • coverage unstable feature to support the -C instrument-coverage rustc 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_variadic feature to add partial support for C variadic functions.

§[v2.3.0] - 2025-05-30

§Added

  • tuple_trait feature to add a core::marker::Tuple bound to FnPtr::Args, allowing better interoperability with other Nightly features such as fn_traits and unboxed_closures.

§Changed

  • use dep:crate optional 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_alloc should now work on i686-pc-windows-msvc without linker errors

§Changed

  • Bundled JIT allocator now uses jit-allocator2, a maintained fork of jit-allocator which fixes a linker issue on i686-pc-windows-msvc.

§[v2.1.0] - 2025-05-29

§Added

  • from_ptr and to_ptr methods to FnPtr trait, to avoid relying on transmute_copy
  • Send and Sync supertraits on FnPtr
  • Support for C-unwind and other -unwind calling 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 FnPtr trait, which was carefully re-designed after attempting to build a function hooking library around closure-ffi. This required changes to the way higher-kinded bare functions are supported; see the doc for the new bare_hrtb! proc macro to learn more.

  • Moved traits to the traits module. All traits used are now fully documented, including the Fn*Thunk traits 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. The BareFn* types now type erase the closure, removing the need for the bare_dyn macro (which was not ideal as it would add an unnecessary layer of indirection). The DST used for type erasure is customizable via the type parameter S of BareFn*Any, with BareFn* and BareFn*Send now being type aliases for the common cases of no additional bounds and a Send bound on the closure, respectively.

  • Removed the bare_dyn! macro as it is no longer needed now that BareFn* type-erases the closure.

  • Replaced the cc::hrtb! by the bare_hrtb! macro, which now works differently. See the doc for more info.

§Added

  • unstable feature enabling support for functionality locked behind unstable Rust features.

§[v0.5.0] - 2025-04-29

§Breaking Changes

  • Changes to the JitAlloc API: flush_instruction_cache and protect_jit_memory now take &self as an argument. This was necessary to make JitAlloc dyn-compatible as part of the custom_jit_alloc feature.

§Added

  • This changelog
  • GH actions to automate publishing of the crate
  • custom_jit_alloc feature allowing downstream crates to implement their own GlobalJitAlloc

§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 JitAlloc on LazyLock (and spin::Lazy on no_std) for easy use with statics

§Fixed

  • ARM/Thumb support

Modules§

bare_closure
Provides the BareFnOnce, BareFnMut and BareFn wrapper types which allow closures to be called through context-free unsafe bare functions.
cc
Defines calling convention marker types for use with BareFn variants.
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 FnThunk implementations from a closure while preserving its Sync/Sendness.
traits
Traits that closure-ffi uses to power its functionality.

Macros§

bare_hrtbproc_macros
Declares a wrapper type around a higher-ranked bare function which can be used with BareFn and friends.
global_jit_allocglobal_jit_alloc and non-default_jit_alloc
Defines a global JitAlloc implementation which GlobalJitAlloc will defer to.

Structs§

BareFnAny
Wrapper around a Fn closure which exposes a bare function thunk that can invoke it without additional arguments.
BareFnMutAny
Wrapper around a FnMut closure which exposes a bare function thunk that can invoke it without additional arguments.
BareFnOnceAny
Wrapper around a FnOnce closure which exposes a bare function thunk that can invoke it without additional arguments.
JitAllocError
Anonymous error that may be returned by JitAlloc implementations when JitAlloc::alloc or JitAlloc::release fail.
UntypedBareFn
Type-erased wrapper around a Fn closure which exposes a pointer to a bare function thunk.
UntypedBareFnMut
Type-erased wrapper around a FnMut closure which exposes a pointer to a bare function thunk.
UntypedBareFnOnce
Type-erased wrapper around a FnOnce closure 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 Fn closure which exposes a bare function thunk that can invoke it without additional arguments.
BareFnMut
Wrapper around a FnMut closure which exposes a bare function thunk that can invoke it without additional arguments.
BareFnMutSync
Wrapper around a FnMut closure which exposes a bare function thunk that can invoke it without additional arguments.
BareFnOnce
Wrapper around a FnOnce closure which exposes a bare function thunk that can invoke it without additional arguments.
BareFnOnceSync
Wrapper around a FnOnce closure which exposes a bare function thunk that can invoke it without additional arguments.
BareFnSync
Wrapper around a Fn closure which exposes a bare function thunk that can invoke it without additional arguments.