getditto / safer_ffi

Write safer FFI code in Rust without polluting it with unsafe code
http://getditto.github.io/safer_ffi
MIT License
915 stars 39 forks source link

safer-ffi-banner

CI guide docs-rs crates-io repository

What is safer_ffi?

safer_ffi is a framework that helps you write foreign function interfaces (FFI) without polluting your Rust code with unsafe { ... } code blocks while making functions far easier to read and maintain.

πŸ“š Read The User Guide πŸ“š

Prerequisites

Minimum Supported Rust Version: 1.66.1

Quickstart

Click to hide #### Small self-contained demo You may try working with the `examples/point` example embedded in the repo: ```bash git clone https://github.com/getditto/safer_ffi && cd safer_ffi (cd examples/point && make) ``` Otherwise, to start using `::safer_ffi`, follow the following steps: ### Crate layout #### Step 1: `Cargo.toml` Edit your `Cargo.toml` like so: ```toml [package] name = "crate_name" version = "0.1.0" edition = "2021" [lib] crate-type = [ "staticlib", # Ensure it gets compiled as a (static) C library # "cdylib", # If you want a shared/dynamic C library (advanced) "lib", # For `generate-headers` and other downstream rust dependents # such as integration `tests/`, doctests, and `examples/` ] [[bin]] name = "generate-headers" required-features = ["headers"] # Do not build unless generating headers. [dependencies] # Use `cargo add` or `cargo search` to find the latest values of x.y.z. # For instance: # cargo add safer-ffi safer-ffi.version = "x.y.z" safer-ffi.features = [] # you may add some later on. [features] # If you want to generate the headers, use a feature-gate # to opt into doing so: headers = ["safer-ffi/headers"] ``` - Where `"x.y.z"` ought to be replaced by the last released version, which you can find by running `cargo search safer-ffi`. - See the [dedicated chapter on `Cargo.toml`][cargo-toml] for more info. #### Step 2: `src/lib.rs` Then, to export a Rust function to FFI, add the [`#[derive_ReprC]`][derive_ReprC] and [`#[ffi_export]`][ffi_export] attributes like so: ```rust ,no_run use ::safer_ffi::prelude::*; /// A `struct` usable from both Rust and C #[derive_ReprC] #[repr(C)] #[derive(Debug, Clone, Copy)] pub struct Point { x: f64, y: f64, } /* Export a Rust function to the C world. */ /// Returns the middle point of `[a, b]`. #[ffi_export] fn mid_point(a: &Point, b: &Point) -> Point { Point { x: (a.x + b.x) / 2., y: (a.y + b.y) / 2., } } /// Pretty-prints a point using Rust's formatting logic. #[ffi_export] fn print_point(point: &Point) { println!("{:?}", point); } // The following function is only necessary for the header generation. #[cfg(feature = "headers")] // c.f. the `Cargo.toml` section pub fn generate_headers() -> ::std::io::Result<()> { ::safer_ffi::headers::builder() .to_file("rust_points.h")? .generate() } ``` - See [the dedicated chapter on `src/lib.rs`][lib-rs] for more info. #### Step 3: `src/bin/generate-headers.rs` ```rust ,ignore fn main() -> ::std::io::Result<()> { ::crate_name::generate_headers() } ``` ### Compilation & header generation ```bash # Compile the C library (in `target/{debug,release}/libcrate_name.ext`) cargo build # --release # Generate the C header cargo run --features headers --bin generate-headers ``` - See [the dedicated chapter on header generation][header-generation] for more info.
Generated C header (rust_points.h) ```C /*! \file */ /******************************************* * * * File auto-generated by `::safer_ffi`. * * * * Do not manually edit this file. * * * *******************************************/ #ifndef __RUST_CRATE_NAME__ #define __RUST_CRATE_NAME__ #ifdef __cplusplus extern "C" { #endif #include #include /** \brief * A `struct` usable from both Rust and C */ typedef struct Point { /** */ double x; /** */ double y; } Point_t; /** \brief * Returns the middle point of `[a, b]`. */ Point_t mid_point ( Point_t const * a, Point_t const * b); /** \brief * Pretty-prints a point using Rust's formatting logic. */ void print_point ( Point_t const * point); #ifdef __cplusplus } /* extern \"C\" */ #endif #endif /* __RUST_CRATE_NAME__ */ ``` ___
## Testing it from C Here is a basic example to showcase FFI calling into our exported Rust functions: ### `main.c` ```C #include #include "rust_points.h" int main (int argc, char const * const argv[]) { Point_t a = { .x = 84, .y = 45 }; Point_t b = { .x = 0, .y = 39 }; Point_t m = mid_point(&a, &b); print_point(&m); return EXIT_SUCCESS; } ``` ### Compilation command ```bash cc -o main{,.c} -L target/debug -l crate_name -l{pthread,dl,m} # Now feel free to run the compiled binary ./main ``` -
Note regarding the extra -l… flags. Those vary based on the version of the Rust standard library being used, and the system being used to compile it. In order to reliably know which ones to use, `rustc` itself ought to be queried for it. Simple command: ```bash rustc --crate-type=staticlib --print=native-static-libs -&1 | sed -nE 's/^note: native-static-libs: (.*)/\1/p' ``` Ideally, you would not query for this information _in a vacuum_ (_e.g._, `/dev/null` file being used as input Rust code just above), and rather, would apply it for your actual code being compiled: ```bash cargo rustc -q -- --print=native-static-libs \ 2>&1 | sed -nE 's/^note: native-static-libs: (.*)/\1/p' ``` And if you really wanted to polish things further, you could use the JSON-formatted compiler output (this, for instance, avoids having to redirect `stderr`). But then you'd have to use a JSON parser, such as `jq`: ```bash RUST_STDLIB_DEPS=$(set -eo pipefail && \ cargo rustc \ --message-format=json \ -- --print=native-static-libs \ | jq -r ' select (.reason == "compiler-message") | .message.message ' | sed -nE 's/^native-static-libs: (.*)/\1/p' \ ) ``` and then use: ```bash cc -o main{,.c} -L target/debug -l crate_name ${RUST_STDLIB_DEPS} ```

which does output:

Point { x: 42.0, y: 42.0 }

πŸš€πŸš€

Development

Tests

safer-ffi includes three different tests suites that can be run.

# In the project root:
cargo test

# FFI tests

make -C ffi_tests

# JavaScript tests

make -C js_tests

# Running the JS tests also gives you instructions for running browser tests.
# Run this command in the `js_tests` directory, open a browser and navigate to
# http://localhost:13337/
wasm-pack build --target web && python3 -m http.server 13337