Handle Management
How Rust objects are managed across the FFI boundary.
The Box Pattern
Rust objects are boxed and converted to raw pointers:
#![allow(unused)] fn main() { // Create: Rust -> C let inner = Box::new(GraphInner::new()); let ptr = Box::into_raw(inner); return ptr as *mut BbxGraph; // Destroy: C -> Rust let inner = Box::from_raw(ptr as *mut GraphInner); drop(inner); // Automatically called when Box goes out of scope }
Type Erasure
The C type is opaque:
typedef struct BbxGraph BbxGraph;
Rust knows the actual type:
#![allow(unused)] fn main() { type PluginGraphInner = GraphInner<PluginGraph>; }
Conversion is safe because:
- We control both sides
- Types match at compile time via generics
- Handle is never dereferenced in C
Null Safety
All functions check for null:
#![allow(unused)] fn main() { pub extern "C" fn bbx_graph_prepare(handle: *mut BbxGraph, ...) -> BbxError { if handle.is_null() { return BbxError::NullPointer; } // ... } }
Ownership Transfer
create(): Rust owns -> Box::into_raw -> C owns handle
use(): C passes handle -> Rust borrows -> returns to C
destroy(): C passes handle -> Rust reclaims -> deallocates
Create
#![allow(unused)] fn main() { Box::into_raw(inner) // Rust gives up ownership }
Use
#![allow(unused)] fn main() { let inner = &mut *(handle as *mut GraphInner); // Borrow, don't take ownership }
Destroy
#![allow(unused)] fn main() { Box::from_raw(handle) // Rust reclaims ownership // Box dropped, memory freed }
RAII in C++
The C++ wrapper manages the handle:
class Graph {
public:
Graph() : m_handle(bbx_graph_create()) {}
~Graph() { if (m_handle) bbx_graph_destroy(m_handle); }
private:
BbxGraph* m_handle;
};