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)')" /> + + + + + + + + + +