Exported functions aren't patched correctly with wasm32-wasip1
Bug report
externref doesn't correctly patch WASM modules built using the wasm32-wasip1 (and wasm32-wasip1-threads) target. Both exported functions with externref returns and with externref parameters fail, with different errors.
The *-wasip1-* targets add an extra level of indirection around exported functions which apparently throws off the patcher.
Imported functions using externref seem to work, presumably because they're not compiled differently.
Steps to reproduce
As far as I can tell, this always happens with wasip1, so the minimal examples are very minimal.
Return type
e.g.
#[externref::externref]
#[export_name = "extern_ref_test"]
pub extern "C" fn extern_ref_test() -> Option<externref::Resource<()>> {
None
}
The processor finished, but the resulting wasm is invalid, e.g. wasmtime reports "Invalid input WebAssembly code at offset 116: type mismatch: expected i32, found externref".
This is the complete wasm after processing. Fixing the type of $extern_ref_test by hand makes it work (or at least makes it validate):
(module $externref_error.wasm
(;@b ;) (type $#type0 (;0;) (func))
(;@e ;) (type $#type1 (;1;) (func (result i32)))
(;@12 ;) (type $#type2 (;2;) (func (result externref)))
(;@16 ;) (type $#type3 (;3;) (func (param i32) (result externref)))
(;@26 ;) (table $#table0 (;0;) 0 externref)
(;@2c ;) (memory $#memory0 (;0;) 16)
(;@31 ;) (export "memory" (memory $#memory0))
(;@3a ;) (export "extern_ref_test" (func $extern_ref_test.command_export))
(;@4c ;) (export "externrefs" (table $#table0))
(;@5d ;) (func $#func0 (;0;) (type $#type3) (param $#local0 i32) (result externref)
(;@5e ;) local.get $#local0
(;@60 ;) i32.const -1
(;@62 ;) i32.eq
(;@63 ;) if $#label0 (result externref)
(;@65 ;) ref.null extern
(;@67 ;) else
(;@68 ;) local.get $#local0
(;@6a ;) table.get $#table0
(;@6c ;) end
)
(;@6f ;) (func $extern_ref_test (;1;) (type $#type1) (result i32)
(;@70 ;) i32.const -1
(;@72 ;) call $#func0
)
(;@76 ;) (func $__wasm_call_dtors (;2;) (type $#type0)
(;@77 ;) call $dummy
(;@79 ;) call $dummy
)
(;@7d ;) (func $extern_ref_test.command_export (;3;) (type $#type2) (result externref)
(;@7e ;) call $extern_ref_test
(;@80 ;) call $__wasm_call_dtors
)
(;@84 ;) (func $dummy (;4;) (type $#type0))
(;@f5 ;) (@producers
(;@10a ;) (language "Rust" "")
(;@110 ;) (language "C11" "")
(;@123 ;) (processed-by "rustc" "1.85.1 (4eb161250 2025-03-15)")
(;@147 ;) (processed-by "clang" "19.1.5-wasi-sdk (https://github.com/llvm/llvm-project ab4b5a2db582958af1ee308a790cfdb42bd24720)")
(;@1ad ;) (processed-by "walrus" "0.23.3")
)
(;@1bd ;) (@custom "target_features" (after code) "\05+\0bbulk-memory+\0amultivalue+\0fmutable-globals+\0freference-types+\08sign-ext")
)
(;@213 ;)
Parameters
#[externref::externref]
#[export_name = "extern_ref_test"]
pub extern "C" fn extern_ref_test(x: externref::Resource<()>) {}
This also produces invalid wasm after processing, the parameter type of the inner function doesn't get replaced: (func $extern_ref_test (;73;) (type $#type2) (param $#local0 i32)
Return type + parameter
#[externref::externref]
#[export_name = "extern_ref_test"]
pub extern "C" fn extern_ref_test(x: externref::Resource<()>) -> Option<externref::Resource<()>> {
None
}
This just makes the processor fail with:
unexpected call to an `externref`-returning function in extern_ref_test at 564. This can be caused by an external WASM manipulation tool such as `wasm-opt`. Please run such tools *after* the externref processor.
Expected behavior
The module should be patched correctly when using wasi. I haven't looked at processor in detail so no idea if this is trivial or impossible to fix :\
Environment
Rust 1.85.1 from rustup on x86_64-linux-gnu
Hi! Terribly sorry for a huge delay. Can confirm that the issue exists, but I don't think there's an easy way to solve it. The externref preprocessor relies on the fact that functions taking / producing externrefs are known in advance (i.e., are a subset of the exports / imports of the module). This is not the case for the wasip1 targets because of the additional wrapping you've mentioned (which happens even on the release compilation profile). AFAIU, a reasonable-complexity solution would be to extend the preprocessor logic to the first function called by transformed export fns if the export proxies to it. I'll try to implement it, but not 100% sure it'll work in all cases, and not giving completion estimates.
No worries, my attempt to use WASM didn't work out anyway so it's not an active problem for me.