πŸ›ƒRust FFI

Rust Foreign Function Interface

Overview

Using Rust to create bindings for a C/C++ repository is definitely possible and can provide the benefits of Rust's memory safety and performance while leveraging existing C/C++ code.

Proposal

Here is a high-level overview of the steps involved in creating bindings between Rust and a C/C++ repo:

  1. Initial the Rust project

  2. Define Rust FFI declarations: Identify the C/C++ functions, types, and data structures you want to interact with from Rust. Declare their counterparts in Rust using the extern keyword and appropriate annotations to specify the C/C++ function signatures, data types, and any necessary safety guarantees.

  3. Link the C/C++ code: Specify the C/C++ codebase you want to bind to in your Rust project's build configuration. This involves specifying the necessary C/C++ headers and libraries for Rust to link against.

  4. Write Rust wrapper code: Create Rust code that wraps the FFI declarations. This wrapper code acts as a bridge between the Rust code and the underlying C/C++ functionality. It handles the conversion of data between the Rust and C/C++ representations, making the interaction seamless.

  5. Build and test the Rust bindings: Use Cargo to build the Rust project and ensure that the bindings are correctly generated. Write tests to verify the behavior and correctness of the Rust bindings, ensuring they match the expected behavior of the original C/C++ code.

  6. Integrate the Rust bindings: Once the Rust bindings are working as expected, you can integrate them into your Rust application or library. This allows you to utilize the functionality provided by the C/C++ code within your Rust codebase.

Libraries

There are some libraries and frameworks available in the Rust ecosystem that can help in creating bindings, such as:

  • libc

  • bindgen

They can automate parts of the process by generating Rust FFI declarations based on C/C++ headers.

Details for the different libraries

libc

It provides Rust bindings to the C standard library (libc). It includes definitions for C types, constants, and functions that are commonly used in C programming.

It is useful when you need to interact with the C standard library or C APIs directly from Rust. It simplifies the process of working with C types and provides a common interface for accessing C functionality.

bindgen

It is a Rust library that generates Rust FFI bindings by parsing C/C++ header files.

It analyzes the headers and automatically generates Rust code that corresponds to the C/C++ declarations, including:

  • struct definitions

  • function signatures

  • constants

It saves you from manually writing the FFI declarations and ensures that the generated Rust code matches the C/C++ interface. It's commonly used when you want to create Rust bindings for a C/C++ library.

Summary

According to the above, libc helps with accessing C types and functions, while bindgen automates the process of generating Rust FFI bindings based on C/C++ headers.

Tutorials for Rust binding C

Using C or C++ inside of a Rust project consists of two major parts:

  • Wrapping the exposed C API for use with Rust

  • Building your C or C++ code to be integrated with the Rust code

As C++ does not have a stable ABI for the Rust compiler to target, it is recommended to use the C ABI when combining Rust with C or C++.

Defining the interface

It is necessary to define (in Rust) what data types and function signatures exist in the linked code. In C or C++, you would include a header(.h or .hpp) file which defines this data.

Wrapping C functions and Datatypes

We will cover manually translating these definitions from C/C++ to Rust. Libraries written in C or C++ will provide a header file defining all types and functions used in public interfaces. It may looks like this:

/* File: cool.h */
typedef struct CoolStruct {
    int x;
    int y;
} CoolStruct;

void cool_function(int i, char c, CoolStruct* cs);

When translated to Rust, this interface would look as such:

/* File: cool_bindings.rs */
#[repr(C)]
pub struct CoolStruct {
    pub x: cty::c_int,
    pub y: cty::c_int,
}

extern "C" {
    pub fn cool_function(
        i: cty::c_int,
        c: cty::c_char,
        cs: *mut CoolStruct
    );
}

By default, Rust does not guarantee order, padding, or the size of data included in a struct. In order to guarantee compatibility with the code, we use #repr(C)]attribute that instructs the Rust compiler to always use the same rules C does for organizing data with a struct.

#[repr(C)]
pub struct CoolStruct { ... }

Due to the flexibility of how C or C++ defines and int or char , it is recommended to use primitive data types defined in cty, which will map types from C to types in Rust.

pub x: cty::c_int,
pub y: cty::c_int,

The statement defines the signature of a function that uses the C ABI called cool_function

extern "C" { pub fn cool_function( ... ); }

Since C does not have a concept of Rust's references, which would like this &mut CoolStruct we instead have a raw pointer but this pointer is unsafe, and the pointer may in fact be a null pointer.

Automatically generating the interface

  1. Gather all C or C++ headers defining interfaces or datatypes you would like to use with Rust.

  2. Write a bindings.h file, which #include "..." each of the files you gathered in step one.

  3. Feed this binding.sh file, along with any compilation flash used to compile your code into bindgen.

    1. Tip: use Builder.ctypes_prefix("cty") / --ctypes-prefix=cty and Builder.use_core() / --use-core to make the generated code #![no_std] compatible.

  4. bindgen will produce the generated Rust code to the output of the terminal window. This file may be piped to a file in your project, such as bindings.rs. You may use this file in your Rust project to interact with C/C++ code compiled and linked as an external library. Tip: don't forget to use the cty crate if your types in the generated bindings are prefixed with cty.\

Building your C/C++ code

As the Rust compiler does not directly know how to compile C or C++ code (or code from any other language, which presents a C interface), it is necessary to compile your non-Rust code ahead of time.

For embedded projects, this most commonly means compiling the C/C++ code to a static archive (such as cool-library.a), which can then be combined with your Rust code at the final linking step.

Using already distributed as a static archive

  • Just convert the provided interface header file as described above, and include the static archive at compile/link time.

Using code exists as a source project

  • it will be necessary to compile your C/C++ code to a static library, either by triggering your existing build system (such as make, CMake, etc.), or by porting the necessary compilation steps to use a tool called the cc crate. For both of these steps, it is necessary to use a build.rs script.

Rust build.rx build scripts

A build.rs script is a file written in Rust syntax, that is executed on your compilation machine, AFTER dependencies of your project have been built, but BEFORE your project is built.

build.rs scripts are useful for generating code (such as via bindgen), calling out to external build systems such as Make, or directly compiling C/C++ through the use of the cc crate.

Triggering external build systems

For projects with complex external projects or build systems, it may be easiest to use std::process::Command to "shell out" to your other build systems by traversing relative paths, calling a fixed command (such as make library), and then copying the resulting static library to the proper location in the target build directory.

Targeting a no_std embedded platform means your build.rs executes only on machines compiling your crate.

Building C/C++ code with the cc crate

For projects with limited dependencies or complexity, it may be easier to instead utilize the cc crate, which provides an idiomatic Rust interface to the compiler provided by the host.

For example below, cargo build will compile and execute it before the build of the package. A static archive named libfoo.a is generated and placed in the target directory.

fn main() {
    cc::Build::new()
        .file("src/foo.c")
        .compile("foo");
}

Repos

Last updated