diff --git a/docs/workflow/wasm-documentation.md b/docs/workflow/wasm-documentation.md
index 5e95f96038bdb2..604561e7b0e494 100644
--- a/docs/workflow/wasm-documentation.md
+++ b/docs/workflow/wasm-documentation.md
@@ -51,7 +51,7 @@ For debugging instructions including VS Code and Chrome DevTools setup, see the
### Running coreclr callhelpers generator
-After building the runtime, use the `generate-coreclr-helpers` script for your platform (`.cmd` or `.sh`) in `src/tasks/WasmAppBuilder` to [re]generate the call helpers in `src/coreclr/vm/wasm`.
+After building the runtime, use the `generate-coreclr-helpers` script for your platform (`.cmd` or `.sh`) in `src/coreclr/vm/wasm` to [re]generate the call helpers in `src/coreclr/vm/wasm`.
## Features and Configuration
diff --git a/eng/Subsets.props b/eng/Subsets.props
index 98d535f29edecc..0674e93cf4ded7 100644
--- a/eng/Subsets.props
+++ b/eng/Subsets.props
@@ -759,6 +759,17 @@
Non-VMR builds normally use the crossgen2 built for the target host SDK from another build leg, but builds without one can opt in.
-->
+
+
+
diff --git a/eng/pipelines/common/templates/browser-wasm-build-tests.yml b/eng/pipelines/common/templates/browser-wasm-build-tests.yml
index 88f99633ea54d5..a2c9e5a32bcab4 100644
--- a/eng/pipelines/common/templates/browser-wasm-build-tests.yml
+++ b/eng/pipelines/common/templates/browser-wasm-build-tests.yml
@@ -92,9 +92,10 @@ jobs:
TargetFolder: '$(Build.SourcesDirectory)/artifacts'
CleanTargetFolder: false
- # Download the CoreCLR runtime pack. The wasm-tools workload manifest now includes
- # the CoreCLR browser-wasm runtime pack, so installing the workload for testing
- # requires the pack to be present in the local package feed.
+ # Download the CoreCLR runtime pack and the host crossgen2 pack. The wasm-tools workload
+ # manifest includes both, so installing the workload for testing requires them to be
+ # present in the local package feed. Only pipelines that build the CoreCLR browser-wasm
+ # runtime (e.g. runtime.yml) can stage them here.
- ${{ if eq(parameters.includeCoreClrRuntimePack, true) }}:
- task: DownloadPipelineArtifact@2
displayName: Download built nugets for CoreCLR runtime
@@ -104,10 +105,12 @@ jobs:
targetPath: '$(Build.SourcesDirectory)/artifacts/BuildArtifacts_browser_wasm_$(_hostedOs)_Release_CoreCLR'
- task: CopyFiles@2
- displayName: Copy CoreCLR runtime pack
+ displayName: Copy CoreCLR runtime pack and host crossgen2 pack
inputs:
SourceFolder: '$(Build.SourcesDirectory)/artifacts/BuildArtifacts_browser_wasm_$(_hostedOs)_Release_CoreCLR'
- Contents: packages/$(_BuildConfig)/Shipping/Microsoft.NETCore.App.Runtime.browser-wasm.*
+ Contents: |
+ packages/$(_BuildConfig)/Shipping/Microsoft.NETCore.App.Runtime.browser-wasm.*
+ packages/$(_BuildConfig)/Shipping/Microsoft.NETCore.App.Crossgen2.*
TargetFolder: '$(Build.SourcesDirectory)/artifacts'
CleanTargetFolder: false
diff --git a/eng/pipelines/extra-platforms/runtime-extra-platforms-wasm.yml b/eng/pipelines/extra-platforms/runtime-extra-platforms-wasm.yml
index 0a7ea4973d2cfa..0dfb371196d872 100644
--- a/eng/pipelines/extra-platforms/runtime-extra-platforms-wasm.yml
+++ b/eng/pipelines/extra-platforms/runtime-extra-platforms-wasm.yml
@@ -209,7 +209,7 @@ jobs:
- browser_wasm_win
jobParameters:
nameSuffix: CoreCLR
- buildArgs: -s clr+libs+packs -c Release -rc $(_BuildConfig) /p:TestAssemblies=false /p:InstallWorkloadForTesting=false
+ buildArgs: -s clr+libs+packs -c Release -rc $(_BuildConfig) /p:TestAssemblies=false /p:InstallWorkloadForTesting=false /p:BuildCrossgen2HostPackForWorkloadTesting=true
timeoutInMinutes: 120
postBuildSteps:
- template: /eng/pipelines/common/wasm-post-build-steps.yml
diff --git a/eng/pipelines/runtime.yml b/eng/pipelines/runtime.yml
index 5cca8c7ca4d443..e58e2d50c0b2af 100644
--- a/eng/pipelines/runtime.yml
+++ b/eng/pipelines/runtime.yml
@@ -130,7 +130,7 @@ extends:
- browser_wasm_win
jobParameters:
nameSuffix: CoreCLR
- buildArgs: -s clr+libs+libs.tests+packs -c Release -rc $(_BuildConfig) /p:TestAssemblies=false /p:TestWasmBuildTests=true /p:ArchiveTests=true /p:InstallWorkloadForTesting=false
+ buildArgs: -s clr+libs+libs.tests+packs -c Release -rc $(_BuildConfig) /p:TestAssemblies=false /p:TestWasmBuildTests=true /p:ArchiveTests=true /p:InstallWorkloadForTesting=false /p:BuildCrossgen2HostPackForWorkloadTesting=true
timeoutInMinutes: 120
postBuildSteps:
- template: /eng/pipelines/common/wasm-post-build-steps.yml
diff --git a/eng/wasm/WasmPInvokeModules.props b/eng/wasm/WasmPInvokeModules.props
index 537351c0268caa..843ce0127f8d20 100644
--- a/eng/wasm/WasmPInvokeModules.props
+++ b/eng/wasm/WasmPInvokeModules.props
@@ -1,12 +1,12 @@
+
+
+
+ $(NetCoreAppCurrent)
+
+
+
+
+
+
+ <_TesthostDir>$([MSBuild]::NormalizeDirectory('$(ArtifactsBinDir)', 'testhost'))
+ <_FrameworkVersion>$(MajorVersion).$(MinorVersion).0
+
+
+ <_BuildScript Condition="$([MSBuild]::IsOSPlatform('Windows'))">.\build.cmd
+ <_BuildScript Condition="'$(_BuildScript)' == ''">./build.sh
+
+
+
+ <_WasmCallHelperTarget Include="browser">
+ $([MSBuild]::EnsureTrailingSlash('$(BrowserScanPath)'))
+ $(_TesthostDir)$(NetCoreAppCurrent)-browser-$(Configuration)-wasm/shared/Microsoft.NETCore.App/$(_FrameworkVersion)/
+
+ <_WasmCallHelperTarget Include="wasi">
+ $([MSBuild]::EnsureTrailingSlash('$(WasiScanPath)'))
+ $(_TesthostDir)$(NetCoreAppCurrent)-wasi-$(Configuration)-wasm/shared/Microsoft.NETCore.App/$(_FrameworkVersion)/
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <_TargetOS>%(_WasmCallHelperTarget.Identity)
+ <_ScanPath>%(_WasmCallHelperTarget.ScanPath)
+
+ <_Crossgen2Path>$([MSBuild]::NormalizePath('$(ArtifactsBinDir)', 'coreclr', '$(_TargetOS).wasm.$(Configuration)', '$(BuildArchitecture)', 'crossgen2', 'crossgen2$(ExeSuffix)'))
+ <_OutputDir>$(MSBuildThisFileDirectory)$(_TargetOS)/
+ <_ResponseFileDir>$([MSBuild]::NormalizeDirectory('$(ArtifactsObjDir)', 'wasm-callhelpers', '$(_TargetOS)'))
+ <_ResponseFile>$(_ResponseFileDir)generate-coreclr-helpers.rsp
+ <_StagingDir>$(_ResponseFileDir)generated/
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <_ScanAssembly Remove="@(_ScanAssembly)" />
+ <_ScanAssembly Include="$(_ScanPath)*.dll" />
+
+
+
+
+
+ <_GeneratorArg Remove="@(_GeneratorArg)" />
+ <_GeneratorArg Include="--targetos:$(_TargetOS)" />
+ <_GeneratorArg Include="--targetarch:wasm" />
+ <_GeneratorArg Include="--generate-portable-callhelpers:$(_StagingDir)" />
+ <_GeneratorArg Include="@(WasmCoreClrFrameworkPInvokeModule->'--directpinvoke:%(Identity)')" />
+ <_GeneratorArg Include="@(_ScanAssembly->'%(FullPath)')" />
+
+
+
+
+
+
+
+ <_StagedCallHelper Remove="@(_StagedCallHelper)" />
+ <_StagedCallHelper Include="$(_StagingDir)*.cpp" />
+
+
+
+
+
+
+
+
diff --git a/src/tasks/WasmAppBuilder/generate-coreclr-helpers.sh b/src/coreclr/vm/wasm/generate-coreclr-helpers.sh
similarity index 50%
rename from src/tasks/WasmAppBuilder/generate-coreclr-helpers.sh
rename to src/coreclr/vm/wasm/generate-coreclr-helpers.sh
index 917b737b0a83a6..c45bf9f729eda6 100755
--- a/src/tasks/WasmAppBuilder/generate-coreclr-helpers.sh
+++ b/src/coreclr/vm/wasm/generate-coreclr-helpers.sh
@@ -53,49 +53,31 @@ case "$config_lower" in
;;
esac
-# Get the repo root (script is in src/tasks/WasmAppBuilder)
+# Get the repo root (script is in src/coreclr/vm/wasm)
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
-repo_root="$(cd "$script_dir/../../.." && pwd)"
+repo_root="$(cd "$script_dir/../../../.." && pwd)"
echo "Configuration: $configuration"
echo "Repo root: $repo_root"
cd "$repo_root"
-# Run the generator for a given target OS.
-# Arguments:
-run_generator() {
- local target_os="$1"
- local scan_path="$2"
- local output_dir="$3"
+# The scan paths, the crossgen2 lookup and the P/Invoke module list all live in the project next
+# to this script, so they are not restated here and in the .cmd.
+args=(
+ build "$script_dir/generate-coreclr-helpers.proj"
+ -t:GenerateCallHelpers
+ "-p:Configuration=$configuration"
+)
- if [[ ! -d "$scan_path" ]]; then
- echo "Error: Scan path for $target_os does not exist: $scan_path"
- echo "Please build the runtime first using: ./build.sh clr+libs -os $target_os -c $configuration"
- exit 1
- fi
-
- echo "[$target_os] Scan path: $scan_path"
- echo "[$target_os] Output path: $output_dir"
- echo "Running generator for $target_os..."
- echo "./dotnet.sh build /t:RunGenerator /p:RuntimeFlavor=CoreCLR /p:TargetOS=$target_os /p:GeneratorOutputPath=$output_dir /p:AssembliesScanPath=$scan_path src/tasks/WasmAppBuilder/WasmAppBuilder.csproj"
- ./dotnet.sh build /t:RunGenerator /p:RuntimeFlavor=CoreCLR "/p:TargetOS=$target_os" "/p:GeneratorOutputPath=$output_dir" "/p:AssembliesScanPath=$scan_path" src/tasks/WasmAppBuilder/WasmAppBuilder.csproj
-}
-
-# Resolve scan paths (allow overrides).
if [[ -n "$browser_scan_path_override" ]]; then
- browser_scan_path="$browser_scan_path_override"
-else
- browser_scan_path="$repo_root/artifacts/bin/testhost/net11.0-browser-$configuration-wasm/shared/Microsoft.NETCore.App/11.0.0/"
+ args+=("-p:BrowserScanPath=$browser_scan_path_override")
fi
if [[ -n "$wasi_scan_path_override" ]]; then
- wasi_scan_path="$wasi_scan_path_override"
-else
- wasi_scan_path="$repo_root/artifacts/bin/testhost/net11.0-wasi-$configuration-wasm/shared/Microsoft.NETCore.App/11.0.0/"
+ args+=("-p:WasiScanPath=$wasi_scan_path_override")
fi
-run_generator "browser" "$browser_scan_path" "$repo_root/src/coreclr/vm/wasm/browser/"
-run_generator "wasi" "$wasi_scan_path" "$repo_root/src/coreclr/vm/wasm/wasi/"
+./dotnet.sh "${args[@]}"
echo "Done!"
diff --git a/src/coreclr/vm/wasm/helpers.cpp b/src/coreclr/vm/wasm/helpers.cpp
index 518d763325a4d2..2774fd7b126c92 100644
--- a/src/coreclr/vm/wasm/helpers.cpp
+++ b/src/coreclr/vm/wasm/helpers.cpp
@@ -1065,15 +1065,15 @@ namespace
return pos;
}
- typedef StringToThunkHash StringToWasmSigThunkHash;
- static StringToWasmSigThunkHash* thunkCache = nullptr;
+ typedef StringToThunkHash StringToPortableSigThunkHash;
+ static StringToPortableSigThunkHash* thunkCache = nullptr;
- // The fixed set of interpreter->native thunks compiled into libcoreclr (g_wasmThunks). It covers the
+ // The fixed set of interpreter->native thunks compiled into libcoreclr (g_portableCallHelperThunks). It covers the
// runtime's own interp->native calls - FCall/QCall/pinvoke and other runtime-native managed methods,
// plus unmanaged calli. crossgen2 does not emit these.
static InterpreterCalliCookie LookupThunkInLibcoreclr(const char* key)
{
- StringToWasmSigThunkHash* table = thunkCache;
+ StringToPortableSigThunkHash* table = thunkCache;
_ASSERTE(table != nullptr && "Wasm thunk cache not initialized. Call InitializeWasmThunkCaches() at EEStartup.");
void* thunk;
if (table->Lookup(key, &thunk))
@@ -1084,7 +1084,7 @@ namespace
// For a managed method the interp->R2R thunk is emitted into the R2R image by crossgen2
// (WasmInterpreterToR2RThunkNode, keyed "M"+signature) for every R2R-compiled method body. Prefer that
- // image (the common case with R2R), then fall back to g_wasmThunks, which is the only source for managed
+ // image (the common case with R2R), then fall back to g_portableCallHelperThunks, which is the only source for managed
// methods whose native code is not R2R (FCall/QCall/pinvoke/runtime stubs).
static InterpreterCalliCookie LookupManagedThunk(const char* key)
{
@@ -1137,8 +1137,8 @@ namespace
return NULL;
}
- // Managed methods (default calling convention) prefer the R2R image and fall back to g_wasmThunks;
- // unmanaged calli are only ever in g_wasmThunks.
+ // Managed methods (default calling convention) prefer the R2R image and fall back to g_portableCallHelperThunks;
+ // unmanaged calli are only ever in g_portableCallHelperThunks.
InterpreterCalliCookie thunk = (callConv == IMAGE_CEE_CS_CALLCONV_DEFAULT)
? LookupManagedThunk(keyBuffer)
: LookupThunkInLibcoreclr(keyBuffer);
@@ -1292,11 +1292,11 @@ namespace
void InitializeWasmThunkCaches()
{
{
- StringToWasmSigThunkHash* newTable = new StringToWasmSigThunkHash();
- newTable->Reallocate(g_wasmThunksCount * StringToWasmSigThunkHash::s_density_factor_denominator / StringToWasmSigThunkHash::s_density_factor_numerator + 1);
- for (size_t i = 0; i < g_wasmThunksCount; i++)
+ StringToPortableSigThunkHash* newTable = new StringToPortableSigThunkHash();
+ newTable->Reallocate(g_portableCallHelperThunksCount * StringToPortableSigThunkHash::s_density_factor_denominator / StringToPortableSigThunkHash::s_density_factor_numerator + 1);
+ for (size_t i = 0; i < g_portableCallHelperThunksCount; i++)
{
- newTable->Add(g_wasmThunks[i].key, g_wasmThunks[i].value);
+ newTable->Add(g_portableCallHelperThunks[i].key, g_portableCallHelperThunks[i].value);
}
thunkCache = newTable;
}
diff --git a/src/coreclr/vm/wasm/wasi/callhelpers-interp-to-managed.cpp b/src/coreclr/vm/wasm/wasi/callhelpers-interp-to-managed.cpp
index 56f77515c21288..03e37ec1bdc4d5 100644
--- a/src/coreclr/vm/wasm/wasi/callhelpers-interp-to-managed.cpp
+++ b/src/coreclr/vm/wasm/wasi/callhelpers-interp-to-managed.cpp
@@ -550,7 +550,7 @@ namespace
}
}
-const StringToWasmSigThunk g_wasmThunks[] = {
+const StringToPortableSigThunk g_portableCallHelperThunks[] = {
{ "Mddddp", (void*)&CallFunc_F64_F64_F64_RetF64_PE },
{ "Mdddp", (void*)&CallFunc_F64_F64_RetF64_PE },
{ "Mddip", (void*)&CallFunc_F64_I32_RetF64_PE },
@@ -633,4 +633,4 @@ const StringToWasmSigThunk g_wasmThunks[] = {
{ "Mvp", (void*)&CallFunc_Void_RetVoid_PE }
};
-const size_t g_wasmThunksCount = sizeof(g_wasmThunks) / sizeof(g_wasmThunks[0]);
+const size_t g_portableCallHelperThunksCount = sizeof(g_portableCallHelperThunks) / sizeof(g_portableCallHelperThunks[0]);
diff --git a/src/coreclr/vm/wasm/wasi/callhelpers-pinvoke.cpp b/src/coreclr/vm/wasm/wasi/callhelpers-pinvoke.cpp
index 89b992b77aae8f..3dceaa7de8574b 100644
--- a/src/coreclr/vm/wasm/wasi/callhelpers-pinvoke.cpp
+++ b/src/coreclr/vm/wasm/wasi/callhelpers-pinvoke.cpp
@@ -346,7 +346,7 @@ static const Entry s_libSystem_Native [] = {
DllImportEntry(SystemNative_GetCryptographicallySecureRandomBytes) // System.IO.Compression
DllImportEntry(SystemNative_GetCwd) // System.Private.CoreLib
DllImportEntry(SystemNative_GetDefaultSearchOrderPseudoHandle) // System.Private.CoreLib
- DllImportEntry(SystemNative_GetErrNo) // System.Net.NameResolution, System.Private.CoreLib
+ DllImportEntry(SystemNative_GetErrNo) // System.Private.CoreLib
DllImportEntry(SystemNative_GetHostEntryForName) // System.Net.NameResolution
DllImportEntry(SystemNative_GetHostName) // System.Net.NameResolution
DllImportEntry(SystemNative_GetIPv4Address) // System.Net.Primitives, System.Net.Sockets
diff --git a/src/installer/pkg/sfx/Microsoft.NETCore.App/Crossgen2/Sdk.props b/src/installer/pkg/sfx/Microsoft.NETCore.App/Crossgen2/Sdk.props
new file mode 100644
index 00000000000000..a5cdb6c55a638d
--- /dev/null
+++ b/src/installer/pkg/sfx/Microsoft.NETCore.App/Crossgen2/Sdk.props
@@ -0,0 +1,18 @@
+
+
+
+ <_Crossgen2PackExeSuffix Condition="$([MSBuild]::IsOSPlatform('Windows'))">.exe
+ $([MSBuild]::NormalizePath('$(MSBuildThisFileDirectory)', '..', 'tools', 'crossgen2$(_Crossgen2PackExeSuffix)'))
+
+
diff --git a/src/installer/pkg/sfx/Microsoft.NETCore.App/Microsoft.NETCore.App.Crossgen2.props b/src/installer/pkg/sfx/Microsoft.NETCore.App/Microsoft.NETCore.App.Crossgen2.props
index a400c42adf25c7..cdec0b7f158ee7 100644
--- a/src/installer/pkg/sfx/Microsoft.NETCore.App/Microsoft.NETCore.App.Crossgen2.props
+++ b/src/installer/pkg/sfx/Microsoft.NETCore.App/Microsoft.NETCore.App.Crossgen2.props
@@ -18,6 +18,10 @@
Include="$(CrossgenPublishProject)"
OutputItemType="_RawCrossgenPublishFiles"
ReferenceOutputAssembly="false" />
+
+
+
false
$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)'))
- $(NoWarn);WASM0066
diff --git a/src/libraries/System.Runtime/tests/System.Globalization.Tests/Invariant/Invariant.Tests.csproj b/src/libraries/System.Runtime/tests/System.Globalization.Tests/Invariant/Invariant.Tests.csproj
index be22e2a7dd3ad0..7d444810c694cd 100644
--- a/src/libraries/System.Runtime/tests/System.Globalization.Tests/Invariant/Invariant.Tests.csproj
+++ b/src/libraries/System.Runtime/tests/System.Globalization.Tests/Invariant/Invariant.Tests.csproj
@@ -4,7 +4,6 @@
true
true
true
- false
false
diff --git a/src/libraries/System.Runtime/tests/System.Runtime.Tests/InvariantTimezone/System.Runtime.InvariantTimezone.Tests.csproj b/src/libraries/System.Runtime/tests/System.Runtime.Tests/InvariantTimezone/System.Runtime.InvariantTimezone.Tests.csproj
index a267620d5b7cd0..dda813137d5bcb 100644
--- a/src/libraries/System.Runtime/tests/System.Runtime.Tests/InvariantTimezone/System.Runtime.InvariantTimezone.Tests.csproj
+++ b/src/libraries/System.Runtime/tests/System.Runtime.Tests/InvariantTimezone/System.Runtime.InvariantTimezone.Tests.csproj
@@ -7,7 +7,6 @@
true
true
true
- false
diff --git a/src/mono/browser/build/BrowserWasmApp.CoreCLR.targets b/src/mono/browser/build/BrowserWasmApp.CoreCLR.targets
index 18033bb3e0c2b3..98360e93f59bb4 100644
--- a/src/mono/browser/build/BrowserWasmApp.CoreCLR.targets
+++ b/src/mono/browser/build/BrowserWasmApp.CoreCLR.targets
@@ -31,7 +31,6 @@
true
true
emcc
- true
<_WasmDefaultFlags>-fwasm-exceptions -sWASM_LEGACY_EXCEPTIONS=0 -msimd128
@@ -95,7 +94,7 @@
AssemblyFile="$(WasmAppBuilderTasksAssemblyPath)"
TaskFactory="TaskHostFactory"
Condition="'$(WasmAppBuilderTasksAssemblyPath)' != ''" />
-
@@ -590,7 +589,7 @@
<_EmccCFlags Include="$(EmccExtraCFlags)" />
-
@@ -654,25 +650,31 @@
<_WasmPInvokeModules Include="libSystem.Native" />
<_WasmPInvokeModules Include="libSystem.IO.Compression.Native" />
<_WasmPInvokeModules Include="libSystem.Globalization.Native" Condition="'$(InvariantGlobalization)' != 'true'" />
- <_WasmIgnoredPInvokeModules Include="libSystem.Globalization.Native" Condition="'$(InvariantGlobalization)' == 'true'" />
<_WasmPInvokeModules Include="libSystem.Native.Browser" />
<_WasmPInvokeModules Include="libSystem.Runtime.InteropServices.JavaScript.Native" />
-
+
- <_WasmManagedAssemblies Include="@(WasmAssembliesToBundle->Distinct())" />
+ <_WasmBundledFile Include="@(WasmAssembliesToBundle->Distinct())" />
+ WasmNestedPublishApp build where this runs doesn't carry PublishReadyToRun. Remapped ahead of the
+ filter below, which reads PE headers and cannot make sense of webcil either. -->
- <_WasmManagedAssembliesR2RRemap Include="@(_WasmManagedAssemblies)" Condition="Exists('$(IntermediateLinkDir)%(FileName)%(Extension)')" />
- <_WasmManagedAssemblies Remove="@(_WasmManagedAssembliesR2RRemap)" />
- <_WasmManagedAssemblies Include="@(_WasmManagedAssembliesR2RRemap->'$(IntermediateLinkDir)%(FileName)%(Extension)')" />
+ <_WasmBundledFileR2RRemap Include="@(_WasmBundledFile)" Condition="Exists('$(IntermediateLinkDir)%(FileName)%(Extension)')" />
+ <_WasmBundledFile Remove="@(_WasmBundledFileR2RRemap)" />
+ <_WasmBundledFile Include="@(_WasmBundledFileR2RRemap->'$(IntermediateLinkDir)%(FileName)%(Extension)')" />
+
+
+
+
<_HasCoreLib Condition="'%(_WasmManagedAssemblies.FileName)%(_WasmManagedAssemblies.Extension)' == 'System.Private.CoreLib.dll'">true
@@ -688,19 +690,41 @@
<_WasmManagedAssemblies Include="$(_CoreLibPath)" />
-
-
-
+
+
+ <_PortableCallHelpersGeneratorExeSuffix Condition="'$(OS)' == 'Windows_NT'">.exe
+ $([MSBuild]::NormalizePath('$(Crossgen2InBuildDir)', 'crossgen2$(_PortableCallHelpersGeneratorExeSuffix)'))
+ $(Crossgen2ToolPath)
+
+
+
+
+
+
+
+ <_PortableCallHelpersGeneratorRsp>$(_WasmIntermediateOutputPath)callhelpers-generator.rsp
+
+
+
+ <_PortableCallHelpersGeneratorArg Include="--targetos:browser" />
+ <_PortableCallHelpersGeneratorArg Include="--targetarch:wasm" />
+ <_PortableCallHelpersGeneratorArg Include="--generate-portable-callhelpers:$(_WasmIntermediateOutputPath)" />
+ <_PortableCallHelpersGeneratorArg Include="@(_WasmPInvokeModules->'--directpinvoke:%(Identity)')" />
+ <_PortableCallHelpersGeneratorArg Include="@(_WasmManagedAssemblies->'%(FullPath)')" />
+
+
+
+
+
+
+
+
+
<_WasmSourceFileToCompileGenerated Remove="@(_WasmSourceFileToCompileGenerated)" />
@@ -747,7 +771,7 @@
-
+
, ) assume are already in scope from
// the in-tree CoreCLR PCH (vm/common.h). The generated .cpp files still
@@ -18,9 +18,9 @@
// * INTERP_STACK_SLOT_SIZE -- defined in interpretershared.h in-tree; the
// interp-to-managed file uses it but does not include that header.
// * LF_INTEROP/LL_INFO1000/LOG/PORTABILITY_ASSERT -- CoreCLR logging
-// primitives used by pinvoke-table.cpp.
+// primitives used by callhelpers-pinvoke.cpp.
//
-// Definitions for symbols declared by (g_wasmThunks,
+// Definitions for symbols declared by (g_portableCallHelperThunks,
// g_ReverseThunks, ...) live in libcoreclr_static.a or in the same generated
// .cpp (the generator emits the table bodies).
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 7b600623e798d8..48286c531fca63 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
@@ -11,7 +11,8 @@
"Microsoft.NET.Sdk.WebAssembly.Pack.${NetVersion}",
"Microsoft.NETCore.App.Runtime.Mono.${NetVersion}.browser-wasm",
"Microsoft.NETCore.App.Runtime.${NetVersion}.browser-wasm",
- "Microsoft.NETCore.App.Runtime.AOT.Cross.${NetVersion}.browser-wasm"
+ "Microsoft.NETCore.App.Runtime.AOT.Cross.${NetVersion}.browser-wasm",
+ "Microsoft.NETCore.App.Crossgen2.${NetVersion}"
],
"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" ]
@@ -452,6 +453,20 @@
"osx-arm64": "Microsoft.NETCore.App.Runtime.AOT.osx-arm64.Cross.wasi-wasm"
}
},
+ "Microsoft.NETCore.App.Crossgen2.${NetVersion}": {
+ "kind": "Sdk",
+ "version": "${PackageVersion}",
+ "alias-to": {
+ "win-x64": "Microsoft.NETCore.App.Crossgen2.win-x64",
+ "win-arm64": "Microsoft.NETCore.App.Crossgen2.win-arm64",
+ "linux-x64": "Microsoft.NETCore.App.Crossgen2.linux-x64",
+ "linux-arm64": "Microsoft.NETCore.App.Crossgen2.linux-arm64",
+ "linux-musl-x64": "Microsoft.NETCore.App.Crossgen2.linux-musl-x64",
+ "linux-musl-arm64": "Microsoft.NETCore.App.Crossgen2.linux-musl-arm64",
+ "osx-x64": "Microsoft.NETCore.App.Crossgen2.osx-x64",
+ "osx-arm64": "Microsoft.NETCore.App.Crossgen2.osx-arm64"
+ }
+ },
"Microsoft.NETCore.App.Runtime.Mono.${NetVersion}.browser-wasm" : {
"kind": "framework",
"version": "${PackageVersion}",
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 ad0e43d11e1f6b..7420fab69354c2 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
@@ -214,10 +214,16 @@
+ the CoreCLR runtime pack via its own KnownRuntimePack.
+
+ crossgen2 is imported for its type system, not to compile ReadyToRun images: it generates the
+ wasm P/Invoke call helpers, which encode struct sizes that cannot be derived from metadata
+ alone. The SDK only resolves its own KnownCrossgen2Pack when PublishReadyToRun is set, so the
+ pack is acquired through the workload instead. -->
+
diff --git a/src/mono/wasi/build/WasiApp.CoreCLR.targets b/src/mono/wasi/build/WasiApp.CoreCLR.targets
index 9415a5ab7f9ca4..a1dd6dd842ceeb 100644
--- a/src/mono/wasi/build/WasiApp.CoreCLR.targets
+++ b/src/mono/wasi/build/WasiApp.CoreCLR.targets
@@ -6,8 +6,8 @@
Performs a per-app native link of the shipping wasihost corehost (libWasiHost.a, from
src/native/corehost/wasihost) so reverse P/Invoke thunks for the app/test
[UnmanagedCallersOnly] methods are covered (the baked libcoreclr_gen_static.a
- only covers framework top-level UCO callbacks). Runs ManagedToNativeGenerator
- (TargetOS=wasi) over the bundle, compiles the generated callhelpers with the wasi-sdk
+ only covers framework top-level UCO callbacks). Runs crossgen2's portable call-helpers
+ generator (TargetOS=wasi) over the bundle, compiles the generated callhelpers with the wasi-sdk
clang, and links libWasiHost.a from the runtime-pack static archives + the app callhelper .o
(replacing libcoreclr_gen_static.a) via wasm-component-ld. The wasi:http import that
System.Net.* pulls in is declared via the component-type link flag
@@ -21,10 +21,7 @@
WasmAppRuntimeFlavor=Mono default. -->
-
+
CoreCLR
@@ -108,7 +105,6 @@
<_WasiPInvokeTablePath>$(_WasiRelinkObjDir)callhelpers-pinvoke.cpp
<_WasiReversePInvokeTablePath>$(_WasiRelinkObjDir)callhelpers-reverse.cpp
<_WasiInterpToNativeTablePath>$(_WasiRelinkObjDir)callhelpers-interp-to-managed.cpp
- <_WasiM2NCachePath>$(_WasiRelinkObjDir)m2n_cache.txt
+ The bundle can carry native payloads named .dll, and the generator reads managed
+ metadata, so narrow the list the same way the browser targets do. -->
- <_WasiManagedAssemblies Include="$(WasmAppDir)managed\*.dll" />
+ <_WasiBundledFile Include="$(WasmAppDir)managed\*.dll" />
<_WasiPInvokeModules Include="libSystem.Native" />
<_WasiPInvokeModules Include="libSystem.IO.Compression.Native" />
<_WasiPInvokeModules Include="libSystem.Globalization.Native" Condition="'$(InvariantGlobalization)' != 'true'" />
- <_WasiIgnoredPInvokeModules Include="libSystem.Globalization.Native" Condition="'$(InvariantGlobalization)' == 'true'" />
-
-
-
+
+
+
+
+
+
+ <_PortableCallHelpersGeneratorExeSuffix Condition="'$(OS)' == 'Windows_NT'">.exe
+ $([MSBuild]::NormalizePath('$(Crossgen2InBuildDir)', 'crossgen2$(_PortableCallHelpersGeneratorExeSuffix)'))
+ $(Crossgen2ToolPath)
+
+
+
+
+
+
+
+ <_PortableCallHelpersGeneratorRsp>$(_WasiRelinkObjDir)callhelpers-generator.rsp
+
+
+
+ <_PortableCallHelpersGeneratorArg Include="--targetos:wasi" />
+ <_PortableCallHelpersGeneratorArg Include="--targetarch:wasm" />
+ <_PortableCallHelpersGeneratorArg Include="--generate-portable-callhelpers:$(_WasiRelinkObjDir)" />
+ <_PortableCallHelpersGeneratorArg Include="@(_WasiPInvokeModules->'--directpinvoke:%(Identity)')" />
+ <_PortableCallHelpersGeneratorArg Include="@(_WasiManagedAssemblies->'%(FullPath)')" />
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
- <_RunGeneratorTargetOS Condition="'$(TargetOS)' == ''">browser
- <_RunGeneratorTargetOS Condition="'$(TargetOS)' != ''">$(TargetOS)
-
-
-
-
-
-
-
diff --git a/src/tasks/WasmAppBuilder/coreclr/InternalCallSignatureCollector.cs b/src/tasks/WasmAppBuilder/coreclr/InternalCallSignatureCollector.cs
deleted file mode 100644
index eda8aedda299b4..00000000000000
--- a/src/tasks/WasmAppBuilder/coreclr/InternalCallSignatureCollector.cs
+++ /dev/null
@@ -1,57 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-using System;
-using System.Collections.Generic;
-using System.Reflection;
-using Microsoft.Build.Framework;
-
-namespace Microsoft.WebAssembly.Build.Tasks.CoreClr;
-
-//
-// Scans assemblies for methods marked with MethodImplAttributes.InternalCall
-// and generates portable entry point signatures for the interpreter-to-native thunks.
-//
-internal sealed class InternalCallSignatureCollector
-{
- private readonly HashSet _signatures = new();
- private readonly LogAdapter _log;
-
- public InternalCallSignatureCollector(LogAdapter log) => _log = log;
-
- public void ScanAssembly(Assembly asm)
- {
- foreach (Type type in asm.GetTypes())
- ScanType(type);
- }
-
- public IEnumerable GetSignatures() => _signatures;
-
- private void ScanType(Type type)
- {
- foreach (var method in type.GetMethods(BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance))
- {
- if ((method.GetMethodImplementationFlags() & MethodImplAttributes.InternalCall) == 0)
- continue;
-
- try
- {
- string? signature = SignatureMapper.MethodToSignature(method, _log, includeThis: true);
- if (signature is null)
- {
- _log.Warning("WASM0001", $"Could not generate signature for InternalCall method '{type.FullName}::{method.Name}'");
- continue;
- }
-
- signature += "p";
-
- if (_signatures.Add(signature))
- _log.LogMessage(MessageImportance.Low, $"Adding InternalCall signature {signature} for method '{type.FullName}.{method.Name}'");
- }
- catch (Exception ex) when (ex is not LogAsErrorException)
- {
- _log.Warning("WASM0001", $"Could not get signature for InternalCall method '{type.FullName}::{method.Name}' because '{ex.Message}'");
- }
- }
- }
-}
diff --git a/src/tasks/WasmAppBuilder/coreclr/InterpToNativeGenerator.cs b/src/tasks/WasmAppBuilder/coreclr/InterpToNativeGenerator.cs
deleted file mode 100644
index e5183182df8088..00000000000000
--- a/src/tasks/WasmAppBuilder/coreclr/InterpToNativeGenerator.cs
+++ /dev/null
@@ -1,222 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-using System;
-using System.IO;
-using System.Linq;
-using System.Text;
-using System.Collections.Generic;
-using System.Globalization;
-using Microsoft.Build.Utilities;
-using Microsoft.Build.Framework;
-using System.Diagnostics.CodeAnalysis;
-
-using JoinedString;
-//
-// This class generates the g_wasmThunks array and CallFunc_* functions used by the CoreCLR interpreter to call native code on WASM.
-// The generated code should be kept in sync with the corresponding CoreCLR runtime code that consumes these thunks and call functions.
-//
-
-#nullable enable
-
-namespace Microsoft.WebAssembly.Build.Tasks.CoreClr;
-
-internal sealed class InterpToNativeGenerator
-{
- private LogAdapter Log { get; set; }
-
- public InterpToNativeGenerator(LogAdapter log) => Log = log;
-
- public void Generate(IEnumerable cookies, string outputPath)
- {
- using TempFileName tmpFileName = new();
- using (var w = File.CreateText(tmpFileName.Path))
- {
- Emit(w, cookies);
- }
-
- if (Utils.CopyIfDifferent(tmpFileName.Path, outputPath, useHash: false))
- Log.LogMessage(MessageImportance.Low, $"Generating managed2native table to '{outputPath}'.");
- else
- Log.LogMessage(MessageImportance.Low, $"Managed2native table in {outputPath} is unchanged.");
- }
-
- private static string SignatureToArguments(string signature)
- {
- var tokens = SignatureMapper.ParseSignatureTokens(signature);
- if (tokens.Count <= 1)
- return "void";
-
- return string.Join(", ", tokens.Skip(1).Select(static t => SignatureMapper.TokenToNativeType(t)));
- }
-
- private static string CallFuncName(IEnumerable args, string result, bool isPortableEntryPointCall)
- {
- var paramTypes = args.Any() ? string.Join("_", args.Select(static t => SignatureMapper.TokenToNameType(t))) : "Void";
-
- return $"CallFunc_{paramTypes}_Ret{result}{(isPortableEntryPointCall ? "_PE" : "")}";
- }
-
- private static void Emit(StreamWriter w, IEnumerable cookies)
- {
- // Use OrderBy because Order() is not available on .NET Framework
- 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(SignatureMapper.GetStructSize(toks[0]));
- }
-
- w.Write(
- """
- // Licensed to the .NET Foundation under one or more agreements.
- // The .NET Foundation licenses this file to you under the MIT license.
- //
-
- //
- // GENERATED FILE, DON'T EDIT
- // Generated by coreclr InterpToNativeGenerator
- //
-
- #include
- #include
-
- // Arguments are passed on the stack with each argument aligned to INTERP_STACK_SLOT_SIZE.
- #define ARG_ADDR(i) (pArgs + (i * INTERP_STACK_SLOT_SIZE))
- #define ARG_IND(i) ((int32_t)((int32_t*)ARG_ADDR(i)))
- #define ARG_I32(i) (*(int32_t*)ARG_ADDR(i))
- #define ARG_I64(i) (*(int64_t*)ARG_ADDR(i))
- #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
- {
- """);
-
- foreach (var signatureValue in signatures)
- {
- string signature = signatureValue;
- try
- {
- var tokens = SignatureMapper.ParseSignatureTokens(signature);
- string returnToken = tokens[0];
- var result = Result(returnToken);
- bool isPortableEntryPointCall = IsPortableEntryPointCall(tokens);
- if (isPortableEntryPointCall)
- {
- // Portable entrypoints have an extra hidden parameter for the portable entrypoint context, so we need to adjust the signature and result accordingly for the call function generation
- tokens.RemoveAt(tokens.Count - 1);
- }
-
- RemoveAsyncCallMarker(tokens);
-
- var args = Args(tokens);
-
- var portableEntryPointComma = args.Count > 0 ? ", " : "";
- var portableEntrypointDeclaration = isPortableEntryPointCall ? portableEntryPointComma + "PCODE" : "";
- var portableEntrypointParam = isPortableEntryPointCall ? portableEntryPointComma + "pPortableEntryPoint" : "";
- var portableEntrypointStackDeclaration = isPortableEntryPointCall ? "int*, " : "";
- var portableEntrypointStackParam = isPortableEntryPointCall ? "&framePointer, " : "";
- var portableEntrypointPointerRD = isPortableEntryPointCall ? "*" : "";
- w.Write(
- $$"""
-
- {{(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")}};
- {{(result.isVoid ? "" : "*" + "((" + result.nativeType + "*)pRet) = ")}}(*fptr)({{portableEntrypointStackParam}}{{string.Join(", ", ArgsWithSlotOffsets(args))}}{{portableEntrypointParam}});
- }
-
- """);
- }
- catch (InvalidSignatureCharException e)
- {
- throw new LogAsErrorException($"Element '{e.Char}' of signature '{signature}' can't be handled by managed2native generator");
- }
- }
-
- w.Write(
- $$"""
- }
-
- const StringToWasmSigThunk g_wasmThunks[] = {
- {{signatures.Join($",{w.NewLine}", signature =>
- {
- string initialSignature = signature;
- var tokens = SignatureMapper.ParseSignatureTokens(signature);
- bool isPortableEntryPointCall = IsPortableEntryPointCall(tokens);
- if (isPortableEntryPointCall)
- tokens.RemoveAt(tokens.Count - 1);
- RemoveAsyncCallMarker(tokens);
- return $" {{ \"M{initialSignature}\", (void*)&{CallFuncName(Args(tokens), SignatureMapper.TokenToNameType(tokens[0]), isPortableEntryPointCall)} }}";
- }
- )}}
- };
-
- const size_t g_wasmThunksCount = sizeof(g_wasmThunks) / sizeof(g_wasmThunks[0]);
-
- """);
-
- static List Args(List tokens)
- {
- return tokens.Count > 1 ? tokens.GetRange(1, tokens.Count - 1) : new List();
- }
-
- static List ArgsWithSlotOffsets(List args)
- {
- var result = new List();
- int slot = 0;
- foreach (var token in args)
- {
- if (token[0] == 'A')
- {
- slot = (slot + 1) & ~1;
- }
-
- result.Add($"{SignatureMapper.TokenToArgType(token)}({slot})");
- slot += SignatureMapper.TokenToSlotCount(token);
- }
-
- return result;
- }
-
- static (bool isVoid, string nativeType) Result(string 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_S{SignatureMapper.GetStructSize(returnToken)}");
- return new(returnToken == "v", SignatureMapper.TokenToNativeType(returnToken));
- }
-
- static bool IsPortableEntryPointCall(List tokens)
- {
- return tokens.Count > 0 && tokens[tokens.Count - 1] == "p";
- }
-
- static bool RemoveAsyncCallMarker(List tokens)
- {
- int asyncMarkerIndex = tokens.IndexOf("a");
- if (asyncMarkerIndex < 0)
- return false;
-
- tokens.RemoveAt(asyncMarkerIndex);
- return true;
- }
- }
-}
diff --git a/src/tasks/WasmAppBuilder/coreclr/ManagedToNativeGenerator.cs b/src/tasks/WasmAppBuilder/coreclr/ManagedToNativeGenerator.cs
deleted file mode 100644
index f033d8b39ca3ed..00000000000000
--- a/src/tasks/WasmAppBuilder/coreclr/ManagedToNativeGenerator.cs
+++ /dev/null
@@ -1,181 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-using System;
-using System.Collections.Generic;
-using System.Diagnostics.CodeAnalysis;
-using System.IO;
-using System.Linq;
-using System.Reflection;
-using System.Text;
-using Microsoft.Build.Framework;
-using Microsoft.Build.Utilities;
-
-namespace Microsoft.WebAssembly.Build.Tasks.CoreClr;
-
-public class ManagedToNativeGenerator : Task
-{
- [Required]
- public string[] Assemblies { get; set; } = Array.Empty();
-
- [Required, NotNull]
- public string[]? PInvokeModules { get; set; }
-
- public string[] IgnoredPInvokeModules { get; set; } = Array.Empty();
-
- [Required, NotNull]
- public string? PInvokeOutputPath { get; set; }
-
- [Required, NotNull]
- public string? ReversePInvokeOutputPath { get; set; }
-
- [Required, NotNull]
- public string? InterpToNativeOutputPath { get; set; }
- public string? CacheFilePath { get; set; }
-
- public bool IsLibraryMode { get; set; }
-
- // When true (default), a P/Invoke to a module that isn't statically linked, ignored,
- // [WasmImportLinkage], "*" or QCall produces a WASM0066 warning. Consumers that scan
- // untrimmed closures full of cross-platform interop (e.g. library-test bundles) set this
- // false so the expected "unresolved module, skip and throw-if-called" case is logged as a
- // message instead of a build-breaking (under warn-as-error) warning.
- public bool WarnOnUnresolvedPInvokeModules { get; set; } = true;
-
- public string TargetOS { get; set; } = "browser";
-
- private static readonly string[] s_knownTargetOSes = new[] { "browser", "wasi" };
-
- [Output]
- public string[]? FileWrites { get; private set; }
-
- public override bool Execute()
- {
- if (Assemblies!.Length == 0)
- {
- Log.LogError($"{nameof(ManagedToNativeGenerator)}.{nameof(Assemblies)} cannot be empty");
- return false;
- }
-
- if (PInvokeModules!.Length == 0)
- {
- Log.LogError($"{nameof(ManagedToNativeGenerator)}.{nameof(PInvokeModules)} cannot be empty");
- return false;
- }
-
- if (string.IsNullOrWhiteSpace(TargetOS))
- {
- Log.LogError($"{nameof(ManagedToNativeGenerator)}.{nameof(TargetOS)} cannot be empty; expected one of: {string.Join(", ", s_knownTargetOSes)}");
- return false;
- }
-
- TargetOS = TargetOS.Trim().ToLowerInvariant();
- if (Array.IndexOf(s_knownTargetOSes, TargetOS) < 0)
- {
- Log.LogError($"{nameof(ManagedToNativeGenerator)}.{nameof(TargetOS)} '{TargetOS}' is not recognized; expected one of: {string.Join(", ", s_knownTargetOSes)}");
- return false;
- }
-
- try
- {
- var logAdapter = new LogAdapter(Log);
- ExecuteInternal(logAdapter);
- return !Log.HasLoggedErrors;
- }
- catch (LogAsErrorException e)
- {
- Log.LogError(e.Message);
- return false;
- }
- }
-
- private void ExecuteInternal(LogAdapter log)
- {
- Dictionary _symbolNameFixups = new();
- List managedAssemblies = FilterOutUnmanagedBinaries(Assemblies);
- var pinvoke = new PInvokeTableGenerator(FixupSymbolName, log, IsLibraryMode, TargetOS, WarnOnUnresolvedPInvokeModules);
- var internalCallCollector = new InternalCallSignatureCollector(log);
-
- var resolver = new PathAssemblyResolver(managedAssemblies);
- using var mlc = new MetadataLoadContext(resolver, "System.Private.CoreLib");
- foreach (string asmPath in managedAssemblies)
- {
- log.LogMessage(MessageImportance.Low, $"Loading {asmPath} to scan for pinvokes and InternalCall methods");
- Assembly asm = mlc.LoadFromAssemblyPath(asmPath);
- pinvoke.ScanAssembly(asm);
-
- if (asmPath.Contains("System.Private.CoreLib", StringComparison.OrdinalIgnoreCase))
- {
- // Only scan System.Private.CoreLib, as all used InternalCall methods should be defined there,
- // and scanning all assemblies can be expensive, and can trigger failures which should be avoided.
- // System.Private.CoreLib is tested such that this should never fail on that binary.
- internalCallCollector.ScanAssembly(asm);
- }
- }
-
- // Pregenerated signatures for commonly used shapes used by R2R code to reduce duplication in generated R2R binaries.
- // The signatures should be in the form of a string where the first character represents the return type and the
- // following characters represent the argument types. The type characters should match those used by the
- // SignatureMapper.CharToNativeType method.
- string[] pregeneratedInterpreterToNativeSignatures = Array.Empty(); // Currently none, but can be added here as needed in the future.
-
- IEnumerable cookies = pinvoke.Generate(PInvokeModules, IgnoredPInvokeModules, PInvokeOutputPath, ReversePInvokeOutputPath);
- cookies = cookies.Concat(internalCallCollector.GetSignatures());
- cookies = cookies.Concat(pregeneratedInterpreterToNativeSignatures);
-
- var m2n = new InterpToNativeGenerator(log);
- m2n.Generate(cookies, InterpToNativeOutputPath);
-
- if (!string.IsNullOrEmpty(CacheFilePath))
- {
- IEnumerable cacheLines = PInvokeModules
- .Select(module => $"module:{module}")
- .Concat(IgnoredPInvokeModules.Select(module => $"ignored:{module}"));
- File.WriteAllLines(CacheFilePath, cacheLines, Encoding.UTF8);
- }
-
- List fileWritesList = new() { PInvokeOutputPath, InterpToNativeOutputPath };
- if (!string.IsNullOrEmpty(CacheFilePath))
- fileWritesList.Add(CacheFilePath);
-
- FileWrites = fileWritesList.ToArray();
-
- string FixupSymbolName(string name)
- {
- if (_symbolNameFixups.TryGetValue(name, out string? fixedName))
- return fixedName;
-
- fixedName = Utils.FixupSymbolName(name);
- _symbolNameFixups[name] = fixedName;
- return fixedName;
- }
- }
-
- private List FilterOutUnmanagedBinaries(string[] assemblies)
- {
- List managedAssemblies = new(assemblies.Length);
- foreach (string asmPath in Assemblies)
- {
- if (!File.Exists(asmPath))
- throw new LogAsErrorException($"Cannot find assembly {asmPath}");
-
- try
- {
- if (!Utils.IsManagedAssembly(asmPath))
- {
- Log.LogMessage(MessageImportance.Low, $"Skipping unmanaged {asmPath}.");
- continue;
- }
- }
- catch (Exception ex)
- {
- Log.LogMessage(MessageImportance.Low, $"Failed to read assembly {asmPath}: {ex}");
- throw new LogAsErrorException($"Failed to read assembly {asmPath}: {ex.Message}");
- }
-
- managedAssemblies.Add(asmPath);
- }
-
- return managedAssemblies;
- }
-}
diff --git a/src/tasks/WasmAppBuilder/coreclr/PInvokeCollector.cs b/src/tasks/WasmAppBuilder/coreclr/PInvokeCollector.cs
deleted file mode 100644
index ba59810c1b54c8..00000000000000
--- a/src/tasks/WasmAppBuilder/coreclr/PInvokeCollector.cs
+++ /dev/null
@@ -1,414 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-using System.Collections.Generic;
-using System;
-using System.Linq;
-using System.Diagnostics.CodeAnalysis;
-using System.Reflection;
-using Microsoft.Build.Framework;
-using Microsoft.Build.Utilities;
-using Microsoft.Build.Tasks;
-using JoinedString;
-
-namespace Microsoft.WebAssembly.Build.Tasks.CoreClr;
-
-#pragma warning disable CA1067
-#pragma warning disable CS0649
-internal sealed class PInvoke : IEquatable
-#pragma warning restore CA1067
-{
- public PInvoke(string entryPoint, string module, MethodInfo method, bool wasmLinkage)
- {
- EntryPoint = entryPoint;
- Module = module;
- Method = method;
- WasmLinkage = wasmLinkage;
- }
-
- public string EntryPoint;
- public string Module;
- public MethodInfo Method;
- public bool Skip;
- public bool WasmLinkage;
-
- public bool Equals(PInvoke? other)
- => other != null &&
- string.Equals(EntryPoint, other.EntryPoint, StringComparison.Ordinal) &&
- string.Equals(Module, other.Module, StringComparison.Ordinal) &&
- string.Equals(Method.ToString(), other.Method.ToString(), StringComparison.Ordinal);
-
- public override string ToString() => $"{{ EntryPoint: {EntryPoint}, Module: {Module}, Method: {Method}, Skip: {Skip} }}";
-}
-#pragma warning restore CS0649
-
-internal sealed class PInvokeComparer : IEqualityComparer
-{
- public bool Equals(PInvoke? x, PInvoke? y)
- {
- if (x == null && y == null)
- return true;
- if (x == null || y == null)
- return false;
-
- return x.Equals(y);
- }
-
- public int GetHashCode(PInvoke pinvoke)
- => $"{pinvoke.EntryPoint}{pinvoke.Module}{pinvoke.Method}".GetHashCode();
-}
-
-
-internal sealed class PInvokeCollector {
- private readonly Dictionary _assemblyDisableRuntimeMarshallingAttributeCache = new();
- private readonly Dictionary _typeUnsupportedOnPlatformCache = new();
- private readonly Dictionary _assemblyUnsupportedOnPlatformCache = new();
- private readonly string _targetOS;
- private LogAdapter Log { get; init; }
-
- public PInvokeCollector(LogAdapter log, string targetOS)
- {
- Log = log;
- _targetOS = targetOS;
- }
-
- public void CollectPInvokes(List pinvokes, List callbacks, HashSet signatures, Type type)
- {
- foreach (var method in type.GetMethods(BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance))
- {
- try
- {
- CollectPInvokesForMethod(method);
- if (DoesMethodHaveCallbacks(method, Log))
- callbacks.Add(new PInvokeCallback(method));
- }
- catch (Exception ex) when (ex is not LogAsErrorException)
- {
- Log.Warning("WASM0001", $"Could not get pinvoke, or callbacks for method '{type.FullName}::{method.Name}' because '{ex}'");
- }
- }
-
- if (HasAttribute(type, "System.Runtime.InteropServices.UnmanagedFunctionPointerAttribute"))
- {
- var method = type.GetMethod("Invoke");
-
- if (method != null)
- {
- string? signature = SignatureMapper.MethodToSignature(method!, Log);
- if (signature == null)
- throw new NotSupportedException($"Unsupported parameter type in method '{type.FullName}.{method.Name}'");
-
- if (signatures.Add(signature))
- Log.LogMessage(MessageImportance.Low, $"Adding pinvoke signature {signature} for method '{type.FullName}.{method.Name}'");
- }
- }
-
- void CollectPInvokesForMethod(MethodInfo method)
- {
- if ((method.Attributes & MethodAttributes.PinvokeImpl) != 0)
- {
- if (IsUnsupportedOnPlatform(method))
- return;
-
- var dllimport = method.CustomAttributes.First(attr => attr.AttributeType.Name == "DllImportAttribute");
- var wasmLinkage = method.CustomAttributes.Any(attr => attr.AttributeType.Name == "WasmImportLinkageAttribute");
- var module = (string)dllimport.ConstructorArguments[0].Value!;
- var entrypoint = (string)dllimport.NamedArguments.First(arg => arg.MemberName == "EntryPoint").TypedValue.Value!;
- pinvokes.Add(new PInvoke(entrypoint, module, method, wasmLinkage));
-
- string? signature = SignatureMapper.MethodToSignature(method, Log);
- if (signature == null)
- {
- throw new NotSupportedException($"Unsupported parameter type in method '{type.FullName}.{method.Name}'");
- }
-
- if (signatures.Add(signature))
- Log.LogMessage(MessageImportance.Low, $"Adding pinvoke signature {signature} for method '{type.FullName}.{method.Name}'");
- }
- }
-
- bool DoesMethodHaveCallbacks(MethodInfo method, LogAdapter log)
- {
- if (!MethodHasCallbackAttributes(method))
- return false;
-
- if (IsUnsupportedOnPlatform(method))
- return false;
-
- if (TryIsMethodGetParametersUnsupported(method, out string? reason))
- {
- Log.Warning("WASM0001", $"Skipping callback '{method.DeclaringType!.FullName}::{method.Name}' because '{reason}'.");
- return false;
- }
-
- if (method.DeclaringType != null && HasAssemblyDisableRuntimeMarshallingAttribute(method.DeclaringType.Assembly))
- return true;
-
- // No DisableRuntimeMarshalling attribute, so check if the params/ret-type are
- // blittable
- bool isVoid = method.ReturnType.FullName == "System.Void";
- if (!isVoid && !IsBlittable(method.ReturnType, log))
- Error($"The return type '{method.ReturnType.FullName}' of pinvoke callback method '{method}' needs to be blittable.");
-
- foreach (var p in method.GetParameters())
- {
- if (!IsBlittable(p.ParameterType, log))
- Error("Parameter types of pinvoke callback method '" + method + "' needs to be blittable.");
- }
-
- return true;
- }
-
- static bool MethodHasCallbackAttributes(MethodInfo method)
- {
- foreach (CustomAttributeData cattr in CustomAttributeData.GetCustomAttributes(method))
- {
- try
- {
- if (cattr.AttributeType.FullName == "System.Runtime.InteropServices.UnmanagedCallersOnlyAttribute" ||
- cattr.AttributeType.Name == "MonoPInvokeCallbackAttribute")
- {
- return true;
- }
- }
- catch
- {
- // Assembly not found, ignore
- }
- }
-
- return false;
- }
- }
-
- public static bool IsBlittable(Type type, LogAdapter log) => PInvokeTableGenerator.IsBlittable(type, log);
-
- private static void Error(string msg) => throw new LogAsErrorException(msg);
-
- internal static bool HasAttribute(MemberInfo element, params string[] attributeNames) => PInvokeTableGenerator.HasAttribute(element, attributeNames);
-
- private static bool TryIsMethodGetParametersUnsupported(MethodInfo method, [NotNullWhen(true)] out string? reason)
- {
- try
- {
- method.GetParameters();
- }
- catch (NotSupportedException nse)
- {
- reason = nse.Message;
- return true;
- }
- catch
- {
- // not concerned with other exceptions
- }
-
- reason = null;
- return false;
- }
-
- private bool HasAssemblyDisableRuntimeMarshallingAttribute(Assembly assembly)
- {
- if (!_assemblyDisableRuntimeMarshallingAttributeCache.TryGetValue(assembly, out var value))
- {
- _assemblyDisableRuntimeMarshallingAttributeCache[assembly] = value = assembly
- .GetCustomAttributesData()
- .Any(d => d.AttributeType.Name == "DisableRuntimeMarshallingAttribute");
- }
-
- return value;
- }
-
- private bool IsUnsupportedOnPlatform(MethodInfo method)
- {
- PlatformSupport methodResult = EvaluatePlatformAttributes(CustomAttributeData.GetCustomAttributes(method));
- if (methodResult == PlatformSupport.Unsupported)
- return true;
- if (methodResult == PlatformSupport.Supported)
- return false;
-
- return IsUnsupportedOnPlatform(method.DeclaringType);
- }
-
- private bool IsUnsupportedOnPlatform(Type? type)
- {
- if (type is null)
- return false;
-
- if (_typeUnsupportedOnPlatformCache.TryGetValue(type, out bool cached))
- return cached;
-
- bool value;
- PlatformSupport typeResult = EvaluatePlatformAttributes(CustomAttributeData.GetCustomAttributes(type));
- if (typeResult == PlatformSupport.Unsupported)
- {
- value = true;
- }
- else if (typeResult == PlatformSupport.Supported)
- {
- value = false;
- }
- else if (type.DeclaringType is not null)
- {
- value = IsUnsupportedOnPlatform(type.DeclaringType);
- }
- else
- {
- value = IsAssemblyUnsupportedOnPlatform(type.Assembly);
- }
-
- _typeUnsupportedOnPlatformCache[type] = value;
- return value;
- }
-
- private bool IsAssemblyUnsupportedOnPlatform(Assembly assembly)
- {
- if (!_assemblyUnsupportedOnPlatformCache.TryGetValue(assembly, out bool value))
- {
- PlatformSupport asmResult = EvaluatePlatformAttributes(assembly.GetCustomAttributesData());
- value = asmResult == PlatformSupport.Unsupported;
- _assemblyUnsupportedOnPlatformCache[assembly] = value;
- }
-
- return value;
- }
-
- private enum PlatformSupport
- {
- Unknown, // No platform attributes were observed at this scope
- Supported, // Explicitly supported here (target appears in a SupportedOSPlatform list)
- Unsupported, // Explicitly unsupported here (target matches UnsupportedOSPlatform, or
- // SupportedOSPlatform is present and does not list the target)
- }
-
- private PlatformSupport EvaluatePlatformAttributes(IList attrs)
- {
- bool hasSupportedOSPlatform = false;
- bool hasSupportedTarget = false;
- foreach (CustomAttributeData cattr in attrs)
- {
- try
- {
- if (cattr.AttributeType.FullName == "System.Runtime.Versioning.UnsupportedOSPlatformAttribute" &&
- cattr.ConstructorArguments.Count > 0 &&
- MatchesTargetOS(cattr.ConstructorArguments[0].Value?.ToString()))
- {
- return PlatformSupport.Unsupported;
- }
- if (cattr.AttributeType.FullName == "System.Runtime.Versioning.SupportedOSPlatformAttribute" &&
- cattr.ConstructorArguments.Count > 0)
- {
- hasSupportedOSPlatform = true;
- if (MatchesTargetOS(cattr.ConstructorArguments[0].Value?.ToString()))
- hasSupportedTarget = true;
- }
- }
- catch
- {
- // Assembly not found, ignore
- }
- }
-
- if (hasSupportedOSPlatform)
- return hasSupportedTarget ? PlatformSupport.Supported : PlatformSupport.Unsupported;
-
- return PlatformSupport.Unknown;
- }
-
- private bool MatchesTargetOS(string? platformName)
- {
- if (string.Equals(platformName, _targetOS, StringComparison.OrdinalIgnoreCase))
- return true;
-
- if (platformName?.StartsWith(_targetOS, StringComparison.OrdinalIgnoreCase) != true)
- return false;
-
-#if NETFRAMEWORK
- string version = platformName.Substring(_targetOS.Length);
-#else
- ReadOnlySpan version = platformName.AsSpan(_targetOS.Length);
-#endif
- return Version.TryParse(version, out _);
- }
-}
-
-internal sealed class PInvokeCallbackComparer : IComparer
-{
- public int Compare(PInvokeCallback? x, PInvokeCallback? y)
- {
- int compare = string.Compare(x!.Key, y!.Key, StringComparison.Ordinal);
- return compare != 0 ? compare : (int)(x.Token - y.Token);
- }
-}
-
-#pragma warning disable CS0649
-internal sealed class PInvokeCallback
-{
- public PInvokeCallback(MethodInfo method)
- {
- Method = method;
- var t = method.DeclaringType!;
- TypeName = t.Name!;
- TypeFullName = t.FullName!;
- AssemblyName = t.Module!.Assembly!.GetName()!.Name!;
- AssemblyFQName = t.Module!.Assembly!.GetName()!.FullName!;
- // Nested types: the runtime reverse-thunk key (vm/wasm/helpers.cpp GetHashCode ->
- // GetFullyQualifiedNameInfo) reports an empty namespace for nested types, so match that
- // here or the emitted g_ReverseThunks key won't be found at lookup time (#130129).
- // This key drops the enclosing-type chain, so nested types with the same simple name in
- // different namespaces collide; the duplicate-key check in PInvokeTableGenerator
- // (EmitNativeToInterp) turns that into a build error.
- // Tracked by https://github.com/dotnet/runtime/issues/130739.
- Namespace = t.IsNested ? string.Empty : t.Namespace;
- MethodName = method.Name!;
- ReturnType = method.ReturnType!;
- IsVoid = ReturnType.Name == "Void";
- Token = (uint)method.MetadataToken;
-
- // FIXME: this is a hack, we need to encode this better and allow reflection in the interp case
- // but either way it needs to match the key generated in get_native_to_interp since the key is
- // used to look up the interp entry function. It must be unique for each callback runtime errors
- // can occur since it is used to look up the index in the wasm_native_to_interp_ftndescs and
- // the signature of the interp entry function must match the native signature
- //
- // the key also needs to survive being encoded in C literals, if in doubt
- // add something like "\U0001F412" to the key on both the managed and unmanaged side
- Key = $"{MethodName}#{Method.GetParameters().Length}:{AssemblyName}:{Namespace}:{TypeName}";
-
- IsExport = false;
- foreach (var attr in method.CustomAttributes)
- {
- if (attr.AttributeType.Name == "UnmanagedCallersOnlyAttribute")
- {
- foreach (var arg in attr.NamedArguments)
- {
- if (arg.MemberName == "EntryPoint")
- {
- EntryPoint = arg.TypedValue.Value!.ToString();
- IsExport = true;
- return;
- }
- }
- }
- }
- }
-
- public string EntryName => $"{AssemblyName}_{Namespace}_{TypeName}_{MethodName}";
-
- public ParameterInfo[] Parameters => Method.GetParameters();
- public string? EntryPoint { get; }
- public MethodInfo Method { get; }
- public string? EntrySymbol { get; set; }
- public string AssemblyName { get; }
- public string AssemblyFQName { get; }
- public string TypeName { get; }
- public string TypeFullName { get; }
- public string? Namespace { get;}
- public string MethodName { get; }
- public Type ReturnType { get;}
- public bool IsExport { get; }
- public bool IsVoid { get; }
- public uint Token { get; }
- public string Key { get; }
-}
-#pragma warning restore CS0649
diff --git a/src/tasks/WasmAppBuilder/coreclr/PInvokeTableGenerator.cs b/src/tasks/WasmAppBuilder/coreclr/PInvokeTableGenerator.cs
deleted file mode 100644
index c0445d8b5e2788..00000000000000
--- a/src/tasks/WasmAppBuilder/coreclr/PInvokeTableGenerator.cs
+++ /dev/null
@@ -1,647 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-using System;
-using System.Collections.Generic;
-using System.Diagnostics.CodeAnalysis;
-using System.IO;
-using System.Linq;
-using System.Text;
-using System.Text.RegularExpressions;
-using System.Reflection;
-using System.Runtime.InteropServices;
-using Microsoft.Build.Framework;
-using Microsoft.Build.Utilities;
-using JoinedString;
-
-namespace Microsoft.WebAssembly.Build.Tasks.CoreClr;
-
-internal sealed class PInvokeTableGenerator
-{
- private LogAdapter Log { get; set; }
- private readonly Func _fixupSymbolName;
- private readonly HashSet signatures = new();
- private readonly List pinvokes = new();
- private readonly List callbacks = new();
- private readonly PInvokeCollector _pinvokeCollector;
- private readonly bool _isLibraryMode;
- private readonly bool _warnOnUnresolvedModules;
-
- public PInvokeTableGenerator(Func fixupSymbolName, LogAdapter log, bool isLibraryMode, string targetOS, bool warnOnUnresolvedModules = true)
- {
- Log = log;
- _fixupSymbolName = fixupSymbolName;
- _pinvokeCollector = new(log, targetOS);
- _isLibraryMode = isLibraryMode;
- _warnOnUnresolvedModules = warnOnUnresolvedModules;
- }
-
- public void ScanAssembly(Assembly asm)
- {
- foreach (Type type in asm.GetTypes())
- _pinvokeCollector.CollectPInvokes(pinvokes, callbacks, signatures, type);
- }
-
- public IEnumerable Generate(string[] pinvokeModules, string[] ignoredPInvokeModules, string outputPathPInvoke, string outputPathReversePInvoke)
- {
- var ignoredModules = new HashSet(ignoredPInvokeModules, StringComparer.Ordinal);
- var modules = new SortedDictionary(StringComparer.Ordinal);
- foreach (var module in pinvokeModules)
- {
- if (!ignoredModules.Contains(module))
- modules[module] = module;
- }
-
- foreach (var module in ignoredModules.OrderBy(module => module, StringComparer.Ordinal))
- Log.LogMessage(MessageImportance.Low, $"Ignoring PInvoke module {module}");
-
- using TempFileName tmpFileNamePInvoke = new();
- using (var w = new JoinedStringStreamWriter(tmpFileNamePInvoke.Path, false))
- {
- EmitPInvokeTable(w, modules, ignoredModules, pinvokes);
- }
-
- using TempFileName tmpFileNameReversePInvoke = new();
- using (var w = new JoinedStringStreamWriter(tmpFileNameReversePInvoke.Path, false))
- {
- EmitNativeToInterp(w, callbacks);
- }
-
- if (Utils.CopyIfDifferent(tmpFileNamePInvoke.Path, outputPathPInvoke, useHash: false))
- Log.LogMessage(MessageImportance.Low, $"Generating pinvoke table to '{outputPathPInvoke}'.");
- else
- Log.LogMessage(MessageImportance.Low, $"PInvoke table in {outputPathPInvoke} is unchanged.");
-
- if (Utils.CopyIfDifferent(tmpFileNameReversePInvoke.Path, outputPathReversePInvoke, useHash: false))
- Log.LogMessage(MessageImportance.Low, $"Generating pinvoke reverse table to '{outputPathReversePInvoke}'.");
- else
- Log.LogMessage(MessageImportance.Low, $"PInvoke reverse table in {outputPathReversePInvoke} is unchanged.");
-
- return signatures;
- }
-
- private void EmitPInvokeTable(StreamWriter w, SortedDictionary modules, HashSet ignoredModules, List pinvokes)
- {
- // What actually gets linked in, captured before the scan below starts adding to modules.
- // The lib-prefix fallback has to resolve against this rather than against modules, or an
- // alias could be derived from another alias, or from a module that is only imported for
- // [WasmImportLinkage] and has no archive behind it at all.
- HashSet linkedModules = new(modules.Keys, StringComparer.Ordinal);
-
- foreach (var pinvoke in pinvokes)
- {
- if (modules.ContainsKey(pinvoke.Module))
- continue;
- if (ignoredModules.Contains(pinvoke.Module))
- continue;
- // A static archive is named libFoo.a, so the module list -- built from the file names
- // of what gets linked in -- carries "libFoo", while the managed side spells the
- // [DllImport] "Foo", the name it would use on Windows. That is also the name the
- // runtime resolver looks up, so accept it as naming the same module.
- if (linkedModules.Contains($"lib{pinvoke.Module}"))
- {
- modules.Add(pinvoke.Module, pinvoke.Module);
- Log.LogMessage(MessageImportance.Low, $"Adding module {pinvoke.Module} for statically linked lib{pinvoke.Module}");
- continue;
- }
- // Handle special modules, and add them to the list of modules
- // otherwise, skip them and throw an exception at runtime if they
- // are called.
- if (pinvoke.WasmLinkage)
- {
- // WasmLinkage means we need to import the module
- modules.Add(pinvoke.Module, pinvoke.Module);
- Log.LogMessage(MessageImportance.Low, $"Adding module {pinvoke.Module} for WasmImportLinkage");
- }
- else if (pinvoke.Module == "*")
- {
- // Special case for * module to indicate static linking without specifying the module
- modules.Add(pinvoke.Module, pinvoke.Module);
- Log.LogMessage(MessageImportance.Low, $"Adding module {pinvoke.Module} for static linking");
- }
- else if (pinvoke.Module != "QCall")
- {
- // Unresolved module: not statically linked, ignored, [WasmImportLinkage], "*" or QCall.
- // By design we skip it and throw at runtime if it is ever called. For hand-authored
- // apps this is likely a bug, so warn; consumers scanning untrimmed closures full of
- // cross-platform interop (library-test bundles) disable the warning to avoid failing
- // the build under warn-as-error for P/Invokes that are never called on wasm.
- if (_warnOnUnresolvedModules)
- Log.Warning("WASM0066", $"PInvoke module '{pinvoke.Module}' for method '{pinvoke.Method.DeclaringType}::{pinvoke.Method.Name}' is not in the list of allowed modules. It is also not a specially treated module.");
- else if (ignoredModules.Add(pinvoke.Module))
- Log.LogMessage(MessageImportance.Low, $"Skipping unresolved PInvoke module '{pinvoke.Module}' for method '{pinvoke.Method.DeclaringType}::{pinvoke.Method.Name}' (not statically linked on wasm; will throw if called)." );
- }
- }
-
- w.WriteLine(
- $$"""
- // Licensed to the .NET Foundation under one or more agreements.
- // The .NET Foundation licenses this file to you under the MIT license.
- //
-
- //
- // GENERATED FILE, DON'T EDIT
- // Generated by coreclr callhelpers generator
- //
-
- #include
- #include
-
- extern "C" {
- """);
-
- var pinvokesGroupedByEntryPoint = pinvokes
- .Where(l => modules.ContainsKey(l.Module))
- .OrderBy(l => l.EntryPoint, StringComparer.Ordinal)
- .GroupBy(CEntryPoint, StringComparer.Ordinal);
- var comparer = new PInvokeComparer();
- foreach (IGrouping group in pinvokesGroupedByEntryPoint)
- {
- var candidates = group.Distinct(comparer).ToArray();
- PInvoke first = candidates[0];
- if (ShouldTreatAsVariadic(candidates))
- {
- string imports = string.Join(Environment.NewLine,
- candidates.Select(
- p => $" {p.Method} (in [{p.Method.DeclaringType?.Assembly.GetName().Name}] {p.Method.DeclaringType})"));
- Log.Warning("WASM0001", $"Found a native function ({first.EntryPoint}) with varargs in {first.Module}." +
- " Calling such functions is not supported, and will fail at runtime." +
- $" Managed DllImports: {Environment.NewLine}{imports}");
-
- foreach (var c in candidates)
- c.Skip = true;
-
- continue;
- }
-
- var decls = new HashSet();
- foreach (var candidate in candidates)
- {
- var decl = GenPInvokeDecl(candidate);
- if (decl is null || decls.Contains(decl))
- continue;
-
- w.WriteLine(decl);
- decls.Add(decl);
- }
- }
-
- w.Write(
- $$"""
- } // extern "C"
-
- """);
-
- var moduleImports = new Dictionary>();
- foreach (var module in modules.Keys)
- {
- // the order here is not important, because we use hash tables, we want it to be stable though
- var imports = pinvokes
- .Where(l => l.Module == module && !l.Skip)
- .OrderBy(l => l.EntryPoint, StringComparer.Ordinal)
- .GroupBy(d => d.EntryPoint, StringComparer.Ordinal)
- .Select(l =>
- {
- PInvoke p = l.First();
- // Runtime resolver looks up by managed EntryPoint.
- // [WasmImportLinkage] mangles the C symbol per module,
- // so emit the entry-point string explicitly rather than
- // stringifying the mangled name via DllImportEntry.
- if (p.WasmLinkage)
- return $" {{ \"{EscapeLiteral(p.EntryPoint)}\", (void*)&{CEntryPoint(p)} }}, // {ListRefs(l)}{w.NewLine}";
- return $" DllImportEntry({CEntryPoint(p)}) // {ListRefs(l)}{w.NewLine}";
- })
- .ToList();
-
- moduleImports[module] = imports;
- w.Write(
- $$"""
-
- static const Entry s_{{_fixupSymbolName(module)}} [] = {
- {{string.Join("", imports)}}};
-
- """);
- }
-
- w.Write(
- $$"""
-
- typedef struct PInvokeTable {
- const char* LibraryName;
- const Entry* Entries;
- size_t EntryCount;
- } PInvokeTable;
-
- static PInvokeTable s_PInvokeTables[] = {
- {{modules.Keys.Join($",{w.NewLine} ", m => $"{{\"{EscapeLiteral(m)}\", s_{_fixupSymbolName(m)}, {moduleImports[m].Count}}}")}}
- };
- const size_t s_PInvokeTablesCount = sizeof(s_PInvokeTables) / sizeof(s_PInvokeTables[0]);
-
- const void* callhelpers_pinvoke_override(const char* library_name, const char* entry_point_name)
- {
- for (size_t i = 0; i < s_PInvokeTablesCount; i++)
- {
- if (strcmp(library_name, s_PInvokeTables[i].LibraryName) == 0)
- {
- LOG((LF_INTEROP, LL_INFO1000, "Wasm callhelpers PInvoke override for: lib: %s, entry: %s \n", library_name, entry_point_name));
- return minipal_resolve_dllimport(s_PInvokeTables[i].Entries, s_PInvokeTables[i].EntryCount, entry_point_name);
- }
- }
-
- return nullptr;
- }
-
- """);
-
- static bool ShouldTreatAsVariadic(PInvoke[] candidates)
- {
- if (candidates.Length < 2)
- return false;
-
- PInvoke first = candidates[0];
- if (!TryIsMethodGetParametersSupported(first.Method, out _))
- return false;
-
- int firstNumArgs = first.Method.GetParameters().Length;
- return candidates
- .Skip(1)
- // detect possible vararg entrypoint usage
- // where the same entrypoint is used with different
- // number of arguments
- .Any(c => TryIsMethodGetParametersSupported(c.Method, out _) &&
- c.Method.GetParameters().Length != firstNumArgs);
- }
-
- static string ListRefs(IGrouping l) =>
- string.Join(", ", l.Select(c => c.Method.DeclaringType!.Module!.Assembly!.GetName()!.Name!).Distinct().OrderBy(n => n));
- }
-
- private string CEntryPoint(PInvoke pinvoke)
- {
- if (pinvoke.WasmLinkage)
- {
- // We mangle the name to avoid collisions with symbols in other modules
- string namespaceName = pinvoke.Method.DeclaringType?.Namespace ?? string.Empty;
- return _fixupSymbolName($"{namespaceName}#{pinvoke.Module}#{pinvoke.EntryPoint}");
- }
- return _fixupSymbolName(pinvoke.EntryPoint);
- }
-
- private static string MapType(Type t) => t.Name switch
- {
- "Void" => "void",
- nameof(Double) => "double",
- nameof(Single) => "float",
- nameof(Int64) => "int64_t",
- nameof(UInt64) => "uint64_t",
- nameof(Int32) => "int32_t",
- nameof(UInt32) => "uint32_t",
- nameof(Int16) => "int32_t",
- nameof(UInt16) => "uint32_t",
- nameof(Char) => "int32_t",
- nameof(Boolean) => "int32_t",
- nameof(SByte) => "int32_t",
- nameof(Byte) => "uint32_t",
- nameof(IntPtr) => "void *",
- nameof(UIntPtr) => "void *",
- _ => PickCTypeNameForUnknownType(t)
- };
-
- private static string PickCTypeNameForUnknownType(Type t)
- {
- // Pass objects by-reference (their address by-value)
- if (!t.IsValueType)
- return "void *";
- // Pass pointers and function pointers by-value
- else if (t.IsPointer || IsFunctionPointer(t))
- return "void *";
- else if (t.IsPrimitive)
- throw new NotImplementedException("No native type mapping for type " + t);
-
- // https://github.com/WebAssembly/tool-conventions/blob/main/BasicCABI.md#function-signatures
- // Any struct or union that recursively (including through nested structs, unions, and arrays)
- // contains just a single scalar value and is not specified to have greater than natural alignment.
- // FIXME: Handle the scenario where there are fields of struct types that contain no members
- var fields = t.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
- if (fields.Length == 1)
- return MapType(fields[0].FieldType);
- else
- return "void *";
- }
-
- // FIXME: System.Reflection.MetadataLoadContext can't decode function pointer types
- // https://github.com/dotnet/runtime/issues/43791
- private static bool TryIsMethodGetParametersSupported(MethodInfo method, [NotNullWhen(false)] out string? reason)
- {
- try
- {
- method.GetParameters();
- }
- catch (NotSupportedException nse)
- {
- reason = nse.Message;
- return false;
- }
- catch
- {
- // not concerned with other exceptions
- }
-
- reason = null;
- return true;
- }
-
- private string? GenPInvokeDecl(PInvoke pinvoke)
- {
- var method = pinvoke.Method;
-
- if (!TryIsMethodGetParametersSupported(pinvoke.Method, out string? reason))
- {
- // Don't use method.ToString() or any of it's parameters, or return type
- // because at least one of those are unsupported, and will throw
- Log.Warning("WASM0001", $"Skipping pinvoke '{pinvoke.Method.DeclaringType!.FullName}::{pinvoke.Method.Name}' because '{reason}'.");
-
- pinvoke.Skip = true;
- return null;
- }
-
- var realReturnType = method.ReturnType;
- var realParameterTypes = method.GetParameters().Select(p => MapType(p.ParameterType)).ToList();
-
- SignatureMapper.TypeToChar(realReturnType, Log, out bool resultIsByRef);
- if (resultIsByRef) {
- realReturnType = typeof(void);
- realParameterTypes.Insert(0, "void *");
- }
-
- var importAttributes = pinvoke.WasmLinkage
- ? $"__attribute__((import_module(\"{EscapeLiteral(pinvoke.Module)}\"),import_name(\"{EscapeLiteral(pinvoke.EntryPoint)}\"))) "
- : "";
- var externKeyword = pinvoke.WasmLinkage ? "extern " : "";
-
- return $" {importAttributes}{externKeyword}{MapType(realReturnType)} {CEntryPoint(pinvoke)} ({string.Join(", ", realParameterTypes)});";
- }
-
- private static string EscapeLiteral(string? input)
- {
- if (input == null)
- return string.Empty;
-
- StringBuilder sb = new StringBuilder();
-
- for (int i = 0; i < input.Length; i++)
- {
- char c = input[i];
-
- sb.Append(c switch
- {
- '\\' => "\\\\",
- '\"' => "\\\"",
- '\n' => "\\n",
- '\r' => "\\r",
- '\t' => "\\t",
- // take special care with surrogate pairs to avoid
- // potential decoding issues in generated C literals
- _ when char.IsHighSurrogate(c) && i + 1 < input.Length && char.IsLowSurrogate(input[i + 1])
- => $"\\U{char.ConvertToUtf32(c, input[++i]):X8}",
- _ when char.IsControl(c) || c > 127
- => $"\\u{(int)c:X4}",
- _ => c.ToString()
- });
- }
-
- return sb.ToString();
- }
-
- // this is eqivalent to `ULONG HashString(LPCWSTR szStr)` in CoreCLR runtime, src/coreclr/inc/utilcode.h
- private static uint HashString(string str)
- {
- uint hash = 5381;
- foreach (char c in str)
- {
- hash = ((hash << 5) + hash) ^ (uint)c;
- }
-
- return hash;
- }
-
- private void EmitNativeToInterp(StreamWriter w, List callbacks)
- {
- // Generate native->interp entry functions
- // These are called by native code, so they need to obtain
- // the interp entry function/arg from a global array
- // They also need to have a signature matching what the
- // native code expects, which is the native signature
- // of the delegate invoke in the [MonoPInvokeCallback]
- // or [UnmanagedCallersOnly] attribute.
- // Only blittable parameter/return types are supposed.
- w.Write(
- $$"""
- // Licensed to the .NET Foundation under one or more agreements.
- // The .NET Foundation licenses this file to you under the MIT license.
- //
-
- //
- // GENERATED FILE, DON'T EDIT
- // Generated by coreclr callhelpers generator
- //
-
- #include
-
- // WASM-TODO: The method lookup would ideally be fully qualified assembly and then methodDef token.
- // The current approach has limitations with overloaded methods.
- extern "C" void LookupUnmanagedCallersOnlyMethodByName(const char* fullQualifiedTypeName, const char* methodName, MethodDesc** ppMD);
- extern "C" void ExecuteInterpretedMethodFromUnmanaged(MethodDesc* pMD, int8_t* args, size_t argSize, int8_t* ret, PCODE callerIp);
-
- """);
-
- var callbackNames = new HashSet();
- var keys = new HashSet();
- int cb_index = 0;
- callbacks = callbacks.OrderBy(c => c, new PInvokeCallbackComparer()).ToList();
- foreach (var cb in callbacks)
- {
- cb.EntrySymbol = FixedSymbolName(cb, Log);
-
- if (callbackNames.Contains(cb.EntrySymbol))
- {
- Error($"Two callbacks with the same symbol '{cb.EntrySymbol}' are not supported.");
- }
- callbackNames.Add(cb.EntrySymbol);
- if (keys.Contains(cb.Key))
- {
- Error($"Two callbacks with the same Name and number of arguments '{cb.Key}' are not supported.");
- }
- keys.Add(cb.Key);
-
- // The signature of the interp entry function
- // This is a gsharedvt_in signature
- var entryArgs = new List();
- if (!cb.IsVoid)
- {
- entryArgs.Add("(int*)&result");
- }
- entryArgs.AddRange(cb.Parameters.Select((_, i) => $"(int*)&arg{i}"));
- entryArgs.Add($"(int*)wasm_native_to_interp_ftndescs [{cb_index}].arg");
-
- var argsArgs = cb.Parameters.Length > 0 ? "(int8_t*)args, sizeof(args)" : "nullptr, 0";
- var argsDeclaration = cb.Parameters.Length > 0
- ? $"\n int64_t args[{cb.Parameters.Length}] = {{ {cb.Parameters.Join(", ", (info, i) => $"(int64_t)arg{i}")} }};\n"
- : string.Empty;
- var parametersDeclaration = cb.Parameters.Join(", ", (info, i) => $"{MapType(info.ParameterType)} arg{i}");
- var exportFunction = cb.IsExport ?
- $$"""
-
-
- extern "C" {{MapType(cb.ReturnType)}} {{cb.EntryPoint}}({{parametersDeclaration}})
- {
- {{(cb.IsVoid ? "" : "return ")}}Call_{{cb.EntrySymbol}}({{cb.Parameters.Join(", ", (info, i) => $"arg{i}")}});
- }
- """ : string.Empty;
- w.Write(
- $$"""
-
- static MethodDesc* MD_{{cb.EntrySymbol}} = nullptr;
- static {{
- MapType(cb.ReturnType)}} Call_{{cb.EntrySymbol}}({{parametersDeclaration}})
- {{{argsDeclaration}}
- // Lazy lookup of MethodDesc for the function export scenario.
- if (!MD_{{cb.EntrySymbol}})
- {
- LookupUnmanagedCallersOnlyMethodByName("{{cb.TypeFullName}}, {{cb.AssemblyName}}", "{{cb.MethodName}}", &MD_{{cb.EntrySymbol}});
- }{{
- (!cb.IsVoid ? $"{w.NewLine}{w.NewLine} {MapType(cb.ReturnType)} result;" : "")}}
- ExecuteInterpretedMethodFromUnmanaged(MD_{{cb.EntrySymbol}}, {{argsArgs}}, {{(cb.IsVoid ? "nullptr" : "(int8_t*)&result")}}, (PCODE)&Call_{{cb.EntrySymbol}});{{
- (!cb.IsVoid ? $"{w.NewLine} return result;" : "")}}
- }{{exportFunction}}
-
- """);
- cb_index++;
- }
-
- w.Write(
- $$"""
-
- const ReverseThunkMapEntry g_ReverseThunks[] =
- {
- {{callbacks.Join($",{w.NewLine}", cb => ThunkMapEntryLine(cb, Log))}}
- };
-
- const size_t g_ReverseThunksCount = sizeof(g_ReverseThunks) / sizeof(g_ReverseThunks[0]);
-
- """);
- }
-
- private string FixedSymbolName(PInvokeCallback cb, LogAdapter Log)
- {
- var paramTypes = cb.Parameters.Length > 0 ? cb.Parameters.Join("_", (info, i) => SignatureMapper.TypeToNameType(info.ParameterType, Log)).ToString() : "Void";
- var sig = $"{paramTypes}_Ret{SignatureMapper.TypeToNameType(cb.ReturnType, Log)}";
-
- return _fixupSymbolName($"{cb.EntryName}_{sig}");
- }
-
-
- private string ThunkMapEntryLine(PInvokeCallback cb, LogAdapter Log)
- {
- var fsName = FixedSymbolName(cb, Log);
-
- return $" {{ {HashString(cb.Key)}, \"{EscapeLiteral(cb.Key)}\", {{ &MD_{fsName}, (void*)&Call_{cb.EntrySymbol} }} }}";
- }
-
- private static readonly Dictionary _blittableCache = new();
-
- public static bool IsFunctionPointer(Type type)
- {
- object? bIsFunctionPointer = type.GetType().GetProperty("IsFunctionPointer")?.GetValue(type);
- return (bIsFunctionPointer is bool b) && b;
- }
-
- public static bool IsBlittable(Type type, LogAdapter log)
- {
- // We maintain a cache of results in order to only produce log messages the first time
- // we analyze a given type. Otherwise, each (successful) use of a user-defined type
- // in a callback or pinvoke would generate duplicate messages.
- lock (_blittableCache)
- if (_blittableCache.TryGetValue(type, out bool blittable))
- return blittable;
-
- bool result = IsBlittableUncached(type, log);
- lock (_blittableCache)
- _blittableCache[type] = result;
- return result;
-
- static bool IsBlittableUncached(Type type, LogAdapter log)
- {
- if (type.IsPrimitive || type.IsByRef || type.IsPointer || type.IsEnum)
- return true;
-
- if (IsFunctionPointer(type))
- return true;
-
- // HACK: SkiaSharp has pinvokes that rely on this
- if (HasAttribute(type, "System.Runtime.InteropServices.UnmanagedFunctionPointerAttribute"))
- return true;
-
- if (type.Name == "__NonBlittableTypeForAutomatedTests__")
- return false;
-
- if (!type.IsValueType)
- {
- log.InfoHigh("WASM0060", "Type {0} is not blittable: Not a ValueType", type);
- return false;
- }
-
- var fields = type.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
-
- if (!type.IsLayoutSequential && (fields.Length > 1))
- {
- log.InfoHigh("WASM0061", "Type {0} is not blittable: LayoutKind is not Sequential", type);
- return false;
- }
-
- foreach (var ft in fields)
- {
- if (!IsBlittable(ft.FieldType, log))
- {
- log.InfoHigh("WASM0062", "Type {0} is not blittable: Field {1} is not blittable", type, ft.Name);
- return false;
- }
- // HACK: Skip literals since they're complicated
- // Ideally we would block initonly fields too since the callee could mutate them, but
- // we rely on being able to pass types like System.Guid which are readonly
- if (ft.IsLiteral)
- {
- log.InfoHigh("WASM0063", "Type {0} is not blittable: Field {1} is literal", type, ft.Name);
- return false;
- }
- }
-
- return true;
- }
- }
-
- public static bool HasAttribute(MemberInfo element, params string[] attributeNames)
- {
- foreach (CustomAttributeData cattr in CustomAttributeData.GetCustomAttributes(element))
- {
- try
- {
- for (int i = 0; i < attributeNames.Length; ++i)
- {
- if (cattr.AttributeType.FullName == attributeNames[i] ||
- cattr.AttributeType.Name == attributeNames[i])
- {
- return true;
- }
- }
- }
- catch
- {
- // Assembly not found, ignore
- }
- }
- return false;
- }
-
- private static void Error(string msg) => throw new LogAsErrorException(msg);
-}
diff --git a/src/tasks/WasmAppBuilder/coreclr/SignatureMapper.cs b/src/tasks/WasmAppBuilder/coreclr/SignatureMapper.cs
deleted file mode 100644
index c1f07ec01067d9..00000000000000
--- a/src/tasks/WasmAppBuilder/coreclr/SignatureMapper.cs
+++ /dev/null
@@ -1,402 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Reflection;
-using System.Runtime.InteropServices;
-using System.Text;
-
-namespace Microsoft.WebAssembly.Build.Tasks.CoreClr;
-
-// Computes Wasm signature strings from reflection metadata.
-// The signature string format is documented in docs/design/coreclr/botr/readytorun-format.md
-// (section "Wasm Signature String Encoding").
-internal static class SignatureMapper
-{
- // Hardcoded struct layouts for types that crossgen2 encodes as struct tokens.
- // The fully general case is handled by crossgen2's type system; these
- // cover the small set of multi-field structs that appear in InternalCall
- // and PInvoke signatures.
- private static readonly Dictionary s_knownStructLayouts = new()
- {
- ["System.Runtime.CompilerServices.QCallModule"] = (8, 8),
- ["System.Runtime.CompilerServices.QCallAssembly"] = (8, 8),
- ["System.Runtime.CompilerServices.QCallTypeHandle"] = (8, 8),
- ["System.GC+GCHeapHardLimitInfo"] = (64, 8),
- // Used by WBT tests
- ["WasmAppBuilderTestsPairStruct"] = (8, 8),
- ["WasmAppBuilderTests.S"] = (8, 8),
- ["WasmAppBuilderTests.Test+S"] = (8, 8),
- };
-
- private static char? TypeToChar(
- Type t,
- LogAdapter log,
- out bool isByRefStruct,
- out int structSize,
- out int structAlignment,
- int depth = 0)
- {
- isByRefStruct = false;
- structSize = 0;
- structAlignment = 0;
-
- if (depth > 5) {
- log.Warning("WASM0064", $"Unbounded recursion detected through parameter type '{t.Name}'");
- return null;
- }
-
- // See https://github.com/WebAssembly/tool-conventions/blob/main/BasicCABI.md
- char? c = null;
- if (t.Namespace == "System")
- {
- c = t.Name switch
- {
- nameof(String) => 'i',
- nameof(Boolean) => 'i',
- nameof(Char) => 'i',
- nameof(SByte) => 'i',
- nameof(Byte) => 'i',
- nameof(Int16) => 'i',
- nameof(UInt16) => 'i',
- nameof(Int32) => 'i',
- nameof(UInt32) => 'i',
- nameof(Int64) => 'l',
- nameof(UInt64) => 'l',
- nameof(Single) => 'f',
- nameof(Double) => 'd',
- // FIXME: These will need to be L for wasm64
- nameof(IntPtr) => 'i',
- nameof(UIntPtr) => 'i',
- "Void" => 'v',
- _ => null
- };
- }
-
- if (c != null)
- return c;
-
- // FIXME: Most of these need to be L for wasm64
- if (t.IsByRef)
- c = 'i';
- else if (t.IsClass)
- c = 'i';
- else if (t.IsInterface)
- c = 'i';
- else if (t.IsEnum)
- {
- Type underlyingType = t.GetEnumUnderlyingType();
- c = TypeToChar(underlyingType, log, out _, out structSize, out structAlignment, ++depth);
- }
- else if (t.IsPointer)
- c = 'i';
- else if (PInvokeTableGenerator.IsFunctionPointer(t))
- c = 'i';
- else if (t.IsValueType)
- {
- var fields = t.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
- if (fields.Length == 1)
- {
- Type fieldType = fields[0].FieldType;
- return TypeToChar(fieldType, log, out isByRefStruct, out structSize, out structAlignment, ++depth);
- }
- else
- {
- string fullName = t.FullName ?? t.Name;
- if (s_knownStructLayouts.TryGetValue(fullName, out (int Size, int Alignment) layout))
- {
- structSize = layout.Size;
- structAlignment = layout.Alignment;
- }
- else
- {
- log.Error("WASM0067",
- $"SignatureMapper: unknown multi-field struct '{fullName}' (fields: {fields.Length}) — add its layout to s_knownStructLayouts in SignatureMapper.cs");
- return null;
- }
-
- c = 'S';
- }
-
- isByRefStruct = true;
- }
- else
- log.Warning("WASM0065", $"Unsupported parameter type '{t.Name}'");
-
- return c;
- }
-
- internal static char? TypeToChar(Type t, LogAdapter log, out bool isByRefStruct, out int structSize, int depth = 0)
- => TypeToChar(t, log, out isByRefStruct, out structSize, out _, depth);
-
- internal static char? TypeToChar(Type t, LogAdapter log, out bool isByRefStruct, int depth = 0)
- => TypeToChar(t, log, out isByRefStruct, out _, depth);
-
- ///
- /// Builds the multi-char token for a type in the signature string.
- /// For most types this is a single character; for multi-field structs it is a struct token.
- ///
- private static string? TypeToSignatureToken(Type t, LogAdapter log, out bool isByRefStruct, bool isReturn = false)
- {
- // Types the wasm ABI splits across several by-value slots are rejected in interop rather
- // than encoded. Exposing them here means teaching the thunk generator that one signature
- // token can map to several native parameters, which is a larger design question; today no
- // InternalCall or PInvoke signature uses one.
- if (IsMultiSlotType(t))
- {
- log.Error("WASM0068",
- $"SignatureMapper: '{t.FullName ?? t.Name}' is passed across multiple wasm slots, which interop signatures do not support");
- isByRefStruct = false;
- return null;
- }
-
- char? c = TypeToChar(t, log, out isByRefStruct, out int structSize, out int structAlignment);
- if (c is null)
- return null;
-
- if (c == 'S' && structSize > 0)
- return $"{(!isReturn && structAlignment > 8 ? 'A' : 'S')}{structSize}";
-
- return c.Value.ToString();
- }
-
- ///
- /// True for a type the wasm ABI passes across several by-value slots: Int128/UInt128 and
- /// Decimal128 as i64 slots, a 256- or 512-bit vector as v128 slots. A single-field wrapper is
- /// passed as the type it wraps, so unwrap first, exactly as crossgen2 and the runtime do.
- ///
- private static bool IsMultiSlotType(Type t)
- {
- for (int depth = 0; depth <= 5; depth++)
- {
- switch (t.Namespace, t.Name)
- {
- case ("System", "Int128"):
- case ("System", "UInt128"):
- case ("System.Numerics", "Decimal128"):
- return true;
- case ("System.Runtime.Intrinsics", "Vector256`1") when HasNumericElementType(t):
- case ("System.Runtime.Intrinsics", "Vector512`1") when HasNumericElementType(t):
- return true;
- }
-
- if (!t.IsValueType || t.IsPrimitive || t.IsEnum)
- {
- return false;
- }
-
- // Only unwrap a wrapper its field fills exactly. Sizes cannot be measured here: these
- // types come from a MetadataLoadContext, where Marshal.SizeOf always throws. A single
- // field fills its struct unless the struct sets a size of its own or places the field
- // at an offset, so treat either as padded and keep the struct ABI.
- FieldInfo[] fields = t.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
- if (fields.Length != 1 ||
- t.StructLayoutAttribute is not { Size: 0, Value: not LayoutKind.Explicit })
- {
- return false;
- }
-
- t = fields[0].FieldType;
- }
-
- return false;
- }
-
- ///
- /// The other two encoders only treat a vector as a sequence of wasm values when its element type
- /// is a primitive numeric, so Vector256<bool> and Vector256<char> use the struct ABI.
- ///
- private static bool HasNumericElementType(Type t)
- {
- Type[] arguments = t.GetGenericArguments();
- if (arguments.Length != 1)
- {
- return false;
- }
-
- // An enum reports its underlying numeric type code, but crossgen2 and the runtime both
- // reject enums as a vector element type.
- if (arguments[0].IsEnum)
- {
- return false;
- }
-
- return Type.GetTypeCode(arguments[0]) is >= TypeCode.SByte and <= TypeCode.Double
- || arguments[0] == typeof(IntPtr) || arguments[0] == typeof(UIntPtr);
- }
-
- public static string? MethodToSignature(MethodInfo method, LogAdapter log, bool includeThis = false)
- {
- string? returnToken = TypeToSignatureToken(method.ReturnType, log, out bool resultIsByRef, isReturn: true);
- if (returnToken is null)
- return null;
-
- var sb = new StringBuilder();
-
- if (resultIsByRef)
- {
- // Struct return — encode as S (the return type token already has the size)
- sb.Append(returnToken);
- }
- else
- {
- sb.Append(returnToken);
- }
-
- if (includeThis && !method.IsStatic)
- {
- sb.Append('T');
- }
-
- foreach (var parameter in method.GetParameters())
- {
- string? paramToken = TypeToSignatureToken(parameter.ParameterType, log, out _);
- if (paramToken is null)
- return null;
-
- sb.Append(paramToken);
- }
-
- return sb.ToString();
- }
-
- ///
- /// Parses a signature string into individual tokens.
- /// Single-char types produce one-char tokens; struct encodings produce multi-char tokens like
- /// "S8" or "A32", and a multi-slot parameter produces a two-char token like "l2" or "V4".
- /// The 'a' and 'p' suffixes are included as their own tokens.
- ///
- public static List ParseSignatureTokens(string signature)
- {
- var tokens = new List();
- int i = 0;
- while (i < signature.Length)
- {
- if (signature[i] is 'S' or 'A')
- {
- int start = i;
- i++; // skip 'S'/'A'
- while (i < signature.Length && char.IsDigit(signature[i]))
- i++;
- tokens.Add(signature.Substring(start, i - start));
- }
- else if (signature[i] is 'l' or 'V' && i + 1 < signature.Length && char.IsDigit(signature[i + 1]))
- {
- tokens.Add(signature.Substring(i, 2));
- i += 2;
- }
- else
- {
- tokens.Add(signature[i].ToString());
- i++;
- }
- }
-
- return tokens;
- }
-
- ///
- /// True for a token describing a type passed by value across several wasm parameters
- /// ("l2", "V2", "V4"). Interop signatures do not use these today.
- ///
- private static bool IsMultiSlotToken(string token)
- => token.Length == 2 && token[0] is 'l' or 'V' && char.IsDigit(token[1]);
-
- private static void RejectMultiSlotToken(string token)
- {
- if (IsMultiSlotToken(token))
- throw new NotSupportedException($"Multi-slot signature token '{token}' is not supported in interop thunks");
- }
-
- public static string TokenToNativeType(string token)
- {
- RejectMultiSlotToken(token);
- return token[0] switch
- {
- 'v' => "void",
- 'i' => "int32_t",
- 'l' => "int64_t",
- 'f' => "float",
- 'd' => "double",
- 'S' or 'A' => "int32_t",
- 'T' => "int32_t",
- 'p' => "PCODE",
- _ => throw new InvalidSignatureCharException(token[0])
- };
- }
-
- public static string TokenToNameType(string token)
- {
- RejectMultiSlotToken(token);
- return token[0] switch
- {
- 'v' => "Void",
- 'i' => "I32",
- 'l' => "I64",
- 'f' => "F32",
- 'd' => "F64",
- 'S' or 'A' => token,
- 'T' => "This",
- 'p' => "PE",
- _ => throw new InvalidSignatureCharException(token[0])
- };
- }
-
- public static string TokenToArgType(string token)
- {
- RejectMultiSlotToken(token);
- return token[0] switch
- {
- 'i' => "ARG_I32",
- 'l' => "ARG_I64",
- 'f' => "ARG_F32",
- 'd' => "ARG_F64",
- 'S' or 'A' => "ARG_IND",
- 'T' => "ARG_I32",
- _ => throw new InvalidSignatureCharException(token[0])
- };
- }
-
- ///
- /// Returns the number of INTERP_STACK_SLOT_SIZE slots consumed by a token.
- /// Struct tokens consume max((size + 7) / 8, 1) slots; all others consume 1.
- ///
- public static int TokenToSlotCount(string token)
- {
- if (token[0] is not ('S' or 'A') || token.Length < 2)
- return 1;
-
- int size = GetStructSize(token);
- return Math.Max((size + 7) / 8, 1);
- }
-
- internal static int GetStructSize(string token)
- {
- return int.Parse(token.Substring(1));
- }
-
- // Legacy single-char overloads — still used by consumers that don't encounter S tokens.
- public static string CharToNativeType(char c) => TokenToNativeType(c.ToString());
- public static string CharToNameType(char c) => TokenToNameType(c.ToString());
- public static string CharToArgType(char c) => TokenToArgType(c.ToString());
-
- public static string TypeToNameType(Type t, LogAdapter log)
- {
- char? c = TypeToChar(t, log, out _);
- if (c is null)
- throw new InvalidSignatureCharException('?');
-
- return CharToNameType(c.Value);
- }
-
- public static bool IsVoidSignature(string signature) => signature[0] == 'v';
-}
-
-internal sealed class InvalidSignatureCharException : Exception
-{
- public char Char { get; private set; }
-
- public InvalidSignatureCharException(char c) : base($"Can't handle signature '{c}'") => Char = c;
-}
diff --git a/src/tasks/WasmAppBuilder/generate-coreclr-helpers.md b/src/tasks/WasmAppBuilder/generate-coreclr-helpers.md
deleted file mode 100644
index e5dcefabb04430..00000000000000
--- a/src/tasks/WasmAppBuilder/generate-coreclr-helpers.md
+++ /dev/null
@@ -1,85 +0,0 @@
-# Generating CoreCLR WebAssembly call helpers
-
-The `generate-coreclr-helpers.cmd` (Windows) and `generate-coreclr-helpers.sh` (Linux/macOS)
-scripts in this directory regenerate the checked-in CoreCLR call-helper source files used by the
-WebAssembly runtime. They run the `RunGenerator` target in
-[`WasmAppBuilder.csproj`](./WasmAppBuilder.csproj), which invokes the
-`ManagedToNativeGenerator` MSBuild task to scan the managed framework assemblies and emit the
-native P/Invoke, reverse-P/Invoke, and interpreter-to-managed call helpers.
-
-The scripts generate **both** WebAssembly variations:
-
-| Target OS | Output directory | Default scan path (testhost) |
-|-----------|---------------------------------|------------------------------|
-| `browser` | `src/coreclr/vm/wasm/browser/` | `artifacts/bin/testhost/net11.0-browser--wasm/shared/Microsoft.NETCore.App/11.0.0/` |
-| `wasi` | `src/coreclr/vm/wasm/wasi/` | `artifacts/bin/testhost/net11.0-wasi--wasm/shared/Microsoft.NETCore.App/11.0.0/` |
-
-Each run emits three files into the output directory:
-
-- `callhelpers-pinvoke.cpp`
-- `callhelpers-reverse.cpp`
-- `callhelpers-interp-to-managed.cpp`
-
-## The P/Invoke module list
-
-Only the framework native libraries listed in
-[`eng/wasm/WasmPInvokeModules.props`](../../../eng/wasm/WasmPInvokeModules.props) get an entry in
-the generated P/Invoke table. The runtime tests share that list — `CLRTest.WasmCorerun.targets`
-generates equivalent tables of its own when it links a test-specific corerun. Adding a module there
-means rerunning these scripts and committing the regenerated files in the same change.
-
-## What needs to be built first
-
-The generator scans the **managed framework assemblies** in the `testhost` folder produced by a
-`clr+libs` build. Because the scripts generate both the `browser` and `wasi` variations, you must
-build **both** WebAssembly flavors before running them. The first build of either flavor also
-downloads and provisions the Emscripten SDK (emsdk) automatically.
-
-From the repository root:
-
-**Windows:**
-```cmd
-.\build.cmd clr+libs -os browser -c Debug
-.\build.cmd clr+libs -os wasi -c Debug
-```
-
-**Linux/macOS:**
-```bash
-./build.sh clr+libs -os browser -c Debug
-./build.sh clr+libs -os wasi -c Debug
-```
-
-Notes:
-
-- Use a matching `-c ` for the configuration you intend to pass to the
- generator script (the script derives the scan path from the configuration name).
-- The `WasmAppBuilder` task itself is built on demand by the generator script (the `RunGenerator`
- target depends on `Build`), so you do not need to build it separately.
-- If a required `testhost` scan path is missing, the script stops and prints the exact
- `build` command needed to produce it.
-
-## Running the generator
-
-Once both flavors are built, run the script from anywhere (it resolves the repo root itself):
-
-**Windows:**
-```cmd
-src\tasks\WasmAppBuilder\generate-coreclr-helpers.cmd -c Debug
-```
-
-**Linux/macOS:**
-```bash
-src/tasks/WasmAppBuilder/generate-coreclr-helpers.sh -c Debug
-```
-
-### Options
-
-| Option | Description |
-|--------|-------------|
-| `-c`, `--configuration ` | Build configuration (default: `Debug`). Determines the default scan paths. |
-| `-s`, `--scan-path ` | Override the default **browser** scan path. |
-| `-w`, `--wasi-scan-path ` | Override the default **wasi** scan path. |
-| `-h`, `--help` | Show usage. |
-
-After running, review and commit any changes to the generated files under
-`src/coreclr/vm/wasm/browser/` and `src/coreclr/vm/wasm/wasi/`.
diff --git a/src/tasks/WasmAppBuilder/mono/ManagedToNativeGenerator.cs b/src/tasks/WasmAppBuilder/mono/ManagedToNativeGenerator.cs
index 6169c9dccf5e13..89d58d22ebd573 100644
--- a/src/tasks/WasmAppBuilder/mono/ManagedToNativeGenerator.cs
+++ b/src/tasks/WasmAppBuilder/mono/ManagedToNativeGenerator.cs
@@ -88,7 +88,7 @@ private void ExecuteInternal(LogAdapter log)
if (ShouldRun(managedAssemblies))
{
var pinvoke = new PInvokeTableGenerator(FixupSymbolName, log, IsLibraryMode, TargetOS);
- var icall = new IcallTableGenerator(RuntimeIcallTableFile, FixupSymbolName, log, isCoreClr: false);
+ var icall = new IcallTableGenerator(RuntimeIcallTableFile, FixupSymbolName, log);
var resolver = new PathAssemblyResolver(managedAssemblies);
using var mlc = new MetadataLoadContext(resolver, "System.Private.CoreLib");
diff --git a/src/tests/Common/CLRTest.WasmCorerun.targets b/src/tests/Common/CLRTest.WasmCorerun.targets
index 78790e4a1698bd..cb0acf44c0b5a5 100644
--- a/src/tests/Common/CLRTest.WasmCorerun.targets
+++ b/src/tests/Common/CLRTest.WasmCorerun.targets
@@ -10,8 +10,8 @@ time the way a .so/.dylib/.dll is elsewhere. It has to be linked into the host u
with a generated table mapping each [DllImport] to the address of its target. That is what this
file does, once per test that has a CMakeProjectReference:
- 1. ManagedToNativeGenerator (src/tasks/WasmAppBuilder) scans the test's managed assemblies plus
- the whole framework closure and emits callhelpers-{pinvoke,reverse,interp-to-managed}.cpp.
+ 1. crossgen2, in its portable call-helpers generation mode, scans the test's managed assemblies
+ plus the whole framework closure and emits callhelpers-{pinvoke,reverse,interp-to-managed}.cpp.
2. Those three sources are compiled with em++ using the response file exported by the CoreCLR
build (see src/coreclr/hosts/corerun/wasm/testkit.cmake).
3. They are linked, together with the test's own static libraries, against the same archives
@@ -60,7 +60,7 @@ against CORE_ROOT, and no SDK is involved. Only the generator task assembly is s
<_WasmCorerunScriptExt Condition="$([MSBuild]::IsOSPlatform('windows'))">.bat
-
@@ -189,6 +189,7 @@ against CORE_ROOT, and no SDK is involved. Only the generator task assembly is s
and corerun.wasm below belong in the output directory. -->
<_WasmCorerunDir>$([MSBuild]::NormalizeDirectory('$(IntermediateOutputPath)', 'wasm-corerun'))
<_WasmCorerunInputManifest>$(_WasmCorerunDir)inputs.txt
+ <_WasmCorerunGeneratorRsp>$(_WasmCorerunDir)callhelpers-generator.rsp
@@ -312,22 +313,44 @@ against CORE_ROOT, and no SDK is involved. Only the generator task assembly is s
-
-
-
+
+
+
+
+
+ <_WasmCorerunGeneratorExeSuffix Condition="'$(OS)' == 'Windows_NT'">.exe
+ <_WasmCorerunGeneratorPath Condition="'$(_WasmCorerunGeneratorPath)' == '' and '$(Crossgen2InBuildDir)' != ''">$([MSBuild]::NormalizePath('$(Crossgen2InBuildDir)', 'crossgen2$(_WasmCorerunGeneratorExeSuffix)'))
+
+
+
+
+
+
+
+
+ <_WasmCorerunGeneratorArg Include="--targetos:$(TargetOS)" />
+ <_WasmCorerunGeneratorArg Include="--targetarch:wasm" />
+ <_WasmCorerunGeneratorArg Include="--generate-portable-callhelpers:$(_WasmCorerunDir)" />
+ <_WasmCorerunGeneratorArg Include="@(_WasmCorerunPInvokeModule->'--directpinvoke:%(Identity)')" />
+ <_WasmCorerunGeneratorArg Include="@(_WasmCorerunManagedAssembly->'%(FullPath)')" />
+
+
+
+
+
+
+
+
+
+