oven-sh/bun · error · Error
slice/str param `${ty}` is not FFI-safe; pass (ptr, len)
Error message
slice/str param `${ty}` is not FFI-safe; pass (ptr, len) What it means
generate-host-exports.ts generates C-ABI thunks for Rust functions marked with a `// HOST_EXPORT(...)` comment. Slices (&[T]) and string slices (&str) are not FFI-safe — they are fat pointers whose layout has no stable C ABI — so ptrify() refuses them and demands the canonical (ptr, len) pair. The function itself can keep ergonomic types internally; only the exported signature must use raw parts.
Source
Thrown at src/codegen/generate-host-exports.ts:134
line: number;
fnName: string;
modPath: string; // `crate::…` (relative to bun_runtime) or `bun_jsc::…`
params: Param[];
ret: string;
shape: "host" | "lazy" | "generic" | "rust";
isUnsafe: boolean;
}
const markerRe = /^\s*\/\/\s*HOST_EXPORT\(\s*([A-Za-z_][A-Za-z0-9_]*)\s*(?:,\s*(jsc|c|rust))?\s*\)\s*$/;
// `pub fn name(` — capture name; the param list and return type are pulled by
// a small balanced-paren scanner because params routinely span lines.
const fnHeadRe = /^\s*pub\s+(unsafe\s+)?fn\s+([A-Za-z_][A-Za-z0-9_]*)\s*\(/;
function ptrify(ty: string): { cTy: string; deref: (n: string) => string } {
ty = ty.trim();
// `&[T]` / `&str` are NOT FFI-safe; reject (caller should use ptr+len).
if (/^&\s*(?:\[|str\b)/.test(ty)) {
throw new Error(`slice/str param \`${ty}\` is not FFI-safe; pass (ptr, len)`);
}
// `&mut T` / `&T` — keep as a reference in the thunk signature. `&T` and
// `*const T` (resp. `&mut T`/`*mut T`) are ABI-identical for `extern "C"`
// when the C++ caller guarantees non-null (it does), so the thunk param can
// be the safe reference type directly and the body needs no `unsafe` deref.
if (/^&/.test(ty)) return { cTy: ty, deref: n => n };
// Already a raw pointer / scalar / `Option<…>` / `JSValue` — pass through.
return { cTy: ty, deref: n => n };
}
function parseParams(list: string, where: string): Param[] {
// Split on top-level commas (ignore `<…>` and `(…)` nesting).
const parts: string[] = [];
let depth = 0,
start = 0;
for (let i = 0; i < list.length; i++) {
const c = list[i];
if (c === "<" || c === "(" || c === "[") depth++;View on GitHub (pinned to 8c5296ac45)
Solutions
- Change the parameter to a raw pointer plus length: `ptr: *const u8, len: usize` (or `*const T, usize`)
- Reconstruct the safe view inside the body with std::slice::from_raw_parts / std::str::from_utf8 (the caller — generated C++ — guarantees validity)
- Keep the ergonomic `&[u8]` API as a separate inner function the FFI wrapper calls
Example fix
// before
// HOST_EXPORT(BunReadPacket)
pub fn read_packet(buf: &[u8]) -> u32 { ... }
// after
// HOST_EXPORT(BunReadPacket)
pub fn read_packet(ptr: *const u8, len: usize) -> u32 {
let buf = unsafe { std::slice::from_raw_parts(ptr, len) };
...
} Defensive patterns
Strategy: validation
Validate before calling
// scan HOST_EXPORT-marked fns for slice/str params before building
import { Glob } from "bun";
for (const f of new Glob("src/**/*.rs").scanSync(".")) {
const lines = (await Bun.file(f).text()).split("\n");
for (let i = 0; i < lines.length; i++) {
if (/\/\/\s*HOST_EXPORT\(/.test(lines[i])) {
// crude: check the following fn signature lines for &str / &[ params
const sig = lines.slice(i, i + 6).join(" ");
if (/&\s*(?:\[|str\b)/.test(sig)) throw new Error(`${f}:${i + 1} HOST_EXPORT has slice/str param — use (ptr, len)`);
}
}
} Type guard
// in generator-side TS: narrow a parsed param type before emitting a thunk
function isFFISafeParamType(ty: string): boolean {
return !/^&\s*(?:\[|str\b)/.test(ty.trim());
} Prevention
- Write HOST_EXPORT signatures C-first: raw pointer + length at the boundary, safe slices reconstructed in the body
- Keep an inner ergonomic `&[u8]` function and let the exported wrapper be the only unsafe-looking layer
When it happens
Trigger: Annotating a `pub fn` with `// HOST_EXPORT(name)` (or the jsc/c variants) where any parameter is typed `&str`, `& [u8]`, `&[T]`, or `&mut [T]`, then running codegen/build.
Common situations: New Rust host functions written Rust-idiomatically first and marked for export second; refactors that change a `*const u8, len: usize` pair back into `&[u8]` for convenience.
Related errors
- ${loc}: HOST_EXPORT(${e.symbol}, rust) param `${p.raw}` is a
- ${loc}: HOST_EXPORT(${e.symbol}, rust) return type `${e.ret}
- ${where}: cannot parse param `${raw}`
- generate-string-map: duplicate key ${JSON.stringify(k)} in $
- Call is not known at bundle-time: '$$${name}${inner.result}'
AI-assisted analysis of oven-sh/bun@8c5296ac45 (2026-08-16).
Data as JSON: /api/errors/46f08c47eb9f6380.
Report an issue: GitHub.