正在加载,请稍候…

Rust Systems Programming Patterns: Zero-Cost Abstractions and FFI

Explore Rust's zero-cost abstractions, comparing trait objects vs generics, writing correct unsafe code, integrating with C via FFI, embedded Rust development, and measuring real performance with criterion benchmarks.

Rust Systems Programming Patterns: Zero-Cost Abstractions and FFI

Rust's promise of "zero-cost abstractions" means you pay no runtime penalty for high-level constructs. This guide explores how to leverage trait objects vs. generics, write safe unsafe code, integrate with C via FFI, develop embedded Rust, and measure where abstractions truly have zero cost.

Zero-Cost Abstractions in Practice

The core principle: what you don't use, you don't pay for. Abstractions compile away:

// High-level iterator chain
fn sum_of_squares(v: &[i32]) -> i32 {
    v.iter()
        .filter(|&&x| x % 2 == 0)
        .map(|&x| x * x)
        .sum()
}

// Compiles to essentially the same assembly as:
fn sum_of_squares_manual(v: &[i32]) -> i32 {
    let mut total = 0;
    for &x in v {
        if x % 2 == 0 {
            total += x * x;
        }
    }
    total
}

Verify with Compiler Explorer (godbolt.org)—both produce identical optimized code.

Trait Objects vs. Generics

Choosing between dynamic dispatch (trait objects) and static dispatch (generics) has real performance implications:

use std::fmt;

trait Drawable {
    fn draw(&self);
    fn bounding_box(&self) -> (f64, f64, f64, f64);
}

struct Circle { x: f64, y: f64, radius: f64 }
struct Rectangle { x: f64, y: f64, width: f64, height: f64 }

impl Drawable for Circle {
    fn draw(&self) { println!("Drawing circle at ({}, {})", self.x, self.y); }
    fn bounding_box(&self) -> (f64, f64, f64, f64) {
        (self.x - self.radius, self.y - self.radius,
         self.x + self.radius, self.y + self.radius)
    }
}

impl Drawable for Rectangle {
    fn draw(&self) { println!("Drawing rect at ({}, {})", self.x, self.y); }
    fn bounding_box(&self) -> (f64, f64, f64, f64) {
        (self.x, self.y, self.x + self.width, self.y + self.height)
    }
}

// Static dispatch: compiler generates separate code for each type
// Fast, no vtable lookup, enables inlining
fn draw_static<T: Drawable>(shape: &T) {
    shape.draw();
}

// Dynamic dispatch: single code path, vtable lookup at runtime
// Slower, prevents inlining, but allows heterogeneous collections
fn draw_dynamic(shape: &dyn Drawable) {
    shape.draw();
}

fn main() {
    let circle = Circle { x: 0.0, y: 0.0, radius: 5.0 };
    let rect = Rectangle { x: 1.0, y: 1.0, width: 10.0, height: 5.0 };

    // Static: monomorphized at compile time
    draw_static(&circle);
    draw_static(&rect);

    // Dynamic: runtime dispatch via vtable
    let shapes: Vec<Box<dyn Drawable>> = vec![
        Box::new(Circle { x: 0.0, y: 0.0, radius: 3.0 }),
        Box::new(Rectangle { x: 0.0, y: 0.0, width: 5.0, height: 3.0 }),
    ];
    for shape in &shapes {
        draw_dynamic(shape.as_ref());
    }
}

Rule of thumb: Use generics for performance-critical code. Use trait objects when you need heterogeneous collections or want to reduce compile times.

Unsafe Code: When and How

Unsafe Rust is necessary for FFI, low-level memory operations, and certain performance-critical patterns:

// Raw pointer manipulation
fn split_at_mut_safe<T>(slice: &mut [T], mid: usize) -> (&mut [T], &mut [T]) {
    let len = slice.len();
    assert!(mid <= len);
    let ptr = slice.as_mut_ptr();
    // Safe to call: ptr is valid, ranges don't overlap
    unsafe {
        (
            std::slice::from_raw_parts_mut(ptr, mid),
            std::slice::from_raw_parts_mut(ptr.add(mid), len - mid),
        )
    }
}

// Atomic operations for lock-free programming
use std::sync::atomic::{AtomicUsize, Ordering};

static COUNTER: AtomicUsize = AtomicUsize::new(0);

fn increment() -> usize {
    COUNTER.fetch_add(1, Ordering::SeqCst)
}

// Wrapping unsafe in a safe API (the correct pattern)
pub struct SafeBuffer {
    ptr: *mut u8,
    len: usize,
    cap: usize,
}

impl SafeBuffer {
    pub fn new(capacity: usize) -> Self {
        let layout = std::alloc::Layout::array::<u8>(capacity).unwrap();
        let ptr = unsafe { std::alloc::alloc(layout) };
        SafeBuffer { ptr, len: 0, cap: capacity }
    }

    pub fn push(&mut self, byte: u8) {
        assert!(self.len < self.cap, "Buffer overflow");
        unsafe {
            *self.ptr.add(self.len) = byte;
        }
        self.len += 1;
    }

    pub fn as_slice(&self) -> &[u8] {
        unsafe { std::slice::from_raw_parts(self.ptr, self.len) }
    }
}

impl Drop for SafeBuffer {
    fn drop(&mut self) {
        let layout = std::alloc::Layout::array::<u8>(self.cap).unwrap();
        unsafe { std::alloc::dealloc(self.ptr, layout); }
    }
}

FFI: Calling C from Rust

Rust's extern "C" blocks enable seamless C interoperability:

// Declare C functions
extern "C" {
    fn strlen(s: *const std::os::raw::c_char) -> usize;
    fn malloc(size: usize) -> *mut std::os::raw::c_void;
    fn free(ptr: *mut std::os::raw::c_void);
    fn printf(format: *const std::os::raw::c_char, ...) -> std::os::raw::c_int;
}

// Safe wrapper
fn c_strlen(s: &str) -> usize {
    let c_str = std::ffi::CString::new(s).unwrap();
    unsafe { strlen(c_str.as_ptr()) }
}

// Exposing Rust functions to C
#[no_mangle]
pub extern "C" fn rust_add(a: i32, b: i32) -> i32 {
    a + b
}

#[no_mangle]
pub extern "C" fn rust_process_bytes(ptr: *const u8, len: usize) -> i32 {
    if ptr.is_null() {
        return -1;
    }
    let slice = unsafe { std::slice::from_raw_parts(ptr, len) };
    slice.iter().sum::<u8>() as i32
}

Using the bindgen tool to auto-generate FFI bindings:

// build.rs
fn main() {
    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");
    bindings.write_to_file("src/bindings.rs").unwrap();
}

Embedded Rust

Rust's no_std mode enables bare-metal programming:

#![no_std]
#![no_main]

use panic_halt as _;
use cortex_m_rt::entry;
use stm32f4xx_hal::{pac, prelude::*};

#[entry]
fn main() -> ! {
    let dp = pac::Peripherals::take().unwrap();
    let rcc = dp.RCC.constrain();
    let clocks = rcc.cfgr.sysclk(84.MHz()).freeze();

    let gpioa = dp.GPIOA.split();
    let mut led = gpioa.pa5.into_push_pull_output();

    loop {
        led.set_high();
        cortex_m::asm::delay(8_000_000);
        led.set_low();
        cortex_m::asm::delay(8_000_000);
    }
}

Performance Measurement

Use criterion for statistically rigorous benchmarks:

[dev-dependencies]
criterion = { version = "0.5", features = ["html_reports"] }

[[bench]]
name = "my_bench"
harness = false
use criterion::{black_box, criterion_group, criterion_main, Criterion, BenchmarkId};

fn fibonacci(n: u64) -> u64 {
    match n {
        0 => 0,
        1 => 1,
        _ => fibonacci(n - 1) + fibonacci(n - 2),
    }
}

fn fibonacci_iterative(n: u64) -> u64 {
    let (mut a, mut b) = (0u64, 1u64);
    for _ in 0..n {
        let tmp = a + b;
        a = b;
        b = tmp;
    }
    a
}

fn bench_fibonacci(c: &mut Criterion) {
    let mut group = c.benchmark_group("fibonacci");
    for i in [10u64, 20, 30].iter() {
        group.bench_with_input(
            BenchmarkId::new("recursive", i),
            i,
            |b, i| b.iter(|| fibonacci(black_box(*i))),
        );
        group.bench_with_input(
            BenchmarkId::new("iterative", i),
            i,
            |b, i| b.iter(|| fibonacci_iterative(black_box(*i))),
        );
    }
    group.finish();
}

criterion_group!(benches, bench_fibonacci);
criterion_main!(benches);

Compile-Time Computation with const

Rust's const evaluation moves work to compile time:

const fn factorial(n: u64) -> u64 {
    if n == 0 { 1 } else { n * factorial(n - 1) }
}

const FACTORIAL_10: u64 = factorial(10); // computed at compile time

// Const generics for type-level integers
struct Matrix<const ROWS: usize, const COLS: usize> {
    data: [[f64; COLS]; ROWS],
}

impl<const N: usize> Matrix<N, N> {
    fn identity() -> Self {
        let mut data = [[0.0; N]; N];
        for i in 0..N {
            data[i][i] = 1.0;
        }
        Matrix { data }
    }
}

fn main() {
    println!("10! = {}", FACTORIAL_10); // 3628800
    let identity: Matrix<3, 3> = Matrix::identity();
}

Conclusion

Rust's zero-cost abstraction model lets you write expressive, high-level code that compiles to tight machine code. Choose generics over trait objects in performance-critical paths, encapsulate unsafe code in safe abstractions, and use FFI to leverage the vast C ecosystem. The combination of safety guarantees and raw performance makes Rust uniquely suited for systems programming in 2026.