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