15-rustTermsLevel_13cbindgen

cbindgen

Level 13 — Unsafe Rust & FFI An official Rust tool and library that parses Rust source code (#[no_mangle] pub extern "C" fn and #[repr(C)] structs) to automatically generate C and C++ header files (.h / .hpp) for exposing Rust libraries to foreign C/C++ projects.


1. Prerequisites


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:

  1. You write exported functions tagged with #[no_mangle] pub extern "C" fn and C-compatible structs tagged with #[repr(C)].
  2. 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.
  3. Writing C headers by hand is tedious and error-prone. If you change a struct field type in Rust from u32 to u64 but forget to update the hand-written C .h header, 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 } \rightarrow C typedef struct { float x; float y; } Point;
  • Rust #[no_mangle] pub extern "C" fn add(a: i32, b: i32) -> i32 \rightarrow C int32_t add(int32_t a, int32_t b);

(2) Reality Metaphor

Imagine an Automated Exporter Printing Press:

  • bindgen is an Importer: it reads foreign C manuals (C headers) and translates them into native instructions for domestic workers (Rust bindings).
  • cbindgen is an Exporter: it scans domestic product blueprints (Rust #[repr(C)] & #[no_mangle] code) and automatically prints standardized international export user manuals (C/C++ .h header 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

Featurebindgencbindgen
Primary DirectionC/C++ \rightarrow RustRust \rightarrow C/C++
Input SourceC/C++ Header Files (.h / .hpp)Rust Source Code (src/*.rs)
Output TargetRust FFI Bindings (bindings.rs)C/C++ Header File (header.h)
Primary RequirementSystem Clang / LLVM installedPure Rust tool (syn AST parser)
Use CaseUsing existing C libraries inside RustExposing 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:

  1. Defines FilterResult (#[repr(C)]) with variants Ok = 0, NullPointer = -1, and InvalidParameter = -2.
  2. Defines EmaFilter containing alpha: f32, current_value: f32, and sample_count: u64.
  3. Exports ema_filter_new, ema_filter_process, and ema_filter_free marked with #[no_mangle] pub extern "C".
  4. Includes unit tests with assert_eq! and assert! 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:

  1. Opaque Pointer Pattern (*mut EmaFilter): By declaring #[repr(C)] pub struct EmaFilter, cbindgen generates typedef 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.
  2. 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. Calling Box::from_raw(handle) inside ema_filter_free re-establishes ownership so Rust's Drop implementation safely frees the heap allocation when the variable goes out of scope.
  3. Discriminant-Explicit Enums (#[repr(C)]): Explicit integer discriminants (Ok = 0, NullPointer = -1) combined with #[repr(C)] ensure cbindgen emits C enum definitions with identical underlying integer sizes and numerical values across toolchains.
  4. 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:

  1. Defines TelemetryPacket (#[repr(C)]) with fixed-size payload array [f32; 4].
  2. Defines BatchResult (#[repr(C)]) containing summary metrics: total_processed: u32, valid_packets: u32, and average_signal: f32.
  3. Implements #[no_mangle] pub extern "C" fn process_telemetry_batch(packets: *const TelemetryPacket, count: usize, out_result: *mut BatchResult) -> i32.
  4. Uses core::slice::from_raw_parts to convert the raw C array into a safe Rust slice and validates packet CRC checksums.
  5. 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:

  1. Fixed-Size Array Mapping ([f32; 4]): cbindgen parses fixed Rust arrays and converts pub payload: [f32; 4] directly to C member array syntax float payload[4];. This guarantees identical structure size and offset layout between Rust and C compilers.
  2. slice::from_raw_parts Safety: 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.
  3. Type Conversions (usize \rightarrow uintptr_t): cbindgen translates Rust's pointer-sized architecture type usize to C's standard uintptr_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:

  1. Implements crypto_compute_hex_hash(data: *const u8, len: usize, out_str: *mut *mut c_char) -> i32.
  2. Formats a mock hash string HASH-<HEX_SUM> into a CString and writes the raw pointer to out_str.
  3. Implements rust_string_free(s: *mut c_char) to reclaim and deallocate the string pointer.
  4. Includes unit tests with assert_eq!, assert!, CStr::from_ptr, and CString::from_raw verifying 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:

  1. C String Formatting (CString): Rust String is stored as length + capacity + buffer without null termination. CString::new() appends a \0 byte and checks for internal null bytes (which would prematurely terminate C strings).
  2. Ownership Transfer (CString::into_raw): Calling c_string.into_raw() returns a *mut c_char pointer and prevents Rust from running the CString destructor. C code receives ownership of the raw heap buffer.
  3. Memory Deallocation (CString::from_raw): C code cannot use standard free() on memory allocated by Rust's allocator without risking allocator corruption. rust_string_free passes the pointer back to CString::from_raw(s), restoring Rust ownership so Rust's memory allocator cleanly reclaims the buffer.
  4. cbindgen Pointer Type Mapping: cbindgen automatically maps *const u8 to const uint8_t * and *mut *mut c_char to char **, generating standard C string parameter prototypes.


7. Key Takeaways

  • cbindgen parses Rust source code to automatically generate C and C++ header files (.h / .hpp).
  • It scans for #[no_mangle] pub extern "C" fn exported functions and #[repr(C)] structs.
  • It is the inverse counterpart to bindgen.
  • Configure crate-type = ["cdylib", "staticlib"] in Cargo.toml when building native libraries for C linkers.
  • Use cbindgen as a CLI tool or run it inside build.rs to keep C headers synchronized with Rust code automatically.
Built with LogoFlowershow