diff --git a/eng/native.props b/eng/native.props deleted file mode 100644 index 6d7f605c9dbc17..00000000000000 --- a/eng/native.props +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - <_RuntimeVariant /> - <_RuntimeVariant Condition="'$(WasmEnableThreads)' == 'true'">-threads - - <_IcuDir Condition="'$(PkgMicrosoft_NETCore_Runtime_ICU_Transport)' != '' and '$(TargetsWasm)' == 'true'">$(PkgMicrosoft_NETCore_Runtime_ICU_Transport)/runtimes/$(TargetOS)-$(TargetArchitecture)$(_RuntimeVariant)/native - <_TzdDir Condition="'$(PkgSystem_Runtime_TimeZoneData)' != '' and '$(TargetsWasm)' == 'true'">$([MSBuild]::NormalizePath('$(PkgSystem_Runtime_TimeZoneData)', 'contentFiles', 'any', 'any', 'data')) - - - - - - - - - - - - - - - - diff --git a/eng/native.wasm.targets b/eng/native.wasm.targets new file mode 100644 index 00000000000000..23217c416c7fe3 --- /dev/null +++ b/eng/native.wasm.targets @@ -0,0 +1,78 @@ + + + + + + + + <_RuntimeVariant /> + <_RuntimeVariant Condition="'$(WasmEnableThreads)' == 'true'">-threads + + <_IcuDir Condition="'$(PkgMicrosoft_NETCore_Runtime_ICU_Transport)' != '' and '$(TargetsWasm)' == 'true'">$(PkgMicrosoft_NETCore_Runtime_ICU_Transport)/runtimes/$(TargetOS)-$(TargetArchitecture)$(_RuntimeVariant)/native + <_TzdDir Condition="'$(PkgSystem_Runtime_TimeZoneData)' != '' and '$(TargetsWasm)' == 'true'">$([MSBuild]::NormalizePath('$(PkgSystem_Runtime_TimeZoneData)', 'contentFiles', 'any', 'any', 'data')) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + <_EmccExportedRuntimeMethods>@(EmccExportedRuntimeMethod -> '%(Identity)',',') + <_EmccExportedFunctions>@(EmccExportedFunction -> '%(Identity)',',') + + + + + + + + + + diff --git a/eng/testing/tests.browser.targets b/eng/testing/tests.browser.targets index 3c5a83a93d255d..3c32cbcc12c784 100644 --- a/eng/testing/tests.browser.targets +++ b/eng/testing/tests.browser.targets @@ -12,6 +12,7 @@ $([MSBuild]::NormalizeDirectory($(BrowserProjectRoot), 'emsdk')) true + false <_WasmMainJSFileName Condition="'$(WasmMainJSPath)' != ''">$([System.IO.Path]::GetFileName('$(WasmMainJSPath)')) <_WasmStrictVersionMatch Condition="'$(ContinuousIntegrationBuild)' == 'true'">true @@ -34,6 +35,9 @@ false true true + + + false diff --git a/eng/testing/workloads-browser.targets b/eng/testing/workloads-browser.targets index ad279f9f506223..6cb7dd24c520b6 100644 --- a/eng/testing/workloads-browser.targets +++ b/eng/testing/workloads-browser.targets @@ -8,7 +8,7 @@ - + - + <_RuntimePackNugetAvailable Include="$(LibrariesShippingPackagesDir)Microsoft.NETCore.App.Runtime.Mono.$(RIDForWorkload).*$(PackageVersionForWorkloadManifests).nupkg" /> <_RuntimePackNugetAvailable Include="$(LibrariesShippingPackagesDir)Microsoft.NETCore.App.Runtime.Mono.*.$(RIDForWorkload).*$(PackageVersionForWorkloadManifests).nupkg" /> <_RuntimePackNugetAvailable Remove="@(_RuntimePackNugetAvailable)" Condition="$([System.String]::new('%(_RuntimePackNugetAvailable.FileName)').EndsWith('.symbols'))" /> + + <_RuntimePackNugetAvailable Include="$(LibrariesShippingPackagesDir)Microsoft.NETCore.App.Runtime.$(RIDForWorkload).*$(PackageVersionForWorkloadManifests).nupkg" /> + <_RuntimePackNugetAvailable Remove="@(_RuntimePackNugetAvailable)" Condition="$([System.String]::new('%(_RuntimePackNugetAvailable.FileName)').EndsWith('.symbols'))" /> + @@ -87,7 +91,7 @@ { + // copy all node/shell env variables to emscripten env + if (globalThis.process && globalThis.process.env) { + for (const [key, value] of Object.entries(process.env)) { + Module.ENV[key] = value; + } + } + + Module.ENV["DOTNET_SYSTEM_GLOBALIZATION_INVARIANT"] = "true"; + }; + Module.preRun = [corePreRun]; + + const runtimeApi = { + Module, + INTERNAL: {}, + runtimeId: 0, + runtimeBuildInfo: { + productVersion: "corerun", + gitHash: "corerun", + buildConfiguration: "corerun", + wasmEnableThreads: false, + wasmEnableSIMD: true, + wasmEnableExceptionHandling: true, + }, + }; + dotnetInternals = [ + runtimeApi, + [], + ]; + + createDotnetRuntime(runtimeApi.Module); +} +selfRun(); diff --git a/src/coreclr/hosts/corerun/wasm/libCorerun.pre.js b/src/coreclr/hosts/corerun/wasm/libCorerun.pre.js deleted file mode 100644 index b38606f0b06c19..00000000000000 --- a/src/coreclr/hosts/corerun/wasm/libCorerun.pre.js +++ /dev/null @@ -1,20 +0,0 @@ -var dotnetInternals = [ - { - Module: Module, - }, - [], -]; -var basePreRun = () => { - // copy all node/shell env variables to emscripten env - if (globalThis.process && globalThis.process.env) { - for (const [key, value] of Object.entries(process.env)) { - ENV[key] = value; - } - } - - ENV["DOTNET_SYSTEM_GLOBALIZATION_INVARIANT"] = "true"; -}; - -// Append to or set the preRun array -Module.preRun = Module.preRun || []; -Module.preRun.push(basePreRun); \ No newline at end of file diff --git a/src/coreclr/runtime.proj b/src/coreclr/runtime.proj index d70f30b12ec4f2..b6320da46f5f3f 100644 --- a/src/coreclr/runtime.proj +++ b/src/coreclr/runtime.proj @@ -11,11 +11,11 @@ - + <_CMakeArgs Include="$(CMakeArgs)" /> diff --git a/src/coreclr/vm/wasm/callhelpers.cpp b/src/coreclr/vm/wasm/callhelpers.cpp index b4d7bdb752a197..7e05e5a02142a1 100644 --- a/src/coreclr/vm/wasm/callhelpers.cpp +++ b/src/coreclr/vm/wasm/callhelpers.cpp @@ -484,6 +484,12 @@ namespace (*fptr)(ARG_I32(0), ARG_I32(1), ARG_I32(2), ARG_IND(3), ARG_IND(4), ARG_I32(5)); } + void CallFunc_I32_I32_IND_RetVoid (PCODE pcode, int8_t *pArgs, int8_t *pRet) + { + void (*fptr)(int32_t, int32_t, int32_t) = (void (*)(int32_t, int32_t, int32_t))pcode; + (*fptr)(ARG_I32(0), ARG_I32(1), ARG_IND(2)); + } + void CallFunc_I32_I32_IND_IND_I32_RetVoid (PCODE pcode, int8_t *pArgs, int8_t *pRet) { void (*fptr)(int32_t, int32_t, int32_t, int32_t, int32_t) = (void (*)(int32_t, int32_t, int32_t, int32_t, int32_t))pcode; @@ -709,6 +715,7 @@ const StringToWasmSigThunk g_wasmThunks[] = { { "viiinn", (void*)&CallFunc_I32_I32_I32_IND_IND_RetVoid }, { "viiinni", (void*)&CallFunc_I32_I32_I32_IND_IND_I32_RetVoid }, { "viinni", (void*)&CallFunc_I32_I32_IND_IND_I32_RetVoid }, + { "viin", (void*)&CallFunc_I32_I32_IND_RetVoid }, { "viinnii", (void*)&CallFunc_I32_I32_IND_IND_I32_I32_RetVoid }, { "vil", (void*)&CallFunc_I32_I64_RetVoid }, { "vili", (void*)&CallFunc_I32_I64_I32_RetVoid }, @@ -752,7 +759,7 @@ static void Call_System_Private_CoreLib_System_Threading_ThreadPool_BackgroundJo // Lazy lookup of MethodDesc for the function export scenario. if (!MD_System_Private_CoreLib_System_Threading_ThreadPool_BackgroundJobHandler_Void_RetVoid) { - LookupMethodByName("System.Threading.ThreadPool, System.Private.CoreLib, Version=10.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e, System.Private.CoreLib", "BackgroundJobHandler", &MD_System_Private_CoreLib_System_Threading_ThreadPool_BackgroundJobHandler_Void_RetVoid); + LookupMethodByName("System.Threading.ThreadPool, System.Private.CoreLib", "BackgroundJobHandler", &MD_System_Private_CoreLib_System_Threading_ThreadPool_BackgroundJobHandler_Void_RetVoid); } ExecuteInterpretedMethodFromUnmanaged(MD_System_Private_CoreLib_System_Threading_ThreadPool_BackgroundJobHandler_Void_RetVoid, nullptr, 0, nullptr); } @@ -768,7 +775,7 @@ static void Call_System_Private_CoreLib_System_Threading_TimerQueue_TimerHandler // Lazy lookup of MethodDesc for the function export scenario. if (!MD_System_Private_CoreLib_System_Threading_TimerQueue_TimerHandler_Void_RetVoid) { - LookupMethodByName("System.Threading.TimerQueue, System.Private.CoreLib, Version=10.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e, System.Private.CoreLib", "TimerHandler", &MD_System_Private_CoreLib_System_Threading_TimerQueue_TimerHandler_Void_RetVoid); + LookupMethodByName("System.Threading.TimerQueue, System.Private.CoreLib", "TimerHandler", &MD_System_Private_CoreLib_System_Threading_TimerQueue_TimerHandler_Void_RetVoid); } ExecuteInterpretedMethodFromUnmanaged(MD_System_Private_CoreLib_System_Threading_TimerQueue_TimerHandler_Void_RetVoid, nullptr, 0, nullptr); } @@ -778,6 +785,27 @@ extern "C" void SystemJS_ExecuteTimerCallback() Call_System_Private_CoreLib_System_Threading_TimerQueue_TimerHandler(); } +static MethodDesc* MD_System_Runtime_InteropServices_JavaScript_System_Runtime_InteropServices_JavaScript_JavaScriptExports_GetManagedStackTrace_IntPtr_RetVoid = nullptr; +static void Call_System_Runtime_InteropServices_JavaScript_System_Runtime_InteropServices_JavaScript_JavaScriptExports_GetManagedStackTrace(void* arg0) +{ + int64_t args[1] = + { + (int64_t)arg0 + }; + + // Lazy lookup of MethodDesc for the function export scenario. + if (!MD_System_Runtime_InteropServices_JavaScript_System_Runtime_InteropServices_JavaScript_JavaScriptExports_GetManagedStackTrace_IntPtr_RetVoid) + { + LookupMethodByName("System.Runtime.InteropServices.JavaScript.JavaScriptExports", "GetManagedStackTrace", &MD_System_Runtime_InteropServices_JavaScript_System_Runtime_InteropServices_JavaScript_JavaScriptExports_GetManagedStackTrace_IntPtr_RetVoid); + } + ExecuteInterpretedMethodFromUnmanaged(MD_System_Runtime_InteropServices_JavaScript_System_Runtime_InteropServices_JavaScript_JavaScriptExports_GetManagedStackTrace_IntPtr_RetVoid, (int8_t*)args, sizeof(args), nullptr); +} + +extern "C" void SystemInteropJS_GetManagedStackTrace(void* arg0) +{ + Call_System_Runtime_InteropServices_JavaScript_System_Runtime_InteropServices_JavaScript_JavaScriptExports_GetManagedStackTrace(arg0); +} + extern const ReverseThunkMapEntry g_ReverseThunks[] = { { 100678287, { &MD_System_Private_CoreLib_System_Threading_ThreadPool_BackgroundJobHandler_Void_RetVoid, (void*)&Call_System_Private_CoreLib_System_Threading_ThreadPool_BackgroundJobHandler } }, diff --git a/src/coreclr/vm/wasm/helpers.cpp b/src/coreclr/vm/wasm/helpers.cpp index 742e3a947da208..efe518f3063ae9 100644 --- a/src/coreclr/vm/wasm/helpers.cpp +++ b/src/coreclr/vm/wasm/helpers.cpp @@ -618,6 +618,10 @@ namespace if (!GetSignatureKey(sig, keyBuffer, keyBufferLen)) return NULL; + void* thunk = LookupThunk(keyBuffer); + if (thunk == NULL) + printf("WASM Calli missing for Key: %s\n", keyBuffer); + return LookupThunk(keyBuffer); } diff --git a/src/libraries/Common/src/Interop/Browser/Interop.Runtime.CoreCLR.cs b/src/libraries/Common/src/Interop/Browser/Interop.Runtime.CoreCLR.cs new file mode 100644 index 00000000000000..d8afeca4a383d4 --- /dev/null +++ b/src/libraries/Common/src/Interop/Browser/Interop.Runtime.CoreCLR.cs @@ -0,0 +1,45 @@ +// 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.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +internal static partial class Interop +{ + internal static unsafe partial class Runtime + { + [LibraryImport(Libraries.JavaScriptNative, EntryPoint = "SystemInteropJS_BindJSImportST")] + public static unsafe partial nint BindJSImportST(void* signature); + + [LibraryImport(Libraries.JavaScriptNative, EntryPoint = "SystemInteropJS_InvokeJSImportST")] + public static partial void InvokeJSImportST(int importHandle, nint args); + + [LibraryImport(Libraries.JavaScriptNative, EntryPoint = "SystemInteropJS_ReleaseCSOwnedObject")] + internal static partial void ReleaseCSOwnedObject(nint jsHandle); + + [LibraryImport(Libraries.JavaScriptNative, EntryPoint = "SystemInteropJS_InvokeJSFunction")] + public static partial void InvokeJSFunction(nint functionHandle, nint data); + + [LibraryImport(Libraries.JavaScriptNative, EntryPoint = "SystemInteropJS_ResolveOrRejectPromise")] + public static partial void ResolveOrRejectPromise(nint data); + + [LibraryImport(Libraries.JavaScriptNative, EntryPoint = "SystemInteropJS_RegisterGCRoot")] + public static partial nint RegisterGCRoot(void* start, int bytesSize, IntPtr name); + [LibraryImport(Libraries.JavaScriptNative, EntryPoint = "SystemInteropJS_DeregisterGCRoot")] + public static partial void DeregisterGCRoot(nint handle); + + [LibraryImport(Libraries.JavaScriptNative, EntryPoint = "SystemInteropJS_CancelPromise")] + public static partial void CancelPromise(nint gcHandle); + + [LibraryImport(Libraries.JavaScriptNative, EntryPoint = "SystemInteropJS_BindAssemblyExports")] + public static partial void BindAssemblyExports(IntPtr assemblyNamePtr); + [LibraryImport(Libraries.JavaScriptNative, EntryPoint = "SystemInteropJS_GetAssemblyExport")] + public static partial void GetAssemblyExport(IntPtr assemblyNamePtr, IntPtr namespacePtr, IntPtr classnamePtr, IntPtr methodNamePtr, int signatureHash, IntPtr* monoMethodPtrPtr); + + // TODO-WASM: delete once we switch to CoreCLR only + [LibraryImport(Libraries.JavaScriptNative, EntryPoint = "SystemInteropJS_AssemblyGetEntryPoint")] + public static partial void AssemblyGetEntryPoint(IntPtr assemblyNamePtr, int auto_insert_breakpoint, void** monoMethodPtrPtr); + } +} diff --git a/src/libraries/Common/src/Interop/Browser/Interop.Runtime.cs b/src/libraries/Common/src/Interop/Browser/Interop.Runtime.Mono.cs similarity index 100% rename from src/libraries/Common/src/Interop/Browser/Interop.Runtime.cs rename to src/libraries/Common/src/Interop/Browser/Interop.Runtime.Mono.cs diff --git a/src/libraries/Common/src/Interop/Unix/Interop.Libraries.cs b/src/libraries/Common/src/Interop/Unix/Interop.Libraries.cs index 7ec51da64f2541..f7c988025cbc92 100644 --- a/src/libraries/Common/src/Interop/Unix/Interop.Libraries.cs +++ b/src/libraries/Common/src/Interop/Unix/Interop.Libraries.cs @@ -13,6 +13,7 @@ internal static partial class Libraries internal const string CryptoNative = "libSystem.Security.Cryptography.Native.OpenSsl"; internal const string CompressionNative = "libSystem.IO.Compression.Native"; internal const string GlobalizationNative = "libSystem.Globalization.Native"; + internal const string JavaScriptNative = "libSystem.Runtime.InteropServices.JavaScript.Native"; internal const string IOPortsNative = "libSystem.IO.Ports.Native"; internal const string HostPolicy = "libhostpolicy"; } diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System.Runtime.InteropServices.JavaScript.csproj b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System.Runtime.InteropServices.JavaScript.csproj index 9643ac9aed0f7f..b31c43f683b309 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System.Runtime.InteropServices.JavaScript.csproj +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System.Runtime.InteropServices.JavaScript.csproj @@ -12,6 +12,7 @@ SR.SystemRuntimeInteropServicesJavaScript_PlatformNotSupported true true + true false $(DefineConstants);FEATURE_WASM_MANAGED_THREADS $(DefineConstants);ENABLE_JS_INTEROP_BY_VALUE @@ -23,7 +24,9 @@ - + + + diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Interop/JavaScriptExports.cs b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Interop/JavaScriptExports.cs index 559a7921352a3b..07f5af47b8305c 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Interop/JavaScriptExports.cs +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Interop/JavaScriptExports.cs @@ -229,6 +229,7 @@ public static void CompleteTask(JSMarshalerArgument* arguments_buffer) } } + [UnmanagedCallersOnly(EntryPoint = "SystemInteropJS_GetManagedStackTrace")] // the marshaled signature is: string GetManagedStackTrace(GCHandle exception) public static void GetManagedStackTrace(JSMarshalerArgument* arguments_buffer) { diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/System.Text.Json.SourceGeneration.Roslyn3.11.Tests.csproj b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/System.Text.Json.SourceGeneration.Roslyn3.11.Tests.csproj deleted file mode 100644 index bde7513156c634..00000000000000 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/System.Text.Json.SourceGeneration.Roslyn3.11.Tests.csproj +++ /dev/null @@ -1,21 +0,0 @@ - - - - 3.11 - - - - - - - - - - - - - - - - - diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/System.Text.Json.SourceGeneration.Roslyn4.4.Tests.csproj b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/System.Text.Json.SourceGeneration.Roslyn4.4.Tests.csproj deleted file mode 100644 index 0fa1bccdb970ba..00000000000000 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/System.Text.Json.SourceGeneration.Roslyn4.4.Tests.csproj +++ /dev/null @@ -1,24 +0,0 @@ - - - - 4.4 - - - - - - - - - - - - - - - - - - - diff --git a/src/libraries/pretest.proj b/src/libraries/pretest.proj index fa615121c17659..96ed94ed4873a0 100644 --- a/src/libraries/pretest.proj +++ b/src/libraries/pretest.proj @@ -19,13 +19,16 @@ - - + + + diff --git a/src/mono/browser/test-main.js b/src/mono/browser/test-main.js index 872e14a2ce4cd9..35a1924c804ccb 100644 --- a/src/mono/browser/test-main.js +++ b/src/mono/browser/test-main.js @@ -5,6 +5,7 @@ // Run runtime tests under a JS shell or a browser // import { dotnet, exit } from './_framework/dotnet.js'; +import { config } from './_framework/dotnet.boot.js'; /***************************************************************************** @@ -253,6 +254,7 @@ globalThis.App = App; // Necessary as tests use it function configureRuntime(dotnet, runArgs) { dotnet + .withConfig(config) .withVirtualWorkingDirectory(runArgs.workingDirectory) .withEnvironmentVariables(runArgs.environmentVariables) .withDiagnosticTracing(runArgs.diagnosticTracing) diff --git a/src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/build/Microsoft.NET.Sdk.WebAssembly.Browser.targets b/src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/build/Microsoft.NET.Sdk.WebAssembly.Browser.targets index 9ecdb110c4abd1..dd7b9bd653ff94 100644 --- a/src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/build/Microsoft.NET.Sdk.WebAssembly.Browser.targets +++ b/src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/build/Microsoft.NET.Sdk.WebAssembly.Browser.targets @@ -195,6 +195,9 @@ Copyright (c) .NET Foundation. All rights reserved. <_WasmEnableWebcil>$(WasmEnableWebcil) <_WasmEnableWebcil Condition="'$(_TargetingNET80OrLater)' != 'true'">false <_WasmEnableWebcil Condition="'$(_WasmEnableWebcil)' == ''">true + + <_WasmEnableWebcil Condition="'$(RuntimeFlavor)' == 'CoreCLR'">false + <_BlazorWebAssemblyJiterpreter>$(BlazorWebAssemblyJiterpreter) <_BlazorWebAssemblyRuntimeOptions>$(BlazorWebAssemblyRuntimeOptions) <_WasmInlineBootConfig>$(WasmInlineBootConfig) diff --git a/src/native/corehost/browserhost/CMakeLists.txt b/src/native/corehost/browserhost/CMakeLists.txt index c53d396e275a34..5f4e23c2680474 100644 --- a/src/native/corehost/browserhost/CMakeLists.txt +++ b/src/native/corehost/browserhost/CMakeLists.txt @@ -109,14 +109,15 @@ target_link_options(browserhost PRIVATE -sMAXIMUM_MEMORY=2147483648 -sALLOW_MEMORY_GROWTH=1 -sSTACK_SIZE=5MB - -sWASM_BIGINT=1 -sMODULARIZE=1 -sEXPORT_ES6=1 -sEXIT_RUNTIME=0 - -sEXPORTED_RUNTIME_METHODS=BROWSER_HOST,cwrap,ccall,HEAP8,HEAPU8,HEAP16,HEAPU16,HEAP32,HEAPU32,HEAP64,HEAPU64,HEAPF32,HEAPF64,safeSetTimeout,maybeExit,exitJS,abort,lengthBytesUTF8,UTF8ToString,stringToUTF8Array - -sEXPORTED_FUNCTIONS=_posix_memalign,_malloc,_free,stackAlloc,stackRestore,stackSave,___cpp_exception,_GetDotNetRuntimeContractDescriptor,_BrowserHost_InitializeCoreCLR,_BrowserHost_ExecuteAssembly + -sEXPORTED_RUNTIME_METHODS=BROWSER_HOST,${CMAKE_EMCC_EXPORTED_RUNTIME_METHODS} + -sEXPORTED_FUNCTIONS=_BrowserHost_InitializeCoreCLR,_BrowserHost_ExecuteAssembly,${CMAKE_EMCC_EXPORTED_FUNCTIONS} -sEXPORT_NAME=createDotnetRuntime - -lnodefs.js) + -sENVIRONMENT=web,webview,worker,node,shell + -lnodefs.js + -Wl,-error-limit=0) target_link_libraries(browserhost PUBLIC BrowserHost-Static diff --git a/src/native/corehost/browserhost/browserhost.cpp b/src/native/corehost/browserhost/browserhost.cpp index 1d37e4c9abc564..1350d13ad59b7d 100644 --- a/src/native/corehost/browserhost/browserhost.cpp +++ b/src/native/corehost/browserhost/browserhost.cpp @@ -68,11 +68,11 @@ static const void* pinvoke_override(const char* library_name, const char* entry_ { return SystemResolveDllImport(entry_point_name); } - if (strcmp(library_name, "libSystem.JavaScript") == 0) + if (strcmp(library_name, "libSystem.JavaScript.Native") == 0) { return SystemJSResolveDllImport(entry_point_name); } - if (strcmp(library_name, "libSystem.Runtime.InteropServices.JavaScript") == 0) + if (strcmp(library_name, "libSystem.Runtime.InteropServices.JavaScript.Native") == 0) { return SystemJSInteropResolveDllImport(entry_point_name); } diff --git a/src/native/corehost/browserhost/host/host.ts b/src/native/corehost/browserhost/host/host.ts index 1646d9b1eda8e7..c7435db8352481 100644 --- a/src/native/corehost/browserhost/host/host.ts +++ b/src/native/corehost/browserhost/host/host.ts @@ -1,12 +1,12 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -import type { CharPtr, VoidPtr, VoidPtrPtr } from "./types"; +import type { CharPtr, VfsAsset, VoidPtr, VoidPtrPtr } from "./types"; import { } from "./cross-linked"; // ensure ambient symbols are declared const loadedAssemblies: Map = new Map(); -export function registerDllBytes(bytes: Uint8Array, asset: { name: string }) { +export function registerDllBytes(bytes: Uint8Array, asset: { name: string, virtualPath: string }) { const sp = Module.stackSave(); try { const sizeOfPtr = 4; @@ -16,13 +16,52 @@ export function registerDllBytes(bytes: Uint8Array, asset: { name: string }) { } const ptr = Module.HEAPU32[ptrPtr as any >>> 2]; - Module.HEAPU8.set(bytes, ptr); + Module.HEAPU8.set(bytes, ptr >>> 0); loadedAssemblies.set(asset.name, { ptr, length: bytes.length }); + loadedAssemblies.set(asset.virtualPath, { ptr, length: bytes.length }); } finally { Module.stackRestore(sp); } } +export function installVfsFile(bytes: Uint8Array, asset: VfsAsset) { + const virtualName: string = typeof (asset.virtualPath) === "string" + ? asset.virtualPath + : asset.name; + const lastSlash = virtualName.lastIndexOf("/"); + let parentDirectory = (lastSlash > 0) + ? virtualName.substring(0, lastSlash) + : null; + let fileName = (lastSlash > 0) + ? virtualName.substring(lastSlash + 1) + : virtualName; + if (fileName.startsWith("/")) + fileName = fileName.substring(1); + if (parentDirectory) { + if (!parentDirectory.startsWith("/")) + parentDirectory = "/" + parentDirectory; + + if (parentDirectory.startsWith("/managed")) { + throw new Error("Cannot create files under /managed virtual directory as it is reserved for NodeFS mounting"); + } + + dotnetLogger.debug(`Creating directory '${parentDirectory}'`); + + Module.FS_createPath( + "/", parentDirectory, true, true // fixme: should canWrite be false? + ); + } else { + parentDirectory = "/"; + } + + dotnetLogger.debug(`Creating file '${fileName}' in directory '${parentDirectory}'`); + + Module.FS_createDataFile( + parentDirectory, fileName, + bytes, true /* canRead */, true /* canWrite */, true /* canOwn */ + ); +} + // bool BrowserHost_ExternalAssemblyProbe(const char* pathPtr, /*out*/ void **outDataStartPtr, /*out*/ int64_t* outSize); export function BrowserHost_ExternalAssemblyProbe(pathPtr: CharPtr, outDataStartPtr: VoidPtrPtr, outSize: VoidPtr) { const path = Module.UTF8ToString(pathPtr); diff --git a/src/native/corehost/browserhost/host/index.ts b/src/native/corehost/browserhost/host/index.ts index 0cd4f70582e677..a01c7d4145544d 100644 --- a/src/native/corehost/browserhost/host/index.ts +++ b/src/native/corehost/browserhost/host/index.ts @@ -5,7 +5,7 @@ import type { InternalExchange, BrowserHostExports, RuntimeAPI, BrowserHostExpor import { InternalExchangeIndex } from "./types"; import { } from "./cross-linked"; // ensure ambient symbols are declared -import { runMain, runMainAndExit, registerDllBytes } from "./host"; +import { runMain, runMainAndExit, registerDllBytes, installVfsFile } from "./host"; export function dotnetInitializeModule(internals: InternalExchange): void { if (!Array.isArray(internals)) throw new Error("Expected internals to be an array"); @@ -19,12 +19,14 @@ export function dotnetInitializeModule(internals: InternalExchange): void { internals[InternalExchangeIndex.BrowserHostExportsTable] = browserHostExportsToTable({ registerDllBytes, + installVfsFile, }); dotnetUpdateInternals(internals, dotnetUpdateInternalsSubscriber); - function browserHostExportsToTable(map:BrowserHostExports):BrowserHostExportsTable { + function browserHostExportsToTable(map: BrowserHostExports): BrowserHostExportsTable { // keep in sync with browserHostExportsFromTable() return [ map.registerDllBytes, + map.installVfsFile, ]; } } diff --git a/src/native/corehost/browserhost/libBrowserHost.footer.js b/src/native/corehost/browserhost/libBrowserHost.footer.js index b7afcafefbee88..acf3f568d84d29 100644 --- a/src/native/corehost/browserhost/libBrowserHost.footer.js +++ b/src/native/corehost/browserhost/libBrowserHost.footer.js @@ -19,7 +19,7 @@ const exports = {}; libBrowserHost(exports); - let commonDeps = ["$libBrowserHostFn", "$DOTNET", "$DOTNET_INTEROP", "$ENV"]; + let commonDeps = ["$libBrowserHostFn", "$DOTNET", "$DOTNET_INTEROP", "$ENV", "$FS", "$NODEFS"]; const lib = { $BROWSER_HOST: { selfInitialize: () => { @@ -54,6 +54,14 @@ // WASM-TODO: remove once globalization is loaded via ICU ENV["DOTNET_SYSTEM_GLOBALIZATION_INVARIANT"] = "true"; + + if (ENVIRONMENT_IS_NODE) { + Module.preInit = [() => { + FS.mkdir("/managed"); + FS.mount(NODEFS, { root: "." }, "/managed"); + FS.chdir("/managed"); + }]; + } } }, }, diff --git a/src/native/corehost/browserhost/loader/bootstrap.ts b/src/native/corehost/browserhost/loader/bootstrap.ts index bbf6042498b990..8ae13b9410d333 100644 --- a/src/native/corehost/browserhost/loader/bootstrap.ts +++ b/src/native/corehost/browserhost/loader/bootstrap.ts @@ -1,13 +1,14 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -import type { LoadBootResourceCallback, JsModuleExports, JsAsset, AssemblyAsset, PdbAsset, WasmAsset, IcuAsset, EmscriptenModuleInternal } from "./types"; +import type { LoadBootResourceCallback, JsModuleExports, JsAsset, AssemblyAsset, PdbAsset, WasmAsset, IcuAsset, EmscriptenModuleInternal, LoaderConfig, DotnetHostBuilder } from "./types"; import { dotnetAssert, dotnetGetInternals, dotnetBrowserHostExports, dotnetUpdateInternals } from "./cross-module"; import { ENVIRONMENT_IS_NODE, ENVIRONMENT_IS_SHELL } from "./per-module"; import { getLoaderConfig } from "./config"; import { BrowserHost_InitializeCoreCLR } from "./run"; -import { createPromiseController } from "./promise-controller"; +import { createPromiseCompletionSource } from "./promise-completion-source"; +import { nodeFs } from "./polyfills"; const scriptUrlQuery = /*! webpackIgnore: true */import.meta.url; const queryIndex = scriptUrlQuery.indexOf("?"); @@ -15,7 +16,7 @@ const modulesUniqueQuery = queryIndex > 0 ? scriptUrlQuery.substring(queryIndex) const scriptUrl = normalizeFileUrl(scriptUrlQuery); const scriptDirectory = normalizeDirectoryUrl(scriptUrl); -const nativeModulePromiseController = createPromiseController(() => { +const nativeModulePromiseController = createPromiseCompletionSource(() => { dotnetUpdateInternals(dotnetGetInternals()); }); @@ -30,10 +31,12 @@ export async function createRuntime(downloadOnly: boolean, loadBootResource?: Lo const config = getLoaderConfig(); if (!config.resources || !config.resources.coreAssembly || !config.resources.coreAssembly.length) throw new Error("Invalid config, resources is not set"); + const nativeModulePromise = loadJSModule(config.resources.jsModuleNative[0], loadBootResource); + const runtimeModulePromise = loadJSModule(config.resources.jsModuleRuntime[0], loadBootResource); const coreAssembliesPromise = Promise.all(config.resources.coreAssembly.map(fetchDll)); + const coreVfsPromise = Promise.all((config.resources.coreVfs || []).map(fetchVfs)); const assembliesPromise = Promise.all(config.resources.assembly.map(fetchDll)); - const runtimeModulePromise = loadJSModule(config.resources.jsModuleRuntime[0], loadBootResource); - const nativeModulePromise = loadJSModule(config.resources.jsModuleNative[0], loadBootResource); + const vfsPromise = Promise.all((config.resources.vfs || []).map(fetchVfs)); // WASM-TODO fetchWasm(config.resources.wasmNative[0]);// start loading early, no await const nativeModule = await nativeModulePromise; @@ -45,6 +48,8 @@ export async function createRuntime(downloadOnly: boolean, loadBootResource?: Lo await nativeModulePromiseController.promise; await coreAssembliesPromise; + await coreVfsPromise; + await vfsPromise; if (!downloadOnly) { BrowserHost_InitializeCoreCLR(); @@ -73,7 +78,17 @@ async function fetchDll(asset: AssemblyAsset): Promise { dotnetBrowserHostExports.registerDllBytes(bytes, asset); } -async function fetchBytes(asset: WasmAsset|AssemblyAsset|PdbAsset|IcuAsset): Promise { +async function fetchVfs(asset: AssemblyAsset): Promise { + if (asset.name && !asset.resolvedUrl) { + asset.resolvedUrl = locateFile(asset.name); + } + const bytes = await fetchBytes(asset); + await nativeModulePromiseController.promise; + + dotnetBrowserHostExports.installVfsFile(bytes, asset); +} + +async function fetchBytes(asset: WasmAsset | AssemblyAsset | PdbAsset | IcuAsset): Promise { dotnetAssert.check(asset && asset.resolvedUrl, "Bad asset.resolvedUrl"); if (ENVIRONMENT_IS_NODE) { const { promises: fs } = await import("fs"); @@ -129,3 +144,57 @@ function isPathAbsolute(path: string): boolean { // windows http://C:/x.json return protocolRx.test(path); } + +export function isShellHosted(): boolean { + return ENVIRONMENT_IS_SHELL && typeof (globalThis as any).arguments !== "undefined"; +} + +export function isNodeHosted(): boolean { + if (!ENVIRONMENT_IS_NODE || globalThis.process.argv.length < 3) { + return false; + } + const argv1 = globalThis.process.argv[1].toLowerCase(); + const argScript = normalizeFileUrl("file:///" + locateFile(argv1)); + const importScript = normalizeFileUrl(locateFile(scriptUrl.toLowerCase())); + + return argScript === importScript; +} + +export async function findResources(dotnet: DotnetHostBuilder): Promise { + if (!ENVIRONMENT_IS_NODE) { + return; + } + const fs = await nodeFs(); + const mountedDir = "/managed"; + const files: string[] = await fs.promises.readdir("."); + const assemblies = files + // TODO-WASM: webCIL + .filter(file => file.endsWith(".dll")) + .map(filename => { + // filename without path + const name = filename.substring(filename.lastIndexOf("/") + 1); + return { virtualPath: mountedDir + "/" + filename, name }; + }); + const mainAssemblyName = globalThis.process.argv[2]; + const runtimeConfigName = mainAssemblyName.replace(/\.dll$/, ".runtimeconfig.json"); + let runtimeConfig = {}; + if (fs.existsSync(runtimeConfigName)) { + const json = await fs.promises.readFile(runtimeConfigName, { encoding: "utf8" }); + runtimeConfig = JSON.parse(json); + } + + const config: LoaderConfig = { + mainAssemblyName, + runtimeConfig, + virtualWorkingDirectory: mountedDir, + resources: { + jsModuleNative: [{ name: "dotnet.native.js" }], + jsModuleRuntime: [{ name: "dotnet.runtime.js" }], + wasmNative: [{ name: "dotnet.native.wasm", }], + coreAssembly: [{ virtualPath: mountedDir + "/System.Private.CoreLib.dll", name: "System.Private.CoreLib.dll" },], + assembly: assemblies, + } + }; + dotnet.withConfig(config); + dotnet.withApplicationArguments(...globalThis.process.argv.slice(3)); +} diff --git a/src/native/corehost/browserhost/loader/dotnet.d.ts b/src/native/corehost/browserhost/loader/dotnet.d.ts index c55cc22cf164e5..bb4e95306d7729 100644 --- a/src/native/corehost/browserhost/loader/dotnet.d.ts +++ b/src/native/corehost/browserhost/loader/dotnet.d.ts @@ -767,7 +767,7 @@ interface IMemoryView extends IDisposable { get length(): number; get byteLength(): number; } -declare function exit(exit_code: number, reason?: any): void; +declare function exit(exitCode: number, reason?: any): void; declare const dotnet: DotnetHostBuilder; declare global { function getDotnetRuntime(runtimeId: number): RuntimeAPI | undefined; diff --git a/src/native/corehost/browserhost/loader/dotnet.ts b/src/native/corehost/browserhost/loader/dotnet.ts index 8f764e324da7e8..f9fc7fe6d1138c 100644 --- a/src/native/corehost/browserhost/loader/dotnet.ts +++ b/src/native/corehost/browserhost/loader/dotnet.ts @@ -12,13 +12,16 @@ import type { DotnetHostBuilder } from "./types"; import { HostBuilder } from "./host-builder"; import { initPolyfills, initPolyfillsAsync } from "./polyfills"; -import { registerRuntime } from "./runtime-list"; import { exit } from "./exit"; import { dotnetInitializeModule } from "."; +import { selfHostNodeJS } from "./run"; initPolyfills(); -registerRuntime(dotnetInitializeModule()); +dotnetInitializeModule(); await initPolyfillsAsync(); export const dotnet: DotnetHostBuilder | undefined = new HostBuilder() as DotnetHostBuilder; export { exit }; + +// Auto-start when in Node.js or Shell environment +selfHostNodeJS(dotnet!).catch(); diff --git a/src/native/corehost/browserhost/loader/exit.ts b/src/native/corehost/browserhost/loader/exit.ts index 5ba4d306139b7c..9ee194abadd1be 100644 --- a/src/native/corehost/browserhost/loader/exit.ts +++ b/src/native/corehost/browserhost/loader/exit.ts @@ -5,12 +5,12 @@ import { dotnetLogger } from "./cross-module"; import { ENVIRONMENT_IS_NODE } from "./per-module"; // WASM-TODO: redirect to host.ts -export function exit(exit_code: number, reason: any): void { +export function exit(exitCode: number, reason: any): void { if (reason) { const reasonStr = (typeof reason === "object") ? `${reason.message || ""}\n${reason.stack || ""}` : reason.toString(); dotnetLogger.error(reasonStr); } if (ENVIRONMENT_IS_NODE) { - (globalThis as any).process.exit(exit_code); + (globalThis as any).process.exit(exitCode); } } diff --git a/src/native/corehost/browserhost/loader/host-builder.ts b/src/native/corehost/browserhost/loader/host-builder.ts index 4114eaa1aeec65..960f4151cd398b 100644 --- a/src/native/corehost/browserhost/loader/host-builder.ts +++ b/src/native/corehost/browserhost/loader/host-builder.ts @@ -98,6 +98,42 @@ export class HostBuilder implements DotnetHostBuilder { Object.assign(Module, moduleConfig); return this; } + withExitOnUnhandledError(): DotnetHostBuilder { + mergeLoaderConfig({ + exitOnUnhandledError: true + }); + return this; + } + withExitCodeLogging(): DotnetHostBuilder { + mergeLoaderConfig({ + logExitCode: true + }); + return this; + } + withElementOnExit(): DotnetHostBuilder { + mergeLoaderConfig({ + appendElementOnExit: true + }); + return this; + } + withInteropCleanupOnExit(): DotnetHostBuilder { + mergeLoaderConfig({ + //TODO + }); + return this; + } + withDumpThreadsOnNonZeroExit(): DotnetHostBuilder { + mergeLoaderConfig({ + //TODO + }); + return this; + } + withConsoleForwarding(): DotnetHostBuilder { + mergeLoaderConfig({ + //TODO + }); + return this; + } async download(): Promise { try { diff --git a/src/native/corehost/browserhost/loader/index.ts b/src/native/corehost/browserhost/loader/index.ts index 92002a3b56df66..63eb64cdd35c5a 100644 --- a/src/native/corehost/browserhost/loader/index.ts +++ b/src/native/corehost/browserhost/loader/index.ts @@ -14,10 +14,11 @@ import GitHash from "consts:gitHash"; import { netLoaderConfig, getLoaderConfig } from "./config"; import { exit } from "./exit"; import { invokeLibraryInitializers } from "./lib-initializers"; -import { check, error, info, warn } from "./logging"; +import { check, error, info, warn, debug } from "./logging"; import { dotnetAssert, dotnetLoaderExports, dotnetLogger, dotnetUpdateInternals, dotnetUpdateInternalsSubscriber } from "./cross-module"; import { rejectRunMainPromise, resolveRunMainPromise, getRunMainPromise } from "./run"; +import { createPromiseCompletionSource, getPromiseCompletionSource, isControllablePromise } from "./promise-completion-source"; export function dotnetInitializeModule(): RuntimeAPI { @@ -38,7 +39,7 @@ export function dotnetInitializeModule(): RuntimeAPI { invokeLibraryInitializers, }; - const internals:InternalExchange = [ + const internals: InternalExchange = [ dotnetApi as RuntimeAPI, //0 [], //1 netLoaderConfig, //2 @@ -53,9 +54,13 @@ export function dotnetInitializeModule(): RuntimeAPI { getRunMainPromise, rejectRunMainPromise, resolveRunMainPromise, + createPromiseCompletionSource, + isControllablePromise, + getPromiseCompletionSource, }; Object.assign(dotnetLoaderExports, loaderFunctions); const logger: LoggerType = { + debug, info, warn, error, @@ -70,9 +75,10 @@ export function dotnetInitializeModule(): RuntimeAPI { dotnetUpdateInternals(internals, dotnetUpdateInternalsSubscriber); return dotnetApi as RuntimeAPI; - function loaderExportsToTable(logger:LoggerType, assert:AssertType, dotnetLoaderExports:LoaderExports):LoaderExportsTable { + function loaderExportsToTable(logger: LoggerType, assert: AssertType, dotnetLoaderExports: LoaderExports): LoaderExportsTable { // keep in sync with loaderExportsFromTable() return [ + logger.debug, logger.info, logger.warn, logger.error, @@ -80,6 +86,9 @@ export function dotnetInitializeModule(): RuntimeAPI { dotnetLoaderExports.resolveRunMainPromise, dotnetLoaderExports.rejectRunMainPromise, dotnetLoaderExports.getRunMainPromise, + dotnetLoaderExports.createPromiseCompletionSource, + dotnetLoaderExports.isControllablePromise, + dotnetLoaderExports.getPromiseCompletionSource, ]; } } diff --git a/src/native/corehost/browserhost/loader/logging.ts b/src/native/corehost/browserhost/loader/logging.ts index 92398ccbaebf40..ce6ab84cf94f6e 100644 --- a/src/native/corehost/browserhost/loader/logging.ts +++ b/src/native/corehost/browserhost/loader/logging.ts @@ -14,6 +14,13 @@ export function check(condition: unknown, messageFactory: string | (() => string const prefix = "DOTNET: "; +export function debug(msg: string | (() => string), ...data: any) { + if (typeof msg === "function") { + msg = msg(); + } + console.debug(prefix + msg, ...data); +} + export function info(msg: string, ...data: any) { console.info(prefix + msg, ...data); } diff --git a/src/native/corehost/browserhost/loader/polyfills.ts b/src/native/corehost/browserhost/loader/polyfills.ts index 5a00c81950616b..917a1deedc1265 100644 --- a/src/native/corehost/browserhost/loader/polyfills.ts +++ b/src/native/corehost/browserhost/loader/polyfills.ts @@ -4,20 +4,6 @@ import { ENVIRONMENT_IS_NODE } from "./per-module"; export function initPolyfills(): void { - if (typeof globalThis.WeakRef !== "function") { - class WeakRefPolyfill { - private _value: T | undefined; - - constructor(value: T) { - this._value = value; - } - - deref(): T | undefined { - return this._value; - } - } - globalThis.WeakRef = WeakRefPolyfill as any; - } if (typeof globalThis.fetch !== "function") { globalThis.fetch = fetchLike as any; } @@ -62,10 +48,31 @@ export async function initPolyfillsAsync(): Promise { // WASM-TODO: performance polyfill for V8 } +let _nodeFs: any | undefined = undefined; +let _nodeUrl: any | undefined = undefined; + +export async function nodeFs(): Promise { + if (ENVIRONMENT_IS_NODE && !_nodeFs) { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore: + _nodeFs = await import(/*! webpackIgnore: true */"fs"); + } + return _nodeFs; +} + +export async function nodeUrl(): Promise { + if (ENVIRONMENT_IS_NODE && !_nodeUrl) { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore: + _nodeUrl = await import(/*! webpackIgnore: true */"node:url"); + } + return _nodeUrl; +} + export async function fetchLike(url: string, init?: RequestInit): Promise { - let node_fs: any | undefined = undefined; - let node_url: any | undefined = undefined; try { + await nodeFs(); + await nodeUrl(); // this need to be detected only after we import node modules in onConfigLoaded const hasFetch = typeof (globalThis.fetch) === "function"; if (ENVIRONMENT_IS_NODE) { @@ -73,19 +80,11 @@ export async function fetchLike(url: string, init?: RequestInit): Promise{ ok: true, headers: { @@ -102,8 +101,6 @@ export async function fetchLike(url: string, init?: RequestInit): Promisee.xml - // https://bugs.chromium.org/p/v8/issues/detail?id=12541 return { ok: true, url, diff --git a/src/native/corehost/browserhost/loader/promise-controller.ts b/src/native/corehost/browserhost/loader/promise-completion-source.ts similarity index 66% rename from src/native/corehost/browserhost/loader/promise-controller.ts rename to src/native/corehost/browserhost/loader/promise-completion-source.ts index cce6bd67a6dd95..efb9f673feb4dc 100644 --- a/src/native/corehost/browserhost/loader/promise-controller.ts +++ b/src/native/corehost/browserhost/loader/promise-completion-source.ts @@ -1,17 +1,15 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -import type { ControllablePromise, PromiseController } from "./types"; +import type { ControllablePromise, PromiseCompletionSource } from "./types"; /// a unique symbol used to mark a promise as controllable -export const promiseControlSymbol = Symbol.for("wasm promise control"); - -// WASM-TODO: PromiseCompletionSource +export const promiseCompletionSourceSymbol = Symbol.for("wasm promise control"); /// Creates a new promise together with a controller that can be used to resolve or reject that promise. /// Optionally takes callbacks to be called immediately after a promise is resolved or rejected. -export function createPromiseController(afterResolve?: () => void, afterReject?: () => void): PromiseController { - let promiseControl: PromiseController = null as unknown as PromiseController; +export function createPromiseCompletionSource(afterResolve?: () => void, afterReject?: () => void): PromiseCompletionSource { + let promiseControl: PromiseCompletionSource = null as unknown as PromiseCompletionSource; const promise = new Promise((resolve, reject) => { promiseControl = { isDone: false, @@ -41,16 +39,16 @@ export function createPromiseController(afterResolve?: () => void, afterRejec }); (promiseControl).promise = promise; const controllablePromise = promise as ControllablePromise; - (controllablePromise as any)[promiseControlSymbol] = promiseControl; + (controllablePromise as any)[promiseCompletionSourceSymbol] = promiseControl; return promiseControl; } -export function getPromiseController(promise: ControllablePromise): PromiseController; -export function getPromiseController(promise: Promise): PromiseController | undefined { - return (promise as any)[promiseControlSymbol]; +export function getPromiseCompletionSource(promise: ControllablePromise): PromiseCompletionSource; +export function getPromiseCompletionSource(promise: Promise): PromiseCompletionSource | undefined { + return (promise as any)[promiseCompletionSourceSymbol]; } export function isControllablePromise(promise: Promise): promise is ControllablePromise { - return (promise as any)[promiseControlSymbol] !== undefined; + return (promise as any)[promiseCompletionSourceSymbol] !== undefined; } diff --git a/src/native/corehost/browserhost/loader/run.ts b/src/native/corehost/browserhost/loader/run.ts index 3daf381b2b0bb4..06b1df3421d272 100644 --- a/src/native/corehost/browserhost/loader/run.ts +++ b/src/native/corehost/browserhost/loader/run.ts @@ -1,14 +1,16 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +import { DotnetHostBuilder } from "../types"; +import { findResources, isNodeHosted, isShellHosted } from "./bootstrap"; import { Module, dotnetAssert } from "./cross-module"; import { exit } from "./exit"; -import { createPromiseController } from "./promise-controller"; +import { createPromiseCompletionSource } from "./promise-completion-source"; let CoreCLRInitialized = false; -const runMainPromiseController = createPromiseController(); +const runMainPromiseController = createPromiseCompletionSource(); -export function BrowserHost_InitializeCoreCLR():void { +export function BrowserHost_InitializeCoreCLR(): void { dotnetAssert.check(!CoreCLRInitialized, "CoreCLR should be initialized just once"); CoreCLRInitialized = true; @@ -22,14 +24,29 @@ export function BrowserHost_InitializeCoreCLR():void { } } -export function resolveRunMainPromise(exitCode:number):void { +export function resolveRunMainPromise(exitCode: number): void { runMainPromiseController.resolve(exitCode); } -export function rejectRunMainPromise(reason:any):void { +export function rejectRunMainPromise(reason: any): void { runMainPromiseController.reject(reason); } -export function getRunMainPromise():Promise { +export function getRunMainPromise(): Promise { return runMainPromiseController.promise; } + +// Auto-start when in NodeJS environment as a entry script +export async function selfHostNodeJS(dotnet: DotnetHostBuilder): Promise { + try { + if (isNodeHosted()) { + await findResources(dotnet); + await dotnet.run(); + } else if (isShellHosted()) { + // because in V8 we can't probe directories to find assemblies + throw new Error("Shell/V8 hosting is not supported"); + } + } catch (err: any) { + exit(1, err); + } +} diff --git a/src/native/corehost/browserhost/sample/CMakeLists.txt b/src/native/corehost/browserhost/sample/CMakeLists.txt index f4fd154250aa62..35b44bc3d347fe 100644 --- a/src/native/corehost/browserhost/sample/CMakeLists.txt +++ b/src/native/corehost/browserhost/sample/CMakeLists.txt @@ -5,12 +5,14 @@ # WASM-TODO: implement proper in-tree project via MSBuild and WASM SDK set(SAMPLE_ASSETS - index.html - main.mjs - dotnet.boot.js + # index.html + # main.mjs + # dotnet.boot.js ${SHARED_LIB_DESTINATION}/dotnet.js ${SHARED_LIB_DESTINATION}/dotnet.js.map ${SHARED_LIB_DESTINATION}/dotnet.d.ts + ${SHARED_LIB_DESTINATION}/dotnet.diagnostics.js + ${SHARED_LIB_DESTINATION}/dotnet.diagnostics.js.map ${SHARED_LIB_DESTINATION}/dotnet.runtime.js ${SHARED_LIB_DESTINATION}/dotnet.runtime.js.map diff --git a/src/native/corehost/corehost.proj b/src/native/corehost/corehost.proj index 23fff553c69459..52214740f05be9 100644 --- a/src/native/corehost/corehost.proj +++ b/src/native/corehost/corehost.proj @@ -5,12 +5,13 @@ package's targets into the build. --> - + true GenerateRuntimeVersionFile $(BuildCoreHostDependsOn);InitializeSourceControlInformationFromSourceControlManager + $(BuildCoreHostDependsOn);GenerateEmccExports;ResolveRuntimeFilesFromLocalBuild $(ArtifactsObjDir)$(TargetRid).$(Configuration)\ $(ArtifactsObjDir)_version.h @@ -166,6 +167,23 @@ --> + + + + <_MicrosoftNetCoreAppRuntimePackNativeDirFiles Include="$(LibrariesSharedFrameworkDir)package.json" /> + <_MicrosoftNetCoreAppRuntimePackNativeDirFiles Include="$(LibrariesSharedFrameworkDir)dotnet.d.ts" /> + <_MicrosoftNetCoreAppRuntimePackNativeDirFiles Include="$(LibrariesSharedFrameworkDir)*.map" /> + <_MicrosoftNetCoreAppRuntimePackNativeDirFiles Include="$(LibrariesSharedFrameworkDir)*.js" /> + <_MicrosoftNetCoreAppRuntimePackNativeDirFiles Include="$(LibrariesSharedFrameworkDir)*.a" /> + <_MicrosoftNetCoreAppRuntimePackNativeDirFiles Include="$(LibrariesSharedFrameworkDir)*.dat" /> + <_MicrosoftNetCoreAppRuntimePackNativeDirFiles Include="$(HostSharedFrameworkDir)libBrowserHost.a" /> + <_MicrosoftNetCoreAppRuntimePackNativeDirFiles Include="$(HostSharedFrameworkDir)dotnet.native.js" /> + <_MicrosoftNetCoreAppRuntimePackNativeDirFiles Include="$(HostSharedFrameworkDir)dotnet.native.wasm" /> + + + , subscriber?:InternalExchangeSubscriber) => void; - export const dotnetUpdateInternalsSubscriber:(internals:InternalExchange) => void; + export const dotnetAssert: AssertType; + export const dotnetLogger: LoggerType; + export const dotnetLoaderExports: LoaderExports; + export const dotnetRuntimeExports: RuntimeExports; + export const dotnetBrowserUtilsExports: BrowserUtilsExports; + export const dotnetUpdateInternals: (internals?: Partial, subscriber?: InternalExchangeSubscriber) => void; + export const dotnetUpdateInternalsSubscriber: (internals: InternalExchange) => void; // ambient in the emscripten closure - export const Module:EmscriptenModuleInternal; + export const Module: EmscriptenModuleInternal; export const ENVIRONMENT_IS_NODE: boolean; - export const ENVIRONMENT_IS_SHELL:boolean; + export const ENVIRONMENT_IS_SHELL: boolean; export const ENVIRONMENT_IS_WEB: boolean; - export const ENVIRONMENT_IS_WORKER:boolean; + export const ENVIRONMENT_IS_WORKER: boolean; export const ENVIRONMENT_IS_SIDECAR: boolean; export const VoidPtrNull: VoidPtr; export const CharPtrNull: CharPtr; export const NativePointerNull: NativePointer; + export function safeSetTimeout(func: Function, timeout: number): number; + export function maybeExit(): void; + export function exitJS(status: number, implicit?: boolean | number): void; + } diff --git a/src/native/libs/Common/JavaScript/cross-module/index.ts b/src/native/libs/Common/JavaScript/cross-module/index.ts index 8ff21c04f224de..620843533886a5 100644 --- a/src/native/libs/Common/JavaScript/cross-module/index.ts +++ b/src/native/libs/Common/JavaScript/cross-module/index.ts @@ -95,24 +95,35 @@ export function dotnetUpdateInternalsSubscriber() { // keep in sync with runtimeExportsToTable() function runtimeExportsFromTable(table: RuntimeExportsTable, runtime: RuntimeExports): void { - Object.assign(runtime, { - }); + const runtimerLocal: RuntimeExports = { + bindJSImportST: table[0], + invokeJSImportST: table[1], + releaseCSOwnedObject: table[2], + resolveOrRejectPromise: table[3], + cancelPromise: table[4], + invokeJSFunction: table[5], + }; + Object.assign(runtime, runtimerLocal); } // keep in sync with loaderExportsToTable() function loaderExportsFromTable(table: LoaderExportsTable, logger: LoggerType, assert: AssertType, dotnetLoaderExports: LoaderExports): void { const loggerLocal: LoggerType = { - info: table[0], - warn: table[1], - error: table[2], + debug: table[0], + info: table[1], + warn: table[2], + error: table[3], }; const assertLocal: AssertType = { - check: table[3], + check: table[4], }; const loaderExportsLocal: LoaderExports = { - resolveRunMainPromise: table[4], - rejectRunMainPromise: table[5], - getRunMainPromise: table[6], + resolveRunMainPromise: table[5], + rejectRunMainPromise: table[6], + getRunMainPromise: table[7], + createPromiseCompletionSource: table[8], + isControllablePromise: table[9], + getPromiseCompletionSource: table[10], }; Object.assign(dotnetLoaderExports, loaderExportsLocal); Object.assign(logger, loggerLocal); @@ -123,6 +134,7 @@ export function dotnetUpdateInternalsSubscriber() { function browserHostExportsFromTable(table: BrowserHostExportsTable, native: BrowserHostExports): void { const nativeLocal: BrowserHostExports = { registerDllBytes: table[0], + installVfsFile: table[1], }; Object.assign(native, nativeLocal); } @@ -130,6 +142,7 @@ export function dotnetUpdateInternalsSubscriber() { // keep in sync with interopJavaScriptExportsToTable() function interopJavaScriptExportsFromTable(table: InteropJavaScriptExportsTable, interop: InteropJavaScriptExports): void { const interopLocal: InteropJavaScriptExports = { + SystemInteropJS_GetManagedStackTrace: table[0], }; Object.assign(interop, interopLocal); } @@ -148,6 +161,7 @@ export function dotnetUpdateInternalsSubscriber() { stringToUTF16: table[1], stringToUTF16Ptr: table[2], stringToUTF8Ptr: table[3], + zeroRegion: table[4], }; Object.assign(interop, interopLocal); } diff --git a/src/native/libs/Common/JavaScript/types/exchange.ts b/src/native/libs/Common/JavaScript/types/exchange.ts index e553bbf67dcf40..9e6db95e3289f0 100644 --- a/src/native/libs/Common/JavaScript/types/exchange.ts +++ b/src/native/libs/Common/JavaScript/types/exchange.ts @@ -1,18 +1,37 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -import type { registerDllBytes } from "../../../../corehost/browserhost/host/host"; -import type { check, error, info, warn } from "../../../../corehost/browserhost/loader/logging"; +import type { installVfsFile, registerDllBytes } from "../../../../corehost/browserhost/host/host"; +import type { check, error, info, warn, debug } from "../../../../corehost/browserhost/loader/logging"; +import type { createPromiseCompletionSource, getPromiseCompletionSource, isControllablePromise } from "../../../../corehost/browserhost/loader/promise-completion-source"; import type { resolveRunMainPromise, rejectRunMainPromise, getRunMainPromise } from "../../../../corehost/browserhost/loader/run"; +import type { zeroRegion } from "../../../System.Native.Browser/utils/memory"; import type { stringToUTF16, stringToUTF16Ptr, stringToUTF8Ptr, utf16ToString } from "../../../System.Native.Browser/utils/strings"; +import type { bindJSImportST, invokeJSFunction, invokeJSImportST } from "../../../System.Runtime.InteropServices.JavaScript.Native/interop/invoke-js"; +import type { releaseCSOwnedObject } from "../../../System.Runtime.InteropServices.JavaScript.Native/interop/gc-handles"; +import type { resolveOrRejectPromise } from "../../../System.Runtime.InteropServices.JavaScript.Native/interop/marshal-to-js"; +import type { cancelPromise } from "../../../System.Runtime.InteropServices.JavaScript.Native/interop/cancelable-promise"; export type RuntimeExports = { + bindJSImportST: typeof bindJSImportST, + invokeJSImportST: typeof invokeJSImportST, + releaseCSOwnedObject: typeof releaseCSOwnedObject, + resolveOrRejectPromise: typeof resolveOrRejectPromise, + cancelPromise: typeof cancelPromise, + invokeJSFunction: typeof invokeJSFunction, } export type RuntimeExportsTable = [ + typeof bindJSImportST, + typeof invokeJSImportST, + typeof releaseCSOwnedObject, + typeof resolveOrRejectPromise, + typeof cancelPromise, + typeof invokeJSFunction, ] export type LoggerType = { + debug: typeof debug, info: typeof info, warn: typeof warn, error: typeof error, @@ -26,9 +45,13 @@ export type LoaderExports = { resolveRunMainPromise: typeof resolveRunMainPromise, rejectRunMainPromise: typeof rejectRunMainPromise, getRunMainPromise: typeof getRunMainPromise, + createPromiseCompletionSource: typeof createPromiseCompletionSource, + isControllablePromise: typeof isControllablePromise, + getPromiseCompletionSource: typeof getPromiseCompletionSource, } export type LoaderExportsTable = [ + typeof debug, typeof info, typeof warn, typeof error, @@ -36,20 +59,27 @@ export type LoaderExportsTable = [ typeof resolveRunMainPromise, typeof rejectRunMainPromise, typeof getRunMainPromise, + typeof createPromiseCompletionSource, + typeof isControllablePromise, + typeof getPromiseCompletionSource, ] export type BrowserHostExports = { registerDllBytes: typeof registerDllBytes + installVfsFile: typeof installVfsFile } export type BrowserHostExportsTable = [ typeof registerDllBytes, + typeof installVfsFile, ] export type InteropJavaScriptExports = { + SystemInteropJS_GetManagedStackTrace: typeof _SystemInteropJS_GetManagedStackTrace, } export type InteropJavaScriptExportsTable = [ + typeof _SystemInteropJS_GetManagedStackTrace, ] export type NativeBrowserExports = { @@ -63,6 +93,7 @@ export type BrowserUtilsExports = { stringToUTF16: typeof stringToUTF16, stringToUTF16Ptr: typeof stringToUTF16Ptr, stringToUTF8Ptr: typeof stringToUTF8Ptr, + zeroRegion: typeof zeroRegion, } export type BrowserUtilsExportsTable = [ @@ -70,4 +101,5 @@ export type BrowserUtilsExportsTable = [ typeof stringToUTF16, typeof stringToUTF16Ptr, typeof stringToUTF8Ptr, + typeof zeroRegion, ] diff --git a/src/native/libs/Common/JavaScript/types/internal.ts b/src/native/libs/Common/JavaScript/types/internal.ts index 1c8dfc922ef92e..f71ff8c3ba83e8 100644 --- a/src/native/libs/Common/JavaScript/types/internal.ts +++ b/src/native/libs/Common/JavaScript/types/internal.ts @@ -94,7 +94,7 @@ export interface ControllablePromise extends Promise { } /// Just a pair of a promise and its controller -export interface PromiseController { +export interface PromiseCompletionSource { readonly promise: ControllablePromise; isDone: boolean; resolve: (value: T | PromiseLike) => void; diff --git a/src/native/libs/Common/JavaScript/types/public-api.ts b/src/native/libs/Common/JavaScript/types/public-api.ts index 6b8c76d373dcef..b7b3bc35358b6d 100644 --- a/src/native/libs/Common/JavaScript/types/public-api.ts +++ b/src/native/libs/Common/JavaScript/types/public-api.ts @@ -724,7 +724,7 @@ export interface IMemoryView extends IDisposable { get byteLength(): number; } -export declare function exit(exit_code: number, reason?: any): void; +export declare function exit(exitCode: number, reason?: any): void; export declare const dotnet: DotnetHostBuilder; diff --git a/src/native/libs/System.Native.Browser/diagnostics/index.ts b/src/native/libs/System.Native.Browser/diagnostics/index.ts new file mode 100644 index 00000000000000..e016d244e745c6 --- /dev/null +++ b/src/native/libs/System.Native.Browser/diagnostics/index.ts @@ -0,0 +1,4 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +export const dummyDiagnosticsExport = 42; diff --git a/src/native/libs/System.Native.Browser/entrypoints.c b/src/native/libs/System.Native.Browser/entrypoints.c index a9fdc1e9ca941f..17ca2e9361e503 100644 --- a/src/native/libs/System.Native.Browser/entrypoints.c +++ b/src/native/libs/System.Native.Browser/entrypoints.c @@ -13,6 +13,7 @@ EXTERN_C int32_t SystemJS_RandomBytes(uint8_t* buffer, int32_t bufferLength); EXTERN_C uint16_t* SystemJS_GetLocaleInfo (const uint16_t* locale, int32_t localeLength, const uint16_t* culture, int32_t cultureLength, const uint16_t* result, int32_t resultMaxLength, int *resultLength); EXTERN_C void SystemJS_ScheduleTimer (int shortestDueTimeMs); EXTERN_C void SystemJS_ScheduleBackgroundJob (); +EXTERN_C void SystemJS_ConsoleClear (); static const Entry s_browserNative[] = { @@ -20,6 +21,7 @@ static const Entry s_browserNative[] = DllImportEntry(SystemJS_GetLocaleInfo) DllImportEntry(SystemJS_ScheduleTimer) DllImportEntry(SystemJS_ScheduleBackgroundJob) + DllImportEntry(SystemJS_ConsoleClear) }; EXTERN_C const void* SystemJSResolveDllImport(const char* name); diff --git a/src/native/libs/System.Native.Browser/libSystem.Native.Browser.footer.js b/src/native/libs/System.Native.Browser/libSystem.Native.Browser.footer.js index af47de94d39bdb..75d01139c89e4d 100644 --- a/src/native/libs/System.Native.Browser/libSystem.Native.Browser.footer.js +++ b/src/native/libs/System.Native.Browser/libSystem.Native.Browser.footer.js @@ -19,7 +19,7 @@ const exports = {}; libNativeBrowser(exports); - let commonDeps = ["$BROWSER_UTILS"]; + let commonDeps = ["$BROWSER_UTILS", "GetDotNetRuntimeContractDescriptor"]; const lib = { $DOTNET: { selfInitialize: () => { diff --git a/src/native/libs/System.Native.Browser/native/cross-linked.ts b/src/native/libs/System.Native.Browser/native/cross-linked.ts index 0ccc52a6322530..89ab6f2bd32d83 100644 --- a/src/native/libs/System.Native.Browser/native/cross-linked.ts +++ b/src/native/libs/System.Native.Browser/native/cross-linked.ts @@ -5,6 +5,7 @@ import { } from "../../Common/JavaScript/cross-linked"; declare global { export const DOTNET: any; + export function _GetDotNetRuntimeContractDescriptor(): void; export function _SystemJS_ExecuteTimerCallback(): void; export function _SystemJS_ExecuteBackgroundJobCallback(): void; } diff --git a/src/native/libs/System.Native.Browser/native/index.ts b/src/native/libs/System.Native.Browser/native/index.ts index b85cd23d022fc0..776f6258ff8859 100644 --- a/src/native/libs/System.Native.Browser/native/index.ts +++ b/src/native/libs/System.Native.Browser/native/index.ts @@ -6,7 +6,7 @@ import { InternalExchangeIndex } from "../types"; export { SystemJS_RandomBytes } from "./crypto"; export { SystemJS_GetLocaleInfo } from "./globalization-locale"; -export { SystemJS_RejectMainPromise, SystemJS_ResolveMainPromise } from "./main"; +export { SystemJS_RejectMainPromise, SystemJS_ResolveMainPromise, SystemJS_ConsoleClear } from "./main"; export { SystemJS_ScheduleTimer, SystemJS_ScheduleBackgroundJob } from "./timer"; export function dotnetInitializeModule(internals: InternalExchange): void { @@ -15,7 +15,7 @@ export function dotnetInitializeModule(internals: InternalExchange): void { dotnetUpdateInternals(internals, dotnetUpdateInternalsSubscriber); // eslint-disable-next-line @typescript-eslint/no-unused-vars - function nativeBrowserExportsToTable(map:NativeBrowserExports):NativeBrowserExportsTable { + function nativeBrowserExportsToTable(map: NativeBrowserExports): NativeBrowserExportsTable { // keep in sync with nativeBrowserExportsFromTable() return [ ]; diff --git a/src/native/libs/System.Native.Browser/native/main.ts b/src/native/libs/System.Native.Browser/native/main.ts index 72ec5fa9634119..cac449056a116a 100644 --- a/src/native/libs/System.Native.Browser/native/main.ts +++ b/src/native/libs/System.Native.Browser/native/main.ts @@ -3,16 +3,16 @@ import { } from "./cross-linked"; // ensure ambient symbols are declared -export function SystemJS_ResolveMainPromise(exitCode: number) { +export function SystemJS_ResolveMainPromise(exitCode: number): void { if (dotnetLoaderExports.resolveRunMainPromise) { dotnetLoaderExports.resolveRunMainPromise(exitCode); } else { // this is for corerun, which does not use the promise - Module.exitJS(exitCode, true); + exitJS(exitCode, true); } } -export function SystemJS_RejectMainPromise(messagePtr: number, messageLength: number, stackTracePtr: number, stackTraceLength: number) { +export function SystemJS_RejectMainPromise(messagePtr: number, messageLength: number, stackTracePtr: number, stackTraceLength: number): void { const message = dotnetBrowserUtilsExports.utf16ToString(messagePtr, messagePtr + messageLength * 2); const stackTrace = dotnetBrowserUtilsExports.utf16ToString(stackTracePtr, stackTracePtr + stackTraceLength * 2); const error = new Error(message); @@ -24,3 +24,8 @@ export function SystemJS_RejectMainPromise(messagePtr: number, messageLength: nu throw error; } } + +export function SystemJS_ConsoleClear(): void { + // eslint-disable-next-line no-console + console.clear(); +} diff --git a/src/native/libs/System.Native.Browser/native/timer.ts b/src/native/libs/System.Native.Browser/native/timer.ts index a6ce86de14611c..89eb6a213b78bb 100644 --- a/src/native/libs/System.Native.Browser/native/timer.ts +++ b/src/native/libs/System.Native.Browser/native/timer.ts @@ -8,10 +8,10 @@ export function SystemJS_ScheduleTimer(shortestDueTimeMs: number): void { globalThis.clearTimeout(DOTNET.lastScheduledTimerId); DOTNET.lastScheduledTimerId = undefined; } - DOTNET.lastScheduledTimerId = Module.safeSetTimeout(SystemJS_ScheduleTimerTick, shortestDueTimeMs); + DOTNET.lastScheduledTimerId = safeSetTimeout(SystemJS_ScheduleTimerTick, shortestDueTimeMs); function SystemJS_ScheduleTimerTick(): void { - Module.maybeExit(); + maybeExit(); _SystemJS_ExecuteTimerCallback(); } } @@ -22,10 +22,10 @@ export function SystemJS_ScheduleBackgroundJob(): void { globalThis.clearTimeout(DOTNET.lastScheduledThreadPoolId); DOTNET.lastScheduledThreadPoolId = undefined; } - DOTNET.lastScheduledThreadPoolId = Module.safeSetTimeout(SystemJS_ScheduleBackgroundJobTick, 0); + DOTNET.lastScheduledThreadPoolId = safeSetTimeout(SystemJS_ScheduleBackgroundJobTick, 0); function SystemJS_ScheduleBackgroundJobTick(): void { - Module.maybeExit(); + maybeExit(); _SystemJS_ExecuteBackgroundJobCallback(); } } diff --git a/src/native/libs/System.Native.Browser/utils/cdac.ts b/src/native/libs/System.Native.Browser/utils/cdac.ts new file mode 100644 index 00000000000000..6a080a8fece04c --- /dev/null +++ b/src/native/libs/System.Native.Browser/utils/cdac.ts @@ -0,0 +1,10 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +import type { RuntimeAPI } from "./types"; +import { Module } from "./cross-module"; + +export function registerCDAC(runtimeApi: RuntimeAPI): void { + runtimeApi.INTERNAL.GetDotNetRuntimeContractDescriptor = () => _GetDotNetRuntimeContractDescriptor(); + runtimeApi.INTERNAL.GetDotNetRuntimeHeap = (ptr: number, length: number) => Module.HEAPU8.subarray(ptr >>> 0, (ptr >>> 0) + length); +} diff --git a/src/native/libs/System.Native.Browser/utils/host.ts b/src/native/libs/System.Native.Browser/utils/host.ts index 0742706f90fec5..bc885d426c3a35 100644 --- a/src/native/libs/System.Native.Browser/utils/host.ts +++ b/src/native/libs/System.Native.Browser/utils/host.ts @@ -11,13 +11,13 @@ import { ENVIRONMENT_IS_NODE } from "./per-module"; // - install global handler for unhandled exceptions and promise rejections // - raise ExceptionHandling.RaiseAppDomainUnhandledExceptionEvent() // eslint-disable-next-line @typescript-eslint/no-unused-vars -export function exit(exit_code: number, reason: any): void { +export function exit(exitCode: number, reason: any): void { if (reason) { const reasonStr = (typeof reason === "object") ? `${reason.message || ""}\n${reason.stack || ""}` : reason.toString(); dotnetLogger.error(reasonStr); } if (ENVIRONMENT_IS_NODE) { - (globalThis as any).process.exit(exit_code); + (globalThis as any).process.exit(exitCode); } } diff --git a/src/native/libs/System.Native.Browser/utils/index.ts b/src/native/libs/System.Native.Browser/utils/index.ts index ca9e9517f2f934..8328cb2b486a57 100644 --- a/src/native/libs/System.Native.Browser/utils/index.ts +++ b/src/native/libs/System.Native.Browser/utils/index.ts @@ -1,7 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -import type { InternalExchange, BrowserUtilsExports, RuntimeAPI, BrowserUtilsExportsTable } from "../types"; +import type { InternalExchange, BrowserUtilsExports, RuntimeAPI, BrowserUtilsExportsTable } from "./types"; import { InternalExchangeIndex } from "../types"; import { } from "./cross-module"; // ensure ambient symbols are declared @@ -9,12 +9,22 @@ import { setHeapB32, setHeapB8, setHeapU8, setHeapU16, setHeapU32, setHeapI8, setHeapI16, setHeapI32, setHeapI52, setHeapU52, setHeapI64Big, setHeapF32, setHeapF64, getHeapB32, getHeapB8, getHeapU8, getHeapU16, getHeapU32, getHeapI8, getHeapI16, getHeapI32, getHeapI52, getHeapU52, getHeapI64Big, getHeapF32, getHeapF64, localHeapViewI8, localHeapViewI16, localHeapViewI32, localHeapViewI64Big, localHeapViewU8, localHeapViewU16, localHeapViewU32, localHeapViewF32, localHeapViewF64, + zeroRegion, } from "./memory"; import { stringToUTF16, stringToUTF16Ptr, stringToUTF8Ptr, utf16ToString } from "./strings"; import { exit, setEnvironmentVariable } from "./host"; import { dotnetUpdateInternals, dotnetUpdateInternalsSubscriber } from "../utils/cross-module"; +import { initPolyfills } from "../utils/polyfills"; +import { registerRuntime } from "./runtime-list"; +import { registerCDAC } from "./cdac"; export function dotnetInitializeModule(internals: InternalExchange): void { + initPolyfills(); + const runtimeApi = internals[InternalExchangeIndex.RuntimeAPI]; + if (typeof runtimeApi !== "object") throw new Error("Expected internals to have RuntimeAPI"); + registerRuntime(runtimeApi); + registerCDAC(runtimeApi); + if (!Array.isArray(internals)) throw new Error("Expected internals to be an array"); const runtimeApiLocal: Partial = { setEnvironmentVariable, @@ -23,8 +33,6 @@ export function dotnetInitializeModule(internals: InternalExchange): void { getHeapB32, getHeapB8, getHeapU8, getHeapU16, getHeapU32, getHeapI8, getHeapI16, getHeapI32, getHeapI52, getHeapU52, getHeapI64Big, getHeapF32, getHeapF64, localHeapViewI8, localHeapViewI16, localHeapViewI32, localHeapViewI64Big, localHeapViewU8, localHeapViewU16, localHeapViewU32, localHeapViewF32, localHeapViewF64, }; - const runtimeApi = internals[InternalExchangeIndex.RuntimeAPI]; - if (typeof runtimeApi !== "object") throw new Error("Expected internals to have RuntimeAPI"); Object.assign(runtimeApi, runtimeApiLocal); internals[InternalExchangeIndex.BrowserUtilsExportsTable] = browserUtilsExportsToTable({ @@ -32,6 +40,7 @@ export function dotnetInitializeModule(internals: InternalExchange): void { stringToUTF16, stringToUTF16Ptr, stringToUTF8Ptr, + zeroRegion, }); dotnetUpdateInternals(internals, dotnetUpdateInternalsSubscriber); function browserUtilsExportsToTable(map: BrowserUtilsExports): BrowserUtilsExportsTable { @@ -41,6 +50,7 @@ export function dotnetInitializeModule(internals: InternalExchange): void { map.stringToUTF16, map.stringToUTF16Ptr, map.stringToUTF8Ptr, + map.zeroRegion, ]; } } diff --git a/src/native/libs/System.Native.Browser/utils/memory.ts b/src/native/libs/System.Native.Browser/utils/memory.ts index 07153b163c9dd2..a29dc6a38de565 100644 --- a/src/native/libs/System.Native.Browser/utils/memory.ts +++ b/src/native/libs/System.Native.Browser/utils/memory.ts @@ -1,7 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -import type { CharPtr, MemOffset, NumberOrPointer, VoidPtr } from "../types"; +import type { CharPtr, MemOffset, NumberOrPointer, VoidPtr } from "./types"; import { Module, dotnetAssert, dotnetLogger } from "./cross-module"; const max_int64_big = BigInt("9223372036854775807"); @@ -25,12 +25,12 @@ export function setHeapB8(offset: MemOffset, value: number | boolean): void { const boolValue = !!value; if (typeof (value) === "number") assertIntInRange(value, 0, 1); - Module.HEAPU8[offset] = boolValue ? 1 : 0; + Module.HEAPU8[offset >>> 0] = boolValue ? 1 : 0; } export function setHeapU8(offset: MemOffset, value: number): void { assertIntInRange(value, 0, 0xFF); - Module.HEAPU8[offset] = value; + Module.HEAPU8[offset >>> 0] = value; } export function setHeapU16(offset: MemOffset, value: number): void { @@ -122,11 +122,11 @@ export function getHeapB32(offset: MemOffset): boolean { } export function getHeapB8(offset: MemOffset): boolean { - return !!(Module.HEAPU8[offset]); + return !!(Module.HEAPU8[offset >>> 0]); } export function getHeapU8(offset: MemOffset): number { - return Module.HEAPU8[offset]; + return Module.HEAPU8[offset >>> 0]; } export function getHeapU16(offset: MemOffset): number { @@ -244,7 +244,7 @@ export function localHeapViewF64(): Float64Array { export function copyBytes(srcPtr: VoidPtr, dstPtr: VoidPtr, bytes: number): void { const heap = localHeapViewU8(); - heap.copyWithin(dstPtr as any, srcPtr as any, srcPtr as any + bytes); + heap.copyWithin(dstPtr as any >>> 0, srcPtr as any >>> 0, (srcPtr as any >>> 0) + bytes); } export function isSharedArrayBuffer(buffer: any): buffer is SharedArrayBuffer { @@ -272,10 +272,10 @@ export function viewOrCopy(view: Uint8Array, start: CharPtr, end: CharPtr): Uint // this condition should be eliminated by rollup on non-threading builds const needsCopy = isSharedArrayBuffer(view.buffer); return needsCopy - ? view.slice(start, end) - : view.subarray(start, end); + ? view.slice(start >>> 0, end >>> 0) + : view.subarray(start >>> 0, end >>> 0); } export function zeroRegion(byteOffset: VoidPtr, sizeBytes: number): void { - localHeapViewU8().fill(0, byteOffset, byteOffset + sizeBytes); + localHeapViewU8().fill(0, byteOffset >>> 0, (byteOffset >>> 0) + sizeBytes); } diff --git a/src/native/libs/System.Native.Browser/utils/polyfills.ts b/src/native/libs/System.Native.Browser/utils/polyfills.ts new file mode 100644 index 00000000000000..fbd524d8474367 --- /dev/null +++ b/src/native/libs/System.Native.Browser/utils/polyfills.ts @@ -0,0 +1,19 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +export function initPolyfills(): void { + if (typeof globalThis.WeakRef !== "function") { + class WeakRefPolyfill { + private _value: T | undefined; + + constructor(value: T) { + this._value = value; + } + + deref(): T | undefined { + return this._value; + } + } + globalThis.WeakRef = WeakRefPolyfill as any; + } +} diff --git a/src/native/corehost/browserhost/loader/runtime-list.ts b/src/native/libs/System.Native.Browser/utils/runtime-list.ts similarity index 100% rename from src/native/corehost/browserhost/loader/runtime-list.ts rename to src/native/libs/System.Native.Browser/utils/runtime-list.ts diff --git a/src/native/libs/System.Native.Browser/utils/strings.ts b/src/native/libs/System.Native.Browser/utils/strings.ts index 85aecade9378f4..5609f121d2cbe5 100644 --- a/src/native/libs/System.Native.Browser/utils/strings.ts +++ b/src/native/libs/System.Native.Browser/utils/strings.ts @@ -20,6 +20,8 @@ export function stringsInit(): void { } export function stringToUTF16(dstPtr: number, endPtr: number, text: string) { + dstPtr = dstPtr >>> 0; + endPtr = endPtr >>> 0; const heapI16 = dotnetApi.localHeapViewU16(); const len = text.length; for (let i = 0; i < len; i++) { @@ -45,6 +47,8 @@ export function stringToUTF8Ptr(str: string): CharPtr { } export function utf16ToString(startPtr: number, endPtr: number): string { + startPtr = startPtr >>> 0; + endPtr = endPtr >>> 0; stringsInit(); if (textDecoderUtf16) { const subArray = viewOrCopy(dotnetApi.localHeapViewU8(), startPtr as any, endPtr as any); @@ -56,6 +60,8 @@ export function utf16ToString(startPtr: number, endPtr: number): string { // V8 does not provide TextDecoder export function utf16ToStringLoop(startPtr: number, endPtr: number): string { + startPtr = startPtr >>> 0; + endPtr = endPtr >>> 0; let str = ""; const heapU16 = dotnetApi.localHeapViewU16(); for (let i = startPtr; i < endPtr; i += 2) { diff --git a/src/native/libs/System.Native.Browser/utils/types.ts b/src/native/libs/System.Native.Browser/utils/types.ts new file mode 100644 index 00000000000000..2786379af7369f --- /dev/null +++ b/src/native/libs/System.Native.Browser/utils/types.ts @@ -0,0 +1,4 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +export * from "../types"; diff --git a/src/native/libs/System.Runtime.InteropServices.JavaScript.Native/entrypoints.c b/src/native/libs/System.Runtime.InteropServices.JavaScript.Native/entrypoints.c index a8016806dc103c..dc41cc1b5bada8 100644 --- a/src/native/libs/System.Runtime.InteropServices.JavaScript.Native/entrypoints.c +++ b/src/native/libs/System.Runtime.InteropServices.JavaScript.Native/entrypoints.c @@ -9,11 +9,21 @@ #endif//EXTERN_C // implemented in JavaScript -EXTERN_C int32_t SystemInteropJS_InvokeJSImportST(uint8_t* buffer, int32_t bufferLength); +EXTERN_C void* SystemInteropJS_BindJSImportST(void* signature); +EXTERN_C void SystemInteropJS_InvokeJSImportST(int32_t functionHandle, void *args); +EXTERN_C void SystemInteropJS_ReleaseCSOwnedObject (int32_t jsHandle); +EXTERN_C void SystemInteropJS_ResolveOrRejectPromise (void *args); +EXTERN_C void SystemInteropJS_CancelPromise (int32_t taskHolderGCHandle); +EXTERN_C void SystemInteropJS_InvokeJSFunction (int32_t functionJSSHandle, void *args); static const Entry s_browserNative[] = { + DllImportEntry(SystemInteropJS_BindJSImportST) DllImportEntry(SystemInteropJS_InvokeJSImportST) + DllImportEntry(SystemInteropJS_ReleaseCSOwnedObject) + DllImportEntry(SystemInteropJS_ResolveOrRejectPromise) + DllImportEntry(SystemInteropJS_CancelPromise) + DllImportEntry(SystemInteropJS_InvokeJSFunction) }; EXTERN_C const void* SystemJSInteropResolveDllImport(const char* name); diff --git a/src/native/libs/System.Runtime.InteropServices.JavaScript.Native/interop/cancelable-promise.ts b/src/native/libs/System.Runtime.InteropServices.JavaScript.Native/interop/cancelable-promise.ts new file mode 100644 index 00000000000000..16918163da632e --- /dev/null +++ b/src/native/libs/System.Runtime.InteropServices.JavaScript.Native/interop/cancelable-promise.ts @@ -0,0 +1,134 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +import { ManagedObject } from "./marshaled-types"; +import { ControllablePromise, GCHandle, MarshalerToCs } from "./types"; +import { isRuntimeRunning } from "./utils"; +import { lookupJsOwnedObject, teardownManagedProxy } from "./gc-handles"; +import { marshalCsObjectToCs } from "./marshal-to-cs"; +import { completeTask } from "./managed-exports"; + +const promiseHolderSymbol = Symbol.for("wasm promise_holder"); + +export function isThenable(js_obj: any): boolean { + // When using an external Promise library like Bluebird the Promise.resolve may not be sufficient + // to identify the object as a Promise. + return Promise.resolve(js_obj) === js_obj || + ((typeof js_obj === "object" || typeof js_obj === "function") && typeof js_obj.then === "function"); +} + +export function wrapAsCancelablePromise(fn: () => Promise): ControllablePromise { + const pcs = dotnetLoaderExports.createPromiseCompletionSource(); + const inner = fn(); + inner.then((data) => pcs.resolve(data)).catch((reason) => pcs.reject(reason)); + return pcs.promise; +} + +export function wrapAsCancelable(inner: Promise): ControllablePromise { + const pcs = dotnetLoaderExports.createPromiseCompletionSource(); + inner.then((data) => pcs.resolve(data)).catch((reason) => pcs.reject(reason)); + return pcs.promise; +} + +export function cancelPromise(task_holder_gc_handle: GCHandle): void { + // cancelation should not arrive earlier than the promise created by marshaling in SystemInteropJS_InvokeJSImportSync + Module.safeSetTimeout(() => { + if (!isRuntimeRunning()) { + dotnetLogger.debug("This promise can't be canceled, mono runtime already exited."); + return; + } + const holder = lookupJsOwnedObject(task_holder_gc_handle) as PromiseHolder; + dotnetAssert.check(!!holder, () => `Expected Promise for GCHandle ${task_holder_gc_handle}`); + holder.cancel(); + }, 0); +} + +export class PromiseHolder extends ManagedObject { + public isResolved = false; + public isPosted = false; + public isPostponed = false; + public data: any = null; + public reason: any = undefined; + public constructor(public promise: Promise, + private gc_handle: GCHandle, + private res_converter?: MarshalerToCs) { + super(); + } + + resolve(data: any) { + if (!isRuntimeRunning()) { + dotnetLogger.debug("This promise resolution can't be propagated to managed code, runtime already exited."); + return; + } + dotnetAssert.check(!this.isResolved, "resolve could be called only once"); + dotnetAssert.check(!this.isDisposed, "resolve is already disposed."); + this.isResolved = true; + this.completeTaskWrapper(data, null); + } + + reject(reason: any) { + if (!isRuntimeRunning()) { + dotnetLogger.debug("This promise rejection can't be propagated to managed code, runtime already exited."); + return; + } + if (!reason) { + reason = new Error() as any; + } + dotnetAssert.check(!this.isResolved, "reject could be called only once"); + dotnetAssert.check(!this.isDisposed, "resolve is already disposed."); + this.isResolved = true; + this.completeTaskWrapper(null, reason); + } + + cancel() { + if (!isRuntimeRunning()) { + dotnetLogger.debug("This promise cancelation can't be propagated to managed code, runtime already exited."); + return; + } + dotnetAssert.check(!this.isResolved, "cancel could be called only once"); + dotnetAssert.check(!this.isDisposed, "resolve is already disposed."); + + if (this.isPostponed) { + // there was racing resolve/reject which was postponed, to retain valid GCHandle + // in this case we just finish the original resolve/reject + // and we need to use the postponed data/reason + this.isResolved = true; + if (this.reason !== undefined) { + this.completeTaskWrapper(null, this.reason); + } else { + this.completeTaskWrapper(this.data, null); + } + } else { + // there is no racing resolve/reject, we can reject/cancel the promise + const promise = this.promise; + assertIsControllablePromise(promise); + const pcs = dotnetLoaderExports.getPromiseCompletionSource(promise); + + const reason = new Error("OperationCanceledException") as any; + reason[promiseHolderSymbol] = this; + pcs.reject(reason); + } + } + + // we can do this just once, because it will be dispose the GCHandle + completeTaskWrapper(data: any, reason: any) { + try { + dotnetAssert.check(!this.isPosted, "Promise is already posted to managed."); + this.isPosted = true; + + // we can unregister the GC handle just on JS side + teardownManagedProxy(this, this.gc_handle, /*skipManaged: */ true); + // order of operations with teardown_managed_proxy matters + // so that managed user code running in the continuation could allocate the same GCHandle number and the local registry would be already ok with that + completeTask(this.gc_handle, reason, data, this.res_converter || marshalCsObjectToCs); + } catch (ex) { + // there is no point to propagate the exception into the unhandled promise rejection + } + } +} + +export function assertIsControllablePromise(promise: Promise): asserts promise is ControllablePromise { + if (!dotnetLoaderExports.isControllablePromise(promise)) { + throw new Error("Expected a controllable promise."); + } +} diff --git a/src/native/libs/System.Runtime.InteropServices.JavaScript.Native/interop/gc-handles.ts b/src/native/libs/System.Runtime.InteropServices.JavaScript.Native/interop/gc-handles.ts new file mode 100644 index 00000000000000..7df6fa8599e72c --- /dev/null +++ b/src/native/libs/System.Runtime.InteropServices.JavaScript.Native/interop/gc-handles.ts @@ -0,0 +1,333 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +import BuildConfiguration from "consts:configuration"; + +import { dotnetAssert, dotnetLogger } from "./cross-module"; + +import type { GCHandle, JSHandle, WeakRefInternal } from "./types"; +import { GCHandleNull } from "./types"; +import { assertJsInterop, isRuntimeRunning } from "./utils"; +import { useWeakRef, createStrongRef, createWeakRef } from "./weak-ref"; +import { jsImportWrapperByFnHandle } from "./invoke-js"; +import { boundCsFunctionSymbol, importedJsFunctionSymbol, proxyDebugSymbol } from "./marshal"; +import { exportsByAssembly } from "./invoke-cs"; +import { releaseJsOwnedObjectByGcHandle } from "./managed-exports"; + +const useFinalizationRegistry = typeof globalThis.FinalizationRegistry === "function"; +let jsOwnedObjectRegistry: FinalizationRegistry; + +// this is array, not map. We maintain list of gaps in _JsHandleFreeList so that it could be as compact as possible +// 0th element is always null, because JSHandle == 0 is invalid handle. +const _CsOwnedObjectsByJsHandle: any[] = [null]; +const _CsOwnedObjectsByJsvHandle: any[] = [null]; +const _JsHandleFreeList: JSHandle[] = []; +let _nextJSHandle = 1; + +export const jsOwnedObjectTable = new Map>(); + +const _GcvHandleFreeList: GCHandle[] = []; +let nextGcvHandle = -2; + +// GCVHandle is like GCHandle, but it's not tracked and allocated by the coreCLR GC, but just by JS. +// It's used when we need to create GCHandle-like identity ahead of time, before calling coreCLR. +// they have negative values, so that they don't collide with GCHandles. +export function allocGcvHandle(): GCHandle { + const gcvHandle = _GcvHandleFreeList.length ? _GcvHandleFreeList.pop() : nextGcvHandle--; + return gcvHandle as any; +} + +export function freeGcvHandle(gcvHandle: GCHandle): void { + _GcvHandleFreeList.push(gcvHandle); +} + +export function isJsvHandle(jsHandle: JSHandle): boolean { + return (jsHandle as any) < -1; +} + +export function isJsHandle(jsHandle: JSHandle): boolean { + return (jsHandle as any) > 0; +} + +export function isGcvHandle(gcHandle: GCHandle): boolean { + return (gcHandle as any) < -1; +} + +// NOTE: FinalizationRegistry and WeakRef are missing on Safari below 14.1 +if (useFinalizationRegistry) { + jsOwnedObjectRegistry = new globalThis.FinalizationRegistry(_jsOwnedObjectFinalized); +} + +export const jsOwnedGcHandleSymbol = Symbol.for("wasm jsOwnedGcHandle"); +export const csOwnedJsHandleSymbol = Symbol.for("wasm cs_owned_jsHandle"); +export const doNotForceDispose = Symbol.for("wasm doNotForceDispose"); + + +export function getJSObjectFromJSHandle(jsHandle: JSHandle): any { + if (isJsHandle(jsHandle)) + return _CsOwnedObjectsByJsHandle[jsHandle]; + if (isJsvHandle(jsHandle)) + return _CsOwnedObjectsByJsvHandle[0 - jsHandle]; + return null; +} + +export function getJsHandleFromJSObject(jsObj: any): JSHandle { + assertJsInterop(); + if (jsObj[csOwnedJsHandleSymbol]) { + return jsObj[csOwnedJsHandleSymbol]; + } + const jsHandle = _JsHandleFreeList.length ? _JsHandleFreeList.pop() : _nextJSHandle++; + + // note _cs_owned_objects_by_jsHandle is list, not Map. That's why we maintain _jsHandle_free_list. + _CsOwnedObjectsByJsHandle[jsHandle] = jsObj; + + if (Object.isExtensible(jsObj)) { + const isPrototype = typeof jsObj === "function" && Object.prototype.hasOwnProperty.call(jsObj, "prototype"); + if (!isPrototype) { + jsObj[csOwnedJsHandleSymbol] = jsHandle; + } + } + // else + // The consequence of not adding the csOwnedJsHandleSymbol is, that we could have multiple JSHandles and multiple proxy instances. + // Throwing exception would prevent us from creating any proxy of non-extensible things. + // If we have weakmap instead, we would pay the price of the lookup for all proxies, not just non-extensible objects. + + return jsHandle as JSHandle; +} + +export function registerWithJsvHandle(jsObj: any, jsvHandle: JSHandle) { + assertJsInterop(); + // note _cs_owned_objects_by_jsHandle is list, not Map. That's why we maintain _jsHandle_free_list. + _CsOwnedObjectsByJsvHandle[0 - jsvHandle] = jsObj; + + if (Object.isExtensible(jsObj)) { + jsObj[csOwnedJsHandleSymbol] = jsvHandle; + } +} + +// note: in MT, this is called from locked JSProxyContext. Don't call anything that would need locking. +export function releaseCSOwnedObject(jsHandle: JSHandle): void { + let obj: any; + if (isJsHandle(jsHandle)) { + obj = _CsOwnedObjectsByJsHandle[jsHandle]; + _CsOwnedObjectsByJsHandle[jsHandle] = undefined; + _JsHandleFreeList.push(jsHandle); + } else if (isJsvHandle(jsHandle)) { + obj = _CsOwnedObjectsByJsvHandle[0 - jsHandle]; + _CsOwnedObjectsByJsvHandle[0 - jsHandle] = undefined; + // see free list in JSProxyContext.FreeJSVHandle + } + dotnetAssert.check(obj !== undefined && obj !== null, "ObjectDisposedException"); + if (typeof obj[csOwnedJsHandleSymbol] !== "undefined") { + obj[csOwnedJsHandleSymbol] = undefined; + } +} + +export function setupManagedProxy(owner: any, gcHandle: GCHandle): void { + assertJsInterop(); + // keep the gcHandle so that we could easily convert it back to original C# object for roundtrip + owner[jsOwnedGcHandleSymbol] = gcHandle; + + // NOTE: this would be leaking C# objects when the browser doesn't support FinalizationRegistry/WeakRef + if (useFinalizationRegistry) { + // register for GC of the C# object after the JS side is done with the object + jsOwnedObjectRegistry.register(owner, gcHandle, owner); + } + + // register for instance reuse + // NOTE: this would be leaking C# objects when the browser doesn't support FinalizationRegistry/WeakRef + const wr = createWeakRef(owner); + jsOwnedObjectTable.set(gcHandle, wr); +} + +export function upgradeManagedProxyToStrongRef(owner: any, gcHandle: GCHandle): void { + const sr = createStrongRef(owner); + if (useFinalizationRegistry) { + jsOwnedObjectRegistry.unregister(owner); + } + jsOwnedObjectTable.set(gcHandle, sr); +} + +export function teardownManagedProxy(owner: any, gcHandle: GCHandle, skipManaged?: boolean): void { + assertJsInterop(); + // The JS object associated with this gcHandle has been collected by the JS GC. + // As such, it's not possible for this gcHandle to be invoked by JS anymore, so + // we can release the tracking weakref (it's null now, by definition), + // and tell the C# side to stop holding a reference to the managed object. + // "The FinalizationRegistry callback is called potentially multiple times" + if (owner) { + gcHandle = owner[jsOwnedGcHandleSymbol]; + owner[jsOwnedGcHandleSymbol] = GCHandleNull; + if (useFinalizationRegistry) { + jsOwnedObjectRegistry.unregister(owner); + } + } + if (gcHandle !== GCHandleNull && jsOwnedObjectTable.delete(gcHandle) && !skipManaged) { + if (isRuntimeRunning() && !forceDisposeProxiesInProgress) { + releaseJsOwnedObjectByGcHandle(gcHandle); + } + } + if (isGcvHandle(gcHandle)) { + freeGcvHandle(gcHandle); + } +} + +export function assertNotDisposed(result: any): GCHandle { + const gcHandle = result[jsOwnedGcHandleSymbol]; + dotnetAssert.check(gcHandle != GCHandleNull, "ObjectDisposedException"); + return gcHandle; +} + +function _jsOwnedObjectFinalized(gcHandle: GCHandle): void { + if (!isRuntimeRunning()) { + // We're shutting down, so don't bother doing anything else. + return; + } + teardownManagedProxy(null, gcHandle); +} + +export function lookupJsOwnedObject(gcHandle: GCHandle): any { + if (!gcHandle) + return null; + const wr = jsOwnedObjectTable.get(gcHandle); + if (wr) { + // this could be null even before _jsOwnedObjectFinalized was called + // TODO: are there race condition consequences ? + return wr.deref(); + } + return null; +} + +let forceDisposeProxiesInProgress = false; + +// when we arrive here from UninstallWebWorkerInterop, the C# will unregister the handles too. +// when called from elsewhere, C# side could be unbalanced!! +export function forceDisposeProxies(disposeMethods: boolean, verbose: boolean): void { + let keepSomeCsAlive = false; + let keepSomeJsAlive = false; + forceDisposeProxiesInProgress = true; + + let doneImports = 0; + let doneExports = 0; + let doneGCHandles = 0; + let doneJSHandles = 0; + // dispose all proxies to C# objects + const gcHandles = [...jsOwnedObjectTable.keys()]; + for (const gcHandle of gcHandles) { + const wr = jsOwnedObjectTable.get(gcHandle); + const obj = wr && wr.deref(); + if (useFinalizationRegistry && obj) { + jsOwnedObjectRegistry.unregister(obj); + } + + if (obj) { + const keepAlive = typeof obj[doNotForceDispose] === "boolean" && obj[doNotForceDispose]; + if (verbose) { + const proxyDebug = BuildConfiguration === "Debug" ? obj[proxyDebugSymbol] : undefined; + if (BuildConfiguration === "Debug" && proxyDebug) { + dotnetLogger.warn(`${proxyDebug} ${typeof obj} was still alive. ${keepAlive ? "keeping" : "disposing"}.`); + } else { + dotnetLogger.warn(`Proxy of C# ${typeof obj} with GCHandle ${gcHandle} was still alive. ${keepAlive ? "keeping" : "disposing"}.`); + } + } + if (!keepAlive) { + const promiseControl = dotnetLoaderExports.createPromiseCompletionSource(obj); + if (promiseControl) { + promiseControl.reject(new Error("WebWorker which is origin of the Task is being terminated.")); + } + if (typeof obj.dispose === "function") { + obj.dispose(); + } + if (obj[jsOwnedGcHandleSymbol] === gcHandle) { + obj[jsOwnedGcHandleSymbol] = GCHandleNull; + } + if (!useWeakRef && wr) wr.dispose!(); + doneGCHandles++; + } else { + keepSomeCsAlive = true; + } + } + } + if (!keepSomeCsAlive) { + jsOwnedObjectTable.clear(); + if (useFinalizationRegistry) { + jsOwnedObjectRegistry = new globalThis.FinalizationRegistry(_jsOwnedObjectFinalized); + } + } + const freeJsHandle = (jsHandle: number, list: any[]): void => { + const obj = list[jsHandle]; + const keepAlive = obj && typeof obj[doNotForceDispose] === "boolean" && obj[doNotForceDispose]; + if (!keepAlive) { + list[jsHandle] = undefined; + } + if (obj) { + if (verbose) { + const proxyDebug = BuildConfiguration === "Debug" ? obj[proxyDebugSymbol] : undefined; + if (BuildConfiguration === "Debug" && proxyDebug) { + dotnetLogger.warn(`${proxyDebug} ${typeof obj} was still alive. ${keepAlive ? "keeping" : "disposing"}.`); + } else { + dotnetLogger.warn(`Proxy of JS ${typeof obj} with JSHandle ${jsHandle} was still alive. ${keepAlive ? "keeping" : "disposing"}.`); + } + } + if (!keepAlive) { + const promiseControl = dotnetLoaderExports.createPromiseCompletionSource(obj); + if (promiseControl) { + promiseControl.reject(new Error("WebWorker which is origin of the Task is being terminated.")); + } + if (typeof obj.dispose === "function") { + obj.dispose(); + } + if (obj[csOwnedJsHandleSymbol] === jsHandle) { + obj[csOwnedJsHandleSymbol] = undefined; + } + doneJSHandles++; + } else { + keepSomeJsAlive = true; + } + } + }; + // dispose all proxies to JS objects + for (let jsHandle = 0; jsHandle < _CsOwnedObjectsByJsHandle.length; jsHandle++) { + freeJsHandle(jsHandle, _CsOwnedObjectsByJsHandle); + } + for (let jsvHandle = 0; jsvHandle < _CsOwnedObjectsByJsvHandle.length; jsvHandle++) { + freeJsHandle(jsvHandle, _CsOwnedObjectsByJsvHandle); + } + if (!keepSomeJsAlive) { + _CsOwnedObjectsByJsHandle.length = 1; + _CsOwnedObjectsByJsvHandle.length = 1; + _nextJSHandle = 1; + _JsHandleFreeList.length = 0; + } + _GcvHandleFreeList.length = 0; + nextGcvHandle = -2; + + if (disposeMethods) { + // dispose all [JSImport] + for (const boundFn of jsImportWrapperByFnHandle) { + if (boundFn) { + const closure = (boundFn)[importedJsFunctionSymbol]; + if (closure) { + closure.disposed = true; + doneImports++; + } + } + } + jsImportWrapperByFnHandle.length = 1; + + // dispose all [JSExport] + const assemblyExports = [...exportsByAssembly.values()]; + for (const assemblyExport of assemblyExports) { + for (const exportName in assemblyExport) { + const boundFn = assemblyExport[exportName]; + const closure = boundFn[boundCsFunctionSymbol]; + if (closure) { + closure.disposed = true; + doneExports++; + } + } + } + exportsByAssembly.clear(); + } + dotnetLogger.info(`forceDisposeProxies done: ${doneImports} imports, ${doneExports} exports, ${doneGCHandles} GCHandles, ${doneJSHandles} JSHandles.`); +} diff --git a/src/native/libs/System.Runtime.InteropServices.JavaScript.Native/interop/index.ts b/src/native/libs/System.Runtime.InteropServices.JavaScript.Native/interop/index.ts index 199fdaeef77062..ddfca8e9a21043 100644 --- a/src/native/libs/System.Runtime.InteropServices.JavaScript.Native/interop/index.ts +++ b/src/native/libs/System.Runtime.InteropServices.JavaScript.Native/interop/index.ts @@ -4,7 +4,12 @@ import type { InternalExchange, RuntimeAPI, RuntimeExports, RuntimeExportsTable } from "./types"; import { InternalExchangeIndex } from "../types"; import { dotnetUpdateInternals, dotnetUpdateInternalsSubscriber } from "./cross-module"; -import { ENVIRONMENT_IS_NODE } from "./per-module"; +import { bindJSImportST, invokeJSFunction, invokeJSImportST, setModuleImports } from "./invoke-js"; +import { getAssemblyExports } from "./invoke-cs"; +import { initializeMarshalersToJs, resolveOrRejectPromise } from "./marshal-to-js"; +import { initializeMarshalersToCs } from "./marshal-to-cs"; +import { releaseCSOwnedObject } from "./gc-handles"; +import { cancelPromise } from "./cancelable-promise"; export function dotnetInitializeModule(internals: InternalExchange): void { if (!Array.isArray(internals)) throw new Error("Expected internals to be an array"); @@ -17,25 +22,28 @@ export function dotnetInitializeModule(internals: InternalExchange): void { Object.assign(runtimeApi, runtimeApiLocal); internals[InternalExchangeIndex.RuntimeExportsTable] = runtimeExportsToTable({ + bindJSImportST, + invokeJSImportST, + releaseCSOwnedObject, + resolveOrRejectPromise, + cancelPromise, + invokeJSFunction, }); dotnetUpdateInternals(internals, dotnetUpdateInternalsSubscriber); + initializeMarshalersToJs(); + initializeMarshalersToCs(); + // eslint-disable-next-line @typescript-eslint/no-unused-vars - function runtimeExportsToTable(map:RuntimeExports):RuntimeExportsTable { + function runtimeExportsToTable(map: RuntimeExports): RuntimeExportsTable { // keep in sync with runtimeExportsFromTable() return [ + bindJSImportST, + invokeJSImportST, + releaseCSOwnedObject, + resolveOrRejectPromise, + cancelPromise, + invokeJSFunction, ]; } } - -// eslint-disable-next-line @typescript-eslint/no-unused-vars -export async function getAssemblyExports(assemblyName: string): Promise { - throw new Error("Not implemented"); - return ENVIRONMENT_IS_NODE; // dummy -} - -// eslint-disable-next-line @typescript-eslint/no-unused-vars -export function setModuleImports(moduleName: string, moduleImports: any): void { - throw new Error("Not implemented"); -} - diff --git a/src/native/libs/System.Runtime.InteropServices.JavaScript.Native/interop/invoke-cs.ts b/src/native/libs/System.Runtime.InteropServices.JavaScript.Native/interop/invoke-cs.ts new file mode 100644 index 00000000000000..eaf6af56a661c7 --- /dev/null +++ b/src/native/libs/System.Runtime.InteropServices.JavaScript.Native/interop/invoke-cs.ts @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +import { bindAssemblyExports } from "./managed-exports"; +import { assertJsInterop } from "./utils"; + +export const exportsByAssembly: Map = new Map(); + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export async function getAssemblyExports(assemblyName: string): Promise { + assertJsInterop(); + const result = exportsByAssembly.get(assemblyName); + if (!result) { + await bindAssemblyExports(assemblyName); + } + + return exportsByAssembly.get(assemblyName) || {}; +} diff --git a/src/native/libs/System.Runtime.InteropServices.JavaScript.Native/interop/invoke-js.ts b/src/native/libs/System.Runtime.InteropServices.JavaScript.Native/interop/invoke-js.ts new file mode 100644 index 00000000000000..1e4144d7fbd18e --- /dev/null +++ b/src/native/libs/System.Runtime.InteropServices.JavaScript.Native/interop/invoke-js.ts @@ -0,0 +1,307 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +import BuildConfiguration from "consts:configuration"; + +import { dotnetBrowserUtilsExports, dotnetApi, dotnetAssert, dotnetLogger, VoidPtrNull, Module } from "./cross-module"; + +import type { BindingClosure, BoundMarshalerToJs, JSFnHandle, JSFunctionSignature, JSHandle, JSMarshalerArguments, VoidPtr, WrappedJSFunction } from "./types"; +import { MarshalerType, MeasuredBlock } from "./types"; +import { getSig, getSignatureArgumentCount, getSignatureFunctionName, getSignatureHandle, getSignatureModuleName, getSignatureType, getSignatureVersion, importedJsFunctionSymbol, isReceiverShouldFree, jsInteropState, boundJsFunctionSymbol } from "./marshal"; +import { assertJsInterop, assertRuntimeRunning, endMeasure, fixupPointer, normalizeException, startMeasure } from "./utils"; +import { bindArgMarshalToJs } from "./marshal-to-js"; +import { getJSObjectFromJSHandle } from "./gc-handles"; +import { bindArgMarshalToCs, marshalExceptionToCs } from "./marshal-to-cs"; + +export const jsImportWrapperByFnHandle: Function[] = [null];// 0th slot is dummy, main thread we free them on shutdown. On web worker thread we free them when worker is detached. +export const importedModulesPromises: Map> = new Map(); +export const importedModules: Map> = new Map(); + +export function setModuleImports(moduleName: string, moduleImports: any): void { + importedModules.set(moduleName, moduleImports); + dotnetLogger.debug(() => `added module imports '${moduleName}'`); +} + +export function bindJSImportST(signature: JSFunctionSignature): VoidPtr { + try { + signature = fixupPointer(signature, 0); + bindJsImport(signature); + return VoidPtrNull; + } catch (ex: any) { + return dotnetBrowserUtilsExports.stringToUTF16Ptr(normalizeException(ex)); + } +} + +export function invokeJSImportST(functionHandle: JSFnHandle, args: JSMarshalerArguments) { + assertRuntimeRunning(); + args = fixupPointer(args, 0); + const boundFn = jsImportWrapperByFnHandle[functionHandle]; + dotnetAssert.check(boundFn, () => `Imported function handle expected ${functionHandle}`); + boundFn(args); +} + +function bindJsImport(signature: JSFunctionSignature): Function { + assertJsInterop(); + const mark = startMeasure(); + + const version = getSignatureVersion(signature); + dotnetAssert.check(version === 2, () => `Signature version ${version} mismatch.`); + + const jsFunctionName = getSignatureFunctionName(signature)!; + const jsModuleName = getSignatureModuleName(signature)!; + const functionHandle = getSignatureHandle(signature); + + dotnetLogger.debug(() => `Binding [JSImport] ${jsFunctionName} from ${jsModuleName} module`); + + const fn = lookupJsImport(jsFunctionName, jsModuleName); + const argsCount = getSignatureArgumentCount(signature); + + const argMarshalers: (BoundMarshalerToJs)[] = new Array(argsCount); + const argCleanup: (Function | undefined)[] = new Array(argsCount); + let hasCleanup = false; + for (let index = 0; index < argsCount; index++) { + const sig = getSig(signature, index + 2); + const marshalerType = getSignatureType(sig); + const argMarshaler = bindArgMarshalToJs(sig, marshalerType, index + 2); + dotnetAssert.check(argMarshaler, "ERR42: argument marshaler must be resolved"); + argMarshalers[index] = argMarshaler; + if (marshalerType === MarshalerType.Span) { + argCleanup[index] = (jsArg: any) => { + if (jsArg) { + jsArg.dispose(); + } + }; + hasCleanup = true; + } + } + const resSig = getSig(signature, 1); + const resmarshalerType = getSignatureType(resSig); + const resConverter = bindArgMarshalToCs(resSig, resmarshalerType, 1); + + const isDiscardNoWait = resmarshalerType == MarshalerType.DiscardNoWait; + const isAsync = resmarshalerType == MarshalerType.Task || resmarshalerType == MarshalerType.TaskPreCreated; + + const closure: BindingClosure = { + fn, + fqn: jsModuleName + ":" + jsFunctionName, + argsCount, + argMarshalers, + resConverter, + hasCleanup, + argCleanup, + isDiscardNoWait, + isAsync, + isDisposed: false, + }; + let boundFn: WrappedJSFunction; + if (isAsync || isDiscardNoWait || hasCleanup) { + boundFn = bindFn(closure); + } else { + if (argsCount == 0 && !resConverter) { + boundFn = bind_fn_0V(closure); + } else if (argsCount == 1 && !resConverter) { + boundFn = bind_fn_1V(closure); + } else if (argsCount == 1 && resConverter) { + boundFn = bind_fn_1R(closure); + } else if (argsCount == 2 && resConverter) { + boundFn = bind_fn_2R(closure); + } else { + boundFn = bindFn(closure); + } + } + + let wrappedFn: WrappedJSFunction = boundFn; + + + // this is just to make debugging easier by naming the function in the stack trace. + // It's not CSP compliant and possibly not performant, that's why it's only enabled in debug builds + // in Release configuration, it would be a trimmed by rollup + if (BuildConfiguration === "Debug" && !jsInteropState.cspPolicy) { + try { + const fname = jsFunctionName.replaceAll(".", "_"); + const url = `//# sourceURL=https://dotnet/JSImport/${fname}`; + const body = `return (function JSImport_${fname}(){ return fn.apply(this, arguments)});`; + wrappedFn = new Function("fn", url + "\r\n" + body)(wrappedFn); + } catch (ex) { + jsInteropState.cspPolicy = true; + } + } + + (wrappedFn)[importedJsFunctionSymbol] = closure; + + jsImportWrapperByFnHandle[functionHandle] = wrappedFn; + + endMeasure(mark, MeasuredBlock.bindJsFunction, jsFunctionName); + + return wrappedFn; +} + +function bind_fn_0V(closure: BindingClosure) { + const fn = closure.fn; + const fqn = closure.fqn; + (closure) = null; + return function boundFn_0V(args: JSMarshalerArguments) { + const mark = startMeasure(); + try { + // call user function + fn(); + } catch (ex) { + marshalExceptionToCs(args, ex); + } finally { + endMeasure(mark, MeasuredBlock.callCsFunction, fqn); + } + }; +} + +function bind_fn_1V(closure: BindingClosure) { + const fn = closure.fn; + const marshaler1 = closure.argMarshalers[0]!; + const fqn = closure.fqn; + (closure) = null; + return function boundFn_1V(args: JSMarshalerArguments) { + const mark = startMeasure(); + try { + const arg1 = marshaler1(args); + // call user function + fn(arg1); + } catch (ex) { + marshalExceptionToCs(args, ex); + } finally { + endMeasure(mark, MeasuredBlock.callCsFunction, fqn); + } + }; +} + +function bind_fn_1R(closure: BindingClosure) { + const fn = closure.fn; + const marshaler1 = closure.argMarshalers[0]!; + const resConverter = closure.resConverter!; + const fqn = closure.fqn; + (closure) = null; + return function boundFn_1R(args: JSMarshalerArguments) { + const mark = startMeasure(); + try { + const arg1 = marshaler1(args); + // call user function + const jsResult = fn(arg1); + resConverter(args, jsResult); + } catch (ex) { + marshalExceptionToCs(args, ex); + } finally { + endMeasure(mark, MeasuredBlock.callCsFunction, fqn); + } + }; +} + +function bind_fn_2R(closure: BindingClosure) { + const fn = closure.fn; + const marshaler1 = closure.argMarshalers[0]!; + const marshaler2 = closure.argMarshalers[1]!; + const resConverter = closure.resConverter!; + const fqn = closure.fqn; + (closure) = null; + return function boundFn_2R(args: JSMarshalerArguments) { + const mark = startMeasure(); + try { + const arg1 = marshaler1(args); + const arg2 = marshaler2(args); + // call user function + const jsResult = fn(arg1, arg2); + resConverter(args, jsResult); + } catch (ex) { + marshalExceptionToCs(args, ex); + } finally { + endMeasure(mark, MeasuredBlock.callCsFunction, fqn); + } + }; +} + +function bindFn(closure: BindingClosure) { + const argsCount = closure.argsCount; + const argMarshalers = closure.argMarshalers; + const resConverter = closure.resConverter; + const argCleanup = closure.argCleanup; + const hasCleanup = closure.hasCleanup; + const fn = closure.fn; + const fqn = closure.fqn; + (closure) = null; + return function boundFn(args: JSMarshalerArguments) { + const receiverShouldFree = isReceiverShouldFree(args); + const mark = startMeasure(); + try { + const jsArgs = new Array(argsCount); + for (let index = 0; index < argsCount; index++) { + const marshaler = argMarshalers[index]!; + const jsArg = marshaler(args); + jsArgs[index] = jsArg; + } + + // call user function + const jsResult = fn(...jsArgs); + + if (resConverter) { + resConverter(args, jsResult); + } + + if (hasCleanup) { + for (let index = 0; index < argsCount; index++) { + const cleanup = argCleanup[index]; + if (cleanup) { + cleanup(jsArgs[index]); + } + } + } + } catch (ex) { + marshalExceptionToCs(args, ex); + } finally { + if (receiverShouldFree) { + Module._free(args as any); + } + endMeasure(mark, MeasuredBlock.callCsFunction, fqn); + } + }; +} + +function lookupJsImport(functionName: string, jsModuleName: string | null): Function { + dotnetAssert.check(functionName && typeof functionName === "string", "functionName must be string"); + + let scope: any = {}; + const parts = functionName.split("."); + if (jsModuleName) { + scope = importedModules.get(jsModuleName); + dotnetAssert.check(scope, () => `ES6 module ${jsModuleName} was not imported yet, please call JSHost.ImportAsync() first in order to invoke ${functionName}.`); + } else if (parts[0] === "INTERNAL") { + scope = dotnetApi.INTERNAL; + parts.shift(); + } else if (parts[0] === "globalThis") { + scope = globalThis; + parts.shift(); + } + + for (let i = 0; i < parts.length - 1; i++) { + const part = parts[i]; + const newscope = scope[part]; + if (!newscope) { + throw new Error(`${part} not found while looking up ${functionName}`); + } + scope = newscope; + } + + const fname = parts[parts.length - 1]; + const fn = scope[fname]; + + if (typeof (fn) !== "function") { + throw new Error(`${functionName} must be a Function but was ${typeof fn}`); + } + + // if the function was already bound to some object it would stay bound to original object. That's good. + return fn.bind(scope); +} + +export function invokeJSFunction(functionJSHandle: JSHandle, args: JSMarshalerArguments): void { + assertRuntimeRunning(); + const boundFn = getJSObjectFromJSHandle(functionJSHandle); + dotnetAssert.check(boundFn && typeof (boundFn) === "function" && boundFn[boundJsFunctionSymbol], () => `Bound function handle expected ${functionJSHandle}`); + args = fixupPointer(args, 0); + boundFn(args); +} diff --git a/src/native/libs/System.Runtime.InteropServices.JavaScript.Native/interop/managed-exports.ts b/src/native/libs/System.Runtime.InteropServices.JavaScript.Native/interop/managed-exports.ts new file mode 100644 index 00000000000000..9b7ef2ae7de91c --- /dev/null +++ b/src/native/libs/System.Runtime.InteropServices.JavaScript.Native/interop/managed-exports.ts @@ -0,0 +1,48 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +import { dotnetInteropJSExports, Module } from "./cross-module"; +import { allocStackFrame, getArg, setArgType, setGcHandle as setGcHandle } from "./marshal"; +import { marshalStringToJs } from "./marshal-to-js"; +import { MarshalerType, type GCHandle, type MarshalerToCs, type MarshalerToJs } from "./types"; +import { assertRuntimeRunning, isRuntimeRunning } from "./utils"; + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export function releaseJsOwnedObjectByGcHandle(gcHandle: GCHandle) { + // TODO-WASM +} + +export function getManagedStackTrace(exceptionGCHandle: GCHandle): string { + assertRuntimeRunning(); + const sp = Module.stackSave(); + try { + const size = 3; + const args = allocStackFrame(size); + + const arg1 = getArg(args, 2); + setArgType(arg1, MarshalerType.Exception); + setGcHandle(arg1, exceptionGCHandle); + + dotnetInteropJSExports.SystemInteropJS_GetManagedStackTrace(args); + + const res = getArg(args, 1); + return marshalStringToJs(res)!; + } finally { + if (isRuntimeRunning()) Module.stackRestore(sp); + } +} + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export function callDelegate(callbackGcHandle: GCHandle, arg1Js: any, arg2Js: any, arg3Js: any, resConverter?: MarshalerToJs, arg1Converter?: MarshalerToCs, arg2Converter?: MarshalerToCs, arg3Converter?: MarshalerToCs) { +} + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export function completeTask(holder_gc_handle: GCHandle, error?: any, data?: any, res_converter?: MarshalerToCs) { + +} + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export function bindAssemblyExports(assemblyName: string): Promise { + // TODO-WASM + return Promise.resolve(); +} diff --git a/src/native/libs/System.Runtime.InteropServices.JavaScript.Native/interop/marshal-to-cs.ts b/src/native/libs/System.Runtime.InteropServices.JavaScript.Native/interop/marshal-to-cs.ts new file mode 100644 index 00000000000000..291bb9093a09d7 --- /dev/null +++ b/src/native/libs/System.Runtime.InteropServices.JavaScript.Native/interop/marshal-to-cs.ts @@ -0,0 +1,513 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +import BuildConfiguration from "consts:configuration"; + +import { dotnetBrowserUtilsExports, dotnetApi, dotnetAssert, Module } from "./cross-module"; + +import type { BoundMarshalerToCs, JSMarshalerArgument, JSMarshalerArguments, JSMarshalerType, MarshalerToCs, MarshalerToJs, TypedArray } from "./types"; +import { JavaScriptMarshalerArgSize, MarshalerType } from "./types"; +import { arrayElementSize, boundJsFunctionSymbol, getArg, getSignatureArg1Type, getSignatureArg2Type, getSignatureArg3Type, getSignatureResType, jsToCsMarshalers, jsInteropState, proxyDebugSymbol, setArgBool, setArgDate, setArgElementType, setArgF32, setArgF64, setArgI16, setArgI32, setArgI52, setArgI64Big, setArgIntptr, setArgLength, setArgProxyContext, setArgType, setArgU16, setArgU8, setGcHandle, setJsHandle, getArgType, getArgGcHandle } from "./marshal"; +import { getMarshalerToJsByType } from "./marshal-to-js"; +import { assertNotDisposed, csOwnedJsHandleSymbol, jsOwnedGcHandleSymbol, getJsHandleFromJSObject, allocGcvHandle, setupManagedProxy } from "./gc-handles"; +import { fixupPointer } from "./utils"; +import { ArraySegment, ManagedError, ManagedObject, MemoryViewType, Span } from "./marshaled-types"; +import { isThenable, PromiseHolder } from "./cancelable-promise"; + +export const jsinteropDoc = "For more information see https://aka.ms/dotnet-wasm-jsinterop"; + +export function initializeMarshalersToCs(): void { + if (jsToCsMarshalers.size == 0) { + jsToCsMarshalers.set(MarshalerType.Array, marshalArrayToCs); + jsToCsMarshalers.set(MarshalerType.Span, _marshalSpanToCs); + jsToCsMarshalers.set(MarshalerType.ArraySegment, _marshalArraySegmentToCs); + jsToCsMarshalers.set(MarshalerType.Boolean, marshalBoolToCs); + jsToCsMarshalers.set(MarshalerType.Byte, _marshalByteToCs); + jsToCsMarshalers.set(MarshalerType.Char, _marshalCharToCs); + jsToCsMarshalers.set(MarshalerType.Int16, _marshalInt16ToCs); + jsToCsMarshalers.set(MarshalerType.Int32, _marshalInt32ToCs); + jsToCsMarshalers.set(MarshalerType.Int52, _marshalInt52ToCs); + jsToCsMarshalers.set(MarshalerType.BigInt64, _marshalBigint64ToCs); + jsToCsMarshalers.set(MarshalerType.Double, _marshalDoubleToCs); + jsToCsMarshalers.set(MarshalerType.Single, _marshalFloatToCs); + jsToCsMarshalers.set(MarshalerType.IntPtr, marshalIntptrToCs); + jsToCsMarshalers.set(MarshalerType.DateTime, _marshalDateTimeToCs); + jsToCsMarshalers.set(MarshalerType.DateTimeOffset, _marshalDateTimeOffsetToCs); + jsToCsMarshalers.set(MarshalerType.String, marshalStringToCs); + jsToCsMarshalers.set(MarshalerType.Exception, marshalExceptionToCs); + jsToCsMarshalers.set(MarshalerType.JSException, marshalExceptionToCs); + jsToCsMarshalers.set(MarshalerType.JSObject, marshalJsObjectToCs); + jsToCsMarshalers.set(MarshalerType.Object, marshalCsObjectToCs); + jsToCsMarshalers.set(MarshalerType.Task, marshalTaskToCs); + jsToCsMarshalers.set(MarshalerType.TaskResolved, marshalTaskToCs); + jsToCsMarshalers.set(MarshalerType.TaskRejected, marshalTaskToCs); + jsToCsMarshalers.set(MarshalerType.Action, _marshalFunctionToCs); + jsToCsMarshalers.set(MarshalerType.Function, _marshalFunctionToCs); + jsToCsMarshalers.set(MarshalerType.None, _marshalNullToCs);// also void + jsToCsMarshalers.set(MarshalerType.Discard, _marshalNullToCs);// also void + jsToCsMarshalers.set(MarshalerType.Void, _marshalNullToCs);// also void + jsToCsMarshalers.set(MarshalerType.DiscardNoWait, _marshalNullToCs);// also void + } +} + +export function bindArgMarshalToCs(sig: JSMarshalerType, marshalerType: MarshalerType, index: number): BoundMarshalerToCs | undefined { + if (marshalerType === MarshalerType.None || marshalerType === MarshalerType.Void || marshalerType === MarshalerType.Discard || marshalerType === MarshalerType.DiscardNoWait) { + return undefined; + } + let resMarshaler: MarshalerToCs | undefined = undefined; + let arg1Marshaler: MarshalerToJs | undefined = undefined; + let arg2Marshaler: MarshalerToJs | undefined = undefined; + let arg3Marshaler: MarshalerToJs | undefined = undefined; + + arg1Marshaler = getMarshalerToJsByType(getSignatureArg1Type(sig)); + arg2Marshaler = getMarshalerToJsByType(getSignatureArg2Type(sig)); + arg3Marshaler = getMarshalerToJsByType(getSignatureArg3Type(sig)); + const marshalerTypeRes = getSignatureResType(sig); + resMarshaler = getMarshalerToCsByType(marshalerTypeRes); + if (marshalerType === MarshalerType.Nullable) { + // nullable has nested type information, it's stored in res slot of the signature. The marshaler is the same as for non-nullable primitive type. + marshalerType = marshalerTypeRes; + } + const converter = getMarshalerToCsByType(marshalerType)!; + const elementType = getSignatureArg1Type(sig); + + const argOffset = index * JavaScriptMarshalerArgSize; + return (args: JSMarshalerArguments, value: any) => { + converter(args + argOffset, value, elementType, resMarshaler, arg1Marshaler, arg2Marshaler, arg3Marshaler); + }; +} + +export function getMarshalerToCsByType(marshalerType: MarshalerType): MarshalerToCs | undefined { + if (marshalerType === MarshalerType.None || marshalerType === MarshalerType.Void) { + return undefined; + } + const converter = jsToCsMarshalers.get(marshalerType); + dotnetAssert.check(converter && typeof converter === "function", () => `ERR30: Unknown converter for type ${marshalerType}`); + return converter; +} + +export function marshalBoolToCs(arg: JSMarshalerArgument, value: any): void { + if (value === null || value === undefined) { + setArgType(arg, MarshalerType.None); + } else { + setArgType(arg, MarshalerType.Boolean); + setArgBool(arg, value); + } +} + +function _marshalByteToCs(arg: JSMarshalerArgument, value: any): void { + if (value === null || value === undefined) { + setArgType(arg, MarshalerType.None); + } else { + setArgType(arg, MarshalerType.Byte); + setArgU8(arg, value); + } +} + +function _marshalCharToCs(arg: JSMarshalerArgument, value: any): void { + if (value === null || value === undefined) { + setArgType(arg, MarshalerType.None); + } else { + setArgType(arg, MarshalerType.Char); + setArgU16(arg, value); + } +} + +function _marshalInt16ToCs(arg: JSMarshalerArgument, value: any): void { + if (value === null || value === undefined) { + setArgType(arg, MarshalerType.None); + } else { + setArgType(arg, MarshalerType.Int16); + setArgI16(arg, value); + } +} + +function _marshalInt32ToCs(arg: JSMarshalerArgument, value: any): void { + if (value === null || value === undefined) { + setArgType(arg, MarshalerType.None); + } else { + setArgType(arg, MarshalerType.Int32); + setArgI32(arg, value); + } +} + +function _marshalInt52ToCs(arg: JSMarshalerArgument, value: any): void { + if (value === null || value === undefined) { + setArgType(arg, MarshalerType.None); + } else { + setArgType(arg, MarshalerType.Int52); + setArgI52(arg, value); + } +} + +function _marshalBigint64ToCs(arg: JSMarshalerArgument, value: any): void { + if (value === null || value === undefined) { + setArgType(arg, MarshalerType.None); + } else { + setArgType(arg, MarshalerType.BigInt64); + setArgI64Big(arg, value); + } +} + +function _marshalDoubleToCs(arg: JSMarshalerArgument, value: any): void { + if (value === null || value === undefined) { + setArgType(arg, MarshalerType.None); + } else { + setArgType(arg, MarshalerType.Double); + setArgF64(arg, value); + } +} + +function _marshalFloatToCs(arg: JSMarshalerArgument, value: any): void { + if (value === null || value === undefined) { + setArgType(arg, MarshalerType.None); + } else { + setArgType(arg, MarshalerType.Single); + setArgF32(arg, value); + } +} + +export function marshalIntptrToCs(arg: JSMarshalerArgument, value: any): void { + if (value === null || value === undefined) { + setArgType(arg, MarshalerType.None); + } else { + setArgType(arg, MarshalerType.IntPtr); + setArgIntptr(arg, value); + } +} + +function _marshalDateTimeToCs(arg: JSMarshalerArgument, value: Date): void { + if (value === null || value === undefined) { + setArgType(arg, MarshalerType.None); + } else { + dotnetAssert.check(value instanceof Date, "Value is not a Date"); + setArgType(arg, MarshalerType.DateTime); + setArgDate(arg, value); + } +} + +function _marshalDateTimeOffsetToCs(arg: JSMarshalerArgument, value: Date): void { + if (value === null || value === undefined) { + setArgType(arg, MarshalerType.None); + } else { + dotnetAssert.check(value instanceof Date, "Value is not a Date"); + setArgType(arg, MarshalerType.DateTimeOffset); + setArgDate(arg, value); + } +} + +export function marshalStringToCs(arg: JSMarshalerArgument, value: string) { + if (value === null || value === undefined) { + setArgType(arg, MarshalerType.None); + } else { + setArgType(arg, MarshalerType.String); + dotnetAssert.check(typeof value === "string", "Value is not a String"); + _marshalStringToCsImpl(arg, value); + } +} + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +function _marshalStringToCsImpl(arg: JSMarshalerArgument, value: string) { + const bufferLen = value.length * 2; + const buffer = Module._malloc(bufferLen);// together with Marshal.FreeHGlobal + dotnetBrowserUtilsExports.stringToUTF16(buffer as any, buffer as any + bufferLen, value); + setArgIntptr(arg, buffer); + setArgLength(arg, value.length); +} + +function _marshalNullToCs(arg: JSMarshalerArgument) { + setArgType(arg, MarshalerType.None); +} + +function _marshalFunctionToCs(arg: JSMarshalerArgument, value: Function, _?: MarshalerType, resConverter?: MarshalerToCs, arg1Converter?: MarshalerToJs, arg2Converter?: MarshalerToJs, arg3Converter?: MarshalerToJs): void { + if (value === null || value === undefined) { + setArgType(arg, MarshalerType.None); + return; + } + dotnetAssert.check(value && value instanceof Function, "Value is not a Function"); + + // TODO: we could try to cache value -> existing JSHandle + const wrapper: any = function delegateWrapper(args: JSMarshalerArguments) { + const exc = getArg(args, 0); + const res = getArg(args, 1); + const arg1 = getArg(args, 2); + const arg2 = getArg(args, 3); + const arg3 = getArg(args, 4); + + const previousPendingSynchronousCall = jsInteropState.isPendingSynchronousCall; + try { + dotnetAssert.check(!wrapper.isDisposed, "Function is disposed and should not be invoked anymore."); + + let arg1Js: any = undefined; + let arg2Js: any = undefined; + let arg3Js: any = undefined; + if (arg1Converter) { + arg1Js = arg1Converter(arg1); + } + if (arg2Converter) { + arg2Js = arg2Converter(arg2); + } + if (arg3Converter) { + arg3Js = arg3Converter(arg3); + } + jsInteropState.isPendingSynchronousCall = true; // this is always synchronous call for now + const resJs = value(arg1Js, arg2Js, arg3Js); + if (resConverter) { + resConverter(res, resJs); + } + + } catch (ex) { + marshalExceptionToCs(exc, ex); + } finally { + jsInteropState.isPendingSynchronousCall = previousPendingSynchronousCall; + } + }; + + wrapper[boundJsFunctionSymbol] = true; + wrapper.isDisposed = false; + wrapper.dispose = () => { + wrapper.isDisposed = true; + }; + const boundFunctionHandle = getJsHandleFromJSObject(wrapper)!; + if (BuildConfiguration === "Debug") { + wrapper[proxyDebugSymbol] = `Proxy of JS Function with JSHandle ${boundFunctionHandle}: ${value.toString()}`; + } + setJsHandle(arg, boundFunctionHandle); + setArgType(arg, MarshalerType.Function);//TODO or action ? +} + +export function marshalTaskToCs(arg: JSMarshalerArgument, value: Promise, _?: MarshalerType, resConverter?: MarshalerToCs) { + const handleIsPreallocated = getArgType(arg) == MarshalerType.TaskPreCreated; + if (value === null || value === undefined) { + setArgType(arg, MarshalerType.None); + } + dotnetAssert.check(isThenable(value), "Value is not a Promise"); + + const gcHandle = handleIsPreallocated ? getArgGcHandle(arg) : allocGcvHandle(); + if (!handleIsPreallocated) { + setGcHandle(arg, gcHandle); + setArgType(arg, MarshalerType.Task); + } + + const holder = new PromiseHolder(value, gcHandle, resConverter); + setupManagedProxy(holder, gcHandle); + + if (BuildConfiguration === "Debug") { + (holder as any)[proxyDebugSymbol] = `PromiseHolder with GCHandle ${gcHandle}`; + } + + value.then(data => holder.resolve(data), reason => holder.reject(reason)); + + throw new Error("TODO-WASM"); +} + +export function marshalExceptionToCs(arg: JSMarshalerArgument, value: any): void { + if (value === null || value === undefined) { + setArgType(arg, MarshalerType.None); + } else if (value instanceof ManagedError) { + setArgType(arg, MarshalerType.Exception); + // this is managed exception round-trip + const gcHandle = assertNotDisposed(value); + setGcHandle(arg, gcHandle); + } else { + dotnetAssert.check(typeof value === "object" || typeof value === "string", () => `Value is not an Error ${typeof value}`); + setArgType(arg, MarshalerType.JSException); + const message = value.toString(); + _marshalStringToCsImpl(arg, message); + const knownJsHandle = value[csOwnedJsHandleSymbol]; + if (knownJsHandle) { + setJsHandle(arg, knownJsHandle); + } else { + const jsHandle = getJsHandleFromJSObject(value)!; + if (BuildConfiguration === "Debug" && Object.isExtensible(value)) { + value[proxyDebugSymbol] = `JS Error with JSHandle ${jsHandle}`; + } + setJsHandle(arg, jsHandle); + } + } +} + +export function marshalJsObjectToCs(arg: JSMarshalerArgument, value: any): void { + if (value === undefined || value === null) { + setArgType(arg, MarshalerType.None); + setArgProxyContext(arg); + } else { + // if value was ManagedObject, it would be double proxied, but the C# signature requires that + dotnetAssert.check(value[jsOwnedGcHandleSymbol] === undefined, () => `JSObject proxy of ManagedObject proxy is not supported. ${jsinteropDoc}`); + dotnetAssert.check(typeof value === "function" || typeof value === "object", () => `JSObject proxy of ${typeof value} is not supported`); + + setArgType(arg, MarshalerType.JSObject); + const jsHandle = getJsHandleFromJSObject(value)!; + if (BuildConfiguration === "Debug" && Object.isExtensible(value)) { + value[proxyDebugSymbol] = `JS Object with JSHandle ${jsHandle}`; + } + setJsHandle(arg, jsHandle); + } +} + +export function marshalCsObjectToCs(arg: JSMarshalerArgument, value: any): void { + if (value === undefined || value === null) { + setArgType(arg, MarshalerType.None); + setArgProxyContext(arg); + } else { + const gcHandle = value[jsOwnedGcHandleSymbol]; + const jsType = typeof (value); + if (gcHandle === undefined) { + if (jsType === "string" || jsType === "symbol") { + setArgType(arg, MarshalerType.String); + _marshalStringToCsImpl(arg, value); + } else if (jsType === "number") { + setArgType(arg, MarshalerType.Double); + setArgF64(arg, value); + } else if (jsType === "bigint") { + // we do it because not all bigint values could fit into Int64 + throw new Error("NotImplementedException: bigint"); + } else if (jsType === "boolean") { + setArgType(arg, MarshalerType.Boolean); + setArgBool(arg, value); + } else if (value instanceof Date) { + setArgType(arg, MarshalerType.DateTime); + setArgDate(arg, value); + } else if (value instanceof Error) { + marshalExceptionToCs(arg, value); + } else if (value instanceof Uint8Array) { + marshalArrayToCsImpl(arg, value, MarshalerType.Byte); + } else if (value instanceof Float64Array) { + marshalArrayToCsImpl(arg, value, MarshalerType.Double); + } else if (value instanceof Int32Array) { + marshalArrayToCsImpl(arg, value, MarshalerType.Int32); + } else if (Array.isArray(value)) { + marshalArrayToCsImpl(arg, value, MarshalerType.Object); + } else if (value instanceof Int16Array + || value instanceof Int8Array + || value instanceof Uint8ClampedArray + || value instanceof Uint16Array + || value instanceof Uint32Array + || value instanceof Float32Array + ) { + throw new Error("NotImplementedException: TypedArray"); + } else if (isThenable(value)) { + marshalTaskToCs(arg, value); + } else if (value instanceof Span) { + throw new Error("NotImplementedException: Span"); + } else if (jsType == "object") { + const jsHandle = getJsHandleFromJSObject(value); + setArgType(arg, MarshalerType.JSObject); + if (BuildConfiguration === "Debug" && Object.isExtensible(value)) { + value[proxyDebugSymbol] = `JS Object with JSHandle ${jsHandle}`; + } + setJsHandle(arg, jsHandle); + } else { + throw new Error(`JSObject proxy is not supported for ${jsType} ${value}`); + } + } else { + assertNotDisposed(value); + if (value instanceof ArraySegment) { + throw new Error("NotImplementedException: ArraySegment. " + jsinteropDoc); + } else if (value instanceof ManagedError) { + setArgType(arg, MarshalerType.Exception); + setGcHandle(arg, gcHandle); + } else if (value instanceof ManagedObject) { + setArgType(arg, MarshalerType.Object); + setGcHandle(arg, gcHandle); + } else { + throw new Error("NotImplementedException " + jsType + ". " + jsinteropDoc); + } + } + } +} + +export function marshalArrayToCs(arg: JSMarshalerArgument, value: Array | TypedArray | undefined | null, elementType?: MarshalerType): void { + dotnetAssert.check(!!elementType, "Expected valid elementType parameter"); + marshalArrayToCsImpl(arg, value, elementType); +} + +export function marshalArrayToCsImpl(arg: JSMarshalerArgument, value: Array | TypedArray | undefined | null, elementType: MarshalerType): void { + if (value === null || value === undefined) { + setArgType(arg, MarshalerType.None); + } else { + const elementSize = arrayElementSize(elementType); + dotnetAssert.check(elementSize != -1, () => `Element type ${elementType} not supported`); + const length = value.length; + const bufferLength = elementSize * length; + const bufferPtr = Module._malloc(bufferLength) as any; + if (elementType == MarshalerType.String) { + dotnetAssert.check(Array.isArray(value), "Value is not an Array"); + dotnetBrowserUtilsExports.zeroRegion(bufferPtr, bufferLength); + for (let index = 0; index < length; index++) { + const elementArg = getArg(bufferPtr, index); + marshalStringToCs(elementArg, value[index]); + } + } else if (elementType == MarshalerType.Object) { + dotnetAssert.check(Array.isArray(value), "Value is not an Array"); + dotnetBrowserUtilsExports.zeroRegion(bufferPtr, bufferLength); + for (let index = 0; index < length; index++) { + const elementArg = getArg(bufferPtr, index); + marshalCsObjectToCs(elementArg, value[index]); + } + } else if (elementType == MarshalerType.JSObject) { + dotnetAssert.check(Array.isArray(value), "Value is not an Array"); + dotnetBrowserUtilsExports.zeroRegion(bufferPtr, bufferLength); + for (let index = 0; index < length; index++) { + const elementArg = getArg(bufferPtr, index); + marshalJsObjectToCs(elementArg, value[index]); + } + } else if (elementType == MarshalerType.Byte) { + dotnetAssert.check(Array.isArray(value) || value instanceof Uint8Array, "Value is not an Array or Uint8Array"); + const bufferOffset = fixupPointer(bufferPtr, 0); + const targetView = dotnetApi.localHeapViewU8().subarray(bufferOffset, bufferOffset + length); + targetView.set(value); + } else if (elementType == MarshalerType.Int32) { + dotnetAssert.check(Array.isArray(value) || value instanceof Int32Array, "Value is not an Array or Int32Array"); + const bufferOffset = fixupPointer(bufferPtr, 2); + const targetView = dotnetApi.localHeapViewI32().subarray(bufferOffset, bufferOffset + length); + targetView.set(value); + } else if (elementType == MarshalerType.Double) { + dotnetAssert.check(Array.isArray(value) || value instanceof Float64Array, "Value is not an Array or Float64Array"); + const bufferOffset = fixupPointer(bufferPtr, 3); + const targetView = dotnetApi.localHeapViewF64().subarray(bufferOffset, bufferOffset + length); + targetView.set(value); + } else { + throw new Error("not implemented"); + } + setArgIntptr(arg, bufferPtr); + setArgType(arg, MarshalerType.Array); + setArgElementType(arg, elementType); + setArgLength(arg, value.length); + } +} + +function _marshalSpanToCs(arg: JSMarshalerArgument, value: Span, elementType?: MarshalerType): void { + dotnetAssert.check(!!elementType, "Expected valid elementType parameter"); + dotnetAssert.check(!value.isDisposed, "ObjectDisposedException"); + checkViewType(elementType, value._viewType); + + setArgType(arg, MarshalerType.Span); + setArgIntptr(arg, value._pointer); + setArgLength(arg, value.length); +} + +// this only supports round-trip +function _marshalArraySegmentToCs(arg: JSMarshalerArgument, value: ArraySegment, elementType?: MarshalerType): void { + dotnetAssert.check(!!elementType, "Expected valid elementType parameter"); + const gcHandle = assertNotDisposed(value); + dotnetAssert.check(gcHandle, "Only roundtrip of ArraySegment instance created by C#"); + checkViewType(elementType, value._viewType); + setArgType(arg, MarshalerType.ArraySegment); + setArgIntptr(arg, value._pointer); + setArgLength(arg, value.length); + setGcHandle(arg, gcHandle); +} + +function checkViewType(elementType: MarshalerType, viewType: MemoryViewType) { + if (elementType == MarshalerType.Byte) { + dotnetAssert.check(MemoryViewType.Byte == viewType, "Expected MemoryViewType.Byte"); + } else if (elementType == MarshalerType.Int32) { + dotnetAssert.check(MemoryViewType.Int32 == viewType, "Expected MemoryViewType.Int32"); + } else if (elementType == MarshalerType.Double) { + dotnetAssert.check(MemoryViewType.Double == viewType, "Expected MemoryViewType.Double"); + } else { + throw new Error(`NotImplementedException ${elementType} `); + } +} + diff --git a/src/native/libs/System.Runtime.InteropServices.JavaScript.Native/interop/marshal-to-js.ts b/src/native/libs/System.Runtime.InteropServices.JavaScript.Native/interop/marshal-to-js.ts new file mode 100644 index 00000000000000..99a9e69386ef97 --- /dev/null +++ b/src/native/libs/System.Runtime.InteropServices.JavaScript.Native/interop/marshal-to-js.ts @@ -0,0 +1,554 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +import BuildConfiguration from "consts:configuration"; + +import { dotnetBrowserUtilsExports, dotnetLoaderExports, dotnetApi, dotnetAssert, dotnetLogger, Module } from "./cross-module"; + +import type { BoundMarshalerToJs, JSHandle, JSMarshalerArgument, JSMarshalerArguments, JSMarshalerType, MarshalerToCs, MarshalerToJs, TypedArray } from "./types"; +import { GCHandleNull, JavaScriptMarshalerArgSize, MarshalerType } from "./types"; +import { arrayElementSize, csToJsMarshalers, getArg, getArgBool, getArgDate, getArgElementType, getArgF32, getArgF64, getArgGcHandle, getArgI16, getArgI32, getArgI52, getArgI64Big, getArgIntptr, getArgJsHandle, getArgLength, getArgType, getArgU16, getArgU8, getSignatureArg1Type, getSignatureArg2Type, getSignatureArg3Type, getSignatureResType, proxyDebugSymbol, setArgType, setJsHandle } from "./marshal"; +import { getMarshalerToCsByType, jsinteropDoc, marshalExceptionToCs } from "./marshal-to-cs"; +import { lookupJsOwnedObject, getJsHandleFromJSObject, getJSObjectFromJSHandle, registerWithJsvHandle, releaseCSOwnedObject, setupManagedProxy, teardownManagedProxy } from "./gc-handles"; +import { assertRuntimeRunning, fixupPointer, isRuntimeRunning } from "./utils"; +import { ArraySegment, ManagedError, ManagedObject, MemoryViewType, Span } from "./marshaled-types"; +import { callDelegate } from "./managed-exports"; + +export function initializeMarshalersToJs(): void { + if (csToJsMarshalers.size == 0) { + csToJsMarshalers.set(MarshalerType.Array, _marshalArrayToJs); + csToJsMarshalers.set(MarshalerType.Span, _marshalSpanToJs); + csToJsMarshalers.set(MarshalerType.ArraySegment, _marshalArraySegmentToJs); + csToJsMarshalers.set(MarshalerType.Boolean, _marshalBoolToJs); + csToJsMarshalers.set(MarshalerType.Byte, _marshalByteToJs); + csToJsMarshalers.set(MarshalerType.Char, _marshalCharToJs); + csToJsMarshalers.set(MarshalerType.Int16, _marshalInt16ToJs); + csToJsMarshalers.set(MarshalerType.Int32, marshalInt32ToJs); + csToJsMarshalers.set(MarshalerType.Int52, _marshalInt52ToJs); + csToJsMarshalers.set(MarshalerType.BigInt64, _marshalBigint64ToJs); + csToJsMarshalers.set(MarshalerType.Single, _marshalFloatToJs); + csToJsMarshalers.set(MarshalerType.IntPtr, _marshalIntptrToJs); + csToJsMarshalers.set(MarshalerType.Double, _marshalDoubleToJs); + csToJsMarshalers.set(MarshalerType.String, marshalStringToJs); + csToJsMarshalers.set(MarshalerType.Exception, marshalExceptionToJs); + csToJsMarshalers.set(MarshalerType.JSException, marshalExceptionToJs); + csToJsMarshalers.set(MarshalerType.JSObject, _marshalJsObjectToJs); + csToJsMarshalers.set(MarshalerType.Object, _marshalCsObjectToJs); + csToJsMarshalers.set(MarshalerType.DateTime, _marshalDatetimeToJs); + csToJsMarshalers.set(MarshalerType.DateTimeOffset, _marshalDatetimeToJs); + csToJsMarshalers.set(MarshalerType.Task, marshalTaskToJs); + csToJsMarshalers.set(MarshalerType.TaskRejected, marshalTaskToJs); + csToJsMarshalers.set(MarshalerType.TaskResolved, marshalTaskToJs); + csToJsMarshalers.set(MarshalerType.TaskPreCreated, beginMarshalTaskToJs); + csToJsMarshalers.set(MarshalerType.Action, _marshalDelegateToJs); + csToJsMarshalers.set(MarshalerType.Function, _marshalDelegateToJs); + csToJsMarshalers.set(MarshalerType.None, _marshalNullToJs); + csToJsMarshalers.set(MarshalerType.Void, _marshalNullToJs); + csToJsMarshalers.set(MarshalerType.Discard, _marshalNullToJs); + csToJsMarshalers.set(MarshalerType.DiscardNoWait, _marshalNullToJs); + } +} + +export function bindArgMarshalToJs(sig: JSMarshalerType, marshalerType: MarshalerType, index: number): BoundMarshalerToJs | undefined { + if (marshalerType === MarshalerType.None || marshalerType === MarshalerType.Void || marshalerType === MarshalerType.Discard || marshalerType === MarshalerType.DiscardNoWait) { + return undefined; + } + + let resMarshaler: MarshalerToJs | undefined = undefined; + let arg1Marshaler: MarshalerToCs | undefined = undefined; + let arg2Marshaler: MarshalerToCs | undefined = undefined; + let arg3Marshaler: MarshalerToCs | undefined = undefined; + + arg1Marshaler = getMarshalerToCsByType(getSignatureArg1Type(sig)); + arg2Marshaler = getMarshalerToCsByType(getSignatureArg2Type(sig)); + arg3Marshaler = getMarshalerToCsByType(getSignatureArg3Type(sig)); + const marshalerTypeRes = getSignatureResType(sig); + resMarshaler = getMarshalerToJsByType(marshalerTypeRes); + if (marshalerType === MarshalerType.Nullable) { + // nullable has nested type information, it's stored in res slot of the signature. The marshaler is the same as for non-nullable primitive type. + marshalerType = marshalerTypeRes; + } + const converter = getMarshalerToJsByType(marshalerType)!; + const elementType = getSignatureArg1Type(sig); + + const argOffset = index * JavaScriptMarshalerArgSize; + return (args: JSMarshalerArguments) => { + return converter(args + argOffset, elementType, resMarshaler, arg1Marshaler, arg2Marshaler, arg3Marshaler); + }; +} + +export function getMarshalerToJsByType(marshalerType: MarshalerType): MarshalerToJs | undefined { + if (marshalerType === MarshalerType.None || marshalerType === MarshalerType.Void) { + return undefined; + } + const converter = csToJsMarshalers.get(marshalerType); + dotnetAssert.check(converter && typeof converter === "function", () => `ERR41: Unknown converter for type ${marshalerType}. ${jsinteropDoc}`); + return converter; +} + +function _marshalBoolToJs(arg: JSMarshalerArgument): boolean | null { + const type = getArgType(arg); + if (type == MarshalerType.None) { + return null; + } + return getArgBool(arg); +} + +function _marshalByteToJs(arg: JSMarshalerArgument): number | null { + const type = getArgType(arg); + if (type == MarshalerType.None) { + return null; + } + return getArgU8(arg); +} + +function _marshalCharToJs(arg: JSMarshalerArgument): number | null { + const type = getArgType(arg); + if (type == MarshalerType.None) { + return null; + } + return getArgU16(arg); +} + +function _marshalInt16ToJs(arg: JSMarshalerArgument): number | null { + const type = getArgType(arg); + if (type == MarshalerType.None) { + return null; + } + return getArgI16(arg); +} + +export function marshalInt32ToJs(arg: JSMarshalerArgument): number | null { + const type = getArgType(arg); + if (type == MarshalerType.None) { + return null; + } + return getArgI32(arg); +} + +function _marshalInt52ToJs(arg: JSMarshalerArgument): number | null { + const type = getArgType(arg); + if (type == MarshalerType.None) { + return null; + } + return getArgI52(arg); +} + +function _marshalBigint64ToJs(arg: JSMarshalerArgument): bigint | null { + const type = getArgType(arg); + if (type == MarshalerType.None) { + return null; + } + return getArgI64Big(arg); +} + +function _marshalFloatToJs(arg: JSMarshalerArgument): number | null { + const type = getArgType(arg); + if (type == MarshalerType.None) { + return null; + } + return getArgF32(arg); +} + +function _marshalDoubleToJs(arg: JSMarshalerArgument): number | null { + const type = getArgType(arg); + if (type == MarshalerType.None) { + return null; + } + return getArgF64(arg); +} + +function _marshalIntptrToJs(arg: JSMarshalerArgument): number | null { + const type = getArgType(arg); + if (type == MarshalerType.None) { + return null; + } + return getArgIntptr(arg); +} + +function _marshalNullToJs(): null { + return null; +} + +function _marshalDatetimeToJs(arg: JSMarshalerArgument): Date | null { + const type = getArgType(arg); + if (type === MarshalerType.None) { + return null; + } + return getArgDate(arg); +} + +// NOTE: at the moment, this can't dispatch async calls (with Task/Promise return type). Therefore we don't have to worry about pre-created Task. +function _marshalDelegateToJs(arg: JSMarshalerArgument, _?: MarshalerType, resConverter?: MarshalerToJs, arg1Converter?: MarshalerToCs, arg2Converter?: MarshalerToCs, arg3Converter?: MarshalerToCs): Function | null { + const type = getArgType(arg); + if (type === MarshalerType.None) { + return null; + } + + const gcHandle = getArgGcHandle(arg); + let result = lookupJsOwnedObject(gcHandle); + if (result === null || result === undefined) { + // this will create new Function for the C# delegate + result = (arg1Js: any, arg2Js: any, arg3Js: any): any => { + dotnetAssert.check(!result.isDisposed, "Delegate is disposed and should not be invoked anymore."); + // arg numbers are shifted by one, the real first is a gc handle of the callback + return callDelegate(gcHandle, arg1Js, arg2Js, arg3Js, resConverter, arg1Converter, arg2Converter, arg3Converter); + }; + result.dispose = () => { + if (!result.isDisposed) { + result.isDisposed = true; + teardownManagedProxy(result, gcHandle); + } + }; + result.isDisposed = false; + if (BuildConfiguration === "Debug") { + (result as any)[proxyDebugSymbol] = `C# Delegate with GCHandle ${gcHandle}`; + } + setupManagedProxy(result, gcHandle); + } + + return result; +} + +export class TaskHolder { + constructor(public promise: Promise, public resolveOrReject: (type: MarshalerType, jsHandle: JSHandle, argInner: JSMarshalerArgument) => void) { + } +} + +export function marshalTaskToJs(arg: JSMarshalerArgument, _?: MarshalerType, resConverter?: MarshalerToJs): Promise | null { + const type = getArgType(arg); + // this path is used only when Task is passed as argument to JSImport and virtual JSHandle would be used + dotnetAssert.check(type != MarshalerType.TaskPreCreated, "Unexpected Task type: TaskPreCreated"); + + // if there is synchronous result, return it + const promise = tryMarshalSyncTaskToJs(arg, type, resConverter); + if (promise !== false) { + return promise; + } + + const jsvHandle = getArgJsHandle(arg); + const holder = createTaskHolder(resConverter); + registerWithJsvHandle(holder, jsvHandle); + if (BuildConfiguration === "Debug") { + (holder as any)[proxyDebugSymbol] = `TaskHolder with JSVHandle ${jsvHandle}`; + } + + return holder.promise; +} + +export function beginMarshalTaskToJs(arg: JSMarshalerArgument, _?: MarshalerType, resConverter?: MarshalerToJs): Promise | null { + // this path is used when Task is returned from JSExport/call_entry_point + const holder = createTaskHolder(resConverter); + const jsHandle = getJsHandleFromJSObject(holder); + if (BuildConfiguration === "Debug") { + (holder as any)[proxyDebugSymbol] = `TaskHolder with JSHandle ${jsHandle}`; + } + setJsHandle(arg, jsHandle); + setArgType(arg, MarshalerType.TaskPreCreated); + return holder.promise; +} + +export function endMarshalTaskToJs(args: JSMarshalerArguments, resConverter: MarshalerToJs | undefined, eagerPromise: Promise | null) { + // this path is used when Task is returned from JSExport/call_entry_point + const res = getArg(args, 1); + const type = getArgType(res); + + // if there is no synchronous result, return eagerPromise we created earlier + if (type === MarshalerType.TaskPreCreated) { + return eagerPromise; + } + + // otherwise drop the eagerPromise's handle + const jsHandle = getJsHandleFromJSObject(eagerPromise); + releaseCSOwnedObject(jsHandle); + + // get the synchronous result + const promise = tryMarshalSyncTaskToJs(res, type, resConverter); + + // make sure we got the result + dotnetAssert.check(promise !== false, () => `Expected synchronous result, got: ${type}`); + + return promise; +} + +function tryMarshalSyncTaskToJs(arg: JSMarshalerArgument, type: MarshalerType, resConverter?: MarshalerToJs): Promise | null | false { + if (type === MarshalerType.None) { + return null; + } + if (type === MarshalerType.TaskRejected) { + return Promise.reject(marshalExceptionToJs(arg)); + } + if (type === MarshalerType.TaskResolved) { + const elementType = getArgElementType(arg); + if (elementType === MarshalerType.Void) { + return Promise.resolve(); + } + // this will change the type to the actual type of the result + setArgType(arg, elementType); + if (!resConverter) { + // when we arrived here from _marshalCsObjectToJs + resConverter = csToJsMarshalers.get(elementType); + } + dotnetAssert.check(resConverter, () => `Unknown subConverter for type ${elementType}. ${jsinteropDoc}`); + + const val = resConverter(arg); + return Promise.resolve(val); + } + return false; +} + +function createTaskHolder(resConverter?: MarshalerToJs) { + const pcs = dotnetLoaderExports.createPromiseCompletionSource(); + const holder = new TaskHolder(pcs.promise, (type, jsHandle, argInner) => { + if (type === MarshalerType.TaskRejected) { + const reason = marshalExceptionToJs(argInner); + pcs.reject(reason); + } else if (type === MarshalerType.TaskResolved) { + const type = getArgType(argInner); + if (type === MarshalerType.Void) { + pcs.resolve(undefined); + } else { + if (!resConverter) { + // when we arrived here from _marshalCsObjectToJs + resConverter = csToJsMarshalers.get(type); + } + dotnetAssert.check(resConverter, () => `Unknown subConverter for type ${type}. ${jsinteropDoc}`); + + const jsValue = resConverter!(argInner); + pcs.resolve(jsValue); + } + } else { + dotnetAssert.check(false, () => `Unexpected type ${type}`); + } + releaseCSOwnedObject(jsHandle); + }); + return holder; +} + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export function marshalStringToJs(arg: JSMarshalerArgument): string | null { + const type = getArgType(arg); + if (type == MarshalerType.None) { + return null; + } + const buffer = getArgIntptr(arg); + const len = getArgLength(arg) * 2; + const value = dotnetBrowserUtilsExports.utf16ToString(buffer, buffer + len); + Module._free(buffer as any); + return value; +} + +export function marshalExceptionToJs(arg: JSMarshalerArgument): Error | null { + const type = getArgType(arg); + if (type == MarshalerType.None) { + return null; + } + if (type == MarshalerType.JSException) { + // this is JSException roundtrip + const jsHandle = getArgJsHandle(arg); + const jsObj = getJSObjectFromJSHandle(jsHandle); + return jsObj; + } + + const gcHandle = getArgGcHandle(arg); + let result = lookupJsOwnedObject(gcHandle); + if (result === null || result === undefined) { + // this will create new ManagedError + const message = marshalStringToJs(arg); + result = new ManagedError(message!); + + if (BuildConfiguration === "Debug") { + (result as any)[proxyDebugSymbol] = `C# Exception with GCHandle ${gcHandle}`; + } + setupManagedProxy(result, gcHandle); + } + + return result; +} + +function _marshalJsObjectToJs(arg: JSMarshalerArgument): any { + const type = getArgType(arg); + if (type == MarshalerType.None) { + return null; + } + const jsHandle = getArgJsHandle(arg); + const jsObj = getJSObjectFromJSHandle(jsHandle); + dotnetAssert.check(jsObj !== undefined, () => `JS object JSHandle ${jsHandle} was not found`); + return jsObj; +} + +function _marshalCsObjectToJs(arg: JSMarshalerArgument): any { + const marshalerType = getArgType(arg); + if (marshalerType == MarshalerType.None) { + return null; + } + if (marshalerType == MarshalerType.JSObject) { + const jsHandle = getArgJsHandle(arg); + const jsObj = getJSObjectFromJSHandle(jsHandle); + return jsObj; + } + + if (marshalerType == MarshalerType.Array) { + const elementType = getArgElementType(arg); + return _marshalArrayToJs_impl(arg, elementType); + } + + if (marshalerType == MarshalerType.Object) { + const gcHandle = getArgGcHandle(arg); + if (gcHandle === GCHandleNull) { + return null; + } + + // see if we have js owned instance for this gcHandle already + let result = lookupJsOwnedObject(gcHandle); + + // If the JS object for this gcHandle was already collected (or was never created) + if (!result) { + result = new ManagedObject(); + if (BuildConfiguration === "Debug") { + (result as any)[proxyDebugSymbol] = `C# Object with GCHandle ${gcHandle}`; + } + setupManagedProxy(result, gcHandle); + } + + return result; + } + + // other types + const converter = csToJsMarshalers.get(marshalerType); + dotnetAssert.check(converter, () => `Unknown converter for type ${marshalerType}. ${jsinteropDoc}`); + return converter(arg); +} + +function _marshalArrayToJs(arg: JSMarshalerArgument, elementType?: MarshalerType): Array | TypedArray | null { + dotnetAssert.check(!!elementType, "Expected valid elementType parameter"); + return _marshalArrayToJs_impl(arg, elementType); +} + +function _marshalArrayToJs_impl(arg: JSMarshalerArgument, elementType: MarshalerType): Array | TypedArray | null { + const type = getArgType(arg); + if (type == MarshalerType.None) { + return null; + } + const elementSize = arrayElementSize(elementType); + dotnetAssert.check(elementSize != -1, () => `Element type ${elementType} not supported`); + const bufferPtr = getArgIntptr(arg); + const length = getArgLength(arg); + let result: Array | TypedArray | null = null; + if (elementType == MarshalerType.String) { + result = new Array(length); + for (let index = 0; index < length; index++) { + const elementArg = getArg(bufferPtr, index); + result[index] = marshalStringToJs(elementArg); + } + } else if (elementType == MarshalerType.Object) { + result = new Array(length); + for (let index = 0; index < length; index++) { + const elementArg = getArg(bufferPtr, index); + result[index] = _marshalCsObjectToJs(elementArg); + } + } else if (elementType == MarshalerType.JSObject) { + result = new Array(length); + for (let index = 0; index < length; index++) { + const elementArg = getArg(bufferPtr, index); + result[index] = _marshalJsObjectToJs(elementArg); + } + } else if (elementType == MarshalerType.Byte) { + const bufferOffset = fixupPointer(bufferPtr, 0); + const sourceView = dotnetApi.localHeapViewU8().subarray(bufferOffset, bufferOffset + length); + result = sourceView.slice();//copy + } else if (elementType == MarshalerType.Int32) { + const bufferOffset = fixupPointer(bufferPtr, 2); + const sourceView = dotnetApi.localHeapViewI32().subarray(bufferOffset, bufferOffset + length); + result = sourceView.slice();//copy + } else if (elementType == MarshalerType.Double) { + const bufferOffset = fixupPointer(bufferPtr, 3); + const sourceView = dotnetApi.localHeapViewF64().subarray(bufferOffset, bufferOffset + length); + result = sourceView.slice();//copy + } else { + throw new Error(`NotImplementedException ${elementType}. ${jsinteropDoc}`); + } + Module._free(bufferPtr); + return result; +} + +function _marshalSpanToJs(arg: JSMarshalerArgument, elementType?: MarshalerType): Span { + dotnetAssert.check(!!elementType, "Expected valid elementType parameter"); + + const bufferPtr = getArgIntptr(arg); + const length = getArgLength(arg); + let result: Span | null = null; + if (elementType == MarshalerType.Byte) { + result = new Span(bufferPtr, length, MemoryViewType.Byte); + } else if (elementType == MarshalerType.Int32) { + result = new Span(bufferPtr, length, MemoryViewType.Int32); + } else if (elementType == MarshalerType.Double) { + result = new Span(bufferPtr, length, MemoryViewType.Double); + } else { + throw new Error(`NotImplementedException ${elementType}. ${jsinteropDoc}`); + } + return result; +} + +function _marshalArraySegmentToJs(arg: JSMarshalerArgument, elementType?: MarshalerType): ArraySegment { + dotnetAssert.check(!!elementType, "Expected valid elementType parameter"); + + const bufferPtr = getArgIntptr(arg); + const length = getArgLength(arg); + let result: ArraySegment | null = null; + if (elementType == MarshalerType.Byte) { + result = new ArraySegment(bufferPtr, length, MemoryViewType.Byte); + } else if (elementType == MarshalerType.Int32) { + result = new ArraySegment(bufferPtr, length, MemoryViewType.Int32); + } else if (elementType == MarshalerType.Double) { + result = new ArraySegment(bufferPtr, length, MemoryViewType.Double); + } else { + throw new Error(`NotImplementedException ${elementType}. ${jsinteropDoc}`); + } + const gcHandle = getArgGcHandle(arg); + if (BuildConfiguration === "Debug") { + (result as any)[proxyDebugSymbol] = `C# ArraySegment with GCHandle ${gcHandle}`; + } + setupManagedProxy(result, gcHandle); + + return result; +} + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export function resolveOrRejectPromise(args: JSMarshalerArguments): void { + if (!isRuntimeRunning()) { + dotnetLogger.debug("This promise resolution/rejection can't be propagated to managed code, mono runtime already exited."); + return; + } + args = fixupPointer(args, 0); + const exc = getArg(args, 0); + // TODO-WASM const receiver_should_free = WasmEnableThreads && is_receiver_should_free(args); + try { + assertRuntimeRunning(); + + const res = getArg(args, 1); + const argHandle = getArg(args, 2); + const argValue = getArg(args, 3); + + const type = getArgType(argHandle); + const jsHandle = getArgJsHandle(argHandle); + + const holder = getJSObjectFromJSHandle(jsHandle) as TaskHolder; + dotnetAssert.check(holder, () => `Cannot find Promise for JSHandle ${jsHandle}`); + + holder.resolveOrReject(type, jsHandle, argValue); + /* TODO-WASM if (receiver_should_free) { + // this works together with AllocHGlobal in JSFunctionBinding.ResolveOrRejectPromise + free(args as any); + } else {*/ + setArgType(res, MarshalerType.Void); + setArgType(exc, MarshalerType.None); + //} + + } catch (ex: any) { + /* TODO-WASM if (receiver_should_free) { + mono_assert(false, () => `Failed to resolve or reject promise ${ex}`); + }*/ + marshalExceptionToCs(exc, ex); + } +} diff --git a/src/native/libs/System.Runtime.InteropServices.JavaScript.Native/interop/marshal.ts b/src/native/libs/System.Runtime.InteropServices.JavaScript.Native/interop/marshal.ts new file mode 100644 index 00000000000000..93168b71096416 --- /dev/null +++ b/src/native/libs/System.Runtime.InteropServices.JavaScript.Native/interop/marshal.ts @@ -0,0 +1,330 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +import { dotnetBrowserUtilsExports, dotnetApi, dotnetAssert, Module } from "./cross-module"; + +import type { GCHandle, JSFunctionSignature, JSHandle, JSMarshalerType, JSMarshalerArgument, JSMarshalerArguments, MarshalerToCs, MarshalerToJs, VoidPtr, PThreadPtr } from "./types"; +import { JavaScriptMarshalerArgSize, JSBindingHeaderOffsets, JSBindingTypeOffsets, JSMarshalerArgumentOffsets, JSMarshalerSignatureHeaderSize, JSMarshalerTypeSize, MarshalerType } from "./types"; + +export const jsInteropState = { + isPendingSynchronousCall: false, + proxyGCHandle: undefined as GCHandle | undefined, + cspPolicy: false, +}; + +export const csToJsMarshalers = new Map(); +export const jsToCsMarshalers = new Map(); +export const boundCsFunctionSymbol = Symbol.for("wasm bound_cs_function"); +export const boundJsFunctionSymbol = Symbol.for("wasm bound_js_function"); +export const importedJsFunctionSymbol = Symbol.for("wasm imported_js_function"); +export const proxyDebugSymbol = Symbol.for("wasm proxyDebug"); + +export function allocStackFrame(size: number): JSMarshalerArguments { + const bytes = JavaScriptMarshalerArgSize * size; + const args = Module.stackAlloc(bytes) as any; + dotnetBrowserUtilsExports.zeroRegion(args, bytes); + setArgsContext(args); + return args; +} + +export function getArg(args: JSMarshalerArguments, index: number): JSMarshalerArgument { + dotnetAssert.check(args, "Null args"); + return args + (index * JavaScriptMarshalerArgSize); +} + +export function isArgsException(args: JSMarshalerArguments): boolean { + dotnetAssert.check(args, "Null args"); + const exceptionType = getArgType(args); + return exceptionType !== MarshalerType.None; +} + +export function isReceiverShouldFree(args: JSMarshalerArguments): boolean { + dotnetAssert.check(args, "Null args"); + return dotnetApi.getHeapB8(args + JSMarshalerArgumentOffsets.ReceiverShouldFree); +} + +export function getSyncDoneSemaphorePtr(args: JSMarshalerArguments): VoidPtr { + dotnetAssert.check(args, "Null args"); + return dotnetApi.getHeapI32(args + JSMarshalerArgumentOffsets.SyncDoneSemaphorePtr) as any; +} + +export function getCallerNativeTid(args: JSMarshalerArguments): PThreadPtr { + dotnetAssert.check(args, "Null args"); + return dotnetApi.getHeapI32(args + JSMarshalerArgumentOffsets.CallerNativeTID) as any; +} + +export function setReceiverShouldFree(args: JSMarshalerArguments): void { + dotnetApi.setHeapB8(args + JSMarshalerArgumentOffsets.ReceiverShouldFree, true); +} + +export function setArgsContext(args: JSMarshalerArguments): void { + dotnetAssert.check(args, "Null args"); + const exc = getArg(args, 0); + const res = getArg(args, 1); + setArgProxyContext(exc); + setArgProxyContext(res); +} + +export function getSig(signature: JSFunctionSignature, index: number): JSMarshalerType { + dotnetAssert.check(signature, "Null signatures"); + return signature + (index * JSMarshalerTypeSize) + JSMarshalerSignatureHeaderSize; +} + +export function getSignatureType(sig: JSMarshalerType): MarshalerType { + dotnetAssert.check(sig, "Null sig"); + return dotnetApi.getHeapU8(sig + JSBindingTypeOffsets.Type); +} + +export function getSignatureResType(sig: JSMarshalerType): MarshalerType { + dotnetAssert.check(sig, "Null sig"); + return dotnetApi.getHeapU8(sig + JSBindingTypeOffsets.ResultMarshalerType); +} + +export function getSignatureArg1Type(sig: JSMarshalerType): MarshalerType { + dotnetAssert.check(sig, "Null sig"); + return dotnetApi.getHeapU8(sig + JSBindingTypeOffsets.Arg1MarshalerType); +} + +export function getSignatureArg2Type(sig: JSMarshalerType): MarshalerType { + dotnetAssert.check(sig, "Null sig"); + return dotnetApi.getHeapU8(sig + JSBindingTypeOffsets.Arg2MarshalerType); +} + +export function getSignatureArg3Type(sig: JSMarshalerType): MarshalerType { + dotnetAssert.check(sig, "Null sig"); + return dotnetApi.getHeapU8(sig + JSBindingTypeOffsets.Arg3MarshalerType); +} + +export function getSignatureArgumentCount(signature: JSFunctionSignature): number { + dotnetAssert.check(signature, "Null signatures"); + return dotnetApi.getHeapI32(signature + JSBindingHeaderOffsets.ArgumentCount); +} + +export function getSignatureVersion(signature: JSFunctionSignature): number { + dotnetAssert.check(signature, "Null signatures"); + return dotnetApi.getHeapI32(signature + JSBindingHeaderOffsets.Version); +} + +export function getSignatureHandle(signature: JSFunctionSignature): number { + dotnetAssert.check(signature, "Null signatures"); + return dotnetApi.getHeapI32(signature + JSBindingHeaderOffsets.ImportHandle); +} + +export function getSignatureFunctionName(signature: JSFunctionSignature): string | null { + dotnetAssert.check(signature, "Null signatures"); + const functionNameOffset = dotnetApi.getHeapI32(signature + JSBindingHeaderOffsets.FunctionNameOffset); + if (functionNameOffset === 0) return null; + const functionNameLength = dotnetApi.getHeapI32(signature + JSBindingHeaderOffsets.FunctionNameLength); + dotnetAssert.check(functionNameOffset, "Null name"); + return dotnetBrowserUtilsExports.utf16ToString(signature + functionNameOffset, signature + functionNameOffset + functionNameLength); +} + +export function getSignatureModuleName(signature: JSFunctionSignature): string | null { + dotnetAssert.check(signature, "Null signatures"); + const moduleNameOffset = dotnetApi.getHeapI32(signature + JSBindingHeaderOffsets.ModuleNameOffset); + if (moduleNameOffset === 0) return null; + const moduleNameLength = dotnetApi.getHeapI32(signature + JSBindingHeaderOffsets.ModuleNameLength); + return dotnetBrowserUtilsExports.utf16ToString(signature + moduleNameOffset, signature + moduleNameOffset + moduleNameLength); +} + +export function getSigType(sig: JSMarshalerType): MarshalerType { + dotnetAssert.check(sig, "Null signatures"); + return dotnetApi.getHeapU8(sig); +} + +export function getArgType(arg: JSMarshalerArgument): MarshalerType { + dotnetAssert.check(arg, "Null arg"); + const type = dotnetApi.getHeapU8(arg + JSMarshalerArgumentOffsets.Type); + return type; +} + +export function getArgElementType(arg: JSMarshalerArgument): MarshalerType { + dotnetAssert.check(arg, "Null arg"); + const type = dotnetApi.getHeapU8(arg + JSMarshalerArgumentOffsets.ElementType); + return type; +} + +export function setArgType(arg: JSMarshalerArgument, type: MarshalerType): void { + dotnetAssert.check(arg, "Null arg"); + dotnetApi.setHeapU8(arg + JSMarshalerArgumentOffsets.Type, type); +} + +export function setArgElementType(arg: JSMarshalerArgument, type: MarshalerType): void { + dotnetAssert.check(arg, "Null arg"); + dotnetApi.setHeapU8(arg + JSMarshalerArgumentOffsets.ElementType, type); +} + +export function getArgBool(arg: JSMarshalerArgument): boolean { + dotnetAssert.check(arg, "Null arg"); + return dotnetApi.getHeapB8(arg); +} + +export function getArgU8(arg: JSMarshalerArgument): number { + dotnetAssert.check(arg, "Null arg"); + return dotnetApi.getHeapU8(arg); +} + +export function getArgU16(arg: JSMarshalerArgument): number { + dotnetAssert.check(arg, "Null arg"); + return dotnetApi.getHeapU16(arg); +} + +export function getArgI16(arg: JSMarshalerArgument): number { + dotnetAssert.check(arg, "Null arg"); + return dotnetApi.getHeapI16(arg); +} + +export function getArgI32(arg: JSMarshalerArgument): number { + dotnetAssert.check(arg, "Null arg"); + return dotnetApi.getHeapI32(arg); +} + +export function getArgIntptr(arg: JSMarshalerArgument): number { + dotnetAssert.check(arg, "Null arg"); + return dotnetApi.getHeapU32(arg); +} + +export function getArgI52(arg: JSMarshalerArgument): number { + dotnetAssert.check(arg, "Null arg"); + // we know that the range check and conversion from Int64 was be done on C# side + return dotnetApi.getHeapF64(arg); +} + +export function getArgI64Big(arg: JSMarshalerArgument): bigint { + dotnetAssert.check(arg, "Null arg"); + return dotnetApi.getHeapI64Big(arg); +} + +export function getArgDate(arg: JSMarshalerArgument): Date { + dotnetAssert.check(arg, "Null arg"); + const unixTime = dotnetApi.getHeapF64(arg); + const date = new Date(unixTime); + return date; +} + +export function getArgF32(arg: JSMarshalerArgument): number { + dotnetAssert.check(arg, "Null arg"); + return dotnetApi.getHeapF32(arg); +} + +export function getArgF64(arg: JSMarshalerArgument): number { + dotnetAssert.check(arg, "Null arg"); + return dotnetApi.getHeapF64(arg); +} + +export function setArgBool(arg: JSMarshalerArgument, value: boolean): void { + dotnetAssert.check(arg, "Null arg"); + dotnetAssert.check(typeof value === "boolean", () => `Value is not a Boolean: ${value} (${typeof (value)})`); + dotnetApi.setHeapB8(arg, value); +} + +export function setArgU8(arg: JSMarshalerArgument, value: number): void { + dotnetAssert.check(arg, "Null arg"); + dotnetApi.setHeapU8(arg, value); +} + +export function setArgU16(arg: JSMarshalerArgument, value: number): void { + dotnetAssert.check(arg, "Null arg"); + dotnetApi.setHeapU16(arg, value); +} + +export function setArgI16(arg: JSMarshalerArgument, value: number): void { + dotnetAssert.check(arg, "Null arg"); + dotnetApi.setHeapI16(arg, value); +} + +export function setArgI32(arg: JSMarshalerArgument, value: number): void { + dotnetAssert.check(arg, "Null arg"); + dotnetApi.setHeapI32(arg, value); +} + +export function setArgIntptr(arg: JSMarshalerArgument, value: VoidPtr): void { + dotnetAssert.check(arg, "Null arg"); + dotnetApi.setHeapU32(arg, value); +} + +export function setArgI52(arg: JSMarshalerArgument, value: number): void { + dotnetAssert.check(arg, "Null arg"); + dotnetAssert.check(Number.isSafeInteger(value), () => `Value is not an integer: ${value} (${typeof (value)})`); + // we know that conversion to Int64 would be done on C# side + dotnetApi.setHeapF64(arg, value); +} + +export function setArgI64Big(arg: JSMarshalerArgument, value: bigint): void { + dotnetAssert.check(arg, "Null arg"); + dotnetApi.setHeapI64Big(arg, value); +} + +export function setArgDate(arg: JSMarshalerArgument, value: Date): void { + dotnetAssert.check(arg, "Null arg"); + // getTime() is always UTC + const unixTime = value.getTime(); + dotnetApi.setHeapF64(arg, unixTime); +} + +export function setArgF64(arg: JSMarshalerArgument, value: number): void { + dotnetAssert.check(arg, "Null arg"); + dotnetApi.setHeapF64(arg, value); +} + +export function setArgF32(arg: JSMarshalerArgument, value: number): void { + dotnetAssert.check(arg, "Null arg"); + dotnetApi.setHeapF32(arg, value); +} + +export function getArgJsHandle(arg: JSMarshalerArgument): JSHandle { + dotnetAssert.check(arg, "Null arg"); + return dotnetApi.getHeapI32(arg + JSMarshalerArgumentOffsets.JSHandle); +} + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export function setArgProxyContext(arg: JSMarshalerArgument): void { + /*TODO-WASM threads only + dotnetAssert.check(arg, "Null arg"); + dotnetApi.setHeapI32(arg + JSMarshalerArgumentOffsets.ContextHandle, jsInteropState.proxyGCHandle); + */ +} + +export function setJsHandle(arg: JSMarshalerArgument, jsHandle: JSHandle): void { + dotnetAssert.check(arg, "Null arg"); + dotnetApi.setHeapI32(arg + JSMarshalerArgumentOffsets.JSHandle, jsHandle); + setArgProxyContext(arg); +} + +export function getArgGcHandle(arg: JSMarshalerArgument): GCHandle { + dotnetAssert.check(arg, "Null arg"); + return dotnetApi.getHeapI32(arg + JSMarshalerArgumentOffsets.GCHandle); +} + +export function setGcHandle(arg: JSMarshalerArgument, gcHandle: GCHandle): void { + dotnetAssert.check(arg, "Null arg"); + dotnetApi.setHeapI32(arg + JSMarshalerArgumentOffsets.GCHandle, gcHandle); + setArgProxyContext(arg); +} + +export function getArgLength(arg: JSMarshalerArgument): number { + dotnetAssert.check(arg, "Null arg"); + return dotnetApi.getHeapI32(arg + JSMarshalerArgumentOffsets.Length); +} + +export function setArgLength(arg: JSMarshalerArgument, size: number): void { + dotnetAssert.check(arg, "Null arg"); + dotnetApi.setHeapI32(arg + JSMarshalerArgumentOffsets.Length, size); +} + +export function getSignatureMarshaler(signature: JSFunctionSignature, index: number): JSHandle { + dotnetAssert.check(signature, "Null signatures"); + const sig = getSig(signature, index); + return dotnetApi.getHeapU32(sig + JSBindingHeaderOffsets.ImportHandle); +} + +export function arrayElementSize(elementType: MarshalerType): number { + return elementType == MarshalerType.Byte ? 1 + : elementType == MarshalerType.Int32 ? 4 + : elementType == MarshalerType.Int52 ? 8 + : elementType == MarshalerType.Double ? 8 + : elementType == MarshalerType.String ? JavaScriptMarshalerArgSize + : elementType == MarshalerType.Object ? JavaScriptMarshalerArgSize + : elementType == MarshalerType.JSObject ? JavaScriptMarshalerArgSize + : -1; +} diff --git a/src/native/libs/System.Runtime.InteropServices.JavaScript.Native/interop/marshaled-types.ts b/src/native/libs/System.Runtime.InteropServices.JavaScript.Native/interop/marshaled-types.ts new file mode 100644 index 00000000000000..1f3b1fe6f0700f --- /dev/null +++ b/src/native/libs/System.Runtime.InteropServices.JavaScript.Native/interop/marshaled-types.ts @@ -0,0 +1,166 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +import { TypedArray, VoidPtr } from "../types"; +import { jsOwnedGcHandleSymbol, teardownManagedProxy } from "./gc-handles"; +import { getManagedStackTrace } from "./managed-exports"; +import { GCHandleNull, IMemoryView } from "./types"; +import { isRuntimeRunning } from "./utils"; + +export const enum MemoryViewType { + Byte = 0, + Int32 = 1, + Double = 2, +} + +abstract class MemoryView implements IMemoryView { + protected constructor(public _pointer: VoidPtr, public _length: number, public _viewType: MemoryViewType) { + } + + abstract dispose(): void; + abstract get isDisposed(): boolean; + + _unsafe_create_view(): TypedArray { + // this view must be short lived so that it doesn't fail after wasm memory growth + // for that reason we also don't give the view out to end user and provide set/slice/copyTo API instead + const view = this._viewType == MemoryViewType.Byte ? new Uint8Array(dotnetApi.localHeapViewU8().buffer, this._pointer as any >>> 0, this._length) + : this._viewType == MemoryViewType.Int32 ? new Int32Array(dotnetApi.localHeapViewI32().buffer, this._pointer as any >>> 0, this._length) + : this._viewType == MemoryViewType.Double ? new Float64Array(dotnetApi.localHeapViewF64().buffer, this._pointer as any >>> 0, this._length) + : null; + if (!view) throw new Error("NotImplementedException"); + return view; + } + + set(source: TypedArray, targetOffset?: number): void { + dotnetAssert.check(!this.isDisposed, "ObjectDisposedException"); + const targetView = this._unsafe_create_view(); + dotnetAssert.check(source && targetView && source.constructor === targetView.constructor, () => `Expected ${targetView.constructor}`); + targetView.set(source, targetOffset || 0 >>> 0); + // TODO consider memory write barrier + } + + copyTo(target: TypedArray, sourceOffset?: number): void { + dotnetAssert.check(!this.isDisposed, "ObjectDisposedException"); + const sourceView = this._unsafe_create_view(); + dotnetAssert.check(target && sourceView && target.constructor === sourceView.constructor, () => `Expected ${sourceView.constructor}`); + const trimmedSource = sourceView.subarray(sourceOffset || 0 >>> 0); + // TODO consider memory read barrier + target.set(trimmedSource); + } + + slice(start?: number, end?: number): TypedArray { + dotnetAssert.check(!this.isDisposed, "ObjectDisposedException"); + const sourceView = this._unsafe_create_view(); + // TODO consider memory read barrier + return sourceView.slice(start || 0 >>> 0, end || 0 >>> 0); + } + + get length(): number { + dotnetAssert.check(!this.isDisposed, "ObjectDisposedException"); + return this._length; + } + + get byteLength(): number { + dotnetAssert.check(!this.isDisposed, "ObjectDisposedException"); + return this._viewType == MemoryViewType.Byte ? this._length + : this._viewType == MemoryViewType.Int32 ? this._length << 2 + : this._viewType == MemoryViewType.Double ? this._length << 3 + : 0; + } +} + + +export class Span extends MemoryView { + private _isDisposed = false; + public constructor(pointer: VoidPtr, length: number, viewType: MemoryViewType) { + super(pointer, length, viewType); + } + dispose(): void { + this._isDisposed = true; + } + get isDisposed(): boolean { + return this._isDisposed; + } +} + +export class ArraySegment extends MemoryView { + public constructor(pointer: VoidPtr, length: number, viewType: MemoryViewType) { + super(pointer, length, viewType); + } + + dispose(): void { + teardownManagedProxy(this, GCHandleNull); + } + + get isDisposed(): boolean { + return (this as any)[jsOwnedGcHandleSymbol] === GCHandleNull; + } +} + +export interface IDisposable { + dispose(): void; + get isDisposed(): boolean; +} + +export class ManagedObject implements IDisposable { + dispose(): void { + teardownManagedProxy(this, GCHandleNull); + } + + get isDisposed(): boolean { + return (this as any)[jsOwnedGcHandleSymbol] === GCHandleNull; + } + + toString(): string { + return `CsObject(gcHandle: ${(this as any)[jsOwnedGcHandleSymbol]})`; + } +} + +export class ManagedError extends Error implements IDisposable { + private superStack: any; + private managedStack: any; + constructor(message: string) { + super(message); + this.superStack = Object.getOwnPropertyDescriptor(this, "stack"); // this works on Chrome + Object.defineProperty(this, "stack", { + get: this.getManageStack, + }); + } + + getSuperStack() { + if (this.superStack) { + if (this.superStack.value !== undefined) + return this.superStack.value; + if (this.superStack.get !== undefined) + return this.superStack.get.call(this); + } + return super.stack; // this works on FF + } + + getManageStack() { + if (this.managedStack) { + return this.managedStack; + } + if (!isRuntimeRunning()) { + this.managedStack = "... omitted managed stack trace.\n" + this.getSuperStack(); + return this.managedStack; + } + const gcHandle = (this as any)[jsOwnedGcHandleSymbol]; + if (gcHandle !== GCHandleNull) { + const managedStack = getManagedStackTrace(gcHandle); + if (managedStack) { + this.managedStack = managedStack + "\n" + this.getSuperStack(); + return this.managedStack; + } + } + return this.getSuperStack(); + } + + dispose(): void { + teardownManagedProxy(this, GCHandleNull); + } + + get isDisposed(): boolean { + return (this as any)[jsOwnedGcHandleSymbol] === GCHandleNull; + } +} diff --git a/src/native/libs/System.Runtime.InteropServices.JavaScript.Native/interop/types.ts b/src/native/libs/System.Runtime.InteropServices.JavaScript.Native/interop/types.ts index 95021536b19918..6a5ea7dafb42ac 100644 --- a/src/native/libs/System.Runtime.InteropServices.JavaScript.Native/interop/types.ts +++ b/src/native/libs/System.Runtime.InteropServices.JavaScript.Native/interop/types.ts @@ -19,4 +19,149 @@ export interface JSMarshalerArgument extends NativePointer { __brand: "JSMarshalerArgument" } +export type PThreadPtr = { + __brand: "PThreadPtr" // like pthread_t in C +} +export type GCHandle = { + __brand: "GCHandle" +} +export type JSHandle = { + __brand: "JSHandle" +} +export type JSFnHandle = { + __brand: "JSFnHandle" +} + +export type WeakRefInternal = WeakRef & { + dispose?: () => void +} + +export const JSHandleDisposed: JSHandle = -1; +export const JSHandleNull: JSHandle = 0; +export const GCHandleNull: GCHandle = 0; +export const GCHandleInvalid: GCHandle = -1; + +export type MarshalerToJs = (arg: JSMarshalerArgument, elementType?: MarshalerType, resConverter?: MarshalerToJs, arg1Converter?: MarshalerToCs, arg2Converter?: MarshalerToCs, arg3Converter?: MarshalerToCs) => any; +export type MarshalerToCs = (arg: JSMarshalerArgument, value: any, elementType?: MarshalerType, resConverter?: MarshalerToCs, arg1Converter?: MarshalerToJs, arg2Converter?: MarshalerToJs, arg3Converter?: MarshalerToJs) => void; +export type BoundMarshalerToJs = (args: JSMarshalerArguments) => any; +export type BoundMarshalerToCs = (args: JSMarshalerArguments, value: any) => void; +// please keep in sync with src\libraries\System.Runtime.InteropServices.JavaScript\src\System\Runtime\InteropServices\JavaScript\MarshalerType.cs +export const enum MarshalerType { + None = 0, + Void = 1, + Discard, + Boolean, + Byte, + Char, + Int16, + Int32, + Int52, + BigInt64, + Double, + Single, + IntPtr, + JSObject, + Object, + String, + Exception, + DateTime, + DateTimeOffset, + + Nullable, + Task, + Array, + ArraySegment, + Span, + Action, + Function, + DiscardNoWait, + + // only on runtime + JSException, + TaskResolved, + TaskRejected, + TaskPreCreated, +} + +export type WrappedJSFunction = (args: JSMarshalerArguments) => void; + +export type BindingClosure = { + fn: Function, + fqn: string, + isDisposed: boolean, + argsCount: number, + argMarshalers: (BoundMarshalerToJs)[], + resConverter: BoundMarshalerToCs | undefined, + hasCleanup: boolean, + isDiscardNoWait: boolean, + isAsync: boolean, + argCleanup: (Function | undefined)[] +} + +// TODO-WASM: drop mono prefixes, move the type +export const enum MeasuredBlock { + emscriptenStartup = "mono.emscriptenStartup", + instantiateWasm = "mono.instantiateWasm", + preRun = "mono.preRun", + preRunWorker = "mono.preRunWorker", + onRuntimeInitialized = "mono.onRuntimeInitialized", + postRun = "mono.postRun", + postRunWorker = "mono.postRunWorker", + startRuntime = "mono.startRuntime", + loadRuntime = "mono.loadRuntime", + bindingsInit = "mono.bindingsInit", + bindJsFunction = "mono.bindJsFunction:", + bindCsFunction = "mono.bindCsFunction:", + callJsFunction = "mono.callJsFunction:", + callCsFunction = "mono.callCsFunction:", + getAssemblyExports = "mono.getAssemblyExports:", + instantiateAsset = "mono.instantiateAsset:", +} + +export const JavaScriptMarshalerArgSize = 32; +// keep in sync with JSMarshalerArgumentImpl offsets +export const enum JSMarshalerArgumentOffsets { + /* eslint-disable @typescript-eslint/no-duplicate-enum-values */ + BooleanValue = 0, + ByteValue = 0, + CharValue = 0, + Int16Value = 0, + Int32Value = 0, + Int64Value = 0, + SingleValue = 0, + DoubleValue = 0, + IntPtrValue = 0, + JSHandle = 4, + GCHandle = 4, + Length = 8, + Type = 12, + ElementType = 13, + ContextHandle = 16, + ReceiverShouldFree = 20, + CallerNativeTID = 24, + SyncDoneSemaphorePtr = 28, +} +export const JSMarshalerTypeSize = 32; +// keep in sync with JSFunctionBinding.JSBindingType +export const enum JSBindingTypeOffsets { + Type = 0, + ResultMarshalerType = 16, + Arg1MarshalerType = 20, + Arg2MarshalerType = 24, + Arg3MarshalerType = 28, +} +export const JSMarshalerSignatureHeaderSize = 4 * 8; // without Exception and Result +// keep in sync with JSFunctionBinding.JSBindingHeader +export const enum JSBindingHeaderOffsets { + Version = 0, + ArgumentCount = 4, + ImportHandle = 8, + FunctionNameOffset = 16, + FunctionNameLength = 20, + ModuleNameOffset = 24, + ModuleNameLength = 28, + Exception = 32, + Result = 64, +} + export * from "../types"; diff --git a/src/native/libs/System.Runtime.InteropServices.JavaScript.Native/interop/utils.ts b/src/native/libs/System.Runtime.InteropServices.JavaScript.Native/interop/utils.ts new file mode 100644 index 00000000000000..767c87619acc8d --- /dev/null +++ b/src/native/libs/System.Runtime.InteropServices.JavaScript.Native/interop/utils.ts @@ -0,0 +1,49 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +export function fixupPointer(signature: any, shiftAmount: number): any { + return ((signature as any) >>> shiftAmount) as any; +} + +export function normalizeException(ex: any) { + let res = "unknown exception"; + if (ex) { + res = ex.toString(); + const stack = ex.stack; + if (stack) { + // Some JS runtimes insert the error message at the top of the stack, some don't, + // so normalize it by using the stack as the result if it already contains the error + if (stack.startsWith(res)) + res = stack; + else + res += "\n" + stack; + } + + // TODO-WASM + // res = mono_wasm_symbolicate_string(res); + } + return res; +} + +export function isRuntimeRunning(): boolean { + // TODO-WASM + return true; +} + +export function assertRuntimeRunning(): void { + // TODO-WASM +} + +export function assertJsInterop(): void { + // TODO-WASM +} + +export function startMeasure(): number { + // TODO-WASM + return 0; +} + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export function endMeasure(mark: number, fqn: string, additionalInfo: string): void { + // TODO-WASM +} diff --git a/src/native/libs/System.Runtime.InteropServices.JavaScript.Native/interop/weak-ref.ts b/src/native/libs/System.Runtime.InteropServices.JavaScript.Native/interop/weak-ref.ts new file mode 100644 index 00000000000000..baf4d1bb99a65a --- /dev/null +++ b/src/native/libs/System.Runtime.InteropServices.JavaScript.Native/interop/weak-ref.ts @@ -0,0 +1,26 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +import { WeakRefInternal } from "./types"; + +export const useWeakRef = typeof globalThis.WeakRef === "function"; + +export function createWeakRef(jsObj: T): WeakRefInternal { + if (useWeakRef) { + return new WeakRef(jsObj); + } else { + // this is trivial WeakRef replacement, which holds strong reference, instead of weak one, when the browser doesn't support it + return createStrongRef(jsObj); + } +} + +export function createStrongRef(jsObj: T): WeakRefInternal { + return { + deref: () => { + return jsObj; + }, + dispose: () => { + jsObj = null!; + } + }; +} diff --git a/src/native/libs/System.Runtime.InteropServices.JavaScript.Native/libSystem.Runtime.InteropServices.JavaScript.Native.footer.js b/src/native/libs/System.Runtime.InteropServices.JavaScript.Native/libSystem.Runtime.InteropServices.JavaScript.Native.footer.js index 3544781dd9fa2a..788a8fa526448d 100644 --- a/src/native/libs/System.Runtime.InteropServices.JavaScript.Native/libSystem.Runtime.InteropServices.JavaScript.Native.footer.js +++ b/src/native/libs/System.Runtime.InteropServices.JavaScript.Native/libSystem.Runtime.InteropServices.JavaScript.Native.footer.js @@ -19,7 +19,7 @@ const exports = {}; libInteropJavaScriptNative(exports); - let commonDeps = ["$DOTNET"]; + let commonDeps = ["$DOTNET", "SystemInteropJS_GetManagedStackTrace"]; const lib = { $DOTNET_INTEROP: { selfInitialize: () => { diff --git a/src/native/libs/System.Runtime.InteropServices.JavaScript.Native/native/cross-linked.ts b/src/native/libs/System.Runtime.InteropServices.JavaScript.Native/native/cross-linked.ts index ea10f4707c6a6c..bdafb667cb1443 100644 --- a/src/native/libs/System.Runtime.InteropServices.JavaScript.Native/native/cross-linked.ts +++ b/src/native/libs/System.Runtime.InteropServices.JavaScript.Native/native/cross-linked.ts @@ -2,4 +2,8 @@ // The .NET Foundation licenses this file to you under the MIT license. import { } from "../../Common/JavaScript/cross-linked"; +import type { JSMarshalerArguments } from "../interop/types"; +declare global { + export function _SystemInteropJS_GetManagedStackTrace(args: JSMarshalerArguments): void; +} diff --git a/src/native/libs/System.Runtime.InteropServices.JavaScript.Native/native/index.ts b/src/native/libs/System.Runtime.InteropServices.JavaScript.Native/native/index.ts index 3dc342b2746f37..cb8762f0f06224 100644 --- a/src/native/libs/System.Runtime.InteropServices.JavaScript.Native/native/index.ts +++ b/src/native/libs/System.Runtime.InteropServices.JavaScript.Native/native/index.ts @@ -1,24 +1,43 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -import type { InternalExchange, InteropJavaScriptExports, InteropJavaScriptExportsTable, JSFnHandle, JSMarshalerArguments } from "../interop/types"; -import { InternalExchangeIndex } from "../types"; +import type { InternalExchange, InteropJavaScriptExports, InteropJavaScriptExportsTable, JSFnHandle, JSFunctionSignature, JSMarshalerArguments, VoidPtr } from "../interop/types"; +import { GCHandle, InternalExchangeIndex, JSHandle } from "../types"; import { } from "./cross-linked"; // ensure ambient symbols are declared -// eslint-disable-next-line @typescript-eslint/no-unused-vars -export function SystemInteropJS_InvokeJSImportST(function_handle: JSFnHandle, args: JSMarshalerArguments) { - // WASM-TODO implementation - dotnetLogger.error("SystemInteropJS_InvokeJSImportST called"); - return - 1; +export function SystemInteropJS_BindJSImportST(signature: JSFunctionSignature): VoidPtr { + return dotnetRuntimeExports.bindJSImportST(signature); +} + +export function SystemInteropJS_InvokeJSImportST(functionHandle: JSFnHandle, args: JSMarshalerArguments): void { + dotnetRuntimeExports.invokeJSImportST(functionHandle, args); +} + +export function SystemInteropJS_ReleaseCSOwnedObject(jsHandle: JSHandle): void { + dotnetRuntimeExports.releaseCSOwnedObject(jsHandle); +} + +export function SystemInteropJS_ResolveOrRejectPromise(args: JSMarshalerArguments): void { + dotnetRuntimeExports.resolveOrRejectPromise(args); +} + +export function SystemInteropJS_CancelPromise(taskHolderGCHandle: GCHandle): void { + dotnetRuntimeExports.cancelPromise(taskHolderGCHandle); +} + +export function SystemInteropJS_InvokeJSFunction(functionJSSHandle: JSHandle, args: JSMarshalerArguments): void { + dotnetRuntimeExports.invokeJSFunction(functionJSSHandle, args); } export function dotnetInitializeModule(internals: InternalExchange): void { internals[InternalExchangeIndex.InteropJavaScriptExportsTable] = interopJavaScriptExportsToTable({ + SystemInteropJS_GetManagedStackTrace: (args) => _SystemInteropJS_GetManagedStackTrace(args), }); // eslint-disable-next-line @typescript-eslint/no-unused-vars function interopJavaScriptExportsToTable(map: InteropJavaScriptExports): InteropJavaScriptExportsTable { // keep in sync with interopJavaScriptExportsFromTable() return [ + map.SystemInteropJS_GetManagedStackTrace, ]; } dotnetUpdateInternals(internals, dotnetUpdateInternalsSubscriber); diff --git a/src/native/libs/build-native.proj b/src/native/libs/build-native.proj index 4b586b6d3e9d13..855e0830097c07 100644 --- a/src/native/libs/build-native.proj +++ b/src/native/libs/build-native.proj @@ -1,5 +1,5 @@ - + @@ -26,7 +26,7 @@ + DependsOnTargets="AcquireWasiSdk;GenerateNativeVersionFile;GenerateEmccExports"> <_BuildNativeEnvironmentVariables>WASI_SDK_PATH=$(RuntimeBuildWasiSdkPath) diff --git a/src/native/rollup.config.defines.js b/src/native/rollup.config.defines.js index 55d08ee96d2919..30853589eb2b08 100644 --- a/src/native/rollup.config.defines.js +++ b/src/native/rollup.config.defines.js @@ -18,7 +18,7 @@ if (process.env.ProductVersion === undefined) { export const configuration = process.env.Configuration !== "Release" && process.env.Configuration !== "RELEASE" ? "Debug" : "Release"; export const productVersion = process.env.ProductVersion; export const isContinuousIntegrationBuild = process.env.ContinuousIntegrationBuild === "true" ? true : false; -export const staticLibDestination = process.env.StaticLibDestination; +export const staticLibDestination = process.env.StaticLibDestination || "../../artifacts/bin/browser-wasm.Debug/corehost"; console.log(`Rollup configuration: Configuration=${configuration}, ProductVersion=${productVersion}, ContinuousIntegrationBuild=${isContinuousIntegrationBuild}`); diff --git a/src/native/rollup.config.js b/src/native/rollup.config.js index 5c69511139bfbc..c3ac696471176b 100644 --- a/src/native/rollup.config.js +++ b/src/native/rollup.config.js @@ -89,6 +89,21 @@ const libBrowserUtils = configure({ } }); +const dotnetDiagnosticsJS = configure({ + input: "./libs/System.Native.Browser/diagnostics/index.ts", + output: [{ + file: staticLibDestination + "/dotnet.diagnostics.js", + }], + terser: { + compress: { + module: true, + }, mangle: { + module: true, + keep_classnames, + } + } +}); + const dotnetRuntimeJS = configure({ input: "./libs/System.Runtime.InteropServices.JavaScript.Native/dotnet.runtime.ts", output: [{ @@ -149,6 +164,7 @@ export default defineConfig([ dotnetDTS, libNativeBrowser, libBrowserUtils, + dotnetDiagnosticsJS, dotnetRuntimeJS, libInteropJavaScriptNative, libBrowserHost,