description |
---|
Rust Foreign Function Interface |
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.
Here is a high-level overview of the steps involved in creating bindings between Rust and a C/C++ repo:
- Initial the Rust project
- 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. - 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.
- 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.
- 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.
- 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.
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.
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.
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.
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.
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++.
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.
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.
- Gather all C or C++ headers defining interfaces or datatypes you would like to use with Rust.
- Write a
bindings.h
file, which#include "..."
each of the files you gathered in step one. - Feed this
binding.sh
file, along with any compilation flash used to compile your code intobindgen
.- Tip: use
Builder.ctypes_prefix("cty")
/--ctypes-prefix=cty
andBuilder.use_core()
/--use-core
to make the generated code#![no_std]
compatible.
- Tip: use
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 asbindings.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 thecty
crate if your types in the generated bindings are prefixed withcty
.\
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 thecc
crate. For both of these steps, it is necessary to use abuild.rs
script.
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.
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.
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");
}
{% embed url="https://github.com/rust-lang/rust-bindgen" %} bindgen {% endembed %}
{% embed url="https://github.com/rust-lang/libc" %} libc for Rust {% endembed %}
{% embed url="https://github.com/rustformers/llm" %} llm(Rust binding C) {% endembed %}
{% embed url="https://docs.rust-embedded.org/book/interoperability/c-with-rust.html" %} The Embedded Rust Book {% endembed %}