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.

JD Cloud Developers
JD Cloud Developers
JD Cloud Developers
Master Rust‑C Interoperability: A Step‑by‑Step FFI Tutorial with Bindgen

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

Run 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.rs

Run 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_sys

Create 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 install

After 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_secp256k1

References

Rust FFI (C vs Rust) learning notes – PDF

bindgen official documentation

Rust FFI programming – bindgen usage example

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

FFICTutorialInteroperabilitybindgensecp256k1
JD Cloud Developers
Written by

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.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.