dotnet / dotnet/runtime

[browser-wasm] CoreCLR runtime tests blocked on interop gaps after test-specific corerun enablement

Open
#131,811 3 comments 0 reactions 1 assignee Claimed by @radekdoulik View on GitHub
arch-wasm area-Interop-coreclr
Dominant language
C#
Stars
18.3k
Forks
5.6k
PR merge metrics
PR metrics pending

Description

## Background

CoreCLR runtime tests (`src/tests`) that call into test-provided native code could not run on `browser-wasm`, because WebAssembly has no dynamic loading — there is no way for the shared `CORE_ROOT/corerun.js` to pick up a test's native library at runtime.

This is now addressed by building a **test-specific `corerun`** for each such test: the test's native code is compiled as a static archive, the WasmAppBuilder `ManagedToNativeGenerator` emits the P/Invoke / reverse-P/Invoke call tables, and both are linked together with the runtime's corerun link kit into a `corerun.wasm` placed next to the test's artifacts. The generated run script prefers that binary over `CORE_ROOT/corerun.js`.

With that in place the whole `src/tests` tree builds for `browser-wasm`, and a first set of tests genuinely runs its native code. This issue tracks the tests that still cannot be enabled, so they can be revisited as the gaps are closed.

## Newly enabled and passing

Nine test assemblies now link their own `corerun`. Comparing each assembly's result with the
native-assets gate on (as on `main`) and off (with this change), **20 test methods go from skipped to
executed**, and none are lost:

| Test | Methods | Newly executed | Remainder still skipped |
| --- | --- | --- | --- |
| `Interop/DllImportSearchPaths/DllImportSearchPathsTest` | 11 | 5 | 3 are `ConditionalFact(IsNativeAot)`; 3 gate on `CanLoadAssemblyInSubdirectory`, which is false for `IsBrowser()` |
| `Interop/ICustomMarshaler/ConflictingNames/MultipleALCs` | 1 | 1 | — |
| `Interop/ICustomMarshaler/ConflictingNames/SameNameDifferentAssembly` | 1 | 1 | — |
| `Interop/SuppressGCTransition/SuppressGCTransitionTest` | 1 | 0 | `SkipOnCoreClr(..., RuntimeTestModes.InterpreterActive)`, and browser CoreCLR is interpreter-only |
| `Interop/UnmanagedCallConv/UnmanagedCallConvTest` | 1 | 1 | — |
| `JIT/Directed/PrimitiveABI/PrimitiveABI` | 10 | 10 | — |
| `JIT/Regression/JitBlue/Runtime_101046` | 1 | 1 | — |
| `JIT/Regression/JitBlue/Runtime_76194` | 1 | 1 | — |
| `JIT/interpreter/InterpreterTester` | 1 | 0 | already ran on `main`; it now exercises its P/Invokes for real |

`SuppressGCTransitionTest` and `InterpreterTester` still build and link their own `corerun`, so they
pick up native coverage as soon as their unrelated skip conditions stop applying.

### Second wave — #133367, after gaps 1 and 2 closed

Nine more projects dropped their opt-out. All 41 were re-tested (built for `browser-wasm` with the property removed, then run); these nine both link a `corerun.wasm` and pass:

`Interop/StructMarshalling/PInvoke/NestedStruct` · `JIT/Directed/StructABI/StructABI` · `JIT/Directed/callconv/{Cdecl,PlatformDefault,StdCall}MemberFunction` · `JIT/Directed/callconv/ThisCall/ThisCallTest` · `JIT/Methodical/Methodical_others` · `JIT/SIMD/JIT.SIMD_{r,ro}`

## Blocking gaps

### 1. `WASM0067` — unknown multi-field structs in `SignatureMapper` — **closed by #131877**

~~`src/tasks/WasmAppBuilder/coreclr/SignatureMapper.cs` carries a hardcoded `s_knownStructSizes` table used to pick an ABI signature for by-value struct arguments. Any struct not in that table fails the build:

```
error WASM0067: SignatureMapper: unknown multi-field struct 'X' (fields: N) - add its size to s_knownStructSizes in SignatureMapper.cs
```

Over 40 distinct structs across the test tree hit this.~~ The generator now computes size and layout from crossgen2's type system.

### 2. Unsupported parameter types in reverse P/Invoke signatures — **struct half closed by #131877**

~~`PInvokeCollector.CollectPInvokes` rejects delegate / `UnmanagedCallersOnly` signatures that take or return structs:

```
System.NotSupportedException: Unsupported parameter type in method 'X.Invoke'
```

Roughly 100 distinct methods are affected.~~ Generic callbacks are still rejected:

```
Parameter types of pinvoke callback method 'System.Int32 CallbackMethodGeneric[T](T)' needs to be blittable
```

### 3. Negative `[DllImport]` tests become hard link errors

Several tests deliberately declare `[DllImport]`s for entry points that do not exist and assert that `EntryPointNotFoundException` is thrown. Everywhere else that resolution is lazy. On wasm the generated table references the symbol statically, so it fails at link time instead:

```
wasm-ld: error: undefined symbol: CallManagedProc_Fastcall
wasm-ld: error: undefined symbol: MarshalPointer_Int_InOut2
wasm-ld: error: undefined symbol: Marshal_Int_InOut2
wasm-ld: error: undefined symbol: SetIsHandleClosedCallback
wasm-ld: error: undefined symbol: UpdateTestObjectAsIDispatch
```

This is a structural tension with the static-table approach. Emitting a stub that raises `EntryPointNotFoundException` for symbols the linker cannot resolve would be one way out.

### 4. Duplicate entries for the same entry point

`DllImportPathTest` imports one entry point through several differently-spelled module names, and the generator emits one definition per spelling, producing a `redefinition of ...` error in `callhelpers-pinvoke.cpp`. The generator should deduplicate on the resolved symbol.

### 5. `NativeLibrary.Load` / `GetExport` are not served by the static table

The generated table only backs `[DllImport]` resolution. Tests that load a library explicitly at runtime still go through `dlopen` and fail even though the native code is linked into their `corerun`:

```
System.DllNotFoundException: Unable to load shared library 'NativeFunctions' or one of its dependencies.
```

Making `NativeLibrary.Load` resolve against the statically linked table would enable this class of test.

### 6. Reached by tests that now run, but will not work on wasm

These build and start, so the plumbing works, but they need something wasm does not provide. None is a gap to close:

| Test | Failure |
| --- | --- |
| `Interop/NativeLibrary/Callback/CallbackTests` | Needs a shared-library *file name*; the shared `NativeLibraryToLoad.GetLibraryFileName` helper throws for any OS other than Windows/Linux/macOS |
| `Interop/ICustomMarshaler/Primitives/ICustomMarshaler_TargetUnix` | `DllNotFoundException` for `libc` — the test loads the platform C library directly |
| `JIT/Directed/callconv/ThisCall/EmptyThisCallTest` | `MarshalDirectiveException: Invalid PInvoke calling convention. Thiscall requires that the first parameter is present and can be enregistered.` |

Five more need `Marshal.GetFunctionPointerForDelegate`, which throws `PlatformNotSupported_DynamicEntrypoint` under `FEATURE_PORTABLE_ENTRYPOINTS` (`src/coreclr/vm/comdelegate.cpp:1254`). Per [@jkotas](https://github.com/dotnet/runtime/pull/131877#discussion_r3906890268) that is deliberate and expected to stay that way, so `BestFitMapping`, `FuncPtrAsDelegateParam`, `CriticalHandles/Test`, `DelegatePInvokeTest` and `ReversePInvokeTest` should keep their opt-out permanently.

### 7. Merged test runners cannot be excluded per project

`Interop/Interop.csproj` and `JIT/Methodical/Methodical_others.csproj` are merged runners that `Compile Include` many test sources directly into their own assembly. The affected P/Invokes therefore live in the wrapper assembly and cannot be turned off one project at a time. Both opted out of the test-specific corerun entirely via `WasmBuildTestCorerun=false`. `Methodical_others` was re-enabled by #133367 once gaps 1 and 2 closed; `Interop.csproj` still opts out, because it also merges the negative `[DllImport]` tests of gap 3. Splitting those sources into their own projects, or closing gap 3, would let it opt back in too.

### 8. `Vector128` cannot be emitted

Four `DisabledRuntimeMarshalling` projects fail the build:

```
crossgen2 : error : Cannot generate an interop thunk for
'DisabledRuntimeMarshallingNative.CallWith(Vector128`1)':
its signature 'vV' contains a 128-bit vector
```

`'V'` has no case in the C++ emission helpers. wasm has v128 natively, so this is missing code rather than a platform limit.

### 9. Six tests now reach the runtime and crash

These previously failed inside the generator, so this is the first time they execute. They look like real ABI defects rather than missing features:

- `RuntimeError: memory access out of bounds` — `DisabledRuntimeMarshalling_Disabled_NativeAssemblyEnabled`, `CriticalHandles/StructTest`, `MarshalStructAsLayoutSeq`, `MarshalStructAsLayoutExp`. All four die while passing a by-value struct; the last line before the trap is e.g. `Calling MarshalStructAsParam_AsSeqByVal2...`
- `RuntimeError: null function or function signature mismatch` — `JIT/Directed/StructABI/EmptyStructs`, `PrimitiveMarshalling/EnumMarshalling/EnumTest`

`CriticalHandles/StructTest` was previously listed under gap 6 with a different symptom (`Early return from JIT/EE interface method`); it now fails this way.

## Tests without a test-specific corerun

The tests below hit one of the gaps above **while building their corerun**, so an `[ActiveIssue]` on
the test method cannot help — the generator walks the assembly's entire interop surface regardless of
which tests xunit would end up running.

They therefore set `WasmBuildTestCorerun=false` instead of being disabled. They still build and run on
`browser`, against `CORE_ROOT`'s shared `corerun.js`, exactly as they did before this work; their
native-dependent methods skip through the existing `PlatformDoesNotSupportNativeTestAssets` gate.
Closing a gap above is what lets a test move from "skips its native methods" to "runs them".

The list below was re-measured after #133367; it is 32 now, and the classification comes from building and running every one of the 41 opt-outs rather than from the original triage.

Test projects (32)

Grouped by what blocks them.

**Gap 3 — linker rejects deliberately-absent entry points (8)**

- `Exceptions/ForeignThread/ForeignThreadExceptions.csproj`
- `Interop/COM/ComWrappers/GlobalInstance/GlobalInstanceTrackerSupportTests_TargetUnix.csproj`
- `Interop/DllImportAttribute/ExactSpelling/ExactSpellingTest.csproj`
- `Interop/Interop.csproj` (merged runner, also gap 7)
- `Interop/PInvoke/CriticalHandles/ArrayTest/ArrayTest.csproj`
- `Interop/PrimitiveMarshalling/Bool/BoolTest.csproj`
- `Interop/UnmanagedCallersOnly/UnmanagedCallersOnlyTest.csproj`
- `Interop/UnmanagedCallersOnlyBasic/UnmanagedCallersOnlyBasicTest.csproj`

**Gap 8 — `Vector128` (4)**

- `Interop/DisabledRuntimeMarshalling/DisabledRuntimeMarshalling_Disabled_NativeAssemblyDisabled.csproj`
- `Interop/DisabledRuntimeMarshalling/DisabledRuntimeMarshalling_Disabled_NativeTypeInAssembly.csproj`
- `Interop/DisabledRuntimeMarshalling/DisabledRuntimeMarshalling_Disabled_NativeTypeInAssembly_ro.csproj`
- `Interop/DisabledRuntimeMarshalling/DisabledRuntimeMarshalling_NativeAssemblyDisabled.csproj`

**Gap 4 — duplicate entries for one entry point (1)**

- `Interop/DllImportAttribute/DllImportPath/DllImportPathTest.csproj`

**Gap 5 — `NativeLibrary.Load` / `GetExport` (4)**

- `Interop/ICustomMarshaler/Primitives/ICustomMarshaler_TargetUnix.csproj` (also gap 6 — loads `libc`)
- `Interop/NativeLibrary/Callback/CallbackStressTest_TargetUnix.csproj`
- `JIT/Directed/aliasing_retbuf/aliasing_retbuf.csproj`
- `baseservices/callconvs/TestCallingConventions.csproj`

**Gap 9 — crashes at run time (6)**

- `Interop/DisabledRuntimeMarshalling/DisabledRuntimeMarshalling_Disabled_NativeAssemblyEnabled.csproj`
- `Interop/PInvoke/CriticalHandles/StructTest/StructTest.csproj`
- `Interop/PrimitiveMarshalling/EnumMarshalling/EnumTest.csproj`
- `Interop/StructMarshalling/PInvoke/MarshalStructAsLayoutExp.csproj`
- `Interop/StructMarshalling/PInvoke/MarshalStructAsLayoutSeq.csproj`
- `JIT/Directed/StructABI/EmptyStructs.csproj`

**Gap 6 — will not work on wasm (8)**

- `Interop/BestFitMapping/BestFitMapping.csproj`
- `Interop/FuncPtrAsDelegateParam/FuncPtrAsDelegateParam.csproj`
- `Interop/NativeLibrary/Callback/CallbackTests.csproj`
- `Interop/PInvoke/CriticalHandles/Test/Test.csproj`
- `Interop/StructMarshalling/ReversePInvoke/MarshalSeqStruct/DelegatePInvoke/DelegatePInvokeTest.csproj`
- `Interop/StructMarshalling/ReversePInvoke/MarshalSeqStruct/ReversePInvoke/ReversePInvokeTest.csproj`
- `JIT/Directed/callconv/ThisCall/EmptyThisCallTest.csproj`
- (`ICustomMarshaler_TargetUnix`, listed under gap 5)

**Not tests — `OutputType=Library` helpers (2)**

- `Interop/DisabledRuntimeMarshalling/Native_Default/DisabledRuntimeMarshallingNative_Default.csproj`
- `Interop/DisabledRuntimeMarshalling/Native_DisabledMarshalling/DisabledRuntimeMarshallingNative_DisabledMarshalling.csproj`

The whole `src/tests/profiler` tree is also disabled on `browser` via its `Directory.Build.props`. Profiler tests launch a second `corerun` process and load the profiler from a shared library path; WebAssembly has neither process creation nor dynamic loading, so they cannot run there regardless of these gaps. These tests were not part of the `browser-wasm` CoreCLR test leg before this change either, so making the exclusion explicit loses no coverage.

> [!NOTE]
> This issue was drafted with the help of GitHub Copilot.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.