Master Rust‑C Interoperability: A Step‑by‑Step FFI Tutorial with Bindgen
This article walks through creating Rust projects that call C functions directly, generate bindings with bindgen, and wrap a complex C library (secp256k1), providing complete commands, Cargo configurations, and build scripts for seamless Rust‑C integration.
Scenario 1 – Calling C Code
Create a new binary crate: cargo new --bin ffi_sample Add the following to Cargo.toml:
[package]
name = "ffi_sample"
version = "0.1.0"
edition = "2021"
build = "build.rs"
[build-dependencies]
cc = "1.0.79"
[dependencies]
libc = "0.2.146"
libloading = "0.8.0"Write a simple C function sample.c:
int add(int a,int b){
return a+b;
}In src/main.rs declare the external function and call it:
use std::os::raw::c_int;
#[link(name = "sample")]
extern "C" {
fn add(a: c_int, b: c_int) -> c_int;
}
fn main() {
let r = unsafe { add(2, 18) };
println!("{:?}", r);
}Compile the C file with a build script build.rs:
fn main() {
cc::Build::new().file("sample.c").compile("sample");
}Project layout:
.
├── Cargo.lock
├── Cargo.toml
├── build.rs
├── sample.c
└── src
└── main.rsRun with cargo run to see the result.
Scenario 2 – Using bindgen to Generate Bindings
Update Cargo.toml to add bindgen:
[build-dependencies]
cc = "1.0.79"
bindgen = "0.65.1"Create header files:
// sample.h
int add(int a, int b); // wrapper.h
#include "sample.h";Rewrite build.rs to compile the C source as a shared library and generate Rust bindings:
use std::path::PathBuf;
fn main() {
println!("cargo:rerun-if-changed=sample.c");
cc::Build::new()
.file("sample.c")
.shared_flag(true)
.compile("sample.so");
println!("cargo:rustc-link-lib=sample.so");
println!("cargo:rerun-if-changed=sample.h");
let bindings = bindgen::Builder::default()
.header("wrapper.h")
.parse_callbacks(Box::new(bindgen::CargoCallbacks))
.generate()
.expect("Unable to generate bindings");
let out_path = PathBuf::from("bindings");
bindings
.write_to_file(out_path.join("sample_bindings.rs"))
.expect("Couldn't write bindings!");
}In src/main.rs include the generated bindings:
include!("../bindings/sample_bindings.rs");
fn main() {
let r = unsafe { add(2, 18) };
println!("{:?}", r);
}Directory tree after changes:
.
├── Cargo.lock
├── Cargo.toml
├── build.rs
├── bindings
│ └── sample_bindings.rs
├── sample.c
├── sample.h
├── wrapper.h
└── src
└── main.rsRun with cargo run to verify the binding works.
Scenario 3 – Wrapping a C Library (secp256k1)
Create a library crate: cargo new --lib wrapper_secp256k1 Configure Cargo.toml:
[package]
name = "wrapper_secp256k1"
version = "0.1.0"
edition = "2021"
[build-dependencies]
cc = "1.0.79"
bindgen = "0.65.1"
[dependencies]Add the secp256k1 source as a submodule:
cd wrapper_secp256k1
git submodule add https://github.com/bitcoin-core/secp256k1 wrapper_secp256k1/secp256k1_sysCreate wrapper.h that includes the library header:
#include "secp256k1_sys/secp256k1/include/secp256k1.h"Write a build script to generate bindings:
use std::path::PathBuf;
fn main() {
println!("cargo:rustc-link-lib=secp256k1");
println!("cargo:rerun-if-changed=wrapper.h");
let bindings = bindgen::Builder::default()
.header("wrapper.h")
.parse_callbacks(Box::new(bindgen::CargoCallbacks))
.generate()
.expect("Unable to generate bindings");
let out_path = PathBuf::from("bindings");
bindings
.write_to_file(out_path.join("bindings.rs"))
.expect("Couldn't write bindings!");
}In src/lib.rs include the bindings and add a test:
include!("../bindings/secp256k1.rs");
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_create_pubkey() {
// secp256k1 returns a public key
let mut pubkey: secp256k1_pubkey = secp256k1_pubkey { data: [0; 64] };
let prikey: u8 = 1;
unsafe {
let context = secp256k1_context_create(SECP256K1_CONTEXT_SIGN);
assert!(!context.is_null());
let ret = secp256k1_ec_pubkey_create(&*context, &mut pubkey, &prikey);
assert_eq!(ret, 1);
}
}
}The test initially fails because the linker cannot find -lsecp256k1. Compile the library manually:
cd secp256k1_sys
./autogen.sh
./configure
make
make installAfter installation, cargo test -p wrapper_secp256k1 succeeds.
Note: a ready‑made Rust wrapper exists at rust‑secp256k1 ; this example demonstrates the low‑level wrapping process.
Full source code for the three projects is available on GitHub:
ffi_sample
wrapper_secp256k1
Typical workflow to run the examples:
git clone https://github.com/jiashiwen/wenpanrust
cd wenpanrust
git submodule init
git submodule update
# Build and run ffi_sample
cargo run -p ffi_sample
# Build and test wrapper_secp256k1
cd wrapper_secp256k1/secp256k1_sys
./autogen.sh && ./configure && make && make install
cd ..
cargo test -p wrapper_secp256k1References
Rust FFI (C vs Rust) learning notes – PDF
bindgen official documentation
Rust FFI programming – bindgen usage example
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
JD Cloud Developers
JD Cloud Developers (Developer of JD Technology) is a JD Technology Group platform offering technical sharing and communication for AI, cloud computing, IoT and related developers. It publishes JD product technical information, industry content, and tech event news. Embrace technology and partner with developers to envision the future.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
