Work through the exercises in order. Each builds on concepts from the previous ones.
Run a single exercise: cargo test ex01
Run all exercises: cargo test
-
Exercise 01 — Call C from Rust (
src/ex01_calling_c.rs) Declare C functions in anextern "C"block and write safe Rust wrappers. Concepts:extern "C", type mapping (int→i32,double→f64),unsafeblocks. -
Exercise 02 — Call Rust from C (
src/ex02_rust_from_c.rs) Write#[no_mangle] pub extern "C"functions that C code can call. Concepts:#[no_mangle], C calling convention, symbol export. -
Exercise 03 —
#[repr(C)]Structs (src/ex03_structs.rs) Define C-compatible structs and pass them by value and by pointer. Concepts:#[repr(C)], pass-by-value vs pass-by-pointer,*const T/*mut T.
-
Exercise 04 — C-String Passing (
src/ex04_strings.rs) Convert between Rust&str/Stringand C*const c_char/*mut c_char. Concepts:CStr(borrow),CString(own),into_raw()/from_raw(), caller-provided buffers. See also:docs/ex04_strings_ffi.mdfor a comprehensive guide on string conversions, common mistakes, and buffer patterns. -
Exercise 05 — Opaque Handles (
src/ex05_opaque.rs) Hide a RustHashMapbehind an opaque*mut Configpointer. Concepts:Box::into_raw/Box::from_raw, create→use→destroy lifecycle, non-repr(C)types. -
Exercise 06 — Arrays & Slices (
src/ex06_arrays.rs) Pass arrays as(pointer, length)pairs across the FFI boundary. Concepts:slice::from_raw_parts,Vec↔(ptr, len)viamem::forget,from_raw_parts. -
Exercise 07 — Callbacks (
src/ex07_callbacks.rs) Function-pointer callbacks in both directions (Rust→C and C→Rust), including thevoid* user_datacontext pattern. Concepts:extern "C" fnpointers, context/"closure" via*mut c_void, Rust fn as C callback. -
Exercise 08 — Memory Management (
src/ex08_memory.rs) Allocate buffers in Rust, hand them to C, free them correctly. Builder pattern with an opaque handle. Concepts: "whoever allocates must free",mem::forget,Vec::from_raw_parts, out-parameters. -
Exercise 09 — Error Handling (
src/ex09_errors.rs) Integer return codes +thread_local!last-error message (likeerrno/GetLastError()). Concepts:thread_local!,RefCell<Option<CString>>, error code conventions.
-
Exercise 10 —
MaybeUninit,NonNull&ManuallyDrop(src/ex10_maybe_uninit.rs) Safer FFI withMaybeUninitfor out-parameters,NonNullfor handle wrappers, andManuallyDropfor transferring ownership of Rust-allocated buffers to C without running destructors. Concepts:MaybeUninit::write(),NonNull::new(),ManuallyDrop,Vec::from_raw_parts, avoiding UB from uninitialized reads, ownership transfer. -
Exercise 11 — C++ Vtable Pattern (
src/ex11_vtable.rs) Implement Rust types behind a C-compatible vtable (struct of function pointers) and call them from C++. Concepts: vtable as#[repr(C)]struct,staticvtable, polymorphic dispatch, C++ interop. -
Exercise 12 — Async / Threaded Interop (
src/ex12_async.rs) Bridge async Rust with synchronous C: completion callbacks,spawn_blocking, opaque runtime handles. Concepts:std::thread::spawn,Sendfor raw pointers,tokio::task::spawn_blocking, runtime lifecycle. -
Exercise 13 — Tagged Unions (
src/ex13_tagged_unions.rs) Represent Rust enums as C-compatible tagged unions (#[repr(C)]struct + union). Concepts:#[repr(u32)],#[repr(C)] union, reading union fields unsafely, discriminant checking. -
Exercise 14 — Panic Safety (
src/ex14_panic_safety.rs) Prevent undefined behavior by catching panics at everyextern "C"boundary. Concepts:std::panic::catch_unwind,UnwindSafe, converting panics to error codes. -
Exercise 15 — Fixing UB with Miri (
src/ex15_miri_ub.rs) Eight functions containing intentional undefined behavior — dangling pointers, aliasing violations, use-after-free, unaligned access, uninitialized reads, and Stacked Borrows violations. Usecargo miri testto detect each bug, then write a fixed version. Concepts: Miri, Stacked Borrows,ptr::read_unaligned,split_at_mut,ptr::write, use-after-free. -
Exercise 16 — Pin & Drop Guarantee (
src/ex16_pin_drop.rs) Implement aListenertype that registers a raw pointer with a simulated C callback registry. The object must not move (or C's pointer dangles), andDropmust deregister before memory is freed. Build a safePinnedListenerwrapper. Concepts:Pin<Box<T>>,PhantomPinned,!Unpin, drop guarantee, self-registering objects, safe API over unsafe internals. -
Exercise 17 — Wrapping C Opaque Handles (
src/ex17_wrapping_c.rs) Wrap a C library (csrc/ex17_cdb.c) that uses opaque handles in a safe, idiomatic Rust API. Declareextern "C"bindings, build an RAIIDatabasestruct withNonNull+Drop, and convert C error codes toResult. Concepts:NonNull<OpaqueType>, RAII wrapper withDrop,CStringfor outbound strings, caller-provided buffers,From<c_int>error mapping, safe API over raw FFI. -
Exercise 18 — Invoking Rust Closures from C (
src/ex18_closures.rs) Pass Rust closures (Fn,FnMut,FnOnce) through C function-pointer +void*context pairs using the trampoline pattern. Covers shared references forFn, mutable references forFnMut, andBox::into_raw/Box::from_rawownership transfer forFnOnce. Concepts: Trampoline functions, genericextern "C" fn,Fn/FnMut/FnOnceacross FFI,*mut c_voidas closure context,Box::into_rawfor ownership transfer. -
Exercise 19 — Boxed
dyn TraitAcross FFI (src/ex19_dyn_trait_ffi.rs) PassBox<dyn Trait>to C as opaque handles. Covers why fat pointers need a wrapper struct, RAII lifecycle, composable trait objects (filter, tee), a dynamic registry (plugin pattern withVec<Box<dyn Trait>>), and downcasting back to concrete types viaAny. Concepts: Fat vs thin pointers,Box<dyn Trait>in a concrete wrapper, opaque handle pattern, trait object composition,Drop-based cleanup, dynamic dispatch registry,Any+downcast_ref. -
Exercise 20 — Global State & Library Init/Shutdown (
src/ex20_global_state.rs) Build a thread-safe metrics library withinit()/shutdown()lifecycle, named counters, and concurrent access. Models the pattern used bycurl_global_init(),SSL_library_init(), etc. Concepts:OnceLock,Mutex, global mutable state, idempotent init, thread-safeextern "C"API. -
Exercise 21 — Bitflags & C-Style Enums (
src/ex21_bitflags.rs) Define a#[repr(transparent)]newtype for bitwise-OR flags (O_RDONLY | O_CREAT-style). ImplementBitOr,BitAnd,contains(), and use flags to control a simulated file API. Concepts:#[repr(transparent)], bitwise operator traits,#[repr(u32)]enums, flag validation,From<Enum>for flags. -
Exercise 22 — Lifetime Encoding with
PhantomData(src/ex22_lifetimes.rs) Wrap a simulated DB + cursor API where cursors borrow from the database. UsePhantomData<&'db T>to let the compiler prevent use-after-free. Also covers borrowed transactions (&mutborrow blocks concurrent access). Concepts:PhantomData, lifetime parameters on FFI wrappers, borrowed vs owned handles, compile-time enforcement of C lifetime contracts. -
Exercise 23 —
transmute& Type Reinterpretation (src/ex23_transmute.rs) Learn whenmem::transmuteis truly needed in FFI (fn ptr ↔void*, nullable callbacks) and when safe alternatives exist (TryFromfor enums,ptr::read_unalignedfor struct deserialization,ascasts,f32::from_bits). Each part shows thetransmuteapproach AND the preferred safe alternative. Concepts:mem::transmute,#[repr(C, packed)],ptr::read_unaligned,Option<extern "C" fn()>null optimization,TryFromfor C enums,slice::from_raw_partsfor serialization. -
Exercise 24 — C-Owned Opaque Handles (
src/ex24_c_handles.rs) The C library allocates an opaqueSessionhandle internally and returns it via an out-parameter (Session **out). Rust receives the handle, wraps it in an RAII struct withNonNull+Drop, and builds a safe API where each C function's firstSession *argument becomes&self/&mut self. Concepts: Out-parameter pattern (*mut *mut T), C-owned opaque handles,NonNull+Drop(C frees, not Rust), zero-sized opaque type, status-code →Resultconversion,CStringfor outbound strings. -
Exercise 25 — Calling C++ Virtual Functions (Direct Vtable Access) (
src/ex25_cpp_virtual.rs) Receive a pointer to a polymorphic C++ object (abstract interface with multiple inheritance) and call its virtual methods from Rust — without writing any extern "C" trampoline functions. Instead, read the vptr from the object and call function pointers by slot index, exploiting the Itanium C++ ABI vtable layout. Covers in/out arguments, out-parameters, destruction via the vtable's deleting destructor, and MI pointer adjustment. Concepts: Itanium C++ ABI vtable layout, vptr reading,#[repr(C)]vtable structs, multiple inheritance pointer adjustment,offset_to_top, deleting vs complete destructors, in/out and out-parameters across virtual calls. See also:docs/ex25_cpp_vtable_abi.mdfor a detailed ABI guide with diagrams. -
Exercise 26 — C++ Exceptions Across FFI (
src/ex26_cpp_exceptions.rs) C++ functions throw various exceptions (std::domain_error, std::invalid_argument, custom class, integer throw). Everyextern "C"wrapper catches all exceptions and stores error info in a thread-local. Rust maps return codes toResult<T, CppException>. Includes callback integration (C++ calls Rust closure). Concepts: try/catch wrappers, thread-local exception storage,catch(...)catch-all, return-code →Result, callback + closure trampoline, non-std::exceptionthrows. See also:docs/ex26_cpp_exceptions.mdfor the C++ exception model and wrapper pattern guide. -
Exercise 27 — Wrapping C++ Classes & STL Types (
src/ex27_cpp_stl.rs) Wrap a non-virtual C++ class (CppStringStack) that usesstd::vector<std::string>internally. Covers the opaque-pointer lifecycle (new/destroy/clone), std::string↔&strconversion, caller-provided buffers, borrowed pointer return (peek— zero-copy with a Rust lifetime guard), callback-based iteration, batch insertion, and factory functions. Concepts: Opaque pointer pattern, RAII wrapper withDrop+Clone,PeekGuard<'a>encoding C++ borrow lifetime,(const char*, size_t)string passing, callback iteration, two-phase buffer protocol, factory wrapping. See also:docs/ex27_cpp_stl_wrapping.mdfor patterns on wrapping C++ classes. -
Exercise 28 — Zero-Sized Types in FFI (
src/ex28_zst_ffi.rs) Explore ZSTs (zero-sized types) in FFI contexts: the#[repr(C)]empty-struct size mismatch with C, opaque incomplete types via[u8; 0], type-safe handles usingPhantomDatamarkers, controllingSend/Syncwith phantom fields, and the typestate pattern for compile-time protocol enforcement. Concepts: ZST layout pitfalls,[u8; 0]opaque types,PhantomData<Tag>for type branding,PhantomData<*mut ()>for!Send/!Sync, typestate pattern, compile-time state machines. -
Exercise 29 —
UnsafeCell& Aliasing in FFI (src/ex29_unsafecell.rs) Understand whyUnsafeCellis needed when C mutates memory that Rust holds as&T. Fix aliasing UB in a#[repr(C)]struct, build a safe sensor wrapper with interior mutability, share atomic counters between Rust and C, and observe the load-caching problem thatUnsafeCellprevents. Concepts:UnsafeCell<T>, LLVMnoalias readonly, interior mutability in#[repr(C)]structs,AtomicI32as FFI-compatibleUnsafeCell, Stacked Borrows. See also:docs/ex29_unsafecell_aliasing.mdfor a deep dive on LLVM IR, aliasing optimizations, and usage patterns.
In-depth guides and tutorials in the docs/ directory:
| Document | Related Exercise | Topic |
|---|---|---|
| ex04_strings_ffi.md | Exercise 04 | Rust ↔ C string conversions, CStr/CString patterns, common mistakes (dangling pointers, leaks, missing NUL), buffer allocation |
| ex14_panic_unwinding.md | Exercise 14 | Panic unwinding, Drop guarantees, unwind vs abort, FFI boundary hazard |
| ex15_miri_tutorial.md | Exercise 15 | Programmer's guide to Miri — reading errors, all 8 UB categories with examples and fixes, Stacked Borrows, Tree Borrows |
| ex16_pin_drop_guarantee.md | Exercise 16 | Pin and the drop guarantee — why Pin<Box<T>> + !Unpin + Drop work together, usage patterns (callbacks, intrusive lists, async I/O) |
| ex25_cpp_vtable_abi.md | Exercise 25 | Itanium C++ ABI vtable layout — vptr, slot numbering, multiple inheritance, pointer adjustment, deleting destructors |
| ex26_cpp_exceptions.md | Exercise 26 | C++ exception model — try/catch wrappers, thread-local error storage, catch(...), non-std::exception throws |
| ex27_cpp_stl_wrapping.md | Exercise 27 | Wrapping C++ classes and STL types — opaque pointers, std::string interop, borrowed returns, callback iteration |
| ex29_unsafecell_aliasing.md | Exercise 29 | UnsafeCell and LLVM aliasing — noalias readonly, LLVM IR examples, when to use UnsafeCell in FFI, Stacked Borrows |
cargo test # run all exercises
cargo test ex01 # run exercise 01 only
cargo test ex05 # run exercise 05 only
cargo build # compile (also builds C/C++ via build.rs)For an experienced Rust developer with no prior FFI experience. Times include reading the exercise, implementing the solution, and passing all tests.
| # | Exercise | Est. Time | Difficulty |
|---|---|---|---|
| 01 | Call C from Rust | 20 min | ★☆☆☆☆ |
| 02 | Call Rust from C | 20 min | ★☆☆☆☆ |
| 03 | #[repr(C)] Structs |
25 min | ★☆☆☆☆ |
| 04 | C-String Passing | 30 min | ★★☆☆☆ |
| 05 | Opaque Handles (Rust-owned) | 30 min | ★★☆☆☆ |
| 06 | Arrays & Slices | 30 min | ★★☆☆☆ |
| 07 | Callbacks | 40 min | ★★☆☆☆ |
| 08 | Memory Management | 45 min | ★★★☆☆ |
| 09 | Error Handling | 40 min | ★★☆☆☆ |
| 10 | MaybeUninit, NonNull & ManuallyDrop |
55 min | ★★★☆☆ |
| 11 | C++ Vtable Pattern | 60 min | ★★★★☆ |
| 12 | Async / Threaded Interop | 60 min | ★★★★☆ |
| 13 | Tagged Unions | 40 min | ★★★☆☆ |
| 14 | Panic Safety | 30 min | ★★☆☆☆ |
| 15 | Fixing UB with Miri | 60 min | ★★★★☆ |
| 16 | Pin & Drop Guarantee | 60 min | ★★★★☆ |
| 17 | Wrapping C Opaque Handles | 50 min | ★★★☆☆ |
| 18 | Closures via Trampolines | 50 min | ★★★☆☆ |
| 19 | Boxed dyn Trait Across FFI |
65 min | ★★★★☆ |
| 20 | Global State & Init/Shutdown | 35 min | ★★☆☆☆ |
| 21 | Bitflags & C-Style Enums | 35 min | ★★☆☆☆ |
| 22 | Lifetimes with PhantomData |
45 min | ★★★☆☆ |
| 23 | transmute & Reinterpretation |
45 min | ★★★☆☆ |
| 24 | C-Owned Opaque Handles | 45 min | ★★★☆☆ |
| 25 | C++ Virtual Functions (Direct Vtable) | 75 min | ★★★★★ |
| 26 | C++ Exceptions Across FFI | 50 min | ★★★☆☆ |
| 27 | Wrapping C++ Classes & STL Types | 60 min | ★★★★☆ |
| 28 | Zero-Sized Types in FFI | 40 min | ★★★☆☆ |
| 29 | UnsafeCell & Aliasing in FFI |
45 min | ★★★★☆ |
| Total | ~20.5 hours |
Tip: Exercises 01–07 build linearly — do them in order. After that, exercises are mostly independent and can be tackled in any order based on interest. Exercise 25 requires understanding ex11 (vtable pattern) first. Exercises 26 and 27 stand alone but pair well with ex25 for a complete C++ interop trilogy.
Stuck? These hints cover the most common stumbling blocks across all exercises. Each hint gives just enough to unblock you without spoiling the solution.
unsafeis a contract, not a warning. When you writeunsafe { }, you are promising that the preconditions documented on the function hold. Read the# Safetydoc comment before everyunsafecall.- Ownership always matters. Before touching a raw pointer ask: "who owns this memory?" If Rust allocated it (
Box::into_raw,CString::into_raw,Vec::as_mut_ptr+mem::forget), Rust must free it. If C allocated it, C must free it. repr(C)≠repr(Rust). If a struct crosses the FFI boundary, it needs#[repr(C)]. Opaque handles that C never dereferences do NOT needrepr(C)— only a pointer crosses.
CStr::from_ptr(ptr)borrows — the pointer must outlive the reference.CString::new(s).unwrap()allocates — use.as_ptr()while theCStringis alive.CString::into_raw()transfers ownership to C; reclaim withCString::from_raw().- Watch out for interior NUL bytes —
CString::newreturnsErrif the input contains\0.
- The pattern is always:
Box::new(value)→Box::into_raw()→ C holds the pointer →Box::from_raw()to reclaim. - The type behind the pointer does NOT need
#[repr(C)]as long as C only stores the pointer and never dereferences the fields. - To wrap a C library's handle, use
NonNull<OpaqueType>+Drop.
- C function pointers are
extern "C" fn(...)— they cannot capture environment. - To pass state, use the
void *user_datapattern: cast your state to*mut c_void, pass it alongside the fn pointer, cast it back inside the callback. - For Rust closures, write a trampoline: a generic
extern "C" fn<F: Fn…>that casts thevoid*back to&F/&mut F/Box<F>depending on the trait. Fn→ pass&closureas context (shared borrow, not consumed).FnMut→ pass&mut closureas context (exclusive borrow, not consumed).FnOnce→ passBox::into_raw(Box::new(closure))as context (consumed viaBox::from_raw).
Box<dyn Trait>is a fat pointer (2 words). C can't store that.- Wrap it:
struct Handle { inner: Box<dyn Trait> }. NowBox::into_raw(Box::new(handle))is a single-word (thin) pointer. Dropon the wrapper frees both the wrapper and the trait object.- Registry pattern: store
Vec<Box<dyn Logger>>inside a struct behind an opaque handle.add()pushes,broadcast()iterates and calls.log()on each. C sees one thin pointer hiding an arbitrarily large collection. - Downcasting: add
fn as_any(&self) -> &dyn Anyto your trait. Each impl returnsself. Thenhandle.inner.as_any().downcast_ref::<ConcreteType>()recovers the concrete type. ReturnsNoneif the type doesn't match. Anyrequires'static— your concrete types must not hold non-'staticborrows.
mem::forget(vec)preventsDrop— you now own the raw pointer.- Reconstruct with
Vec::from_raw_parts(ptr, len, cap)to free. - Never mix allocators: don't
free()Rust memory orBox::from_rawC memory.
ManuallyDrop::new(v)wraps a value and suppresses its destructor — the value stays valid but won't be freed when it goes out of scope.- Use this when transferring a
Vecto C: wrap inManuallyDrop, extract(ptr, len, capacity), hand the triple to C. - To reclaim:
Vec::from_raw_parts(ptr, len, cap)— the resultingVecdrops normally. ManuallyDropvsmem::forget: both suppress drop, butManuallyDroplets you read fields (.as_ptr(),.len()) after suppressing drop.mem::forgetconsumes the value so you can't access it afterwards.
- Return
0for success, negative for error. Store the full message in athread_local!. thread_local!+RefCell<Option<CString>>is the Rust equivalent oferrno+strerror.
- Unwinding across
extern "C"is undefined behavior. Always wrap withstd::panic::catch_unwind. catch_unwindrequiresUnwindSafe. UseAssertUnwindSafe(|| { ... })if the compiler complains.- Pattern:
catch_unwind(|| { /* real work */ }).unwrap_or_else(|_| ERROR_CODE).
static GLOBAL: OnceLock<Mutex<T>> = OnceLock::new();— initialized exactly once, thread-safe.OnceLock::get_or_init(|| ...)is idempotent and lock-free after first init.- Always check that the library was initialized before accessing state.
- Use
#[repr(transparent)]on a newtype:struct Flags(u32). ABI identical tou32. contains(other):(self.0 & other.0) == other.0.- Implement
BitOr,BitAnd,BitOrAssign— they're one-liners.
PhantomData<&'a T>tells the compiler "I logically borrow a&'a T" without storing one.- Use it when a raw pointer should have a lifetime but can't (because raw pointers are
'static). - The compiler will then prevent use-after-free at compile time.
&selfborrow incursor()→ multiple cursors OK, no mutation.&mut selfborrow intransaction()→ exclusive access.
- Only use
transmutewhen there is no safe alternative. The most common legitimate case:*mut c_void↔ function pointer —ascasts don't work here. Option<extern "C" fn(...)>is guaranteed to have the same layout as a nullable pointer. You do NOT need transmute for optional callbacks — just declare them asOption<…>in yourextern "C"block.- Integer →
#[repr(C)]enum: prefermatch+TryFrom. If you transmute an integer that doesn't match any variant, it's instant UB — even if you never read the value. - Bytes → struct: prefer
ptr::read_unaligned+ size check over transmute.transmuterequires exact size and alignment;read_unalignedis more forgiving. - Bytes → struct with bool/enum fields: even
ptr::read_unalignedcan be UB if the bit pattern is invalid for those types. Validate first or deserialize field by field. - Safe alternatives cheat sheet:
*mut T↔*mut c_void→ useasf32↔u32bits →f32::from_bits/to_bits- Integer ↔ integer →
asorFrom/TryFrom - Struct → bytes →
slice::from_raw_parts(ptr as *const u8, size_of::<T>())
- The C library calls
calloc/malloc— Rust must NOT callBox::from_raw. Call C's owndestroyfunction in yourDrop. - The out-parameter pattern:
let mut ptr: *mut CSession = std::ptr::null_mut();then pass&mut ptr(which is*mut *mut CSession) to the C function. - Wrap the raw pointer in
NonNull<CSession>immediately after creation.NonNull::new(ptr).ok_or(Error)?handles the null case. - For
get_option-style functions that fill a caller-provided buffer, use a stack array:let mut buf = [0u8; 256];. After the C call, find the NUL terminator withCStr::from_ptr(buf.as_ptr() as *const c_char). &selfvs&mut self: functions that only read (is_connected,get_option) take&self; functions that mutate state (connect,send) take&mut self.
- A vtable is just a
#[repr(C)]struct ofextern "C" fnpointers. - Use
staticvtables (not heap-allocated) — one per concrete type. - The
destroyfunction pointer in the vtable must reconstruct theBoxto free.
- Itanium ABI only: This works on GCC/Clang (Linux, macOS, BSD). It does NOT work on MSVC.
- The vptr is the first field of every polymorphic C++ object. Read it:
let vptr = *(obj as *const *const VtableStruct). - Virtual destructors occupy two slots in the Itanium ABI: slot 0 = complete destructor (D1, no dealloc), slot 1 = deleting destructor (D0, with dealloc). Your methods start at slot 2.
- To destroy: call the deleting destructor (slot 1) through the primary base's vtable. The primary base pointer equals the allocation address.
- Multiple inheritance: A class inheriting from N bases has N vptrs in its object layout. Casting to a non-primary base adjusts the pointer. Always use the correct interface pointer for each vtable.
- Verify slot numbering: Use
clang++ -Xclang -fdump-vtable-layouts -c file.cpp 2>&1 | c++filtto dump the vtable. Or write a C++ helper that readsvptr[slot]and compare against your#[repr(C)]struct in tests. - In/out arguments work the same through direct vtable calls — just pass
&mut valueas*mut f64. The virtual method writes through the pointer. offset_to_topatvptr[-2]tells you the byte offset from the sub-object back to the complete object — useful for MI diagnostics.- See
docs/ex25_cpp_vtable_abi.mdfor the full ABI reference with ASCII diagrams.
- Rule #1: An exception that escapes an
extern "C"function is undefined behavior. Everyextern "C"wrapper MUST havetry { ... } catch (...) { ... }. - Always end your catch chain with
catch (...)— C++ can throw non-std::exceptiontypes (e.g.throw 42;). - Use a thread-local
ExceptionInfostruct to store the exception's message, type name, and error code. The Rust side retrieves it after checking the return code. This mirrors theerrno/GetLastError()pattern. - Order catch blocks from most-specific to least-specific:
ProcessingError→std::invalid_argument→std::exception→.... - For callbacks (C++ → Rust → C++): the Rust callback should never panic. Return an error code instead. Use
catch_unwind()as a safety net if needed. - On the Rust side, build a
CppExceptionerror type and acheck(rc) → Result<(), CppException>helper. Then every wrapper is just:check(unsafe { cpp_ex_foo(...) })?; Ok(result). - See
docs/ex26_cpp_exceptions.mdfor the full exception model guide.
- Opaque pointer pattern: The C++ class lives on the heap. Rust holds
*mut CppStringStack(a zero-size opaque type) inside a RAII wrapper withDrop. - std::string input: Pass
(&str).as_ptr()+len— no NUL terminator needed. The C++ side constructsstd::string(ptr, len). - std::string output (owned): Use a two-phase protocol: first call with
buf = nullto get the needed length, then allocate and call again. - std::string output (borrowed):
peek()returns a pointer directly into C++ memory. Wrap it in aPeekGuard<'a>that borrows&'a StringStackto prevent mutation while the pointer is live. - Clone = C++ copy constructor:
cpp_stk_clone()callsnew CppStringStack(*s). Implement Rust'sClonetrait. - Callback iteration:
for_each()passes each element to a callback. Use a trampoline that appends to aVec<String>through a*mut c_voidcontext pointer. - Factory functions: C++ static methods like
from_csv()becomeextern "C"functions that return new opaque pointers. - See
docs/ex27_cpp_stl_wrapping.mdfor the full wrapping patterns guide.
- Use
Pin<Box<T>>when C stores a raw pointer back to your struct. PhantomPinnedmakes the type!Unpinso it can't be moved out of thePin.Dropmust deregister from C before the memory is freed.
- Run with
cargo +nightly miri test ex15. - Miri catches: dangling pointers, aliasing violations (
&and&mutto same address), unaligned reads, use-after-free. ptr::read_unalignedfor unaligned access;split_at_mutto get two&mutto non-overlapping parts.
- Segfault? You probably used a pointer after free, or cast to the wrong type. Run under Miri or Valgrind.
- Garbled strings? Check that your
CStringis still alive when C reads the pointer. A common bug:CString::new(s).unwrap().as_ptr()— theCStringis a temporary and is freed immediately! - Link errors? Make sure
build.rscompiles your C file and the function name matches exactly (no name mangling — useextern "C"). - Use
cargo test -- --nocaptureto seeprintln!output from tests. - Use
#[repr(C)]and check sizes:assert_eq!(std::mem::size_of::<MyStruct>(), EXPECTED_SIZE);catches layout mismatches early.