Repository navigation
Codegen: FFI returns:"i64" treats string-pointer returns as Number; auto-detect to string is name-based #222
Description
Perry: 0.5.326 native (linux x86_64).
When an FFI function in package.json's perry.nativeLibrary.functions is declared with returns: "i64" but returns a Perry-format *const StringHeader pointer, perry's codegen emits a numeric path (sitofp of the i64 to f64) and the TS-side as any cast leaves the value as a Number. data.length returns 0; data.charCodeAt(i) returns the decimal digits of the pointer's integer value; the actual string contents are unreachable.
The auto-detect-string heuristic at crates/perry-codegen/src/lower_call.rs:475-481 only fires when the manifest is silent on return type, and only matches by function name substring (read_file / clipboard_text / file_dialog):
let returns_string = matches!(manifest_ret, Some("string") | Some("ptr"))
|| matches!(ext_return_type, HirType::String)
|| (manifest_ret.is_none() && has_string_args && (
name.contains("read_file")
|| name.contains("clipboard_text")
|| name.contains("file_dialog")
));So returns: "i64" always wins over the heuristic, and the heuristic itself depends on naming.
Minimal repro
package.json:
{
"perry": {
"nativeLibrary": {
"module": "myffi",
"functions": [
{ "name": "ffi_get_string", "params": [], "returns": "i64" }
],
"targets": { "linux": { "crate": "native/", "lib": "libmyffi.a" } }
}
}
}native/src/lib.rs:
#[no_mangle]
pub extern "C" fn ffi_get_string() -> *const u8 {
// Allocate a 20-byte Perry 0.5.x StringHeader + UTF-8 bytes for "hello".
let s = b"hello";
let total = 20 + s.len();
unsafe {
let p = std::alloc::alloc(std::alloc::Layout::from_size_align(total, 4).unwrap());
*(p as *mut u32) = 5; // utf16_len
*(p.add(4) as *mut u32) = 5; // byte_len
*(p.add(8) as *mut u32) = 5; // capacity
*(p.add(12) as *mut u32) = 1; // refcount
*(p.add(16) as *mut u32) = 0; // flags
std::ptr::copy_nonoverlapping(s.as_ptr(), p.add(20), 5);
p
}
}main.ts:
declare function ffi_get_string(): number;
const s = ffi_get_string() as any as string;
console.log("length:", s.length, "char[0]:", s.charCodeAt(0));Output: length: 0 char[0]: 104 (104 = 'h' from "hello…" — but only because Number.toString().charCodeAt(0) happens to coincide for short numbers)
Expected: length: 5 char[0]: 104
Switching the manifest to "returns": "string" produces the correct output.
Real-world impact
Caught when bringing bloom-engine's Linux backend up. bloom_read_file, bloom_get_clipboard_text, bloom_open_file_dialog, bloom_save_file_dialog were all declared returns: "i64" despite returning *const StringHeader. On perry 0.5.x the file contents looked like garbled numbers; the author had relied on the (older?) auto-detect heuristic to coerce them to strings. Workaround: change the four entries to returns: "string". PR: Bloom-Engine/engine@8562897.
Suggestions
- Treat
returns: "i64"as a handle type and require an explicitreturns: "string"/"ptr"for string-shaped returns (current behavior — but make this a hard error when a TS callsite usesas stringagainst the return value? at least surface a warning). - Or: drop the name-based heuristic entirely (it silently rewards
bloom_read_fileoverbloom_load_text) and require explicit declaration. - Document the contract in the
package.jsonperry.nativeLibraryreference.
Activity
fix(codegen): closes #222 — explicit FFI returns:"i64_str" for string…