Summary
Follow-up to #5621 (now merged). With ergonomic camelCase exports routing to their js_<pkg>_* symbols, a whole class of bindings still can't run: those whose functions take descriptor objects marshalled to a "string" FFI param via JSON.stringify. #5621 rewrites the call site directly to the FFI symbol, bypassing the package's wrapper body — so the object never gets stringified and hits js_native_abi_check_string_ptr, which throws TypeError: Expected string for native string parameter.
This blocks @perryts/webgpu end-to-end: ~20 of its 69 functions take a GPUBufferDescriptor / GPURenderPipelineDescriptor / GPUCanvasConfiguration / etc. object and declare the corresponding manifest param as "string" (the binding's documented design is "the TS side JSON.stringifys the descriptor and we serde_json-deserialize on the Rust side").
Evidence
Built perry at the #5621 merge (7b15b680b), linked @perryts/webgpu's real staticlib, ran:
const adapter = await requestAdapter(); // -> 1 ✓ routes
const { device } = await adapterRequestDevice(adapter); // -> device 2 ✓ routes
const sh = deviceCreateShaderModule(device, "@compute ..."); // -> 4 ✓ STRING arg
const buf = deviceCreateBuffer(device, { size: 16, usage: 0x80 });// OBJECT arg
// TypeError: Expected string for native string parameter
So #5621 routing is confirmed working for no-arg / handle-arg / real-string-arg functions. It's specifically object → "string" param that fails, because the wrapper body that would have done JSON.stringify(descriptor) is bypassed by the call-site rewrite.
crates/perry-runtime/src/native_abi.rs:137 (js_native_abi_check_string_ptr) validates rather than coerces — a non-string throws.
Root cause
Before #5621, descriptor bindings worked (when routing worked at all) because the camelCase wrapper had a real body: deviceCreateBuffer(d, desc) => js_webgpu_device_create_buffer(d, JSON.stringify(desc)). #5621's call-site rewrite (lower_call/extern_func.rs, ffi_aliases) replaces deviceCreateBuffer(...) with a direct call to js_webgpu_device_create_buffer(...) — the wrapper body never runs, so the descriptor reaches the boundary as a live object.
There's no longer any TS-side hook to marshal the object, so marshalling has to move into the FFI boundary itself.
Proposed fix
Add a manifest param descriptor that means "JSON-serialize this JS value to a string before the call":
At the call site, a "json" param lowers the arg through JSON.stringify (the existing js_json_stringify runtime entry) and passes the resulting string pointer — the native side still receives a *const StringHeader and serde_json-deserializes it unchanged, so no binding Rust change is needed beyond the manifest edit. Objects, arrays, and primitives all serialize; the type is explicit and opt-in, so existing "string" params (real strings, e.g. WGSL source) keep their strict check.
Alternative (less preferred): make "string" auto-JSON.stringify a non-string arg. Works with zero manifest changes but is implicit and would mask "passed the wrong thing" bugs — a dedicated "json" type is cleaner.
Note on binding-side packaging (already handled, for context)
For @perryts/webgpu to even reach this point, two binding-side manifest fixes were needed and applied (not perry bugs):
With both in place + the #5621 build, the routing works perfectly up to the first descriptor-object call — which is where this issue picks up.
Environment
Local perry built from 7b15b680b (#5621 merge), macOS arm64.
Summary
Follow-up to #5621 (now merged). With ergonomic camelCase exports routing to their
js_<pkg>_*symbols, a whole class of bindings still can't run: those whose functions take descriptor objects marshalled to a"string"FFI param viaJSON.stringify. #5621 rewrites the call site directly to the FFI symbol, bypassing the package's wrapper body — so the object never gets stringified and hitsjs_native_abi_check_string_ptr, which throwsTypeError: Expected string for native string parameter.This blocks
@perryts/webgpuend-to-end: ~20 of its 69 functions take aGPUBufferDescriptor/GPURenderPipelineDescriptor/GPUCanvasConfiguration/ etc. object and declare the corresponding manifest param as"string"(the binding's documented design is "the TS sideJSON.stringifys the descriptor and weserde_json-deserialize on the Rust side").Evidence
Built
perryat the #5621 merge (7b15b680b), linked@perryts/webgpu's real staticlib, ran:So #5621 routing is confirmed working for no-arg / handle-arg / real-string-arg functions. It's specifically object →
"string"param that fails, because the wrapper body that would have doneJSON.stringify(descriptor)is bypassed by the call-site rewrite.crates/perry-runtime/src/native_abi.rs:137(js_native_abi_check_string_ptr) validates rather than coerces — a non-string throws.Root cause
Before #5621, descriptor bindings worked (when routing worked at all) because the camelCase wrapper had a real body:
deviceCreateBuffer(d, desc) => js_webgpu_device_create_buffer(d, JSON.stringify(desc)). #5621's call-site rewrite (lower_call/extern_func.rs,ffi_aliases) replacesdeviceCreateBuffer(...)with a direct call tojs_webgpu_device_create_buffer(...)— the wrapper body never runs, so the descriptor reaches the boundary as a live object.There's no longer any TS-side hook to marshal the object, so marshalling has to move into the FFI boundary itself.
Proposed fix
Add a manifest param descriptor that means "JSON-serialize this JS value to a string before the call":
{ "name": "js_webgpu_device_create_buffer", "params": ["i64", "json"], "returns": "i64" }At the call site, a
"json"param lowers the arg throughJSON.stringify(the existingjs_json_stringifyruntime entry) and passes the resulting string pointer — the native side still receives a*const StringHeaderandserde_json-deserializes it unchanged, so no binding Rust change is needed beyond the manifest edit. Objects, arrays, and primitives all serialize; the type is explicit and opt-in, so existing"string"params (real strings, e.g. WGSL source) keep their strict check.Alternative (less preferred): make
"string"auto-JSON.stringifya non-string arg. Works with zero manifest changes but is implicit and would mask "passed the wrong thing" bugs — a dedicated"json"type is cleaner.Note on binding-side packaging (already handled, for context)
For
@perryts/webgputo even reach this point, two binding-side manifest fixes were needed and applied (not perry bugs):targets[*].crate+lib(without them perry builds the staticlib but never links it — silent undefined symbols; see also the XOR-validation gap I mentioned on Native-library imports with ergonomic camelCase exports don't route to their js_<pkg>_* FFI symbols #5621).exports.perryentry pointing at the.tssource, so the import is classifiedModuleKind::NativeCompiledand Native-library imports with ergonomic camelCase exports don't route to their js_<pkg>_* FFI symbols #5621's alias block (gated atcompile.rs:~2491) is actually reached.With both in place + the #5621 build, the routing works perfectly up to the first descriptor-object call — which is where this issue picks up.
Environment
Local
perrybuilt from7b15b680b(#5621 merge), macOS arm64.