FFI - calling C from everything¶
Why it matters¶
Sooner or later every runtime needs to call a C library - CUDA, OpenSSL, SQLite, libcurl, RocksDB, the kernel's syscall surface. The mechanism for doing so (the Foreign Function Interface) is one of the highest-leverage and highest-risk parts of any language. Get it right and you get the C ecosystem for free; get it wrong and you get segfaults, ABI breakage, and memory bugs that crash the host runtime, not just your code.
Every path on this site covers FFI from its language's angle. The contrasts are dramatic - Rust's extern "C" and Java's Panama are 30 years apart in design.
The lens, per path¶
Rust - extern "C", the most direct¶
Month 4 - Unsafe, FFI, Macros. extern "C" fn declarations, #[repr(C)] structs, bindgen to auto-generate bindings from C headers, cbindgen for the reverse direction. FFI calls cost roughly one indirect function call - no marshaling, no thread state transition.
The unique thing here: FFI is essentially zero-cost. Rust's calling convention and C's are the same for extern "C" functions; there's no JIT to bail out of, no GC to coordinate with.
The trap
lifetimes don't cross the FFI boundary. A *const T from C has no lifetime - it's the caller's responsibility to ensure the pointer outlives every use. Most FFI bugs in Rust are use-after-free where the C-side allocation outlived the Rust borrow's assumption.
Go - cgo, "convenient until it isn't"¶
Month 4 - Reflection, Codegen, Plugins. import "C" with a special comment-as-prelude. The cgo compiler generates Go ↔ C glue.
The unique thing here: cgo calls are expensive. Each call transitions the goroutine out of the Go scheduler (locks an OS thread for the duration), runs C, transitions back. Cost: hundreds of nanoseconds per call vs. ~5ns for a Go function call.
The trap
"cgo is not Go." A cgo-heavy Go program loses many of Go's properties - escape analysis can't see across the boundary, GC has to be conservative, cross-compilation gets harder, the runtime's preemption story changes. Production wisdom: batch cgo calls to amortize the transition cost; or pick Rust.
Java - Panama (Foreign Function & Memory API), the modern way¶
Month 2 - JVM & Bytecode Week 7 (introduction); referenced from Months 4-5. java.lang.foreign.MemorySegment, MemoryLayout, Linker.nativeLinker(). Stable in JDK 22+.
The unique thing here: Panama replaces JNI for ~all new code. JNI required writing C glue code, generated headers (javah/javac -h), manual reference management. Panama is declarative: describe the C function's signature in Java, get a MethodHandle you can call.
The legacy contrast: JNI is still everywhere in the ecosystem. New code should use Panama; old codebases should plan migration. JNI's safety properties are roughly Rust's unsafe block without the type system's help.
Python - ctypes, cffi, and the C API¶
Month 3 - Runtime & Performance. Three options:
- ctypes (stdlib) - load a .so, declare argtypes/restype, call. Pure Python; slowest per call.
- cffi - describe the C ABI in Python or by parsing headers, generate stubs. Faster than ctypes; the SQLAlchemy/cryptography ecosystem default.
- C API extensions (PyObject*, Py_INCREF, the full CPython API) - fastest, most invasive, used by NumPy, PyTorch, and any perf-critical extension. PEP 703 (free-threading) is changing the rules here.
The unique thing here: Python is the most-used FFI consumer of any language on this list - the entire scientific Python stack is C/C++/Fortran behind a Python facade. Mastering FFI is non-optional for serious Python work.
The trap
the GIL during native calls. C extensions must explicitly Py_BEGIN_ALLOW_THREADS to release the GIL during long-running work, or they serialize everything else.
Other paths¶
- AI Systems (Month 2 - GPU Programming) - CUDA's host/device boundary is FFI under the hood. PyTorch's C++ extensions use the Python C API.
- Linux Kernel (Month 1 - Kernel Foundations) - the syscall ABI is itself an FFI surface.
vdsois FFI optimized. - Containers (Month 1 - OCI Foundations) - runtimes call into libseccomp, libcap, libcontainer via FFI.
The contrasts that teach¶
| Aspect | Rust extern "C" |
Go cgo | Java Panama | Java JNI (legacy) | Python ctypes/cffi | Python C API |
|---|---|---|---|---|---|---|
| Per-call overhead | ~0 (ABI match) | hundreds of ns | tens of ns | tens of ns | μs (ctypes) / ~hundred ns (cffi) | tens of ns |
| Memory safety | unsafe block | runtime checks | type-checked layouts | manual ref mgmt | Python-side only | full C API discipline |
| Build complexity | bindgen optional | compiler + cgo | none beyond JDK | C toolchain + glue | none / cffi compile | C toolchain + Python.h |
| Cross-compile | excellent (cargo) | painful (cgo) | trivial (JVM is platform-portable) | painful (per-platform .so) | trivial (load at runtime) | per-platform builds |
| Idiomatic for new code? | yes | sometimes (avoid if hot) | yes | no (legacy) | yes (cffi > ctypes) | for perf-critical only |
The most clarifying read: Rust extern "C" + Java Panama side-by-side. Two languages with vastly different runtimes both decided "describe the C ABI declaratively in our language and let the compiler/JIT do the rest." It's the modern FFI pattern. Everything else is either lower-level (kernel syscalls, raw C API) or older (cgo, JNI, ctypes).
What to read first¶
- You write Python and use NumPy/PyTorch/anything-with-a-C-core → Python Month 3, then go read the source of one extension you use daily.
- You want fast FFI → Rust Month 4. The model is "FFI is free; safety is your job."
- You write Java and need to call C → Java Month 2 Week 7 (Panama), then the Panama docs. Do not start with JNI.
- You write Go and are considering cgo → Go Month 4, then measure. Most cgo usage that ships in production should be a separate Rust microservice.
- You write any kernel-adjacent code → Linux Month 1. The syscall ABI is the FFI everyone else implements on top of.