Advanced Projects

C Library Binding

A safe C library binding wraps raw unsafe extern "C" foreign declarations, #[repr(C)] structs, and opaque pointer handles inside an idiomatic Rust RAII type that enforces lifetime bounds (PhantomData<&'a [u8]>), checks error codes, prevents null-pointer dereferences, and releases C-allocated resources automatically in Drop.

Two-Layer FFI Architecture (-sys + Safe Wrapper)

+-------------------------------------------------------------------+
| Safe Rust API (`ChecksumContext`, `CrcResult`)                    |
|   - Validates slices (`as_ptr()`, `len()`)                        |
|   - Converts error codes into `Result<u32, FfiError>`             |
|   - Calls `c_crc32_free(self.raw)` automatically in `Drop`        |
+---------------------------------+---------------------------------+
                                  |
                                  v
+-------------------------------------------------------------------+
| Raw FFI Layer (`#[repr(C)]`, `unsafe extern "C"`)                 |
|   - Opaque handle `#[repr(C)] pub struct RawCrcCtx { _priv: [u8;0]}`|
|   - C ABI functions (`c_crc32_new`, `c_crc32_update`, `c_crc32_free`)|
+-------------------------------------------------------------------+
FFI Hazard Unsafe C Behavior Safe Rust Wrapper Mitigation
Resource leak Caller forgets to call c_crc32_free(ctx) impl Drop for ChecksumContext unconditionally frees NonNull<RawCrcCtx>
Null pointer return malloc failure returns NULL NonNull::new(raw) converts NULL into Err(FfiError::OutOfMemory)
Dangling buffer pointer Passing freed pointer or wrong byte count Wrapper takes &[u8] and passes slice.as_ptr() and slice.len() together
Unwinding across FFI Panic escaping extern "C" is UB C-callable callbacks wrap body in std::panic::catch_unwind

Complete Self-Contained C ABI Library and Safe Rust Wrapper

use std::ffi::{CStr, c_char, c_int};
use std::ptr::NonNull;

// ---------------------------------------------------------------------------
// 1. Raw C-ABI Layer (simulating a compiled C library linked into the binary)
// ---------------------------------------------------------------------------
#[repr(C)]
pub struct RawCrcCtx {
    state: u32,
    bytes_processed: usize,
}

#[unsafe(no_mangle)]
pub unsafe extern "C" fn c_crc32_create(seed: u32) -> *mut RawCrcCtx {
    Box::into_raw(Box::new(RawCrcCtx {
        state: !seed,
        bytes_processed: 0,
    }))
}

#[unsafe(no_mangle)]
pub unsafe extern "C" fn c_crc32_update(
    ctx: *mut RawCrcCtx,
    data: *const u8,
    len: usize,
) -> c_int {
    if ctx.is_null() || (data.is_null() && len > 0) {
        return -1;
    }
    // SAFETY: Caller guarantees `ctx` is valid and `data` points to `len` initialized bytes.
    let ctx_ref = unsafe { &mut *ctx };
    let bytes = if len == 0 {
        &[][..]
    } else {
        unsafe { std::slice::from_raw_parts(data, len) }
    };

    for &b in bytes {
        ctx_ref.state ^= b as u32;
        for _ in 0..8 {
            let mask = (ctx_ref.state & 1).wrapping_neg();
            ctx_ref.state = (ctx_ref.state >> 1) ^ (0xEDB8_8320 & mask);
        }
    }
    ctx_ref.bytes_processed += len;
    0
}

#[unsafe(no_mangle)]
pub unsafe extern "C" fn c_crc32_finalize(
    ctx: *const RawCrcCtx,
    out_crc: *mut u32,
    out_bytes: *mut usize,
) -> c_int {
    if ctx.is_null() || out_crc.is_null() || out_bytes.is_null() {
        return -1;
    }
    unsafe {
        *out_crc = !(*ctx).state;
        *out_bytes = (*ctx).bytes_processed;
    }
    0
}

#[unsafe(no_mangle)]
pub unsafe extern "C" fn c_crc32_free(ctx: *mut RawCrcCtx) {
    if !ctx.is_null() {
        // SAFETY: `ctx` was allocated by `c_crc32_create` via `Box::into_raw`.
        unsafe {
            drop(Box::from_raw(ctx));
        }
    }
}

#[unsafe(no_mangle)]
pub extern "C" fn c_crc32_version() -> *const c_char {
    c"libcrc32-ffi 1.4.0".as_ptr()
}

// ---------------------------------------------------------------------------
// 2. Safe Idiomatic Rust Wrapper Layer
// ---------------------------------------------------------------------------
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FfiError {
    AllocationFailed,
    InvalidArgument,
}

pub struct Crc32Hasher {
    raw: NonNull<RawCrcCtx>,
}

// SAFETY: `Crc32Hasher` uniquely owns its `RawCrcCtx` allocation.
unsafe impl Send for Crc32Hasher {}

impl Crc32Hasher {
    pub fn new(seed: u32) -> Result<Self, FfiError> {
        // SAFETY: `c_crc32_create` has no preconditions and returns a heap pointer or NULL.
        let ptr = unsafe { c_crc32_create(seed) };
        let raw = NonNull::new(ptr).ok_or(FfiError::AllocationFailed)?;
        Ok(Self { raw })
    }

    pub fn update(&mut self, chunk: &[u8]) -> Result<(), FfiError> {
        // SAFETY: `self.raw` is valid and `chunk.as_ptr()` is valid for `chunk.len()` bytes.
        let rc = unsafe { c_crc32_update(self.raw.as_ptr(), chunk.as_ptr(), chunk.len()) };
        if rc == 0 {
            Ok(())
        } else {
            Err(FfiError::InvalidArgument)
        }
    }

    pub fn finish(&self) -> Result<(u32, usize), FfiError> {
        let mut crc = 0u32;
        let mut total_bytes = 0usize;
        // SAFETY: `self.raw`, `&mut crc`, and `&mut total_bytes` are valid non-null pointers.
        let rc = unsafe { c_crc32_finalize(self.raw.as_ptr(), &mut crc, &mut total_bytes) };
        if rc == 0 {
            Ok((crc, total_bytes))
        } else {
            Err(FfiError::InvalidArgument)
        }
    }

    pub fn library_version() -> &'static str {
        // SAFETY: `c_crc32_version` returns a static null-terminated C string literal.
        let c_str = unsafe { CStr::from_ptr(c_crc32_version()) };
        c_str.to_str().unwrap_or("unknown")
    }
}

impl Drop for Crc32Hasher {
    fn drop(&mut self) {
        // SAFETY: `self.raw` was created via `c_crc32_create` and is freed exactly once in `Drop`.
        unsafe {
            c_crc32_free(self.raw.as_ptr());
        }
    }
}

fn main() {
    println!("Binding version: {}", Crc32Hasher::library_version());

    let mut hasher = Crc32Hasher::new(0).expect("failed to allocate C context");
    hasher.update(b"123456789").unwrap();

    let (crc, total_bytes) = hasher.finish().unwrap();
    // Standard IEEE 802.3 CRC-32 of ASCII "123456789" is 0xCBF43926
    println!(
        "CRC-32 of \"123456789\" ({} bytes) = 0x{:08X}",
        total_bytes, crc
    );
}
Binding version: libcrc32-ffi 1.4.0
CRC-32 of "123456789" (9 bytes) = 0xCBF43926