| name | rust-kernel-ffi |
| description | Build a Rust cdylib that a TypeScript/Bun runtime calls via koffi FFI, for a security kernel or enforcement core. Use when exposing Rust to Node/Bun (#[no_mangle] extern "C", JSON-over-C-string functions + a free fn), bridging a Rust crypto/capability primitive to a daemon, loading a .dylib/.so with koffi and degrading gracefully when it's absent, RustCrypto HMAC-SHA256 + constant-time compare, crate-type=["rlib","cdylib"], or panic-free fail-closed Rust across an FFI boundary. NOT for: WebAssembly (use wasm-bindgen), async-across-FFI, moving Rust structs/Vec/String across the boundary without a free fn, or pure Rust-only code with no FFI. |
| allowed-tools | Read, Write, Edit, Bash, Grep, Glob, WebSearch |
| license | Apache-2.0 |
| metadata | {"category":"Systems & Runtime","tags":["rust","ffi","cdylib","koffi","security-kernel","typescript-bun"],"provenance":{"kind":"first-party","owners":["port-daddy"]},"pairs-with":[{"skill":"rust-debugging-mastery","reason":"Debugging a segfault/SIGABRT at the FFI boundary needs the general Rust debugging toolkit this skill's failure-mode table only summarizes."},{"skill":"rust-with-claude-code","reason":"Day-to-day Rust authoring conventions (cargo workflow, error handling, testing) that this skill assumes but does not re-teach."},{"skill":"rust-app-distribution","reason":"Shipping the built cdylib inside a distributed binary/installer is the packaging half of what build-core.sh only builds locally."},{"skill":"gpui-rust-console","reason":"A gpui console process that also loads this cdylib shares the same koffi-loader-with-fallback pattern for its own native integrations."}],"io-contract":{"kind":"deliverable","consumes":["[Truncated]","[Truncated]"],"produces":["[Truncated]","[Truncated]"]}} |
Rust Kernel FFI (cdylib ⇄ TypeScript via koffi)
Build a Rust shared library that a Node/Bun daemon calls over a C ABI. The canonical
in-repo template is core/harbor-card-rs/src/lib.rs (the #[no_mangle] extern "C"
exports) loaded by lib/arbiter.ts (the koffi loader with graceful fallback),
built by scripts/build-core.sh. Teach those patterns; don't reinvent them.
The one rule that prevents most disasters
Never let a panic unwind across an extern "C" boundary — it is undefined behavior.
Either wrap every export body in std::panic::catch_unwind and return a sentinel
(false/null) on catch, or set panic = "abort" in [profile.release]. A security
kernel should do BOTH: abort in release, catch_unwind for defense in depth.
Decision points
Returning a string/buffer to TS?
├── No → return a primitive (bool / u64 / i32). No allocation, no free fn. Simplest.
└── Yes → CString::into_raw() to hand out + a matching #[no_mangle] free fn the TS
caller MUST invoke (from_raw reclaims it). Forgetting the free fn = leak.
Crossing the boundary with structured data?
├── Marshal as JSON over `*const c_char` + `usize` len (harbor-card-rs pattern).
│ Validate: null ptr → sentinel; len==0 or len>BOUND → sentinel; from_utf8 → sentinel;
│ serde_json::from_str → sentinel. Five guards before any logic.
└── Never pass a Rust struct/enum directly (ABI is not C-stable). #[repr(C)] only for
genuine C structs of primitives.
Security-critical compare (MAC/tag)?
├── Constant-time fold-XOR: `for i {acc |= a[i]^b[i]}; acc==0`. Never early-return.
└── Length differs → return false up front (length is not secret), then fold equal-length.
Dylib missing at runtime (source install / CI doesn't build it)?
└── Degrade gracefully: capture the load error, return null from the loader, and let the
caller fall back to a pure-TS path (arbiter.ts falls back to cap-attenuation-monitor).
The FFI is an UPGRADE, not a hard dependency. CI unit-tests do NOT build the dylib.
Worked example — JSON-in/JSON-out export + free fn + koffi side
Rust (crate-type = ["rlib", "cdylib"]; deps hmac, sha2, serde_json):
use std::ffi::{CStr, CString};
use std::os::raw::c_char;
use std::panic::{catch_unwind, AssertUnwindSafe};
(req: * c_char, len: ) * c_char {
= ((|| {
req.() || len == || len > { ; }
= { std::slice::(req * , len) };
= std::::(bytes).()?;
: VerifyRequest = serde_json::(s).()?;
= (parsed);
CString::(serde_json::(&result).()?).()
}));
out {
((c)) => c.(),
_ => std::ptr::(),
}
}
(ptr: * c_char) {
!ptr.() { { (CString::(ptr)); } }
}