From 0d0bf9d279fe728b1401c7caf51bf6c6db575628 Mon Sep 17 00:00:00 2001 From: Larry Ewing Date: Sun, 5 Jul 2026 12:18:47 -0500 Subject: [PATCH 1/7] Prototype: Enable wasm-tools workload for CoreCLR WASM targets MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a new 'wasm-tools-coreclr' workload that provides native relinking support for CoreCLR browser-wasm projects without pulling in Mono dependencies (MonoAOTCompiler, MonoTargets.Sdk, Mono runtime pack). Unlike Mono, CoreCLR doesn't need its runtime pack bundled in the workload — the SDK resolves it via the standard KnownRuntimePack mechanism. The workload only provides: - Build targets for native relinking (BrowserWasmApp.CoreCLR.targets) - Emscripten SDK (shared via microsoft-net-sdk-emscripten) Changes: - Package BrowserWasmApp.CoreCLR.targets into WebAssembly.Sdk NuGet pack - Add conditional import in Sdk.targets.in (UseMonoRuntime switches path) - Add wasm-tools-coreclr workload to manifest (no Mono deps) - Add CoreCLR import group in WorkloadManifest.targets.in - Skip MonoAOTCompiler import when UseMonoRuntime=false Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Microsoft.NET.Runtime.WebAssembly.Sdk.pkgproj | 1 + .../Sdk/Sdk.targets.in | 3 ++- .../WorkloadManifest.json.in | 9 +++++++++ .../WorkloadManifest.targets.in | 12 ++++++++++-- 4 files changed, 22 insertions(+), 3 deletions(-) diff --git a/src/mono/nuget/Microsoft.NET.Runtime.WebAssembly.Sdk/Microsoft.NET.Runtime.WebAssembly.Sdk.pkgproj b/src/mono/nuget/Microsoft.NET.Runtime.WebAssembly.Sdk/Microsoft.NET.Runtime.WebAssembly.Sdk.pkgproj index 52e43c93f0336a..e27ba5d289a1e5 100644 --- a/src/mono/nuget/Microsoft.NET.Runtime.WebAssembly.Sdk/Microsoft.NET.Runtime.WebAssembly.Sdk.pkgproj +++ b/src/mono/nuget/Microsoft.NET.Runtime.WebAssembly.Sdk/Microsoft.NET.Runtime.WebAssembly.Sdk.pkgproj @@ -17,6 +17,7 @@ + diff --git a/src/mono/nuget/Microsoft.NET.Runtime.WebAssembly.Sdk/Sdk/Sdk.targets.in b/src/mono/nuget/Microsoft.NET.Runtime.WebAssembly.Sdk/Sdk/Sdk.targets.in index 5185a8cdd05f9a..9d3818cdbe0b5f 100644 --- a/src/mono/nuget/Microsoft.NET.Runtime.WebAssembly.Sdk/Sdk/Sdk.targets.in +++ b/src/mono/nuget/Microsoft.NET.Runtime.WebAssembly.Sdk/Sdk/Sdk.targets.in @@ -13,5 +13,6 @@ - + + diff --git a/src/mono/nuget/Microsoft.NET.Workload.Mono.Toolchain.Current.Manifest/WorkloadManifest.json.in b/src/mono/nuget/Microsoft.NET.Workload.Mono.Toolchain.Current.Manifest/WorkloadManifest.json.in index 4287c024b8f898..e9faa4fb6aaaaf 100644 --- a/src/mono/nuget/Microsoft.NET.Workload.Mono.Toolchain.Current.Manifest/WorkloadManifest.json.in +++ b/src/mono/nuget/Microsoft.NET.Workload.Mono.Toolchain.Current.Manifest/WorkloadManifest.json.in @@ -15,6 +15,15 @@ "extends": [ "microsoft-net-runtime-mono-tooling", "microsoft-net-sdk-emscripten" ], "platforms": [ "win-x64", "win-arm64", "linux-x64", "linux-arm64", "linux-musl-x64", "linux-musl-arm64", "osx-x64", "osx-arm64" ] }, + "wasm-tools-coreclr": { + "description": ".NET WebAssembly build tools (CoreCLR) for ${NetVersion}.0", + "packs": [ + "Microsoft.NET.Runtime.WebAssembly.Sdk.${NetVersion}", + "Microsoft.NET.Sdk.WebAssembly.Pack.${NetVersion}" + ], + "extends": [ "microsoft-net-sdk-emscripten" ], + "platforms": [ "win-x64", "win-arm64", "linux-x64", "linux-arm64", "linux-musl-x64", "linux-musl-arm64", "osx-x64", "osx-arm64" ] + }, "wasm-experimental": { "description": ".NET WebAssembly experimental tooling for ${NetVersion}.0", "packs": [ diff --git a/src/mono/nuget/Microsoft.NET.Workload.Mono.Toolchain.Current.Manifest/WorkloadManifest.targets.in b/src/mono/nuget/Microsoft.NET.Workload.Mono.Toolchain.Current.Manifest/WorkloadManifest.targets.in index 4c69e7792d7529..ad0e43d11e1f6b 100644 --- a/src/mono/nuget/Microsoft.NET.Workload.Mono.Toolchain.Current.Manifest/WorkloadManifest.targets.in +++ b/src/mono/nuget/Microsoft.NET.Workload.Mono.Toolchain.Current.Manifest/WorkloadManifest.targets.in @@ -153,7 +153,7 @@ - + @@ -204,7 +204,7 @@ - + @@ -212,6 +212,14 @@ + + + + + + From 873e8672ba39ee4e61012c2df2e8c0d789bd1725 Mon Sep 17 00:00:00 2001 From: Larry Ewing Date: Sun, 5 Jul 2026 13:02:31 -0500 Subject: [PATCH 2/7] Fix BrowserWasmApp.CoreCLR.targets for workload import path Guard the native.wasm.targets import with a condition so it only loads when RepositoryEngineeringDir is set (in-tree builds). For the workload path, inline the GenerateEmccExports target with the required exported runtime methods and functions. Emscripten SDK acquisition is already handled by EmSdkRepo.Defaults.props + the emscripten workload extension. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../build/BrowserWasmApp.CoreCLR.targets | 57 ++++++++++++++++++- 1 file changed, 55 insertions(+), 2 deletions(-) diff --git a/src/mono/browser/build/BrowserWasmApp.CoreCLR.targets b/src/mono/browser/build/BrowserWasmApp.CoreCLR.targets index 89bb233df7d208..1a7615170de340 100644 --- a/src/mono/browser/build/BrowserWasmApp.CoreCLR.targets +++ b/src/mono/browser/build/BrowserWasmApp.CoreCLR.targets @@ -75,8 +75,12 @@ - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From daa5eb777482672331e71a56667d368ae441e463 Mon Sep 17 00:00:00 2001 From: Larry Ewing Date: Sun, 5 Jul 2026 16:20:39 -0500 Subject: [PATCH 3/7] Fix CoreCLR WASM runtime pack and test issues - Include libminipal.a in browser-wasm CoreCLR runtime pack. It was excluded from LibrariesSharedFrameworkDir glob but unlike libz.a and libSystem.IO.Compression.Native.a it only exists in sharedFramework/ (not the parent LibrariesNativeArtifactsPath), so the exclusion prevented it from shipping at all. libminipal.a is a distinct library from libcoreclrminipal.a and is needed by the emcc linker. - Fix PInvokeTableGeneratorTests to use correct CoreCLR output filename callhelpers-pinvoke.cpp instead of pinvoke-table.cpp. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- eng/liveBuilds.targets | 3 +-- src/mono/wasm/Wasm.Build.Tests/PInvokeTableGeneratorTests.cs | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/eng/liveBuilds.targets b/eng/liveBuilds.targets index 6dd6ce0407489d..9675c20fde1e56 100644 --- a/eng/liveBuilds.targets +++ b/eng/liveBuilds.targets @@ -245,8 +245,7 @@ $(LibrariesSharedFrameworkDir)*.a; $(LibrariesSharedFrameworkDir)*.dat; " - Exclude="$(LibrariesSharedFrameworkDir)libminipal.a; - $(LibrariesSharedFrameworkDir)libSystem.IO.Compression.Native.a; + Exclude="$(LibrariesSharedFrameworkDir)libSystem.IO.Compression.Native.a; $(LibrariesSharedFrameworkDir)libz.a" IsNative="true" /> diff --git a/src/mono/wasm/Wasm.Build.Tests/PInvokeTableGeneratorTests.cs b/src/mono/wasm/Wasm.Build.Tests/PInvokeTableGeneratorTests.cs index 13adf51cd4925b..79ff413f306cf2 100644 --- a/src/mono/wasm/Wasm.Build.Tests/PInvokeTableGeneratorTests.cs +++ b/src/mono/wasm/Wasm.Build.Tests/PInvokeTableGeneratorTests.cs @@ -334,7 +334,7 @@ private async Task EnsureWasmAbiRulesAreFollowed(Configuration config, bool aot) // FIXME: Not possible in in-process mode for some reason, even with verbosity at "diagnostic" // Assert.Contains("Adding pinvoke signature FD for method 'Test.", output); - string pinvokeTableFileName = IsCoreClrRuntime ? "pinvoke-table.cpp" : "pinvoke-table.h"; + string pinvokeTableFileName = IsCoreClrRuntime ? "callhelpers-pinvoke.cpp" : "pinvoke-table.h"; string pinvokeTable = File.ReadAllText(Path.Combine(objDir, pinvokeTableFileName)); // Verify that the invoke is in the pinvoke table. Under various circumstances we will silently skip it, // for example if the module isn't found From fd3b441b1bb9f7b857df718d2109b46a2c8d81ba Mon Sep 17 00:00:00 2001 From: Larry Ewing Date: Sun, 5 Jul 2026 16:51:43 -0500 Subject: [PATCH 4/7] Fix static web asset ordering for CoreCLR WASM native relink The _CoreCLRWasmNativeForBuild target must run before _ComputeWasmBuildCandidates (not just _ResolveWasmOutputs) so the relinked dotnet.native.wasm replaces the runtime pack version in the static web assets pipeline. Without this, the runtime pack binary is served and user P/Invoke modules fail with DllNotFoundException. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/mono/browser/build/BrowserWasmApp.CoreCLR.targets | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mono/browser/build/BrowserWasmApp.CoreCLR.targets b/src/mono/browser/build/BrowserWasmApp.CoreCLR.targets index 1a7615170de340..36e716e98c6760 100644 --- a/src/mono/browser/build/BrowserWasmApp.CoreCLR.targets +++ b/src/mono/browser/build/BrowserWasmApp.CoreCLR.targets @@ -230,7 +230,7 @@ served binary would be the original (runtime pack) dotnet.native.wasm. --> @@ -371,7 +371,7 @@ <_CoreCLRWasmBuildAppCoreDependsOn> _CoreCLRWasmInitialize; _CoreCLRSetupEmscripten; - _GatherWasmFilesToBuild; + _CoreCLRGatherWasmFiles; _CoreCLRPrepareForNativeBuild; _CoreCLRGenerateManagedToNative; _CoreCLRWriteCompileRsp; @@ -387,6 +387,16 @@ Condition="'$(WasmBuildNative)' == 'true'" DependsOnTargets="$(_CoreCLRWasmBuildAppCoreDependsOn)" /> + + + From 0b6d88860b1d9421ac401fdaa18d4973f5927141 Mon Sep 17 00:00:00 2001 From: Larry Ewing Date: Sun, 5 Jul 2026 18:57:28 -0500 Subject: [PATCH 6/7] Implement struct return thunks in InterpToNativeGenerator Remove the PORTABILITY_ASSERT for indirect struct returns in the CoreCLR WASM interp-to-native thunk generator. For struct return types (S tokens), emit C typedefs of the correct size so emcc generates the proper sret ABI calling convention. This fixes the runtime crash when CoreCLR WASM code calls native functions that return structs. Also remove the native-mono test category filter from EnsureWasmAbiRulesAreFollowedInInterpreter since it now works on CoreCLR. Fixes https://github.com/dotnet/runtime/issues/120897 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../PInvokeTableGeneratorTests.cs | 1 - .../coreclr/InterpToNativeGenerator.cs | 30 +++++++++++++++++-- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/src/mono/wasm/Wasm.Build.Tests/PInvokeTableGeneratorTests.cs b/src/mono/wasm/Wasm.Build.Tests/PInvokeTableGeneratorTests.cs index 79ff413f306cf2..ed3dcd6f7c11ab 100644 --- a/src/mono/wasm/Wasm.Build.Tests/PInvokeTableGeneratorTests.cs +++ b/src/mono/wasm/Wasm.Build.Tests/PInvokeTableGeneratorTests.cs @@ -367,7 +367,6 @@ public async Task EnsureWasmAbiRulesAreFollowedInAOT(Configuration config, bool [Theory] [BuildAndRun(aot: false)] - [TestCategory("native-mono")] // coreclr ActiveIssue: https://github.com/dotnet/runtime/issues/120897 public async Task EnsureWasmAbiRulesAreFollowedInInterpreter(Configuration config, bool aot) => await EnsureWasmAbiRulesAreFollowed(config, aot); diff --git a/src/tasks/WasmAppBuilder/coreclr/InterpToNativeGenerator.cs b/src/tasks/WasmAppBuilder/coreclr/InterpToNativeGenerator.cs index 0a5fb08b362030..ee5794f8b1f2a3 100644 --- a/src/tasks/WasmAppBuilder/coreclr/InterpToNativeGenerator.cs +++ b/src/tasks/WasmAppBuilder/coreclr/InterpToNativeGenerator.cs @@ -63,6 +63,15 @@ private static void Emit(StreamWriter w, IEnumerable cookies) var signatures = cookies.OrderBy(c => c).Distinct().ToArray(); Array.Sort(signatures, StringComparer.Ordinal); + // Collect unique struct return sizes so we can emit typedefs + var structReturnSizes = new SortedSet(); + foreach (var sig in signatures) + { + var toks = SignatureMapper.ParseSignatureTokens(sig); + if (toks[0][0] == 'S' && toks[0].Length > 1) + structReturnSizes.Add(int.Parse(toks[0].Substring(1))); + } + w.Write( """ // Licensed to the .NET Foundation under one or more agreements. @@ -85,6 +94,17 @@ private static void Emit(StreamWriter w, IEnumerable cookies) #define ARG_F32(i) (*(float*)ARG_ADDR(i)) #define ARG_F64(i) (*(double*)ARG_ADDR(i)) + """); + + // Emit typedefs for struct return types so emcc generates the correct sret ABI + foreach (var size in structReturnSizes) + { + w.WriteLine($"typedef struct {{ char d[{size}]; }} wasm_ret_S{size};"); + } + + w.Write( + """ + namespace { """); @@ -104,7 +124,6 @@ private static void Emit(StreamWriter w, IEnumerable cookies) tokens.RemoveAt(tokens.Count - 1); } var args = Args(tokens); - var portabilityAssert = returnToken[0] == 'S' ? "PORTABILITY_ASSERT(\"Indirect struct return is not yet implemented.\");\n " : ""; var portableEntryPointComma = args.Count > 0 ? ", " : ""; var portableEntrypointDeclaration = isPortableEntryPointCall ? portableEntryPointComma + "PCODE" : ""; @@ -118,7 +137,7 @@ private static void Emit(StreamWriter w, IEnumerable cookies) {{(isPortableEntryPointCall ? "NOINLINE " : "")}}static void {{CallFuncName(args, SignatureMapper.TokenToNameType(returnToken), isPortableEntryPointCall)}}(PCODE {{(isPortableEntryPointCall ? "pPortableEntryPoint" : "pcode")}}, int8_t* pArgs, int8_t* pRet) {{{(isPortableEntryPointCall ? "\n alignas(16) int framePointer = TERMINATE_R2R_STACK_WALK;" : "")}} {{result.nativeType}} (*fptr)({{portableEntrypointStackDeclaration}}{{string.Join(", ", args.Select(static t => SignatureMapper.TokenToNativeType(t)))}}{{portableEntrypointDeclaration}}) = {{portableEntrypointPointerRD}}({{result.nativeType}} ({{portableEntrypointPointerRD}}*)({{portableEntrypointStackDeclaration}}{{string.Join(", ", args.Select(static t => SignatureMapper.TokenToNativeType(t)))}}{{portableEntrypointDeclaration}})){{(isPortableEntryPointCall ? "(pPortableEntryPoint)" : "pcode")}}; - {{portabilityAssert}}{{(result.isVoid ? "" : "*" + "((" + result.nativeType + "*)pRet) = ")}}(*fptr)({{portableEntrypointStackParam}}{{string.Join(", ", ArgsWithSlotOffsets(args))}}{{portableEntrypointParam}}); + {{(result.isVoid ? "" : "*" + "((" + result.nativeType + "*)pRet) = ")}}(*fptr)({{portableEntrypointStackParam}}{{string.Join(", ", ArgsWithSlotOffsets(args))}}{{portableEntrypointParam}}); } """); @@ -169,7 +188,12 @@ static List ArgsWithSlotOffsets(List args) } static (bool isVoid, string nativeType) Result(string returnToken) - => new(returnToken == "v", SignatureMapper.TokenToNativeType(returnToken)); + { + // For struct returns, use the typedef so emcc generates the correct sret ABI + if (returnToken[0] == 'S' && returnToken.Length > 1) + return (false, $"wasm_ret_{returnToken}"); + return new(returnToken == "v", SignatureMapper.TokenToNativeType(returnToken)); + } static bool IsPortableEntryPointCall(List tokens) { From f5c0e673e64b3bf5e0a9a06ce18d9d0d272c289f Mon Sep 17 00:00:00 2001 From: Larry Ewing Date: Sun, 5 Jul 2026 19:08:54 -0500 Subject: [PATCH 7/7] Fix duplicate libminipal.a in runtime pack packaging Restore the exclusion of libminipal.a from the LibrariesSharedFrameworkDir glob. The file is already included via CoreCLRSharedFrameworkDir (line 111), so including it from both locations causes NU5118 duplicate file error during nupkg creation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- eng/liveBuilds.targets | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/eng/liveBuilds.targets b/eng/liveBuilds.targets index 9675c20fde1e56..6dd6ce0407489d 100644 --- a/eng/liveBuilds.targets +++ b/eng/liveBuilds.targets @@ -245,7 +245,8 @@ $(LibrariesSharedFrameworkDir)*.a; $(LibrariesSharedFrameworkDir)*.dat; " - Exclude="$(LibrariesSharedFrameworkDir)libSystem.IO.Compression.Native.a; + Exclude="$(LibrariesSharedFrameworkDir)libminipal.a; + $(LibrariesSharedFrameworkDir)libSystem.IO.Compression.Native.a; $(LibrariesSharedFrameworkDir)libz.a" IsNative="true" />