Rust unsafe FFI vs Cgo: who owns memory safety?
Tests your grasp of where compiler guarantees end at the FFI boundary. A strong answer contrasts Rust raw-pointer validity and aliasing invariants in unsafe blocks against Cgo's automatic copying, pointer-passing restrictions, and runtime thread-switching…
WHAT THIS TESTS: This question probes whether you understand the exact division of labor between the compiler and the programmer when crossing language boundaries. In Rust, unsafe is a trust boundary where you manually assert invariants the borrow checker normally enforces. In Go, Cgo is a runtime boundary where the toolchain inserts checks and copies to protect the garbage collector. The interviewer wants to see that you know Rust does not abandon safety inside unsafe, it merely makes you the guarantor, while Go trades performance and flexibility for automatic protection.
A GOOD ANSWER COVERS: First, a concrete scenario such as calling a C compression library that expects raw pointers to input and output buffers. Second, the Rust responsibilities inside the unsafe block: ensuring pointers are non-null and valid for the entire call, that output capacity is at least as large as the C function will write, that Rust aliasing rules are not violated across the boundary, and that the extern signature is correct because the compiler cannot check it. Third, the safe wrapper pattern in Rust, where the unsafe internals are hidden behind a safe API that validates lengths and lifetimes. Fourth, the Cgo model: Go automatically copies string and slice data into the C heap, forbids Go code from storing Go pointers in C memory, and switches an OS thread from Go's lightweight stack to C's stack, incurring runtime overhead. Fifth, the comparative responsibility: the Rust developer bears the full proof burden for zero-cost abstraction, whereas the Go developer must only manage C-allocated memory and obey the no-GC-pointers-in-C rule, letting the runtime handle the rest.
COMMON WRONG ANSWERS: Claiming that unsafe means Rust stops managing memory or that the compiler ignores the block entirely. Asserting that Cgo lets you freely pass Go pointers to C and store them indefinitely. Conflating unsafe Rust with C by saying there are no rules inside the block. Failing to mention that incorrect extern declarations in Rust are undefined behavior even if the code compiles. Saying Cgo is zero-cost or that it does not copy data across the boundary.
LIKELY FOLLOW-UPS: How would you prevent a dangling pointer if the C library stores a callback or retains the buffer after the call returns? What happens if the C library is not thread-safe and you call it from multiple goroutines or Rust threads? How do you test an FFI boundary without valgrind or sanitizers? Why might you choose to write the binding in Rust versus Go for a latency-sensitive service?
ONE CONCRETE EXAMPLE: Suppose you are binding the snappy compress function from the snappy C library. In Rust, you use an unsafe block to pass the input buffer pointer and a freshly allocated vector with capacity from snappy max compressed length. You must ensure the vector outlives the C call, then set the true length afterward. In Go via Cgo, you pass a Go slice but the runtime may copy it to C memory; you still malloc the output buffer in C and free it explicitly, but you cannot hand a Go pointer to the C library and let the library keep it after the function returns.
Read the original → doc.rust-lang.org
Get five bites like this every day.
Tezvyn delivers a daily feed of 60-second tech bites with quizzes to lock in what you learn.