Interop Best Practices
Rules for safe language boundaries, clear error mapping, and explicit memory ownership.
How to Use This List
- Apply to every new FFI surface, PyO3 export, or napi-rs function
- Document ownership in the header, README, and TypeScript types
- Integration tests in the host language are mandatory
A - Boundary Design
- One obvious integration mechanism per host. PyO3, napi-rs, or C ABI, not all three mixed.
- Coarse-grained exports. Batch work to amortize call overhead.
- Stable ABI only with
repr(C). Never expose Rust default layouts. - Version function exported.
lib_version()for compatibility checks.
B - Memory and Ownership
- Every allocator has a matching free. Document in header comments.
- Copy small data by default. Borrow only with documented lifetime.
- Validate pointer + length pairs. Reject null and overflow lengths.
- No panics across boundary. Map to error codes or host exceptions.
C - Errors and Threading
- Map errors to host idioms.
PyErr, JSError, Cstatuscodes. - Document thread safety. Which functions are reentrant or require GIL.
- Release GIL for CPU-only Rust. PyO3
allow_threadson hot paths. - Threadsafe callbacks explicit. napi
ThreadsafeFunctionwhen needed.
D - Build and Release
- CI matrix for shipped triples. Linux, macOS, Windows at minimum.
- Manylinux / macOS universal2 / MSVC prebuilds. Node and Python users lack Rust.
- Semver on public boundary API. Breaking FFI bumps major version.
- Supply chain audit.
cargo deny+ host package audit.
E - Testing and Docs
- Host-language integration tests. ctypes, pytest, or node test runner.
- Layout tests for C structs.
size_ofvs Csizeofin CI. - Examples in both languages. Rust unit test + Python/TS sample script.
- Ownership diagram in README. Who allocates, who frees, who may mutate.
FAQs
PyO3 or ctypes from Python?
PyO3 for extension modules and ergonomics. ctypes for loading arbitrary cdylib without building as module.
When is C ABI required?
Whenever the consumer is not Rust: C++, Swift, Go, JVM, Unity, etc.
Can I expose async to Python?
Yes with pyo3-async-runtimes and a running event loop policy documented in README.
JSON at the boundary?
Fine for control plane. Avoid for hot loops moving megabytes.
How do I deprecate exports?
Keep symbol, log warning, remove next major. Never repurpose enum values.
Multiple hosts one crate?
Split into core Rust crate plus thin py, napi, and ffi adapter crates.
WASM vs native?
WASM for sandboxed plugins; native for max performance and OS API access.
Security review?
Treat FFI as attack surface. Fuzz C API entrypoints.
Logging across boundary?
Host provides log callback registered at init, or return structured error strings only.
Documentation generation?
cbindgen for C, napi for .d.ts, Sphinx via pyo3 docstrings for Python.
Related
- Interop Basics - strategy overview
- Data Marshalling - ownership patterns
- PyO3 - Python extensions
- Node.js with napi-rs - Node addons
Stack versions: This page was written for Rust 1.97.0 (edition 2024), Tokio 1.x, Axum 0.8, serde 1.0, sqlx 0.8, clap 4, and Polars 0.46+.