You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
[wasm][mono][AOT] Nullable<T> of value types defined in interpreted assemblies still crash when corelib is AOT'd (null function / signature mismatch) #131945
Follow-up to #131537. The AOT-compiler fix for that issue emits the Nullable<T> box/unbox gsharedvt out/in-sig wrappers for a fixed set of corelib value types (Int128, UInt128, Half, Decimal, Guid, DateTime, DateTimeOffset, TimeSpan, DateOnly, TimeOnly, IntPtr, UIntPtr). That resolves the reported System.Text.Json cases, but it cannot cover value types defined in a user assembly that runs interpreted while corelib is AOT'd.
Boxing Nullable<CustomStruct> through the non-generic IEnumerator.Current (e.g. new System.Collections.Queue(new List<CustomStruct?>{...})) still crashes, with one of two symptoms depending on the struct's gsharedvt layout:
Layout matches a covered corelib type (e.g. a 16-byte { long; long } struct shares the layout of Int128): the out-sig wrapper is found (it is layout-keyed), but the concrete Nullable<CustomStruct>.Box body is NULL → MONO_WASM: null function.
Define a custom value type in the app assembly and box a Nullable<T> of it through the non-generic enumerator:
usingSystem.Collections;usingSystem.Collections.Generic;structCustom16{publiclongA;publiclongB;}// 16-byte, 8-align → same gsharedvt layout as Int128// Queue(ICollection) enumerates via the non-generic IEnumerator, whose Current// getter boxes each element Nullable<Custom16> -> object.var_=newQueue(newList<Custom16?>{newCustom16{A=1,B=2}});
Build with /p:RunAOTCompilation=true and run.
Observed:
MONO_WASM: null function
RuntimeError: null function
at aot_instances_aot_wrapper_gsharedvt_out_sig_obj_..._Mono_dValueTuple_602_3cbyte_2c_20Mono_dValueTuple_602_3clong_2c_20long_3e_3e_
...
WASM EXIT 1
A struct with a novel layout (e.g. { int; int; int }, { long; long; long }, or one containing a GC reference) instead reproduces the original function signature mismatch.
Expected: the value round-trips like any corelib Nullable<T>.
Root cause
There are two halves, and only one is layout-shareable:
The out-sig wrapperobject(Nullable<T>) is layout-normalized — mini_get_underlying_signature → get_wrapper_shared_type → get_wrapper_shared_vtype maps Nullable<Int128> to Mono.ValueTuple<byte, ValueTuple<long,long>>. So one representative per gsharedvt layout covers all same-layout payloads, which is why a custom 16-byte struct reuses the wrapper emitted for Int128.
The concrete Nullable<T>.Box/Unbox body is per-type and cannot be gsharedvt-shared in the llvmonly minimal gsharedvt. corelib's AOT compiler can only pre-emit it for types it can enumerate — it has no way to know about a user struct. At runtime, class_type_info (for MONO_RGCTX_INFO_NULLABLE_CLASS_BOX/UNBOX) calls mono_jit_compile_method(Box), which in aot-only llvmonly returns NULL (mini-runtime.c, the if (mono_llvm_only) return NULL; branch) and there is no interpreter fallback for this rgctx path. The layout-keyed wrapper is then invoked with a NULL target → "null function".
Because the concrete body cannot be produced by the AOT compiler, this class of failure cannot be fixed purely in the AOT compiler; it needs a runtime-side change.
High-level fix proposal
Two options (not mutually exclusive):
A. Interpreter fallback for the Nullable box/unbox rgctx path (localized).
In class_type_info for MONO_RGCTX_INFO_NULLABLE_CLASS_BOX/UNBOX, when mono_jit_compile_method returns NULL in aot-only llvmonly, materialize an interpreter entry (create_method_pointer_llvmonly) for the concrete Nullable<T>.Box/Unbox and adapt it with the existing (layout-keyed, AOT'd) out/in-sig wrapper. The wrapper stays AOT'd; only the small box/unbox body runs interpreted. Smallest change; removes both the null function and signature mismatch symptoms for any interpreted payload whose layout wrapper exists, and the wrapper set can stay corelib-scoped.
B. Route Nullable box/unbox through a generic by-pointer helper (broader).
Plain (non-Nullable) value-type boxing already works for arbitrary gsharedvt types because it goes through a helper that takes the value by pointer + an rgctx (class) — a fixed signature, no per-type method or wrapper. Making Nullable box/unbox use the same by-pointer shape would remove the concrete-method dependency entirely and make the whole object(Nullable<T>) wrapper machinery unnecessary. More invasive, but eliminates the entire class of problem (and the corelib type list added for #131537).
Summary
Follow-up to #131537. The AOT-compiler fix for that issue emits the
Nullable<T>box/unbox gsharedvt out/in-sig wrappers for a fixed set of corelib value types (Int128,UInt128,Half,Decimal,Guid,DateTime,DateTimeOffset,TimeSpan,DateOnly,TimeOnly,IntPtr,UIntPtr). That resolves the reportedSystem.Text.Jsoncases, but it cannot cover value types defined in a user assembly that runs interpreted while corelib is AOT'd.Boxing
Nullable<CustomStruct>through the non-genericIEnumerator.Current(e.g.new System.Collections.Queue(new List<CustomStruct?>{...})) still crashes, with one of two symptoms depending on the struct's gsharedvt layout:{ long; long }struct shares the layout ofInt128): the out-sig wrapper is found (it is layout-keyed), but the concreteNullable<CustomStruct>.Boxbody isNULL→MONO_WASM: null function.function signature mismatch, exactly like [wasm][mono][AOT] Number_AsCollectionElement_RoundTrip crashes with unction signature mismatch boxing Nullable<Int128> #131537.This is the same underlying limitation as #66220, generalized to user value types.
Repro
WASM, Mono, AOT'd corelib + interpreted app (the
WasmTestOnChrome-MONO-STshape).Nullable<T>of it through the non-generic enumerator:/p:RunAOTCompilation=trueand run.Observed:
A struct with a novel layout (e.g.
{ int; int; int },{ long; long; long }, or one containing a GC reference) instead reproduces the originalfunction signature mismatch.Expected: the value round-trips like any corelib
Nullable<T>.Root cause
There are two halves, and only one is layout-shareable:
object(Nullable<T>)is layout-normalized —mini_get_underlying_signature→get_wrapper_shared_type→get_wrapper_shared_vtypemapsNullable<Int128>toMono.ValueTuple<byte, ValueTuple<long,long>>. So one representative per gsharedvt layout covers all same-layout payloads, which is why a custom 16-byte struct reuses the wrapper emitted forInt128.Nullable<T>.Box/Unboxbody is per-type and cannot be gsharedvt-shared in the llvmonly minimal gsharedvt. corelib's AOT compiler can only pre-emit it for types it can enumerate — it has no way to know about a user struct. At runtime,class_type_info(forMONO_RGCTX_INFO_NULLABLE_CLASS_BOX/UNBOX) callsmono_jit_compile_method(Box), which in aot-only llvmonly returnsNULL(mini-runtime.c, theif (mono_llvm_only) return NULL;branch) and there is no interpreter fallback for this rgctx path. The layout-keyed wrapper is then invoked with aNULLtarget → "null function".Because the concrete body cannot be produced by the AOT compiler, this class of failure cannot be fixed purely in the AOT compiler; it needs a runtime-side change.
High-level fix proposal
Two options (not mutually exclusive):
A. Interpreter fallback for the Nullable box/unbox rgctx path (localized).
In
class_type_infoforMONO_RGCTX_INFO_NULLABLE_CLASS_BOX/UNBOX, whenmono_jit_compile_methodreturnsNULLin aot-only llvmonly, materialize an interpreter entry (create_method_pointer_llvmonly) for the concreteNullable<T>.Box/Unboxand adapt it with the existing (layout-keyed, AOT'd) out/in-sig wrapper. The wrapper stays AOT'd; only the small box/unbox body runs interpreted. Smallest change; removes both thenull functionandsignature mismatchsymptoms for any interpreted payload whose layout wrapper exists, and the wrapper set can stay corelib-scoped.B. Route Nullable box/unbox through a generic by-pointer helper (broader).
Plain (non-Nullable) value-type boxing already works for arbitrary gsharedvt types because it goes through a helper that takes the value by pointer + an rgctx (class) — a fixed signature, no per-type method or wrapper. Making Nullable box/unbox use the same by-pointer shape would remove the concrete-method dependency entirely and make the whole
object(Nullable<T>)wrapper machinery unnecessary. More invasive, but eliminates the entire class of problem (and the corelib type list added for #131537).Related
Int128/UInt128/Half/… by the AOT-compiler change).gsharedvt_out_sigcrash.Note
This issue was drafted with GitHub Copilot.