Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 68 additions & 5 deletions src/mono/browser/build/BrowserWasmApp.CoreCLR.targets
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,12 @@
<WasmEnvironmentVariable Condition="'$(_WasmPerfInstInterval)' != ''" Include="DOTNET_EventPipeThreadSamplingRate" Value="$(_WasmPerfInstInterval)" />
</ItemGroup>

<!-- Import shared native.wasm.targets for Emscripten SDK, ICU, exported functions/methods -->
<Import Project="$(RepositoryEngineeringDir)native.wasm.targets" />
<!-- Import shared native.wasm.targets for Emscripten SDK, ICU, exported functions/methods.
Only available in-tree (RepositoryEngineeringDir is set by the build system).
For the workload path we use EmSdkRepo.Defaults.props and inline the exported
functions/methods below. -->
<Import Project="$(RepositoryEngineeringDir)native.wasm.targets"
Condition="'$(RepositoryEngineeringDir)' != '' and Exists('$(RepositoryEngineeringDir)native.wasm.targets')" />

<!-- Derive Emscripten paths from EMSDK_PATH when workload paths are not set -->
<Import Project="$(MSBuildThisFileDirectory)EmSdkRepo.Defaults.props"
Expand All @@ -92,6 +96,55 @@
TaskFactory="TaskHostFactory"
Condition="'$(WasmAppBuilderTasksAssemblyPath)' != ''" />

<!-- Exported runtime methods and functions for the emcc link step.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In Mono we store some of the build/link flags as json file and restore it into MSBuild properties. I don't remember what was our original motivation for that. Maybe DRY ?

In the in-tree path native.wasm.targets also defines a GenerateEmccExports target
with the same items plus NativeCMakeArg entries; the last definition wins in MSBuild
and the CMake args are unused during app relink, so the override is harmless. -->
<Target Name="GenerateEmccExports"
Condition="'$(TargetsBrowser)' == 'true'">
<ItemGroup>
<EmccExportedRuntimeMethod Include="FS" />
<EmccExportedRuntimeMethod Include="HEAP8" />
<EmccExportedRuntimeMethod Include="HEAP16" />
<EmccExportedRuntimeMethod Include="HEAPU8" />
<EmccExportedRuntimeMethod Include="HEAPU16" />
<EmccExportedRuntimeMethod Include="HEAP32" />
<EmccExportedRuntimeMethod Include="HEAPU32" />
<EmccExportedRuntimeMethod Include="HEAPF32" />
<EmccExportedRuntimeMethod Include="HEAPF64" />
<EmccExportedRuntimeMethod Include="HEAP64" />
<EmccExportedRuntimeMethod Include="HEAPU64" />
<EmccExportedRuntimeMethod Include="out" />
<EmccExportedRuntimeMethod Include="err" />
<EmccExportedRuntimeMethod Include="ccall" />
<EmccExportedRuntimeMethod Include="cwrap" />
<EmccExportedRuntimeMethod Include="setValue" />
<EmccExportedRuntimeMethod Include="getValue" />
<EmccExportedRuntimeMethod Include="UTF8ToString" />
<EmccExportedRuntimeMethod Include="UTF8ArrayToString" />
<EmccExportedRuntimeMethod Include="lengthBytesUTF8" />
<EmccExportedRuntimeMethod Include="stringToUTF8Array" />
<EmccExportedRuntimeMethod Include="safeSetTimeout" />
<EmccExportedRuntimeMethod Include="runtimeKeepalivePush" />
<EmccExportedRuntimeMethod Include="runtimeKeepalivePop" />
<EmccExportedRuntimeMethod Include="abort" />

<EmccExportedFunction Include="_free" />
<EmccExportedFunction Include="_htons" />
<EmccExportedFunction Include="_malloc" />
<EmccExportedFunction Include="_sbrk" />
<EmccExportedFunction Include="_memalign" />
<EmccExportedFunction Include="_posix_memalign" />
<EmccExportedFunction Include="_memset" />
<EmccExportedFunction Include="_ntohs" />
<EmccExportedFunction Include="stackAlloc" />
<EmccExportedFunction Include="stackRestore" />
<EmccExportedFunction Include="stackSave" />
<EmccExportedFunction Include="___stack_pointer" />
<EmccExportedFunction Include="___coreclr_wasm_rtlrestorecontext_tag" />
</ItemGroup>
</Target>

<!-- Allow running/debugging from VS -->
<ItemGroup>
<ProjectCapability Include="DotNetCoreWeb"/>
Expand Down Expand Up @@ -177,7 +230,7 @@
served binary would be the original (runtime pack) dotnet.native.wasm. -->
<Target Name="_CoreCLRWasmNativeForBuild"
DependsOnTargets="WasmBuildApp"
BeforeTargets="_ResolveWasmOutputs"
BeforeTargets="_ComputeWasmBuildCandidates;_ResolveWasmOutputs"
Condition="'$(IsBrowserWasmProject)' == 'true' and
'$(WasmBuildingForNestedPublish)' != 'true' and
'$(WasmBuildOnlyAfterPublish)' != 'true' and
Expand Down Expand Up @@ -232,7 +285,7 @@
_GatherWasmFilesToBuild (unlike _ComputeWasmBuildCandidates) does not pull it in on its own.
-->
<Target Name="_CoreCLRResolveRuntimePackVsAppLocalConflicts"
BeforeTargets="_ComputeWasmBuildCandidates;_GatherWasmFilesToBuild"
BeforeTargets="_ComputeWasmBuildCandidates;_GatherWasmFilesToBuild;_CoreCLRGatherWasmFiles"
DependsOnTargets="ResolveReferences"
Condition="'$(IsBrowserWasmProject)' == 'true'">
<PropertyGroup>
Expand Down Expand Up @@ -318,7 +371,7 @@
<_CoreCLRWasmBuildAppCoreDependsOn>
_CoreCLRWasmInitialize;
_CoreCLRSetupEmscripten;
_GatherWasmFilesToBuild;
_CoreCLRGatherWasmFiles;
_CoreCLRPrepareForNativeBuild;
_CoreCLRGenerateManagedToNative;
_CoreCLRWriteCompileRsp;
Expand All @@ -334,6 +387,16 @@
Condition="'$(WasmBuildNative)' == 'true'"
DependsOnTargets="$(_CoreCLRWasmBuildAppCoreDependsOn)" />

<!-- During a nested publish, _GatherWasmFilesToPublish already populated
WasmAssembliesToBundle from ResolvedFileToPublish (which includes app
satellite assemblies). Calling _GatherWasmFilesToBuild would overwrite
that list with build-time items (IntermediateAssembly + ReferenceCopyLocalPaths)
which omit app satellites, causing ComputeWasmPublishAssets to fail.
Only call the SDK's _GatherWasmFilesToBuild for non-publish builds. -->
<Target Name="_CoreCLRGatherWasmFiles"
DependsOnTargets="_GatherWasmFilesToBuild"
Condition="'$(WasmBuildingForNestedPublish)' != 'true'" />

<!-- ======================== Initialize ======================== -->

<Target Name="_CoreCLRWasmInitialize">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

<PackageFile Include="$(BrowserProjectRoot)build\BrowserWasmApp.props" TargetPath="Sdk" />
<PackageFile Include="$(BrowserProjectRoot)build\BrowserWasmApp.targets" TargetPath="Sdk" />
<PackageFile Include="$(BrowserProjectRoot)build\BrowserWasmApp.CoreCLR.targets" TargetPath="Sdk" />

<PackageFile Include="$(WasmProjectRoot)build\WasmApp.Common.props" TargetPath="Sdk" />
<PackageFile Include="$(WasmProjectRoot)build\WasmApp.Common.targets" TargetPath="Sdk" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,6 @@
</PropertyGroup>

<Import Project="$(MSBuildThisFileDirectory)\BrowserWasmApp.props" />
<Import Project="$(MSBuildThisFileDirectory)\BrowserWasmApp.targets" />
<Import Project="$(MSBuildThisFileDirectory)\BrowserWasmApp.targets" Condition="'$(UseMonoRuntime)' != 'false'" />
<Import Project="$(MSBuildThisFileDirectory)\BrowserWasmApp.CoreCLR.targets" Condition="'$(UseMonoRuntime)' == 'false'" />
</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -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}"
],

@akoeplinger akoeplinger Jul 6, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should probably add the coreclr browser-wasm runtime pack here
(and as you know this will need to actually be done in the sdk repo as well)

"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": [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@

<!-- start of TFM specific logic, make sure every node has a TargetsCurrent/TargetsNet* condition -->

<Import Condition="'$(TargetsCurrent)' == 'true' and '$(RunAOTCompilation)' == 'true' and ('$(UsingBrowserRuntimeWorkload)' == 'true' or '$(UsingMobileWorkload)' == 'true' or '$(UsingWasiRuntimeWorkload)' == 'true')" Project="Sdk.props" Sdk="Microsoft.NET.Runtime.MonoAOTCompiler.Task.${NetVersion}" />
<Import Condition="'$(TargetsCurrent)' == 'true' and '$(RunAOTCompilation)' == 'true' and '$(UseMonoRuntime)' != 'false' and ('$(UsingBrowserRuntimeWorkload)' == 'true' or '$(UsingMobileWorkload)' == 'true' or '$(UsingWasiRuntimeWorkload)' == 'true')" Project="Sdk.props" Sdk="Microsoft.NET.Runtime.MonoAOTCompiler.Task.${NetVersion}" />

<ImportGroup Condition="'$(TargetsCurrent)' == 'true' and ('$(TargetPlatformIdentifier)' == 'android' or '$(_IsAndroidLibraryMode)' == 'true')">
<Import Project="Sdk.props" Sdk="Microsoft.NET.Runtime.MonoTargets.Sdk.${NetVersion}" />
Expand Down Expand Up @@ -204,14 +204,22 @@
<Import Project="Sdk.props" Sdk="Microsoft.NETCore.App.Runtime.AOT.Cross.${NetVersion}.tvossimulator-x64" />
</ImportGroup>

<ImportGroup Condition="'$(TargetsCurrent)' == 'true' and '$(RuntimeIdentifier)' == 'browser-wasm' and '$(UsingBrowserRuntimeWorkload)' == 'true'">
<ImportGroup Condition="'$(TargetsCurrent)' == 'true' and '$(RuntimeIdentifier)' == 'browser-wasm' and '$(UsingBrowserRuntimeWorkload)' == 'true' and '$(UseMonoRuntime)' != 'false'">
<Import Project="Sdk.props" Sdk="Microsoft.NET.Runtime.MonoTargets.Sdk.${NetVersion}" />
<Import Project="Sdk.props" Sdk="Microsoft.NET.Runtime.WebAssembly.Sdk.${NetVersion}" />
<Import Project="Sdk.targets" Sdk="Microsoft.NET.Runtime.WebAssembly.Sdk.${NetVersion}" />
<Import Project="Sdk.props" Sdk="Microsoft.NETCore.App.Runtime.AOT.Cross.${NetVersion}.browser-wasm" />
<Import Project="Sdk.targets" Sdk="Microsoft.NET.Runtime.MonoTargets.Sdk.${NetVersion}" />
</ImportGroup>

<!-- CoreCLR browser-wasm: only needs the WebAssembly.Sdk (build targets + emscripten).
No MonoTargets.Sdk, no MonoAOTCompiler, no AOT.Cross pack — the SDK resolves
the CoreCLR runtime pack and crossgen2 via its own KnownRuntimePack/KnownCrossgen2Pack. -->
<ImportGroup Condition="'$(TargetsCurrent)' == 'true' and '$(RuntimeIdentifier)' == 'browser-wasm' and '$(UsingBrowserRuntimeWorkload)' == 'true' and '$(UseMonoRuntime)' == 'false'">
<Import Project="Sdk.props" Sdk="Microsoft.NET.Runtime.WebAssembly.Sdk.${NetVersion}" />
<Import Project="Sdk.targets" Sdk="Microsoft.NET.Runtime.WebAssembly.Sdk.${NetVersion}" />
</ImportGroup>

<ImportGroup Condition="'$(TargetsCurrent)' == 'true' and '$(RuntimeIdentifier)' == 'wasi-wasm' and '$(UsingWasiRuntimeWorkload)' == 'true'">
<Import Project="Sdk.props" Sdk="Microsoft.NET.Runtime.MonoTargets.Sdk.${NetVersion}" />
<Import Project="Sdk.props" Sdk="Microsoft.NET.Runtime.WebAssembly.Wasi.Sdk.${NetVersion}" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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);

Expand Down
30 changes: 27 additions & 3 deletions src/tasks/WasmAppBuilder/coreclr/InterpToNativeGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,15 @@ private static void Emit(StreamWriter w, IEnumerable<string> 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<int>();
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.
Expand All @@ -85,6 +94,17 @@ private static void Emit(StreamWriter w, IEnumerable<string> 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
{
""");
Expand All @@ -104,7 +124,6 @@ private static void Emit(StreamWriter w, IEnumerable<string> 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" : "";
Expand All @@ -118,7 +137,7 @@ private static void Emit(StreamWriter w, IEnumerable<string> 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}});
}

""");
Expand Down Expand Up @@ -169,7 +188,12 @@ static List<string> ArgsWithSlotOffsets(List<string> 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<string> tokens)
{
Expand Down