How to Fix: error trying to import generic arrayref functions with concrete array types
Imported WebAssembly functions can fail when a module expects generic array references but the host or another module provides a function typed with a concrete array type. The validator treats these signatures as different, even when they look conceptually compatible.
This issue shows up in GC-enabled WebAssembly scenarios where one module exports a function like (param arrayref), while another tries to import or link it using a more specific array definition such as a named array heap type. The result is a type mismatch during validation or instantiation.
At a high level, the bug is not about array values themselves. It is about function type equality across module boundaries. In WebAssembly, imported and exported functions must match exactly according to the engine’s type rules. A function accepting a concrete array type is not automatically interchangeable with a function accepting the more general arrayref, and vice versa.
Understanding the Root Cause
The root cause is variance and exact signature matching for imported functions.
Consider these two ideas:
-
arrayref is a general reference type representing any array reference.
-
A concrete array type, defined inside a module, is a specific heap type with its own identity.
Even if every concrete array value can conceptually be viewed as an arrayref, that does not mean a function typed with one can be substituted for a function typed with the other in all contexts.
Why? Because function parameters are checked strictly. If module A exports:
(func (export "len") (param arrayref) ...)
and module B imports:
(import "a" "len" (func (param (ref $my_array)) ...))
those are different function types. The imported declaration is asking for a function that accepts only (ref $my_array), while the exported function accepts any arrayref. WebAssembly validation does not assume these are interchangeable across modules.
This becomes even more important with module-local type identities. A concrete array type declared in one module is not automatically identical to a similarly shaped type declared in another module. Structural similarity is not enough when the runtime expects exact canonicalized type compatibility rules.
In short, the failure happens because:
- The exporter and importer do not expose the exact same function signature.
- Generic reference types and concrete heap types are related by subtyping rules for values, but not freely swappable in imported function declarations.
- Concrete array types may carry module-specific type identity, making cross-module linking stricter than expected.
Step-by-Step Solution
The safest fix is to make the function signatures match exactly at the module boundary. Then, if needed, perform casts or wrappers inside the receiving module.
1. Export and import the same parameter type
If the shared API should be generic, keep it generic on both sides:
(module
(func (export "len") (param arrayref) (result i32)
;; implementation here
)
)
And import it the same way:
(module
(import "a" "len" (func $len (param arrayref) (result i32)))
)
This avoids type mismatch during linking.
2. Add a wrapper when one side needs a concrete array type internally
If your consuming module wants to work with a specific array type, do not force that concrete type into the import signature. Import the generic function first, then call it from a wrapper.
(module
(type $my_array (array mut i32))
(import "a" "len" (func $generic_len (param arrayref) (result i32)))
(func (export "len_my_array") (param $arr (ref $my_array)) (result i32)
local.get $arr
call $generic_len
)
)
This works because the wrapper converts usage at the call site, while preserving a valid generic import boundary.
3. If the exported function is concrete, import it as concrete only when the exact type identity matches
This is the trickiest case. If module A defines a concrete array type and exports a function using that exact type, module B cannot usually recreate a “matching” local type declaration and expect it to link. In many engines and toolchains, that type identity is not portable across independently defined modules.
Instead, redesign the boundary to use a common supertype such as arrayref if cross-module reuse is required.
4. Rust test strategy for reproducing and fixing the issue
When building a regression test in Rust with WAT modules, create two modules: one exporter and one importer. First show the failure with mismatched signatures, then update the importer to use the generic parameter.
#[test]
fn test_arrayref_import_signature_match() -> Result<(), Box<dyn std::error::Error>> {
let module_a = wat::parse_str(r#"
(module
(func (export "len") (param arrayref) (result i32)
i32.const 0
)
)
"#)?;
let module_b = wat::parse_str(r#"
(module
(import "a" "len" (func $len (param arrayref) (result i32)))
)
"#)?;
// Compile and link using your runtime of choice.
// The key assertion is that matching signatures validate successfully.
Ok(())
}
To demonstrate the problematic case, change the import to a concrete array type:
#[test]
fn test_arrayref_import_mismatch() -> Result<(), Box<dyn std::error::Error>> {
let module_b = wat::parse_str(r#"
(module
(type $arr (array mut i32))
(import "a" "len" (func $len (param (ref $arr)) (result i32)))
)
"#)?;
// Expect validation or instantiation failure here,
// because (param arrayref) != (param (ref $arr)).
Ok(())
}
If you are maintaining the engine or validator, this test is useful for confirming that generic-to-concrete substitution is rejected consistently and that diagnostics clearly explain the mismatch.
5. Prefer boundary-stable API design
For shared WebAssembly interfaces between modules, expose:
- arrayref when the function should accept any array
- Concrete array types only when both sides share the same canonical type context
- Wrappers for specialized internal logic
This is the most reliable long-term design because it minimizes cross-module type identity problems.
Common Edge Cases
1. Matching shapes but different type identities
Two modules may each define:
(type $arr (array mut i32))
They still are not guaranteed to be the same type for import/export compatibility. Shape equivalence does not automatically mean boundary compatibility.
2. Nullable versus non-nullable references
arrayref is typically nullable, while (ref $arr) is non-nullable unless written as (ref null $arr). A mismatch in nullability alone can cause validation errors.
3. Using anyref, eqref, or structref interchangeably
These reference categories sit at different points in the heap type hierarchy. A function accepting eqref is not the same as one accepting arrayref, even if arrays are equality-reference values.
4. Runtime support differences
Some runtimes may still gate GC features behind flags or experimental support. If your WAT looks correct but compilation fails earlier than expected, verify the engine configuration and feature set in the relevant project documentation.
5. Host embedding assumptions
If you bridge through Rust, JavaScript, or another host API, the embedding layer may erase or re-express type details. The actual failing point might be module validation, linker checks, or host function registration rather than the call itself.
FAQ
Can I import a function typed with arrayref as if it accepted my concrete array type?
No. Even though your concrete array is a kind of array reference, imported function signatures must match according to WebAssembly’s function typing rules. Use the same boundary type and wrap it locally if needed.
Why does defining the same array type in both modules not fix the issue?
Because concrete GC heap types can carry module-specific identity. Two separate declarations that look identical in WAT are not necessarily interchangeable at link time.
What is the best fix for a reusable cross-module API?
Use a generic supertype such as arrayref at the import/export boundary, then convert or specialize inside the module with wrapper functions. This keeps the public interface stable and avoids concrete type identity pitfalls.
If you are fixing this issue in a compiler, validator, or runtime, the expected behavior is usually to reject the import when the function type differs and to emit an error that explicitly names both signatures. If you are fixing application code, the practical solution is almost always the same: make the import/export signatures identical, and move specialization behind a wrapper.