Systems Programming Best Practices
Rules for safe, portable, well-tested systems and FFI code in Rust.
How to Use This List
- Review before merging FFI or platform-specific PRs
- Treat unchecked
unsafeas a security-sensitive change - Run Miri and cross-platform CI on systems code
A - Safety Boundaries
- Minimize
unsafescope. Keep FFI in dedicated modules with reviews. - RAII for every resource. Files, sockets, mappings, and handles get
Drop. - Document
# Safetypreconditions. Everyunsafe fnlists caller obligations. - No panics across FFI. Catch at the boundary; return error codes to C.
B - ABI and Memory
-
repr(C)for exported structs. Never assume Rust default layout crosses FFI. - Explicit ownership rules. Pair allocators with free functions; document lifetimes.
- Validate lengths before casts. Check buffer size and alignment before pointer casts.
- Layout tests in CI. Compare
size_of/align_ofagainst C headers.
C - Portability
- CI on Linux, macOS, Windows. Catch
cfgdrift early. - Use
Path, not strings. Avoid hardcoded/separators. - Abstract platform APIs. Isolate
cfg(unix)/cfg(windows)modules. - Test musl and glibc. Alpine vs Debian behavior differs.
D - Processes and I/O
- Reap child processes. Always
waitafterspawn. - Graceful shutdown. Handle SIGINT/SIGTERM; drain work before exit.
- Blocking off async executors. Use
spawn_blockingfor syscall-heavy work. - Atomic file writes. Temp file + rename for config and state.
E - Testing and Tooling
- Miri on FFI tests. Catch undefined behavior in CI.
- Valgrind/LSan periodically. Find leaks in C interop paths.
- Pin native library versions. Document minimum system library versions.
- Sanitize user paths. Reject
..traversal in privileged tools.
FAQs
How much unsafe is too much?
If safe wrappers cannot hide it, split into smaller reviewed functions with clear invariants.
When is bindgen required?
When the C API has more than a handful of functions or changes frequently.
Should I use nix or libc?
Prefer std, then nix, then raw libc as you need lower-level control.
FFI in async servers?
Never block the runtime. Offload to blocking threads or use async-native I/O.
How do I audit third-party C libs?
Track CVEs, enable ASan in tests, and sandbox privileged operations.
Stable ABI promises?
Only commit to ABI stability with repr(C), semver, and explicit compatibility tests.
Signal handlers?
Set atomic flags only. Defer real work to the main thread or runtime.
File permissions on Unix?
Set explicit modes on created files. Do not rely on umask defaults for secrets.
Windows service vs CLI?
Share core logic in a library; thin binaries for service and CLI entrypoints.
Supply chain for native code?
Vendor known hashes for downloaded SDKs. Verify checksums in build.rs.
Related
- Systems Basics - std foundations
- Building Sound Bindings - safe wrappers
- Cross-Platform Systems Code -
cfgpatterns - Working with Raw Memory - layout rules
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+.