PyO3 & FFI Safety
The Challenge
Section titled “The Challenge”The boundary between Rust and Python is the most critical part of the JustAPI runtime. Getting it wrong can cause use-after-free errors, GIL deadlocks, or undefined behavior.
GIL Acquisition Discipline
Section titled “GIL Acquisition Discipline”Rust → Python Calls
Section titled “Rust → Python Calls”When Rust needs to call Python code, it must hold the GIL:
Python::with_gil(|py| { let handler = py.import("app")?.getattr("handle")?; let result = handler.call1((request,))?; result.extract::<Vec<u8>>()})Python → Rust Calls
Section titled “Python → Rust Calls”When Python calls into Rust (via PyO3 bindings), the GIL is already held:
#[pyfunction]fn process(data: Vec<u8>) -> PyResult<Vec<u8>> { // GIL is held — safe to call Python APIs}GIL Release During I/O
Section titled “GIL Release During I/O”The GIL is released during all I/O operations using py.allow_threads:
py.allow_threads(|| { // Network I/O, DB queries, JSON parsing // No GIL — runs fully parallel let result = fetch_from_database(&query)?; Ok(result)})Zero-Copy Buffer Protocol
Section titled “Zero-Copy Buffer Protocol”JustAPI uses PyO3’s buffer protocol to expose Rust memory to Python without copying:
// Rust side: expose request body as a memory viewimpl PyBufferProtocol for Request { fn get_buffer(&self, view: &mut PyBufferView) -> PyResult<()> { view.fill_with_slice(&self.body) }}Safety Rules
Section titled “Safety Rules”- Bind to
#[pyclass]: Rust buffers exposed to Python must be owned by a#[pyclass]that outlives the Python reference - Don’t hold across await: Never hold a Python buffer reference across a
.awaitpoint - Extract before spawn: Extract all data from Python objects before
tokio::spawn
Common Pitfalls
Section titled “Common Pitfalls”| Failure | Root Cause | Mitigation |
|---|---|---|
| Use-after-free | Buffer freed while Python holds view | Bind to #[pyclass] |
| GIL deadlock | Two threads each waiting for GIL | Never hold Rust Mutex across GIL |
!Send violation |
Python object across task boundary | Extract before tokio::spawn |
Aliased &mut |
Through FFI | Use PyCell/PyRefMut |
The unsafe Policy
Section titled “The unsafe Policy”Every unsafe block in justapi-py must have a // SAFETY: comment:
// SAFETY: The GIL is held (guaranteed by `Python::with_gil`),// so the pointer to `py_obj` is valid and won't be moved.let ptr = py_obj.as_ptr();Free-Threaded Python (PEP 703)
Section titled “Free-Threaded Python (PEP 703)”When compiled for Python 3.13t/3.14t:
#[cfg(feature = "free-threaded")]// GIL synchronization is disabled — Python handlers run truly parallelSee Also
Section titled “See Also”- Zero-GIL Architecture — Concurrency model
- Rust Core Deep Dive — Runtime internals
- Coding Standards — unsafe policy and conventions