{"record":{"id":"46f08c47eb9f6380","repo":"oven-sh/bun","slug":"slice-str-param-ty-is-not-ffi-safe-pass-ptr","errorCode":null,"errorMessage":"slice/str param `${ty}` is not FFI-safe; pass (ptr, len)","messagePattern":"slice/str param `(.+?)` is not FFI-safe; pass \\(ptr, len\\)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/codegen/generate-host-exports.ts","lineNumber":134,"sourceCode":"  line: number;\n  fnName: string;\n  modPath: string; // `crate::…` (relative to bun_runtime) or `bun_jsc::…`\n  params: Param[];\n  ret: string;\n  shape: \"host\" | \"lazy\" | \"generic\" | \"rust\";\n  isUnsafe: boolean;\n}\n\nconst markerRe = /^\\s*\\/\\/\\s*HOST_EXPORT\\(\\s*([A-Za-z_][A-Za-z0-9_]*)\\s*(?:,\\s*(jsc|c|rust))?\\s*\\)\\s*$/;\n// `pub fn name(` — capture name; the param list and return type are pulled by\n// a small balanced-paren scanner because params routinely span lines.\nconst fnHeadRe = /^\\s*pub\\s+(unsafe\\s+)?fn\\s+([A-Za-z_][A-Za-z0-9_]*)\\s*\\(/;\n\nfunction ptrify(ty: string): { cTy: string; deref: (n: string) => string } {\n  ty = ty.trim();\n  // `&[T]` / `&str` are NOT FFI-safe; reject (caller should use ptr+len).\n  if (/^&\\s*(?:\\[|str\\b)/.test(ty)) {\n    throw new Error(`slice/str param \\`${ty}\\` is not FFI-safe; pass (ptr, len)`);\n  }\n  // `&mut T` / `&T` — keep as a reference in the thunk signature. `&T` and\n  // `*const T` (resp. `&mut T`/`*mut T`) are ABI-identical for `extern \"C\"`\n  // when the C++ caller guarantees non-null (it does), so the thunk param can\n  // be the safe reference type directly and the body needs no `unsafe` deref.\n  if (/^&/.test(ty)) return { cTy: ty, deref: n => n };\n  // Already a raw pointer / scalar / `Option<…>` / `JSValue` — pass through.\n  return { cTy: ty, deref: n => n };\n}\n\nfunction parseParams(list: string, where: string): Param[] {\n  // Split on top-level commas (ignore `<…>` and `(…)` nesting).\n  const parts: string[] = [];\n  let depth = 0,\n    start = 0;\n  for (let i = 0; i < list.length; i++) {\n    const c = list[i];\n    if (c === \"<\" || c === \"(\" || c === \"[\") depth++;","sourceCodeStart":116,"sourceCodeEnd":152,"githubUrl":"https://github.com/oven-sh/bun/blob/8c5296ac459e8252d3cd702f3fbcbb0c249d95d5/src/codegen/generate-host-exports.ts#L116-L152","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\n// HOST_EXPORT(BunReadPacket)\npub fn read_packet(buf: &[u8]) -> u32 { ... }\n\n// after\n// HOST_EXPORT(BunReadPacket)\npub fn read_packet(ptr: *const u8, len: usize) -> u32 {\n    let buf = unsafe { std::slice::from_raw_parts(ptr, len) };\n    ...\n}","handlingStrategy":"validation","validationCode":"// scan HOST_EXPORT-marked fns for slice/str params before building\nimport { Glob } from \"bun\";\nfor (const f of new Glob(\"src/**/*.rs\").scanSync(\".\")) {\n  const lines = (await Bun.file(f).text()).split(\"\\n\");\n  for (let i = 0; i < lines.length; i++) {\n    if (/\\/\\/\\s*HOST_EXPORT\\(/.test(lines[i])) {\n      // crude: check the following fn signature lines for &str / &[ params\n      const sig = lines.slice(i, i + 6).join(\" \");\n      if (/&\\s*(?:\\[|str\\b)/.test(sig)) throw new Error(`${f}:${i + 1} HOST_EXPORT has slice/str param — use (ptr, len)`);\n    }\n  }\n}","typeGuard":"// in generator-side TS: narrow a parsed param type before emitting a thunk\nfunction isFFISafeParamType(ty: string): boolean {\n  return !/^&\\s*(?:\\[|str\\b)/.test(ty.trim());\n}","tryCatchPattern":null,"preventionTips":["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"],"tags":["ffi","codegen","rust","abi"],"backgroundTag":null,"analyzedSha":"8c5296ac459e8252d3cd702f3fbcbb0c249d95d5","analyzedAt":"2026-08-16T08:01:58.794Z","schemaVersion":2},"datasetVersion":"2026-08-16T08:17:34.114Z"}