cbindgen
cbindgen
Level 13 — Unsafe Rust & FFI An official Rust tool and library that parses Rust source code (
#[no_mangle] pub extern "C" fnand#[repr(C)]structs) to automatically generate C and C++ header files (.h/.hpp) for exposing Rust libraries to foreign C/C++ projects.
1. Prerequisites
- FFI (Foreign Function Interface) — Understanding cross-language binary interoperability.
extern "C"— Exporting Rust functions using C ABI conventions.bindgen— The counterpart tool (generating Rust bindings from C headers).
2. Term Category
Rust FFI Tooling (automatic Rust crate to C header generator): cbindgen is the inverse counterpart to bindgen. When developing a Rust crate intended to be compiled into a static (.a) or dynamic (.so / .dylib / .dll) native library for consumption by C, C++, Swift, or C# projects, cbindgen parses your Rust code AST and outputs 100% accurate C/C++ header files (my_library.h).
3. Explanation
(1) Design Motivation — "Why did we design this?"
When building a high-performance Rust library meant to replace or augment legacy C/C++ modules:
- You write exported functions tagged with
#[no_mangle] pub extern "C" fnand C-compatible structs tagged with#[repr(C)]. - To allow foreign C/C++ developers to call your Rust library, they need a C header file (
.h) declaring struct layouts, function prototypes, and constant macros matching your Rust library's exact binary ABI. - Writing C headers by hand is tedious and error-prone. If you change a struct field type in Rust from
u32tou64but forget to update the hand-written C.hheader, the foreign C project will experience binary layout corruption and Undefined Behavior.
cbindgen solves this by reversing the translation pipeline. It inspects your Rust codebase's Abstract Syntax Tree (AST) using syn and generates idiomatic C/C++ header files matching your exported Rust API contracts:
- Rust
#[repr(C)] pub struct Point { pub x: f32, pub y: f32 }Ctypedef struct { float x; float y; } Point; - Rust
#[no_mangle] pub extern "C" fn add(a: i32, b: i32) -> i32Cint32_t add(int32_t a, int32_t b);
(2) Reality Metaphor
Imagine an Automated Exporter Printing Press:
bindgenis an Importer: it reads foreign C manuals (C headers) and translates them into native instructions for domestic workers (Rust bindings).cbindgenis an Exporter: it scans domestic product blueprints (Rust#[repr(C)]&#[no_mangle]code) and automatically prints standardized international export user manuals (C/C++.hheader files) for foreign distributors (C/C++ developers).
(3) Code Examples
Short Snippet (Sample Rust Export Code vs cbindgen Generated C Header Output)
Input Rust Code (src/lib.rs):
use std::os::raw::c_int;
/// A C-compatible struct representing an entity's 2D position.
#[repr(C)]
pub struct Vector2D {
pub x: f32,
pub y: f32,
}
/// Multiplies a 2D vector by a scalar value.
#[no_mangle]
pub extern "C" fn scale_vector(vec: *mut Vector2D, scalar: f32) {
if let Some(v) = unsafe { vec.as_mut() } {
v.x *= scalar;
v.y *= scalar;
}
}
Output C Header Generated by cbindgen (my_library.h):
#ifndef MY_LIBRARY_H
#define MY_LIBRARY_H
#include <stdint.h>
#include <stdbool.h>
/**
* A C-compatible struct representing an entity's 2D position.
*/
typedef struct Vector2D {
float x;
float y;
} Vector2D;
#ifdef __cplusplus
extern "C" {
#endif // __cplusplus
/**
* Multiplies a 2D vector by a scalar value.
*/
void scale_vector(struct Vector2D *vec, float scalar);
#ifdef __cplusplus
} // extern "C"
#endif // __cplusplus
#endif /* MY_LIBRARY_H */
Fuller Example (Configuring cbindgen inside build.rs)
// Cargo.toml setup:
// [lib]
// crate-type = ["cdylib", "rlib"]
//
// [build-dependencies]
// cbindgen = "0.26"
// build.rs (Automatically generates header file on `cargo build`)
use std::env;
fn main() {
let crate_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
// Generate C header bindings for current crate
cbindgen::Builder::new()
.with_crate(crate_dir)
.with_language(cbindgen::Language::C)
.generate()
.expect("Unable to generate C header bindings")
.write_to_file("include/my_rust_library.h");
}
4. bindgen vs cbindgen Comparison
| Feature | bindgen | cbindgen |
|---|---|---|
| Primary Direction | C/C++ Rust | Rust C/C++ |
| Input Source | C/C++ Header Files (.h / .hpp) | Rust Source Code (src/*.rs) |
| Output Target | Rust FFI Bindings (bindings.rs) | C/C++ Header File (header.h) |
| Primary Requirement | System Clang / LLVM installed | Pure Rust tool (syn AST parser) |
| Use Case | Using existing C libraries inside Rust | Exposing Rust libraries to C/C++ projects |
4. Common Mistakes & Pitfalls
Mistake 1: Forgetting #[no_mangle] or pub extern "C"
The mistake: Declaring a Rust function pub fn calculate() or extern "C" fn calculate() without #[no_mangle], and expecting cbindgen to export it into the header.
Why it's wrong: cbindgen only generates C prototypes for functions that are both publicly exported (pub), marked with C ABI (extern "C"), and explicitly annotated with #[no_mangle].
Incorrect:
// ❌ Ignored by cbindgen! Missing `#[no_mangle]` and `extern "C"`!
pub fn process_data(val: i32) -> i32 { val + 1 }
Fix:
// Correct: cbindgen detects this function and outputs C prototype
#[no_mangle]
pub extern "C" fn process_data(val: i32) -> i32 { val + 1 }
Mistake 2: Forgetting #[repr(C)] on Structs in Exported Functions
The mistake: Exporting an extern "C" function that accepts a Rust struct parameter missing the #[repr(C)] layout attribute.
Why it's wrong: If a struct lacks #[repr(C)], cbindgen will emit a warning or skip generating a C struct definition because Rust's default layout #[repr(Rust)] is incompatible with C.
Incorrect:
// ❌ Missing #[repr(C)]! cbindgen warns that struct is not C-safe.
pub struct Color { pub r: u8, pub g: u8, pub b: u8 }
#[no_mangle]
pub extern "C" fn set_color(c: Color) { ... }
Fix:
// Correct: `#[repr(C)]` allows cbindgen to output `typedef struct Color { ... } Color;`
#[repr(C)]
pub struct Color { pub r: u8, pub g: u8, pub b: u8 }
#[no_mangle]
pub extern "C" fn set_color(c: Color) { ... }
Mistake 3: Omitting crate-type = ["cdylib"] in Cargo.toml
The mistake: Building a Rust library intended for C consumption without setting crate-type = ["cdylib"] or ["staticlib"] in Cargo.toml.
Why it's wrong: Standard Rust crates output .rlib files (Rust-specific library format). C compilers cannot link .rlib files; C linkers require native shared objects (.so / .dylib / .dll) or static archives (.a / .lib).
Incorrect:
# Cargo.toml (Default produces only .rlib)
[package]
name = "my_lib"
Fix:
# Cargo.toml (Produces native C-compatible dynamic & static libraries)
[lib]
crate-type = ["cdylib", "staticlib"]
5. Practice Exercises
Exercise 1: Opaque Pointer Handles & Safe Lifecycle Management in Embedded C FFI
Scenario: Problem Statement:
You are developing an Exponential Moving Average (EMA) signal filter module in Rust to be integrated into an embedded C system (e.g., an industrial sensor gateway). The legacy C runtime needs to create a filter instance, feed raw sensor samples, read filtered results, and destroy the instance when finished.
To protect internal state from direct C mutation and prevent struct binary layout mismatches across cross-compilers, Rust must export the filter instance via an opaque pointer handle (*mut EmaFilter). All exported functions must return a C-compatible status enum (#[repr(C)] pub enum FilterResult).
Requirements: Write the complete Rust code that:
- Defines
FilterResult(#[repr(C)]) with variantsOk = 0,NullPointer = -1, andInvalidParameter = -2. - Defines
EmaFiltercontainingalpha: f32,current_value: f32, andsample_count: u64. - Exports
ema_filter_new,ema_filter_process, andema_filter_freemarked with#[no_mangle] pub extern "C". - Includes unit tests with
assert_eq!andassert!verifying null pointer protection, parameter validation, filter logic, and memory destruction.
Answer
Rust Implementation (src/lib.rs):
Implementation
use std::ptr;
/// C-compatible status codes returned across FFI boundary.
#[repr(C)]
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
pub enum FilterResult {
Ok = 0,
NullPointer = -1,
InvalidParameter = -2,
}
/// Internal EMA filter state.
/// Marking with #[repr(C)] allows cbindgen to declare an opaque handle: typedef struct EmaFilter EmaFilter;
#[repr(C)]
pub struct EmaFilter {
alpha: f32,
current_value: f32,
sample_count: u64,
}
/// Allocates a new EmaFilter instance on the heap and writes the handle pointer to `out_handle`.
///
/// # Safety
/// `out_handle` must be a valid, non-null pointer to a `*mut EmaFilter` variable in C memory.
#[no_mangle]
pub unsafe extern "C" fn ema_filter_new(
alpha: f32,
out_handle: *mut *mut EmaFilter,
) -> FilterResult {
if out_handle.is_null() {
return FilterResult::NullPointer;
}
if alpha <= 0.0 || alpha > 1.0 {
return FilterResult::InvalidParameter;
}
let filter = Box::new(EmaFilter {
alpha,
current_value: 0.0,
sample_count: 0,
});
// Relinquish Box ownership and return raw pointer to C
*out_handle = Box::into_raw(filter);
FilterResult::Ok
}
/// Updates the filter with a new raw sample and writes the updated output to `out_filtered`.
///
/// # Safety
/// `handle` must be a valid pointer generated by `ema_filter_new`.
/// `out_filtered` must point to valid, writable f32 memory.
#[no_mangle]
pub unsafe extern "C" fn ema_filter_process(
handle: *mut EmaFilter,
raw_sample: f32,
out_filtered: *mut f32,
) -> FilterResult {
if handle.is_null() || out_filtered.is_null() {
return FilterResult::NullPointer;
}
let filter = &mut *handle;
if filter.sample_count == 0 {
filter.current_value = raw_sample;
} else {
filter.current_value = filter.alpha * raw_sample + (1.0 - filter.alpha) * filter.current_value;
}
filter.sample_count += 1;
*out_filtered = filter.current_value;
FilterResult::Ok
}
/// Destroys an EmaFilter instance and deallocates its heap memory.
///
/// # Safety
/// `handle` must be a valid pointer allocated by `ema_filter_new`, or null.
#[no_mangle]
pub unsafe extern "C" fn ema_filter_free(handle: *mut EmaFilter) -> FilterResult {
if handle.is_null() {
return FilterResult::NullPointer;
}
// Reclaim Box ownership so Rust drops it and frees memory
let _ = Box::from_raw(handle);
FilterResult::Ok
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_ema_filter_lifecycle() {
unsafe {
let mut handle: *mut EmaFilter = ptr::null_mut();
// 1. Verify null handle validation
let res_null = ema_filter_new(0.5, ptr::null_mut());
assert_eq!(res_null, FilterResult::NullPointer);
// 2. Verify invalid alpha parameter
let res_invalid = ema_filter_new(1.5, &mut handle);
assert_eq!(res_invalid, FilterResult::InvalidParameter);
// 3. Successful allocation
let res_ok = ema_filter_new(0.5, &mut handle);
assert_eq!(res_ok, FilterResult::Ok);
assert!(!handle.is_null());
// 4. Sample 1: first sample sets initial current_value
let mut output: f32 = 0.0;
let res_p1 = ema_filter_process(handle, 10.0, &mut output);
assert_eq!(res_p1, FilterResult::Ok);
assert_eq!(output, 10.0);
// 5. Sample 2: 0.5 * 20.0 + 0.5 * 10.0 = 15.0
let res_p2 = ema_filter_process(handle, 20.0, &mut output);
assert_eq!(res_p2, FilterResult::Ok);
assert_eq!(output, 15.0);
// 6. Safe destruction
let res_free = ema_filter_free(handle);
assert_eq!(res_free, FilterResult::Ok);
}
}
}
Expected cbindgen Header Output (ema_filter.h):
#ifndef EMA_FILTER_H
#define EMA_FILTER_H
#include <stdint.h>
#include <stdbool.h>
typedef enum FilterResult {
Ok = 0,
NullPointer = -1,
InvalidParameter = -2,
} FilterResult;
typedef struct EmaFilter EmaFilter;
#ifdef __cplusplus
extern "C" {
#endif // __cplusplus
enum FilterResult ema_filter_new(float alpha, struct EmaFilter **out_handle);
enum FilterResult ema_filter_process(struct EmaFilter *handle,
float raw_sample,
float *out_filtered);
enum FilterResult ema_filter_free(struct EmaFilter *handle);
#ifdef __cplusplus
} // extern "C"
#endif // __cplusplus
#endif /* EMA_FILTER_H */
Step-by-Step Technical Explanation:
- Opaque Pointer Pattern (
*mut EmaFilter): By declaring#[repr(C)] pub struct EmaFilter,cbindgengeneratestypedef struct EmaFilter EmaFilter;as a forward struct declaration in C. Because C code only receives pointers (EmaFilter*), the internal fields (alpha,sample_count) remain hidden and encapsulated inside Rust. - Heap Transfer via
Box::into_raw&Box::from_raw:Box::into_raw(filter)releases heap memory ownership from Rust's automatic memory management, returning a raw pointer for C. CallingBox::from_raw(handle)insideema_filter_freere-establishes ownership so Rust'sDropimplementation safely frees the heap allocation when the variable goes out of scope. - Discriminant-Explicit Enums (
#[repr(C)]): Explicit integer discriminants (Ok = 0,NullPointer = -1) combined with#[repr(C)]ensurecbindgenemits Cenumdefinitions with identical underlying integer sizes and numerical values across toolchains. - Safety Contract & Pointer Checks: All exported
extern "C"functions check for.is_null()before dereferencing raw pointers (*handle), guaranteeing that invalid or null pointers passed by C return error status codes instead of triggering Undefined Behavior (UB).
Exercise 2: Telemetry Batch Processing & #[repr(C)] Fixed Array Layouts
Scenario: Problem Statement:
An automotive diagnostic system written in C streams telemetry batches into a Rust processing module. Each telemetry packet consists of a timestamp (u64), sensor ID (u32), 4 floating-point signal values ([f32; 4]), and a checksum (u32).
To ensure cbindgen outputs exact field layout and offset alignment matching C struct definitions without compiler padding discrepancies, Rust structs must use #[repr(C)].
Requirements: Write the complete Rust code that:
- Defines
TelemetryPacket(#[repr(C)]) with fixed-size payload array[f32; 4]. - Defines
BatchResult(#[repr(C)]) containing summary metrics:total_processed: u32,valid_packets: u32, andaverage_signal: f32. - Implements
#[no_mangle] pub extern "C" fn process_telemetry_batch(packets: *const TelemetryPacket, count: usize, out_result: *mut BatchResult) -> i32. - Uses
core::slice::from_raw_partsto convert the raw C array into a safe Rust slice and validates packet CRC checksums. - Includes unit tests with
assert_eq!verifying batch statistics, CRC filtering, empty counts, and null pointer guards.
Answer
Rust Implementation (src/lib.rs):
Implementation
use std::slice;
/// Fixed-layout packet matching foreign C struct layout.
#[repr(C)]
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct TelemetryPacket {
pub timestamp_ms: u64,
pub sensor_id: u32,
pub payload: [f32; 4],
pub crc32: u32,
}
/// Summary statistics output buffer for foreign C callers.
#[repr(C)]
#[derive(Debug, Default, PartialEq)]
pub struct BatchResult {
pub total_processed: u32,
pub valid_packets: u32,
pub average_signal: f32,
}
/// Internal utility function calculating CRC checksum over packet payload.
fn calculate_crc(sensor_id: u32, payload: &[f32; 4]) -> u32 {
let mut hash = sensor_id;
for &val in payload {
hash = hash.wrapping_mul(31).wrapping_add(val.to_bits());
}
hash
}
/// Processes a C array of TelemetryPacket items and populates out_result.
/// Returns 0 on success, or -1 if any pointer parameter is null.
///
/// # Safety
/// - `packets` must point to an array of at least `count` valid TelemetryPacket elements.
/// - `out_result` must point to valid, writable BatchResult memory.
#[no_mangle]
pub unsafe extern "C" fn process_telemetry_batch(
packets: *const TelemetryPacket,
count: usize,
out_result: *mut BatchResult,
) -> i32 {
if packets.is_null() || out_result.is_null() {
return -1;
}
if count == 0 {
*out_result = BatchResult::default();
return 0;
}
// Reconstruct contiguous slice from raw pointer and count length
let packet_slice = slice::from_raw_parts(packets, count);
let mut valid_count = 0u32;
let mut total_signal = 0.0f32;
let mut signal_samples = 0u32;
for pkt in packet_slice {
let expected_crc = calculate_crc(pkt.sensor_id, &pkt.payload);
if pkt.crc32 == expected_crc {
valid_count += 1;
for &val in &pkt.payload {
total_signal += val;
signal_samples += 1;
}
}
}
let avg = if signal_samples > 0 {
total_signal / (signal_samples as f32)
} else {
0.0
};
*out_result = BatchResult {
total_processed: count as u32,
valid_packets: valid_count,
average_signal: avg,
};
0
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_process_telemetry_batch() {
let pkt1_crc = calculate_crc(101, &[1.0, 2.0, 3.0, 4.0]);
let pkt2_crc = calculate_crc(102, &[5.0, 6.0, 7.0, 8.0]);
let batch = vec![
TelemetryPacket {
timestamp_ms: 1000,
sensor_id: 101,
payload: [1.0, 2.0, 3.0, 4.0], // Sum = 10.0
crc32: pkt1_crc,
},
TelemetryPacket {
timestamp_ms: 1005,
sensor_id: 102,
payload: [5.0, 6.0, 7.0, 8.0], // Sum = 26.0
crc32: pkt2_crc,
},
TelemetryPacket {
timestamp_ms: 1010,
sensor_id: 103,
payload: [0.0, 0.0, 0.0, 0.0],
crc32: 0xDEADBEEF, // Invalid CRC
},
];
let mut result = BatchResult::default();
unsafe {
// 1. Process valid batch
let status = process_telemetry_batch(batch.as_ptr(), batch.len(), &mut result);
assert_eq!(status, 0);
assert_eq!(result.total_processed, 3);
assert_eq!(result.valid_packets, 2);
// (10.0 + 26.0) / 8 samples = 36.0 / 8 = 4.5
assert_eq!(result.average_signal, 4.5);
// 2. Process zero count
let empty_status = process_telemetry_batch(batch.as_ptr(), 0, &mut result);
assert_eq!(empty_status, 0);
assert_eq!(result.total_processed, 0);
// 3. Null pointer guard
let null_status = process_telemetry_batch(std::ptr::null(), 5, &mut result);
assert_eq!(null_status, -1);
}
}
}
Expected cbindgen Header Output (telemetry.h):
#ifndef TELEMETRY_H
#define TELEMETRY_H
#include <stdint.h>
#include <stdbool.h>
typedef struct TelemetryPacket {
uint64_t timestamp_ms;
uint32_t sensor_id;
float payload[4];
uint32_t crc32;
} TelemetryPacket;
typedef struct BatchResult {
uint32_t total_processed;
uint32_t valid_packets;
float average_signal;
} BatchResult;
#ifdef __cplusplus
extern "C" {
#endif // __cplusplus
int32_t process_telemetry_batch(const struct TelemetryPacket *packets,
uintptr_t count,
struct BatchResult *out_result);
#ifdef __cplusplus
} // extern "C"
#endif // __cplusplus
#endif /* TELEMETRY_H */
Step-by-Step Technical Explanation:
- Fixed-Size Array Mapping (
[f32; 4]):cbindgenparses fixed Rust arrays and convertspub payload: [f32; 4]directly to C member array syntaxfloat payload[4];. This guarantees identical structure size and offset layout between Rust and C compilers. slice::from_raw_partsSafety: C represents buffers as a pointer (*const TelemetryPacket) and integer length (usize). In Rust,slice::from_raw_parts(packets, count)wraps raw memory into a safe slice&[TelemetryPacket], enabling standard iterator operations (for pkt in packet_slice) with array boundary checks.- Type Conversions (
usizeuintptr_t):cbindgentranslates Rust's pointer-sized architecture typeusizeto C's standarduintptr_t(defined in<stdint.h>), ensuring portability across 32-bit and 64-bit hardware targets.
Exercise 3: Cross-Language Heap Allocation & C-String Ownership Transfer
Scenario: Problem Statement:
A C network service requires a Rust cryptographic backend to compute formatted hex digest strings for arbitrary byte buffers. Because C strings are null-terminated (const char*) while Rust String / &str objects are length-prefixed UTF-8 byte buffers without trailing nulls, Rust must convert its output into a null-terminated CString, relinquish heap ownership to C via CString::into_raw(), and provide an explicit destructor function (rust_string_free) to prevent memory leaks.
Requirements: Write the complete Rust code that:
- Implements
crypto_compute_hex_hash(data: *const u8, len: usize, out_str: *mut *mut c_char) -> i32. - Formats a mock hash string
HASH-<HEX_SUM>into aCStringand writes the raw pointer toout_str. - Implements
rust_string_free(s: *mut c_char)to reclaim and deallocate the string pointer. - Includes unit tests with
assert_eq!,assert!,CStr::from_ptr, andCString::from_rawverifying string formatting, null checks, and memory cleanup.
Answer
Rust Implementation (src/lib.rs):
Implementation
use std::ffi::{CStr, CString};
use std::os::raw::c_char;
use std::ptr;
use std::slice;
/// Computes a formatted hex hash string for input raw data and transfers ownership to C.
///
/// # Safety
/// - `data` must point to `len` readable bytes (or be null when `len == 0`).
/// - `out_str` must point to valid writable pointer memory in C (`char**`).
/// - Caller must free the generated string by calling `rust_string_free`.
#[no_mangle]
pub unsafe extern "C" fn crypto_compute_hex_hash(
data: *const u8,
len: usize,
out_str: *mut *mut c_char,
) -> i32 {
if out_str.is_null() {
return -1;
}
if data.is_null() && len > 0 {
return -2;
}
let input_bytes = if len == 0 || data.is_null() {
&[]
} else {
slice::from_raw_parts(data, len)
};
// Calculate checksum and format hex output
let checksum: u32 = input_bytes.iter().map(|&b| b as u32).sum();
let formatted = format!("HASH-{:08X}", checksum);
// Convert Rust String into null-terminated CString
match CString::new(formatted) {
Ok(c_string) => {
// Transfer raw pointer ownership to foreign caller
*out_str = c_string.into_raw();
0
}
Err(_) => -3,
}
}
/// Frees a null-terminated string previously returned by `crypto_compute_hex_hash`.
///
/// # Safety
/// - `s` must be a pointer created by `CString::into_raw`, or null.
/// - Must not be called multiple times on the same pointer (double-free avoidance).
#[no_mangle]
pub unsafe extern "C" fn rust_string_free(s: *mut c_char) {
if s.is_null() {
return;
}
// Reclaim CString ownership; dropping it automatically deallocates memory
let _ = CString::from_raw(s);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_hex_hash_string_transfer_and_free() {
unsafe {
let payload = b"Hello, cbindgen FFI!";
let mut out_ptr: *mut c_char = ptr::null_mut();
// 1. Compute hash and extract C string pointer
let status = crypto_compute_hex_hash(payload.as_ptr(), payload.len(), &mut out_ptr);
assert_eq!(status, 0);
assert!(!out_ptr.is_null());
// 2. Read C string in test assertion using CStr
let c_str = CStr::from_ptr(out_ptr);
let str_slice = c_str.to_str().unwrap();
let expected_sum: u32 = payload.iter().map(|&b| b as u32).sum();
let expected_str = format!("HASH-{:08X}", expected_sum);
assert_eq!(str_slice, expected_str);
// 3. Deallocate string memory
rust_string_free(out_ptr);
// 4. Test error handling on null input buffer with non-zero length
let null_status = crypto_compute_hex_hash(ptr::null(), 10, &mut out_ptr);
assert_eq!(null_status, -2);
}
}
}
Expected cbindgen Header Output (crypto.h):
#ifndef CRYPTO_H
#define CRYPTO_H
#include <stdint.h>
#include <stdbool.h>
#ifdef __cplusplus
extern "C" {
#endif // __cplusplus
int32_t crypto_compute_hex_hash(const uint8_t *data,
uintptr_t len,
char **out_str);
void rust_string_free(char *s);
#ifdef __cplusplus
} // extern "C"
#endif // __cplusplus
#endif /* CRYPTO_H */
Step-by-Step Technical Explanation:
- C String Formatting (
CString): RustStringis stored as length + capacity + buffer without null termination.CString::new()appends a\0byte and checks for internal null bytes (which would prematurely terminate C strings). - Ownership Transfer (
CString::into_raw): Callingc_string.into_raw()returns a*mut c_charpointer and prevents Rust from running theCStringdestructor. C code receives ownership of the raw heap buffer. - Memory Deallocation (
CString::from_raw): C code cannot use standardfree()on memory allocated by Rust's allocator without risking allocator corruption.rust_string_freepasses the pointer back toCString::from_raw(s), restoring Rust ownership so Rust's memory allocator cleanly reclaims the buffer. cbindgenPointer Type Mapping:cbindgenautomatically maps*const u8toconst uint8_t *and*mut *mut c_chartochar **, generating standard C string parameter prototypes.
6. Related Terms
bindgen— The counterpart tool (parsing C headers to generate Rust code).- FFI (Foreign Function Interface) — The cross-language interface mechanism.
extern "C"— C calling convention keyword drivingcbindgenexport scanning.
7. Key Takeaways
cbindgenparses Rust source code to automatically generate C and C++ header files (.h/.hpp).- It scans for
#[no_mangle] pub extern "C" fnexported functions and#[repr(C)]structs. - It is the inverse counterpart to
bindgen. - Configure
crate-type = ["cdylib", "staticlib"]inCargo.tomlwhen building native libraries for C linkers. - Use
cbindgenas a CLI tool or run it insidebuild.rsto keep C headers synchronized with Rust code automatically.