diff --git a/eng/testing/tests.browser.targets b/eng/testing/tests.browser.targets index 5e88f3b4922cbd..cf0958e4301d28 100644 --- a/eng/testing/tests.browser.targets +++ b/eng/testing/tests.browser.targets @@ -25,6 +25,7 @@ $([MSBuild]::NormalizeDirectory($(BrowserProjectRoot), 'emsdk')) true + false <_WasmMainJSFileName Condition="'$(WasmMainJSPath)' != ''">$([System.IO.Path]::GetFileName('$(WasmMainJSPath)')) <_WasmStrictVersionMatch Condition="'$(ContinuousIntegrationBuild)' == 'true'">true @@ -47,6 +48,11 @@ false true true + + + false + false + false diff --git a/eng/testing/workloads-browser.targets b/eng/testing/workloads-browser.targets index 4bb3ddb7502fdf..5dba0dfca8219b 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'))" /> + @@ -93,7 +97,7 @@ g__Callback|72_0#1:System.Private.CoreLib:System:GC */, - { 2638830556, 1336557534, { &MD_System_Private_CoreLib_System_Threading_ThreadPool_BackgroundJobHandler_Void_RetVoid, (void*)&Call_System_Private_CoreLib_System_Threading_ThreadPool_BackgroundJobHandler_Void_RetVoid } } /* alternate key source: BackgroundJobHandler#0:System.Private.CoreLib:System.Threading:ThreadPool */, - { 2638833091, 3378852959, { &MD_System_Private_CoreLib_System_GC_ConfigCallback_I32_I32_I32_I32_I64_RetVoid, (void*)&Call_System_Private_CoreLib_System_GC_ConfigCallback_I32_I32_I32_I32_I64_RetVoid } } /* alternate key source: ConfigCallback#5:System.Private.CoreLib:System:GC */, - { 2638826837, 1196551088, { &MD_System_Private_CoreLib_System_Globalization_CalendarData_EnumCalendarInfoCallback_I32_I32_RetVoid, (void*)&Call_System_Private_CoreLib_System_Globalization_CalendarData_EnumCalendarInfoCallback_I32_I32_RetVoid } } /* alternate key source: EnumCalendarInfoCallback#2:System.Private.CoreLib:System.Globalization:CalendarData */, - { 2638856334, 2613312799, { &MD_System_Private_CoreLib_Internal_Runtime_InteropServices_ComActivator_GetClassFactoryForTypeInternal_I32_RetI32, (void*)&Call_System_Private_CoreLib_Internal_Runtime_InteropServices_ComActivator_GetClassFactoryForTypeInternal_I32_RetI32 } } /* alternate key source: GetClassFactoryForTypeInternal#1:System.Private.CoreLib:Internal.Runtime.InteropServices:ComActivator */, - { 2638856344, 993231473, { &MD_System_Private_CoreLib_Internal_Runtime_InteropServices_ComponentActivator_GetFunctionPointer_I32_I32_I32_I32_I32_I32_RetI32, (void*)&Call_System_Private_CoreLib_Internal_Runtime_InteropServices_ComponentActivator_GetFunctionPointer_I32_I32_I32_I32_I32_I32_RetI32 } } /* alternate key source: GetFunctionPointer#6:System.Private.CoreLib:Internal.Runtime.InteropServices:ComponentActivator */, - { 2638856347, 3422156547, { &MD_System_Private_CoreLib_Internal_Runtime_InteropServices_ComponentActivator_LoadAssembly_I32_I32_I32_RetI32, (void*)&Call_System_Private_CoreLib_Internal_Runtime_InteropServices_ComponentActivator_LoadAssembly_I32_I32_I32_RetI32 } } /* alternate key source: LoadAssembly#3:System.Private.CoreLib:Internal.Runtime.InteropServices:ComponentActivator */, - { 2638856348, 542185314, { &MD_System_Private_CoreLib_Internal_Runtime_InteropServices_ComponentActivator_LoadAssemblyAndGetFunctionPointer_I32_I32_I32_I32_I32_I32_RetI32, (void*)&Call_System_Private_CoreLib_Internal_Runtime_InteropServices_ComponentActivator_LoadAssemblyAndGetFunctionPointer_I32_I32_I32_I32_I32_I32_RetI32 } } /* alternate key source: LoadAssemblyAndGetFunctionPointer#6:System.Private.CoreLib:Internal.Runtime.InteropServices:ComponentActivator */, - { 2638856345, 3765950975, { &MD_System_Private_CoreLib_Internal_Runtime_InteropServices_ComponentActivator_LoadAssemblyBytes_I32_I32_I32_I32_I32_I32_RetI32, (void*)&Call_System_Private_CoreLib_Internal_Runtime_InteropServices_ComponentActivator_LoadAssemblyBytes_I32_I32_I32_I32_I32_I32_RetI32 } } /* alternate key source: LoadAssemblyBytes#6:System.Private.CoreLib:Internal.Runtime.InteropServices:ComponentActivator */, - { 2638812170, 343912841, { &MD_System_Private_CoreLib_System_Runtime_InteropServices_TypeMapLazyDictionary_NewExternalTypeEntry_I32_I32_RetI32, (void*)&Call_System_Private_CoreLib_System_Runtime_InteropServices_TypeMapLazyDictionary_NewExternalTypeEntry_I32_I32_RetI32 } } /* alternate key source: NewExternalTypeEntry#2:System.Private.CoreLib:System.Runtime.InteropServices:TypeMapLazyDictionary */, - { 2638812169, 3327247096, { &MD_System_Private_CoreLib_System_Runtime_InteropServices_TypeMapLazyDictionary_NewProxyTypeEntry_I32_I32_RetI32, (void*)&Call_System_Private_CoreLib_System_Runtime_InteropServices_TypeMapLazyDictionary_NewProxyTypeEntry_I32_I32_RetI32 } } /* alternate key source: NewProxyTypeEntry#2:System.Private.CoreLib:System.Runtime.InteropServices:TypeMapLazyDictionary */, - { 2638856333, 4239234100, { &MD_System_Private_CoreLib_Internal_Runtime_InteropServices_ComActivator_RegisterClassForTypeInternal_I32_RetI32, (void*)&Call_System_Private_CoreLib_Internal_Runtime_InteropServices_ComActivator_RegisterClassForTypeInternal_I32_RetI32 } } /* alternate key source: RegisterClassForTypeInternal#1:System.Private.CoreLib:Internal.Runtime.InteropServices:ComActivator */, - { 2638830480, 167179540, { &MD_System_Private_CoreLib_System_Threading_TimerQueue_TimerHandler_Void_RetVoid, (void*)&Call_System_Private_CoreLib_System_Threading_TimerQueue_TimerHandler_Void_RetVoid } } /* alternate key source: TimerHandler#0:System.Private.CoreLib:System.Threading:TimerQueue */, - { 2638856332, 2150642223, { &MD_System_Private_CoreLib_Internal_Runtime_InteropServices_ComActivator_UnregisterClassForTypeInternal_I32_RetI32, (void*)&Call_System_Private_CoreLib_Internal_Runtime_InteropServices_ComActivator_UnregisterClassForTypeInternal_I32_RetI32 } } /* alternate key source: UnregisterClassForTypeInternal#1:System.Private.CoreLib:Internal.Runtime.InteropServices:ComActivator */ + { 2638833081, 3863938719, { &MD_System_Private_CoreLib_System_GC__RegisterNoGCRegionCallback_g__Callback_7C_72_0_I32_RetVoid, (void*)&Call_System_Private_CoreLib_System_GC__RegisterNoGCRegionCallback_g__Callback_7C_72_0_I32_RetVoid } } /* alternate key source: g__Callback|72_0#1:System.Private.CoreLib:System:GC */, + { 2638830557, 1336557534, { &MD_System_Private_CoreLib_System_Threading_ThreadPool_BackgroundJobHandler_Void_RetVoid, (void*)&Call_System_Private_CoreLib_System_Threading_ThreadPool_BackgroundJobHandler_Void_RetVoid } } /* alternate key source: BackgroundJobHandler#0:System.Private.CoreLib:System.Threading:ThreadPool */, + { 363313056, 2901966433, { &MD_System_Runtime_InteropServices_JavaScript_System_Runtime_InteropServices_JavaScript_JavaScriptExports_BindAssemblyExports_I32_RetVoid, (void*)&Call_System_Runtime_InteropServices_JavaScript_System_Runtime_InteropServices_JavaScript_JavaScriptExports_BindAssemblyExports_I32_RetVoid } } /* alternate key source: BindAssemblyExports#1:System.Runtime.InteropServices.JavaScript:System.Runtime.InteropServices.JavaScript:JavaScriptExports */, + { 363313059, 2601830388, { &MD_System_Runtime_InteropServices_JavaScript_System_Runtime_InteropServices_JavaScript_JavaScriptExports_CallDelegate_I32_RetVoid, (void*)&Call_System_Runtime_InteropServices_JavaScript_System_Runtime_InteropServices_JavaScript_JavaScriptExports_CallDelegate_I32_RetVoid } } /* alternate key source: CallDelegate#1:System.Runtime.InteropServices.JavaScript:System.Runtime.InteropServices.JavaScript:JavaScriptExports */, + { 363313063, 433365813, { &MD_System_Runtime_InteropServices_JavaScript_System_Runtime_InteropServices_JavaScript_JavaScriptExports_CallJSExport_I32_I32_RetVoid, (void*)&Call_System_Runtime_InteropServices_JavaScript_System_Runtime_InteropServices_JavaScript_JavaScriptExports_CallJSExport_I32_I32_RetVoid } } /* alternate key source: CallJSExport#2:System.Runtime.InteropServices.JavaScript:System.Runtime.InteropServices.JavaScript:JavaScriptExports */, + { 363313058, 3113228365, { &MD_System_Runtime_InteropServices_JavaScript_System_Runtime_InteropServices_JavaScript_JavaScriptExports_CompleteTask_I32_RetVoid, (void*)&Call_System_Runtime_InteropServices_JavaScript_System_Runtime_InteropServices_JavaScript_JavaScriptExports_CompleteTask_I32_RetVoid } } /* alternate key source: CompleteTask#1:System.Runtime.InteropServices.JavaScript:System.Runtime.InteropServices.JavaScript:JavaScriptExports */, + { 2638833090, 3378852959, { &MD_System_Private_CoreLib_System_GC_ConfigCallback_I32_I32_I32_I32_I64_RetVoid, (void*)&Call_System_Private_CoreLib_System_GC_ConfigCallback_I32_I32_I32_I32_I64_RetVoid } } /* alternate key source: ConfigCallback#5:System.Private.CoreLib:System:GC */, + { 2638826838, 1196551088, { &MD_System_Private_CoreLib_System_Globalization_CalendarData_EnumCalendarInfoCallback_I32_I32_RetVoid, (void*)&Call_System_Private_CoreLib_System_Globalization_CalendarData_EnumCalendarInfoCallback_I32_I32_RetVoid } } /* alternate key source: EnumCalendarInfoCallback#2:System.Private.CoreLib:System.Globalization:CalendarData */, + { 2638856330, 2613312799, { &MD_System_Private_CoreLib_Internal_Runtime_InteropServices_ComActivator_GetClassFactoryForTypeInternal_I32_RetI32, (void*)&Call_System_Private_CoreLib_Internal_Runtime_InteropServices_ComActivator_GetClassFactoryForTypeInternal_I32_RetI32 } } /* alternate key source: GetClassFactoryForTypeInternal#1:System.Private.CoreLib:Internal.Runtime.InteropServices:ComActivator */, + { 2638856340, 993231473, { &MD_System_Private_CoreLib_Internal_Runtime_InteropServices_ComponentActivator_GetFunctionPointer_I32_I32_I32_I32_I32_I32_RetI32, (void*)&Call_System_Private_CoreLib_Internal_Runtime_InteropServices_ComponentActivator_GetFunctionPointer_I32_I32_I32_I32_I32_I32_RetI32 } } /* alternate key source: GetFunctionPointer#6:System.Private.CoreLib:Internal.Runtime.InteropServices:ComponentActivator */, + { 363313057, 1081971317, { &MD_System_Runtime_InteropServices_JavaScript_System_Runtime_InteropServices_JavaScript_JavaScriptExports_GetManagedStackTrace_I32_RetVoid, (void*)&Call_System_Runtime_InteropServices_JavaScript_System_Runtime_InteropServices_JavaScript_JavaScriptExports_GetManagedStackTrace_I32_RetVoid } } /* alternate key source: GetManagedStackTrace#1:System.Runtime.InteropServices.JavaScript:System.Runtime.InteropServices.JavaScript:JavaScriptExports */, + { 2638856343, 3422156547, { &MD_System_Private_CoreLib_Internal_Runtime_InteropServices_ComponentActivator_LoadAssembly_I32_I32_I32_RetI32, (void*)&Call_System_Private_CoreLib_Internal_Runtime_InteropServices_ComponentActivator_LoadAssembly_I32_I32_I32_RetI32 } } /* alternate key source: LoadAssembly#3:System.Private.CoreLib:Internal.Runtime.InteropServices:ComponentActivator */, + { 2638856344, 542185314, { &MD_System_Private_CoreLib_Internal_Runtime_InteropServices_ComponentActivator_LoadAssemblyAndGetFunctionPointer_I32_I32_I32_I32_I32_I32_RetI32, (void*)&Call_System_Private_CoreLib_Internal_Runtime_InteropServices_ComponentActivator_LoadAssemblyAndGetFunctionPointer_I32_I32_I32_I32_I32_I32_RetI32 } } /* alternate key source: LoadAssemblyAndGetFunctionPointer#6:System.Private.CoreLib:Internal.Runtime.InteropServices:ComponentActivator */, + { 2638856341, 3765950975, { &MD_System_Private_CoreLib_Internal_Runtime_InteropServices_ComponentActivator_LoadAssemblyBytes_I32_I32_I32_I32_I32_I32_RetI32, (void*)&Call_System_Private_CoreLib_Internal_Runtime_InteropServices_ComponentActivator_LoadAssemblyBytes_I32_I32_I32_I32_I32_I32_RetI32 } } /* alternate key source: LoadAssemblyBytes#6:System.Private.CoreLib:Internal.Runtime.InteropServices:ComponentActivator */, + { 2638812171, 343912841, { &MD_System_Private_CoreLib_System_Runtime_InteropServices_TypeMapLazyDictionary_NewExternalTypeEntry_I32_I32_RetI32, (void*)&Call_System_Private_CoreLib_System_Runtime_InteropServices_TypeMapLazyDictionary_NewExternalTypeEntry_I32_I32_RetI32 } } /* alternate key source: NewExternalTypeEntry#2:System.Private.CoreLib:System.Runtime.InteropServices:TypeMapLazyDictionary */, + { 2638812170, 3327247096, { &MD_System_Private_CoreLib_System_Runtime_InteropServices_TypeMapLazyDictionary_NewProxyTypeEntry_I32_I32_RetI32, (void*)&Call_System_Private_CoreLib_System_Runtime_InteropServices_TypeMapLazyDictionary_NewProxyTypeEntry_I32_I32_RetI32 } } /* alternate key source: NewProxyTypeEntry#2:System.Private.CoreLib:System.Runtime.InteropServices:TypeMapLazyDictionary */, + { 2638856329, 4239234100, { &MD_System_Private_CoreLib_Internal_Runtime_InteropServices_ComActivator_RegisterClassForTypeInternal_I32_RetI32, (void*)&Call_System_Private_CoreLib_Internal_Runtime_InteropServices_ComActivator_RegisterClassForTypeInternal_I32_RetI32 } } /* alternate key source: RegisterClassForTypeInternal#1:System.Private.CoreLib:Internal.Runtime.InteropServices:ComActivator */, + { 363313116, 1403522766, { &MD_System_Runtime_InteropServices_JavaScript_System_Runtime_InteropServices_JavaScript_JavaScriptExports_ReleaseJSOwnedObjectByGCHandle_I32_RetVoid, (void*)&Call_System_Runtime_InteropServices_JavaScript_System_Runtime_InteropServices_JavaScript_JavaScriptExports_ReleaseJSOwnedObjectByGCHandle_I32_RetVoid } } /* alternate key source: ReleaseJSOwnedObjectByGCHandle#1:System.Runtime.InteropServices.JavaScript:System.Runtime.InteropServices.JavaScript:JavaScriptExports */, + { 2638830481, 167179540, { &MD_System_Private_CoreLib_System_Threading_TimerQueue_TimerHandler_Void_RetVoid, (void*)&Call_System_Private_CoreLib_System_Threading_TimerQueue_TimerHandler_Void_RetVoid } } /* alternate key source: TimerHandler#0:System.Private.CoreLib:System.Threading:TimerQueue */, + { 2638856328, 2150642223, { &MD_System_Private_CoreLib_Internal_Runtime_InteropServices_ComActivator_UnregisterClassForTypeInternal_I32_RetI32, (void*)&Call_System_Private_CoreLib_Internal_Runtime_InteropServices_ComActivator_UnregisterClassForTypeInternal_I32_RetI32 } } /* alternate key source: UnregisterClassForTypeInternal#1:System.Private.CoreLib:Internal.Runtime.InteropServices:ComActivator */ }; const size_t g_ReverseThunksCount = sizeof(g_ReverseThunks) / sizeof(g_ReverseThunks[0]); diff --git a/src/coreclr/vm/wasm/helpers.cpp b/src/coreclr/vm/wasm/helpers.cpp index 50227addc82619..fdcc82e0d927e0 100644 --- a/src/coreclr/vm/wasm/helpers.cpp +++ b/src/coreclr/vm/wasm/helpers.cpp @@ -624,10 +624,10 @@ namespace return NULL; void* thunk = LookupThunk(keyBuffer); -#ifdef _DEBUG + if (thunk == NULL) printf("WASM calli missing for key: %s\n", keyBuffer); -#endif + return thunk; } 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..540484499e53ec --- /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* methodHandlePtr); + + // 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/tests/WasmTestRunner/WasmTestRunner.cs b/src/libraries/Common/tests/WasmTestRunner/WasmTestRunner.cs index 8c393b95f69c21..54fbcb29749130 100644 --- a/src/libraries/Common/tests/WasmTestRunner/WasmTestRunner.cs +++ b/src/libraries/Common/tests/WasmTestRunner/WasmTestRunner.cs @@ -3,11 +3,13 @@ using System; using System.Collections.Generic; +using System.IO; +using System.Reflection; +using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; using Microsoft.DotNet.XHarness.TestRunners.Common; using Microsoft.DotNet.XHarness.TestRunners.Xunit; -using System.Runtime.CompilerServices; public class WasmTestRunner : WasmApplicationEntryPoint { @@ -29,6 +31,13 @@ public static Task Main(string[] args) { return MainAsync(args); } + + protected override IEnumerable GetTestAssemblies() + { + AssemblyName an = new AssemblyName(Path.GetFileNameWithoutExtension(TestAssembly)); + Assembly assembly = Assembly.Load(an); + return new[] { new TestAssemblyInfo(assembly, TestAssembly) }; + } #endif public static async Task MainAsync(string[] args) 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..55ebce93258c36 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,9 +12,12 @@ SR.SystemRuntimeInteropServicesJavaScript_PlatformNotSupported true true + true false $(DefineConstants);FEATURE_WASM_MANAGED_THREADS $(DefineConstants);ENABLE_JS_INTEROP_BY_VALUE + $(DefineConstants);MONO + $(DefineConstants);CORECLR true @@ -23,9 +26,12 @@ - + + + - + + @@ -35,6 +41,8 @@ + + diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Interop/JavaScriptExports.CoreCLR.cs b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Interop/JavaScriptExports.CoreCLR.cs new file mode 100644 index 00000000000000..9b4c8a491abfbd --- /dev/null +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Interop/JavaScriptExports.CoreCLR.cs @@ -0,0 +1,162 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; + +namespace System.Runtime.InteropServices.JavaScript +{ + // this maps to src\native\libs\System.Runtime.InteropServices.JavaScript.Native\interop\managed-exports.ts + // the public methods are protected from trimming by DynamicDependency on JSFunctionBinding.BindJSFunction + internal static unsafe partial class JavaScriptExports + { + [UnmanagedCallersOnly(EntryPoint = "SystemInteropJS_ReleaseJSOwnedObjectByGCHandle")] + // The JS layer invokes this method when the JS wrapper for a JS owned object has been collected by the JS garbage collector + // the marshaled signature is: void ReleaseJSOwnedObjectByGCHandle(GCHandle gcHandle) + public static void ReleaseJSOwnedObjectByGCHandle(JSMarshalerArgument* argumentsBuffer) + { + ref JSMarshalerArgument argException = ref argumentsBuffer[0]; // initialized by caller in alloc_stack_frame() + ref JSMarshalerArgument arg1 = ref argumentsBuffer[2]; // initialized and set by caller + + try + { + // when we arrive here, we are on the thread which owns the proxies or on IO thread + var ctx = argException.ToManagedContext; + ctx.ReleaseJSOwnedObjectByGCHandle(arg1.slot.GCHandle); + } + catch (Exception ex) + { + Environment.FailFast($"ReleaseJSOwnedObjectByGCHandle: Unexpected synchronous failure (ManagedThreadId {Environment.CurrentManagedThreadId}): " + ex); + } + } + + [UnmanagedCallersOnly(EntryPoint = "SystemInteropJS_CallDelegate")] + // the marshaled signature is: TRes? CallDelegate(GCHandle callback, T1? arg1, T2? arg2, T3? arg3) + public static void CallDelegate(JSMarshalerArgument* argumentsBuffer) + { + ref JSMarshalerArgument argException = ref argumentsBuffer[0]; // initialized by JS caller in alloc_stack_frame() + // argResult is initialized by JS caller + ref JSMarshalerArgument arg1 = ref argumentsBuffer[2];// initialized and set by JS caller + // arg_2 set by JS caller when there are arguments + // arg_3 set by JS caller when there are arguments + // arg_4 set by JS caller when there are arguments + try + { + GCHandle callback_gc_handle = (GCHandle)arg1.slot.GCHandle; + if (callback_gc_handle.Target is JSHostImplementation.ToManagedCallback callback) + { + // arg_2, arg_3, arg_4, argResult are processed by the callback + callback(argumentsBuffer); + } + else + { + throw new InvalidOperationException(SR.NullToManagedCallback); + } + } + catch (Exception ex) + { + argException.ToJS(ex); + } + } + + [UnmanagedCallersOnly(EntryPoint = "SystemInteropJS_CompleteTask")] + // the marshaled signature is: void CompleteTask(GCHandle holder, Exception? exceptionResult, T? result) + public static void CompleteTask(JSMarshalerArgument* argumentsBuffer) + { + ref JSMarshalerArgument argException = ref argumentsBuffer[0]; // initialized by caller in alloc_stack_frame() + ref JSMarshalerArgument argResult = ref argumentsBuffer[1]; // initialized by caller in alloc_stack_frame() + ref JSMarshalerArgument arg1 = ref argumentsBuffer[2];// initialized and set by caller + // arg_2 set by caller when this is SetException call + // arg_3 set by caller when this is SetResult call + + try + { + // when we arrive here, we are on the thread which owns the proxies or on IO thread + var ctx = argException.ToManagedContext; + var holder = ctx.GetPromiseHolder(arg1.slot.GCHandle); + JSHostImplementation.ToManagedCallback callback; + + callback = holder.Callback!; + ctx.ReleasePromiseHolder(arg1.slot.GCHandle); + + // arg_2, arg_3 are processed by the callback + // JSProxyContext.PopOperation() is called by the callback + callback!(argumentsBuffer); + } + catch (Exception ex) + { + Environment.FailFast($"CompleteTask: Unexpected synchronous failure (ManagedThreadId {Environment.CurrentManagedThreadId}): " + ex); + } + } + + [UnmanagedCallersOnly(EntryPoint = "SystemInteropJS_GetManagedStackTrace")] + // the marshaled signature is: string GetManagedStackTrace(GCHandle exception) + public static void GetManagedStackTrace(JSMarshalerArgument* argumentsBuffer) + { + ref JSMarshalerArgument argException = ref argumentsBuffer[0]; // initialized by caller in alloc_stack_frame() + ref JSMarshalerArgument argResult = ref argumentsBuffer[1]; // used as return value + ref JSMarshalerArgument arg1 = ref argumentsBuffer[2];// initialized and set by caller + try + { + // when we arrive here, we are on the thread which owns the proxies + argException.AssertCurrentThreadContext(); + + GCHandle exception_gc_handle = (GCHandle)arg1.slot.GCHandle; + if (exception_gc_handle.Target is Exception exception) + { + argResult.ToJS(exception.StackTrace); + } + else + { + throw new InvalidOperationException(SR.UnableToResolveHandleAsException); + } + } + catch (Exception ex) + { + argException.ToJS(ex); + } + } + + [UnmanagedCallersOnly(EntryPoint = "SystemInteropJS_BindAssemblyExports")] + // the marshaled signature is: Task BindAssemblyExports(string assemblyName) + public static void BindAssemblyExports(JSMarshalerArgument* argumentsBuffer) + { + ref JSMarshalerArgument argException = ref argumentsBuffer[0]; // initialized by caller in alloc_stack_frame() + ref JSMarshalerArgument argResult = ref argumentsBuffer[1]; // used as return value + ref JSMarshalerArgument arg1 = ref argumentsBuffer[2];// initialized and set by caller + try + { + string? assemblyName; + // when we arrive here, we are on the thread which owns the proxies + argException.AssertCurrentThreadContext(); + arg1.ToManaged(out assemblyName); + + var result = JSHostImplementation.BindAssemblyExports(assemblyName); + + argResult.ToJS(result); + } + catch (Exception ex) + { + argException.ToJS(ex); + } + } + + [UnmanagedCallersOnly(EntryPoint = "SystemInteropJS_CallJSExport")] + public static void CallJSExport(int methodHandle, JSMarshalerArgument* argumentsBuffer) + { + ref JSMarshalerArgument argException = ref argumentsBuffer[0]; // initialized by caller in alloc_stack_frame() + var ctx = argException.AssertCurrentThreadContext(); + if (!ctx.s_JSExportByHandle.TryGetValue(methodHandle, out var jsExport)) + { + argException.ToJS(new InvalidOperationException("Unable to resolve JSExport by handle")); + return; + } + jsExport(new IntPtr(argumentsBuffer)); + } + } +} 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.Mono.cs similarity index 100% rename from src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Interop/JavaScriptExports.cs rename to src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Interop/JavaScriptExports.Mono.cs diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Interop/JavaScriptImports.Generated.cs b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Interop/JavaScriptImports.Generated.cs index 788c657d2a7e2b..052d1f6c042f8b 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Interop/JavaScriptImports.Generated.cs +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Interop/JavaScriptImports.Generated.cs @@ -46,7 +46,7 @@ internal static unsafe partial class JavaScriptImports public static partial Task DynamicImport(string moduleName, string moduleUrl); [JSImport("INTERNAL.bindCsFunction")] - public static partial void BindCSFunction(IntPtr monoMethod, string assemblyName, string namespaceName, string shortClassName, string methodName, int signatureHash, IntPtr signature); + public static partial void BindCSFunction(IntPtr methodHandle, string assemblyName, string namespaceName, string shortClassName, string methodName, int signatureHash, IntPtr signature); #if DEBUG [JSImport("globalThis.console.log")] diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSHostImplementation.CoreCLR.cs b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSHostImplementation.CoreCLR.cs new file mode 100644 index 00000000000000..314711be519a5f --- /dev/null +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSHostImplementation.CoreCLR.cs @@ -0,0 +1,82 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.Loader; +using System.Threading; +using System.Threading.Tasks; + +namespace System.Runtime.InteropServices.JavaScript +{ + internal static partial class JSHostImplementation + { + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "It's kept from trimming by ModuleInitializerAttribute in the generated code.")] + [UnconditionalSuppressMessage("Trimming", "IL2075", Justification = "It's kept from trimming by ModuleInitializerAttribute in the generated code.")] + public static Task BindAssemblyExports(string? assemblyName) + { + ArgumentException.ThrowIfNullOrEmpty(assemblyName); + + Assembly assembly = AssemblyLoadContext.Default.LoadFromAssemblyName(new AssemblyName(assemblyName)); + Type? generatedInitializerType = assembly.GetType("System.Runtime.InteropServices.JavaScript.__GeneratedInitializer", throwOnError: true); + if (generatedInitializerType == null) + { + throw new ArithmeticException($"BindAssemblyExports: __GeneratedInitializer type not found in assembly '{assemblyName}'"); + } + MethodInfo? registerMethod = generatedInitializerType.GetMethod("__Register_", BindingFlags.NonPublic | BindingFlags.Static, binder: null, Type.EmptyTypes, modifiers: null); + if (registerMethod == null) + { + throw new ArithmeticException($"BindAssemblyExports: __Register_ method not found in type '{generatedInitializerType.FullName}' in assembly '{assemblyName}'"); + } + registerMethod.Invoke(null, null); + return Task.CompletedTask; + } + + // this reflection based approach is used by JSExport assemblies generated by Net10 or below + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "It's kept from trimming by DynamicDependencyAttribute in the generated code.")] + [UnconditionalSuppressMessage("Trimming", "IL2075", Justification = "It's kept from trimming by DynamicDependencyAttribute in the generated code.")] + public static unsafe JSFunctionBinding BindManagedFunction(string fullyQualifiedName, int signatureHash, ReadOnlySpan signatures) + { + var ctx = JSProxyContext.CurrentThreadContext; + var (assemblyName, nameSpace, shortClassName, methodName) = ParseFQN(fullyQualifiedName); + var wrapperName = $"__Wrapper_{methodName}_{signatureHash}"; + shortClassName = shortClassName.Replace('/', '+'); + + // get MethodInfo from the fully qualified name + var assembly = Assembly.Load(new AssemblyName(assemblyName)); + var clazz = string.IsNullOrEmpty(nameSpace) + ? assembly.GetType(shortClassName) + : assembly.GetType(nameSpace + "." + shortClassName); + if (clazz == null) + { + Environment.FailFast($"Can't find {nameSpace}{shortClassName} in {assemblyName} assembly"); + } + var wrapperInfo = clazz.GetMethod(wrapperName, BindingFlags.Static | BindingFlags.NonPublic); + if (wrapperInfo == null) + { + Environment.FailFast($"Can't find method wrapper {wrapperName} in {nameSpace}.{shortClassName} in {assemblyName} assembly"); + } + + Action wrapper = (IntPtr args) => + { + object boxedLegacyArgs = Pointer.Box((void*)args, typeof(JSMarshalerArgument*)); + // real signature is void (JSMarshalerArgument* args) + wrapperInfo.Invoke(null, new object?[] { boxedLegacyArgs }); + }; + + int methodHandle = ctx.NextJSExportHandle++; + ctx.s_JSExportByHandle[methodHandle] = wrapper; + + var signature = GetMethodSignature(signatures, null, null); + + // this will hit JS side possibly on another thread, depending on JSProxyContext.CurrentThreadContext + JavaScriptImports.BindCSFunction(methodHandle, assemblyName, nameSpace, shortClassName, methodName, signatureHash, (IntPtr)signature.Header); + + FreeMethodSignatureBuffer(signature); + + return signature; + } + } +} diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSHostImplementation.Mono.cs b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSHostImplementation.Mono.cs new file mode 100644 index 00000000000000..0fc75826e74c8b --- /dev/null +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSHostImplementation.Mono.cs @@ -0,0 +1,55 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.Loader; +using System.Threading; +using System.Threading.Tasks; + +namespace System.Runtime.InteropServices.JavaScript +{ + internal static partial class JSHostImplementation + { + public static Task BindAssemblyExports(string? assemblyName) + { + Interop.Runtime.BindAssemblyExports(Marshal.StringToCoTaskMemUTF8(assemblyName)); + return Task.CompletedTask; + } + + public static unsafe JSFunctionBinding BindManagedFunction(string fullyQualifiedName, int signatureHash, ReadOnlySpan signatures) + { + var (assemblyName, nameSpace, shortClassName, methodName) = ParseFQN(fullyQualifiedName); + + IntPtr monoMethod; + Interop.Runtime.GetAssemblyExport( + // FIXME: Pass UTF-16 through directly so C can work with it, doing the conversion + // in C# pulls in a bunch of dependencies we don't need this early in startup. + // I tested removing the UTF8 conversion from this specific call, but other parts + // of startup I can't identify still pull in UTF16->UTF8 conversion, so it's not + // worth it to do that yet. + Marshal.StringToCoTaskMemUTF8(assemblyName), + Marshal.StringToCoTaskMemUTF8(nameSpace), + Marshal.StringToCoTaskMemUTF8(shortClassName), + Marshal.StringToCoTaskMemUTF8(methodName), + signatureHash, + &monoMethod); + + if (monoMethod == IntPtr.Zero) + { + Environment.FailFast($"Can't find {nameSpace}{shortClassName}{methodName} in {assemblyName}.dll"); + } + + var signature = GetMethodSignature(signatures, null, null); + + // this will hit JS side possibly on another thread, depending on JSProxyContext.CurrentThreadContext + JavaScriptImports.BindCSFunction(monoMethod, assemblyName, nameSpace, shortClassName, methodName, signatureHash, (IntPtr)signature.Header); + + FreeMethodSignatureBuffer(signature); + + return signature; + } + } +} diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSHostImplementation.cs b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSHostImplementation.cs index 26a133fbbc6021..c8ce61359aa8fb 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSHostImplementation.cs +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSHostImplementation.cs @@ -273,45 +273,6 @@ public static void LoadSatelliteAssembly(byte[] dllBytes) } } - public static Task BindAssemblyExports(string? assemblyName) - { - Interop.Runtime.BindAssemblyExports(Marshal.StringToCoTaskMemUTF8(assemblyName)); - return Task.CompletedTask; - } - - public static unsafe JSFunctionBinding BindManagedFunction(string fullyQualifiedName, int signatureHash, ReadOnlySpan signatures) - { - var (assemblyName, nameSpace, shortClassName, methodName) = ParseFQN(fullyQualifiedName); - - IntPtr monoMethod; - Interop.Runtime.GetAssemblyExport( - // FIXME: Pass UTF-16 through directly so C can work with it, doing the conversion - // in C# pulls in a bunch of dependencies we don't need this early in startup. - // I tested removing the UTF8 conversion from this specific call, but other parts - // of startup I can't identify still pull in UTF16->UTF8 conversion, so it's not - // worth it to do that yet. - Marshal.StringToCoTaskMemUTF8(assemblyName), - Marshal.StringToCoTaskMemUTF8(nameSpace), - Marshal.StringToCoTaskMemUTF8(shortClassName), - Marshal.StringToCoTaskMemUTF8(methodName), - signatureHash, - &monoMethod); - - if (monoMethod == IntPtr.Zero) - { - Environment.FailFast($"Can't find {nameSpace}{shortClassName}{methodName} in {assemblyName}.dll"); - } - - var signature = GetMethodSignature(signatures, null, null); - - // this will hit JS side possibly on another thread, depending on JSProxyContext.CurrentThreadContext - JavaScriptImports.BindCSFunction(monoMethod, assemblyName, nameSpace, shortClassName, methodName, signatureHash, (IntPtr)signature.Header); - - FreeMethodSignatureBuffer(signature); - - return signature; - } - #if FEATURE_WASM_MANAGED_THREADS [UnsafeAccessor(UnsafeAccessorKind.Field, Name = "external_eventloop")] private static extern ref bool GetThreadExternalEventloop(Thread @this); @@ -334,29 +295,34 @@ public static RuntimeMethodHandle GetMethodHandleFromIntPtr(IntPtr ptr) // The BCL implementations of IndexOf/LastIndexOf/Trim are vectorized & fast, // but they pull in a bunch of code that is otherwise not necessarily // useful during early app startup, so we use simple scalar implementations - private static int SmallIndexOf (string s, char ch, int direction = 1) { + private static int SmallIndexOf(string s, char ch, int direction = 1) + { if (s.Length < 1) return -1; int start_index = (direction > 0) ? 0 : s.Length - 1, end_index = (direction > 0) ? s.Length - 1 : 0; - for (int i = start_index; i != end_index; i += direction) { + for (int i = start_index; i != end_index; i += direction) + { if (s[i] == ch) return i; } return -1; } - private static string SmallTrim (string s) { + private static string SmallTrim(string s) + { if (s.Length < 1) return s; int head = 0, tail = s.Length - 1; - while (head < s.Length) { + while (head < s.Length) + { if (s[head] == ' ') head++; else break; } - while (tail >= 0) { + while (tail >= 0) + { if (s[tail] == ' ') tail--; else @@ -368,7 +334,7 @@ private static string SmallTrim (string s) { return s; } - public static (string assemblyName, string nameSpace, string shortClassName, string methodName) ParseFQN(string fqn) + private static (string assemblyName, string nameSpace, string shortClassName, string methodName) ParseFQN(string fqn) { var assembly = fqn.Substring(SmallIndexOf(fqn, '[') + 1, SmallIndexOf(fqn, ']') - 1); fqn = SmallTrim(fqn); diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSProxyContext.cs b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSProxyContext.cs index cd1509ba8309a9..608643b9b07eb4 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSProxyContext.cs +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSProxyContext.cs @@ -23,6 +23,8 @@ internal sealed class JSProxyContext : IDisposable // they have negative values, so that they don't collide with JSHandles. private nint NextJSVHandle = -2; private readonly List JSVHandleFreeList = new(); + internal Dictionary> s_JSExportByHandle = new Dictionary>(); + internal int NextJSExportHandle = 1; #if !FEATURE_WASM_MANAGED_THREADS private JSProxyContext() @@ -491,7 +493,7 @@ public static void ReleaseCSOwnedObject(JSObject jso, bool skipJS) if (!ctx.ThreadCsOwnedObjects.Remove(jsHandle)) { Environment.FailFast($"ReleaseCSOwnedObject expected to find registration for JSHandle: {jsHandle}, ManagedThreadId: {Environment.CurrentManagedThreadId}. {Environment.NewLine} {Environment.StackTrace}"); - }; + } if (!skipJS) { #if FEATURE_WASM_MANAGED_THREADS diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System.Runtime.InteropServices.JavaScript.Tests.csproj b/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System.Runtime.InteropServices.JavaScript.Tests.csproj index c959554c57a73e..64c4df3b921503 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System.Runtime.InteropServices.JavaScript.Tests.csproj +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System.Runtime.InteropServices.JavaScript.Tests.csproj @@ -21,6 +21,7 @@ Suppress the NU1511 warning in the whole project as putting it on a P2P doesn't work: https://github.com/NuGet/Home/issues/14121 --> $(NoWarn);NU1511 false + true @@ -58,7 +59,9 @@ + diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/JavaScriptTestHelper.mjs b/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/JavaScriptTestHelper.mjs index f4f2772fdda8b5..148a2722a44afe 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/JavaScriptTestHelper.mjs +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/JavaScriptTestHelper.mjs @@ -437,7 +437,7 @@ export async function backbackAsync(arg1, arg2, arg3) { } export async function callJavaScriptLibrary(a, b) { - const exports = await App.runtime.getAssemblyExports("JavaScriptLibrary.dll"); + const exports = await App.runtime.getAssemblyExports("JavaScriptLibrary"); return exports.JavaScriptLibrary.JavaScriptInterop.ExportedMethod(a, b); } @@ -454,7 +454,7 @@ globalThis.rebound = { } export async function setup() { - dllExports = await App.runtime.getAssemblyExports("System.Runtime.InteropServices.JavaScript.Tests.dll"); + dllExports = await App.runtime.getAssemblyExports("System.Runtime.InteropServices.JavaScript.Tests"); } // console.log('JavaScriptTestHelper:' Object.keys(globalThis.JavaScriptTestHelper)); diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/SecondRuntimeTest.js b/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/SecondRuntimeTest.js index f711c31c93e27c..47c9e42d0e2dc8 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/SecondRuntimeTest.js +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/SecondRuntimeTest.js @@ -21,6 +21,6 @@ export async function runSecondRuntimeAndTestStaticState(guid) { } async function getIncrementStateFunction(runtime) { - const exports = await runtime.getAssemblyExports("System.Runtime.InteropServices.JavaScript.Tests.dll"); + const exports = await runtime.getAssemblyExports("System.Runtime.InteropServices.JavaScript.Tests"); return exports.System.Runtime.InteropServices.JavaScript.Tests.SecondRuntimeTest.Interop.IncrementState; } \ No newline at end of file diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/WebWorkerTestHelper.mjs b/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/WebWorkerTestHelper.mjs index e2a8cfadfaea7e..b30e9f548b7dd8 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/WebWorkerTestHelper.mjs +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/WebWorkerTestHelper.mjs @@ -16,7 +16,7 @@ export async function setup() { console.error(getDotnetRuntime); throw new Error("runtime is null or undefined"); } - dllExports = await runtime.getAssemblyExports("System.Runtime.InteropServices.JavaScript.Tests.dll"); + dllExports = await runtime.getAssemblyExports("System.Runtime.InteropServices.JavaScript.Tests"); if (!dllExports) { throw new Error("dllExports is null or undefined"); } 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/build/WasmApp.InTree.props b/src/mono/browser/build/WasmApp.InTree.props index 2fef6aa17d9993..ed52bd0c7b3467 100644 --- a/src/mono/browser/build/WasmApp.InTree.props +++ b/src/mono/browser/build/WasmApp.InTree.props @@ -29,7 +29,7 @@ $(NetCoreAppCurrent) $([MSBuild]::NormalizeDirectory($(MonoProjectRoot), 'browser', 'emsdk')) false - true + true false false diff --git a/src/mono/browser/test-main.js b/src/mono/browser/test-main.js index 872e14a2ce4cd9..3ecda3bbee992e 100644 --- a/src/mono/browser/test-main.js +++ b/src/mono/browser/test-main.js @@ -256,11 +256,11 @@ function configureRuntime(dotnet, runArgs) { .withVirtualWorkingDirectory(runArgs.workingDirectory) .withEnvironmentVariables(runArgs.environmentVariables) .withDiagnosticTracing(runArgs.diagnosticTracing) - .withExitOnUnhandledError() - .withExitCodeLogging() - .withElementOnExit() - .withInteropCleanupOnExit() - .withDumpThreadsOnNonZeroExit() + //TODO .withExitOnUnhandledError() + //TODO .withExitCodeLogging() + //TODO .withElementOnExit() + //TODO .withInteropCleanupOnExit() + //TODO .withDumpThreadsOnNonZeroExit() .withConfig({ loadAllSatelliteResources: true, jsThreadBlockingMode: "ThrowWhenBlockingWait", @@ -299,7 +299,7 @@ function configureRuntime(dotnet, runArgs) { dotnet.withWaitingForDebugger(-1); } if (runArgs.forwardConsole) { - dotnet.withConsoleForwarding(); + // TODO dotnet.withConsoleForwarding(); } } 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 bad785110dabbf..7dea52587f03ec 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 @@ -203,6 +203,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) @@ -345,6 +348,9 @@ Copyright (c) .NET Foundation. All rights reserved. + <_WebCilAssetsCandidates Update="@(_WebCilAssetsCandidates)"> + %(Identity) + <_WasmFingerprintPatterns Include="WasmFiles" Pattern="*.wasm" Expression="#[.{fingerprint}]!" /> <_WasmFingerprintPatterns Include="DllFiles" Pattern="*.dll" Expression="#[.{fingerprint}]!" /> <_WasmFingerprintPatterns Include="DatFiles" Pattern="*.dat" Expression="#[.{fingerprint}]!" /> diff --git a/src/mono/sample/wasm/Directory.Build.props b/src/mono/sample/wasm/Directory.Build.props index 7a509c489b84c6..06105dda891e05 100644 --- a/src/mono/sample/wasm/Directory.Build.props +++ b/src/mono/sample/wasm/Directory.Build.props @@ -15,6 +15,11 @@ + + false + false + false + bin $(MSBuildProjectDirectory)\bin\wwwroot\ false diff --git a/src/mono/sample/wasm/Directory.Build.targets b/src/mono/sample/wasm/Directory.Build.targets index baa756debe46da..542a378baa0517 100644 --- a/src/mono/sample/wasm/Directory.Build.targets +++ b/src/mono/sample/wasm/Directory.Build.targets @@ -37,6 +37,11 @@ + + + + + Main(string[] args) { DisplayMeaning(42); - return 0; + return Task.FromResult(0); } [JSImport("Sample.Test.displayMeaning", "main.js")] internal static partial void DisplayMeaning(int meaning); + + [JSExport] + internal static async Task PrintMeaning(Task meaningPromise) + { + Console.WriteLine("Meaning of life is " + await meaningPromise); + } } } diff --git a/src/mono/sample/wasm/browser/wwwroot/main.js b/src/mono/sample/wasm/browser/wwwroot/main.js index 20fd268d65b9ac..b1bc9a8b4176de 100644 --- a/src/mono/sample/wasm/browser/wwwroot/main.js +++ b/src/mono/sample/wasm/browser/wwwroot/main.js @@ -4,11 +4,16 @@ import { dotnet, exit } from './_framework/dotnet.js' function displayMeaning(meaning) { + console.log(`Meaning of life is ${meaning}`); document.getElementById("out").innerHTML = `${meaning}`; } +function delay(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); +} + try { - const { setModuleImports } = await dotnet + const { setModuleImports, getAssemblyExports, runMain } = await dotnet .withElementOnExit() .withExitOnUnhandledError() .create(); @@ -21,7 +26,11 @@ try { } }); - await dotnet.run(); + await runMain(); + + const exports = await getAssemblyExports("Wasm.Browser.Sample"); + await exports.Sample.Test.PrintMeaning(delay(2000).then(() => 42)); + console.log("Program has exited normally."); } catch (err) { exit(2, err); diff --git a/src/native/corehost/browserhost/host/host.ts b/src/native/corehost/browserhost/host/host.ts index 689f960ba903e5..a72d4c1667362e 100644 --- a/src/native/corehost/browserhost/host/host.ts +++ b/src/native/corehost/browserhost/host/host.ts @@ -17,8 +17,10 @@ export function registerDllBytes(bytes: Uint8Array, asset: { name: string, virtu const ptr = Module.HEAPU32[ptrPtr as any >>> 2]; Module.HEAPU8.set(bytes, ptr >>> 0); - loadedAssemblies.set(asset.name, { ptr, length: bytes.length }); loadedAssemblies.set(asset.virtualPath, { ptr, length: bytes.length }); + if (!asset.virtualPath.startsWith("/")) { + loadedAssemblies.set("/" + asset.virtualPath, { ptr, length: bytes.length }); + } } finally { Module.stackRestore(sp); } @@ -94,6 +96,7 @@ export function BrowserHost_ExternalAssemblyProbe(pathPtr: CharPtr, outDataStart Module.HEAPU32[((outSize as any) + 4) >>> 2] = 0; return true; } + dotnetLogger.debug(`Assembly not found: '${path}'`); Module.HEAPU32[outDataStartPtr as any >>> 2] = 0; Module.HEAPU32[outSize as any >>> 2] = 0; Module.HEAPU32[((outSize as any) + 4) >>> 2] = 0; @@ -105,6 +108,9 @@ export async function runMain(mainAssemblyName?: string, args?: string[]): Promi if (!mainAssemblyName) { mainAssemblyName = config.mainAssemblyName!; } + if (!mainAssemblyName.endsWith(".dll")) { + mainAssemblyName += ".dll"; + } const mainAssemblyNamePtr = dotnetBrowserUtilsExports.stringToUTF8Ptr(mainAssemblyName) as any; if (!args) { diff --git a/src/native/corehost/browserhost/loader/assets.ts b/src/native/corehost/browserhost/loader/assets.ts index c214e61b8f4a5b..d89d16a2904fe0 100644 --- a/src/native/corehost/browserhost/loader/assets.ts +++ b/src/native/corehost/browserhost/loader/assets.ts @@ -64,7 +64,7 @@ export async function createRuntime(downloadOnly: boolean, loadBootResource?: Lo async function loadJSModule(asset: JsAsset): Promise { if (asset.name && !asset.resolvedUrl) { - asset.resolvedUrl = locateFile(asset.name); + asset.resolvedUrl = locateFile(asset.name, true); } if (!asset.resolvedUrl) throw new Error("Invalid config, resources is not set"); return await import(/* webpackIgnore: true */ asset.resolvedUrl); diff --git a/src/native/corehost/browserhost/loader/bootstrap.ts b/src/native/corehost/browserhost/loader/bootstrap.ts index b2f7843759065e..17f0a407fc3335 100644 --- a/src/native/corehost/browserhost/loader/bootstrap.ts +++ b/src/native/corehost/browserhost/loader/bootstrap.ts @@ -11,13 +11,21 @@ const modulesUniqueQuery = queryIndex > 0 ? scriptUrlQuery.substring(queryIndex) const scriptUrl = normalizeFileUrl(scriptUrlQuery); const scriptDirectory = normalizeDirectoryUrl(scriptUrl); -export function locateFile(path: string) { - if ("URL" in globalThis) { - return new URL(path, scriptDirectory).toString(); +export function locateFile(path: string, isModule = false): string { + let res; + if (isPathAbsolute(path)) { + res = path; + } else if (globalThis.URL) { + res = new globalThis.URL(path, scriptDirectory).href; + } else { + res = scriptDirectory + path; } - if (isPathAbsolute(path)) return path; - return scriptDirectory + path + modulesUniqueQuery; + if (isModule) { + res += modulesUniqueQuery; + } + + return res; } function normalizeFileUrl(filename: string) { diff --git a/src/native/corehost/browserhost/loader/host-builder.ts b/src/native/corehost/browserhost/loader/host-builder.ts index 465f60ae7fbd7d..bad4e591ff61a6 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/corehost.proj b/src/native/corehost/corehost.proj index 8b20592f5fd6f8..52214740f05be9 100644 --- a/src/native/corehost/corehost.proj +++ b/src/native/corehost/corehost.proj @@ -167,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" /> + + + (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 20a0cc4e766b45..915f4113c9ac81 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, dynamicImport, getDotnetInstance, getGlobalThis, getProperty, getTypeOfProperty, hasProperty, invokeJSFunction, invokeJSImportST, setModuleImports, setProperty } from "./invoke-js"; +import { bindCsFunction, 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"); @@ -15,27 +20,39 @@ export function dotnetInitializeModule(internals: InternalExchange): void { const runtimeApi = internals[InternalExchangeIndex.RuntimeAPI]; if (typeof runtimeApi !== "object") throw new Error("Expected internals to have RuntimeAPI"); Object.assign(runtimeApi, runtimeApiLocal); + Object.assign(runtimeApi.INTERNAL, { + hasProperty, + getTypeOfProperty, + getProperty, + setProperty, + getGlobalThis, + getDotnetInstance, + dynamicImport, + bindCsFunction + }); internals[InternalExchangeIndex.RuntimeExportsTable] = runtimeExportsToTable({ + bindJSImportST, + invokeJSImportST, + releaseCSOwnedObject, + resolveOrRejectPromise, + cancelPromise, + invokeJSFunction, }); dotnetUpdateInternals(internals, dotnetUpdateInternalsSubscriber); - // eslint-disable-next-line @typescript-eslint/no-unused-vars + initializeMarshalersToJs(); + initializeMarshalersToCs(); + function runtimeExportsToTable(map: RuntimeExports): RuntimeExportsTable { // keep in sync with runtimeExportsFromTable() return [ + map.bindJSImportST, + map.invokeJSImportST, + map.releaseCSOwnedObject, + map.resolveOrRejectPromise, + map.cancelPromise, + map.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..bc7f753add5dff --- /dev/null +++ b/src/native/libs/System.Runtime.InteropServices.JavaScript.Native/interop/invoke-cs.ts @@ -0,0 +1,363 @@ +// 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 type { BindingClosureCS, BoundMarshalerToCs, CSFnHandle, JSFunctionSignature } from "./types"; + +import { dotnetAssert, dotnetLogger, Module } from "./cross-module"; + +import { bindAssemblyExports, invokeJSExport } from "./managed-exports"; +import { allocStackFrame, getSig, getSignatureType, getSignatureArgumentCount, getSignatureVersion, jsInteropState, boundCsFunctionSymbol } from "./marshal"; +import { bindArgMarshalToCs } from "./marshal-to-cs"; +import { bindArgMarshalToJs, endMarshalTaskToJs } from "./marshal-to-js"; +import { assertJsInterop, assertRuntimeRunning, endMeasure, isRuntimeRunning, startMeasure } from "./utils"; +import { MarshalerType, MeasuredBlock } from "./types"; + +export const exportsByAssembly: Map = new Map(); + +export async function getAssemblyExports(assemblyName: string): Promise { + assertJsInterop(); + if (assemblyName.endsWith(".dll")) { + assemblyName = assemblyName.substring(0, assemblyName.length - 4); + } + const result = exportsByAssembly.get(assemblyName); + if (!result) { + await bindAssemblyExports(assemblyName); + } + + return exportsByAssembly.get(assemblyName) || {}; +} + +export function bindCsFunction(methodHandle: CSFnHandle, assemblyName: string, namespaceName: string, shortClassName: string, methodName: string, signatureHash: number, signature: JSFunctionSignature): void { + const fullyQualifiedName = `[${assemblyName}] ${namespaceName}.${shortClassName}:${methodName}`; + const mark = startMeasure(); + dotnetLogger.debug(() => `Binding [JSExport] ${namespaceName}.${shortClassName}:${methodName} from ${assemblyName} assembly`); + const version = getSignatureVersion(signature); + dotnetAssert.check(version === 2, () => `Signature version ${version} mismatch.`); + + + const argsCount = getSignatureArgumentCount(signature); + + const argMarshalers: (BoundMarshalerToCs)[] = new Array(argsCount); + for (let index = 0; index < argsCount; index++) { + const sig = getSig(signature, index + 2); + const marshalerType = getSignatureType(sig); + const argMarshaler = bindArgMarshalToCs(sig, marshalerType, index + 2); + dotnetAssert.check(argMarshaler, "ERR43: argument marshaler must be resolved"); + argMarshalers[index] = argMarshaler; + } + + const resSig = getSig(signature, 1); + let resMarshalerType = getSignatureType(resSig); + + const isAsync = resMarshalerType == MarshalerType.Task; + const isDiscardNoWait = resMarshalerType == MarshalerType.DiscardNoWait; + if (isAsync) { + resMarshalerType = MarshalerType.TaskPreCreated; + } + const resConverter = bindArgMarshalToJs(resSig, resMarshalerType, 1); + + const closure: BindingClosureCS = { + methodHandle, + fullyQualifiedName, + argsCount, + argMarshalers, + resConverter, + isAsync, + isDiscardNoWait, + isDisposed: false, + }; + let boundFn: Function; + + if (isAsync) { + if (argsCount == 1 && resConverter) { + boundFn = bindFn_1RA(closure); + } else if (argsCount == 2 && resConverter) { + boundFn = bindFn_2RA(closure); + } else { + boundFn = bindFn(closure); + } + } else if (isDiscardNoWait) { + boundFn = bindFn(closure); + } else { + if (argsCount == 0 && !resConverter) { + boundFn = bindFn_0V(closure); + } else if (argsCount == 1 && !resConverter) { + boundFn = bindFn_1V(closure); + } else if (argsCount == 1 && resConverter) { + boundFn = bindFn_1R(closure); + } else if (argsCount == 2 && resConverter) { + boundFn = bindFn_2R(closure); + } else { + boundFn = bindFn(closure); + } + } + + // this is just to make debugging easier. + // 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 url = `//# sourceURL=https://dotnet/JSExport/${methodName}`; + const body = `return (function JSExport_${methodName}(){ return fn.apply(this, arguments)});`; + boundFn = new Function("fn", url + "\r\n" + body)(boundFn); + } catch (ex) { + jsInteropState.cspPolicy = true; + } + } + + (boundFn)[boundCsFunctionSymbol] = closure; + + walkExportsToSeFunction(assemblyName, namespaceName, shortClassName, methodName, signatureHash, boundFn); + endMeasure(mark, MeasuredBlock.bindCsFunction, fullyQualifiedName); +} + +function bindFn_0V(closure: BindingClosureCS) { + const method = closure.methodHandle; + const fqn = closure.fullyQualifiedName; + (closure) = null; + return function boundFn_0V() { + const mark = startMeasure(); + assertRuntimeRunning(); + const sp = Module.stackSave(); + try { + const size = 2; + const args = allocStackFrame(size); + // call C# side + invokeJSExport(method, args); + } finally { + if (isRuntimeRunning()) Module.stackRestore(sp); + + endMeasure(mark, MeasuredBlock.callCsFunction, fqn); + } + }; +} + +function bindFn_1V(closure: BindingClosureCS) { + const method = closure.methodHandle; + const marshaler1 = closure.argMarshalers[0]!; + const fqn = closure.fullyQualifiedName; + return function boundFn_1V(arg1: any) { + const mark = startMeasure(); + assertRuntimeRunning(); + const sp = Module.stackSave(); + try { + const size = 3; + const args = allocStackFrame(size); + marshaler1(args, arg1); + + // call C# side + invokeJSExport(method, args); + } finally { + if (isRuntimeRunning()) Module.stackRestore(sp); + + endMeasure(mark, MeasuredBlock.callCsFunction, fqn); + } + }; +} + +function bindFn_1R(closure: BindingClosureCS) { + const method = closure.methodHandle; + const marshaler1 = closure.argMarshalers[0]!; + const resConverter = closure.resConverter!; + const fqn = closure.fullyQualifiedName; + return function boundFn_1R(arg1: any) { + const mark = startMeasure(); + assertRuntimeRunning(); + const sp = Module.stackSave(); + try { + const size = 3; + const args = allocStackFrame(size); + marshaler1(args, arg1); + + // call C# side + invokeJSExport(method, args); + + const jsResult = resConverter(args); + return jsResult; + } finally { + if (isRuntimeRunning()) Module.stackRestore(sp); + + endMeasure(mark, MeasuredBlock.callCsFunction, fqn); + } + }; +} + +function bindFn_1RA(closure: BindingClosureCS) { + const methodHandle = closure.methodHandle; + const marshaler1 = closure.argMarshalers[0]!; + const resConverter = closure.resConverter!; + const fqn = closure.fullyQualifiedName; + (closure) = null; + return function bindFn_1RA(arg1: any) { + const mark = startMeasure(); + assertRuntimeRunning(); + const sp = Module.stackSave(); + try { + const size = 3; + const args = allocStackFrame(size); + marshaler1(args, arg1); + + // pre-allocate the promise + let promise = resConverter(args); + + // call C# side + invokeJSExport(methodHandle, args); + + // in case the C# side returned synchronously + promise = endMarshalTaskToJs(args, undefined, promise); + + return promise; + } finally { + if (isRuntimeRunning()) Module.stackRestore(sp); + + endMeasure(mark, MeasuredBlock.callCsFunction, fqn); + } + }; +} + +function bindFn_2R(closure: BindingClosureCS) { + const method = closure.methodHandle; + const marshaler1 = closure.argMarshalers[0]!; + const marshaler2 = closure.argMarshalers[1]!; + const resConverter = closure.resConverter!; + const fqn = closure.fullyQualifiedName; + (closure) = null; + return function boundFn_2R(arg1: any, arg2: any) { + const mark = startMeasure(); + assertRuntimeRunning(); + const sp = Module.stackSave(); + try { + const size = 4; + const args = allocStackFrame(size); + marshaler1(args, arg1); + marshaler2(args, arg2); + + // call C# side + invokeJSExport(method, args); + + const jsResult = resConverter(args); + return jsResult; + } finally { + if (isRuntimeRunning()) Module.stackRestore(sp); + + endMeasure(mark, MeasuredBlock.callCsFunction, fqn); + } + }; +} + +function bindFn_2RA(closure: BindingClosureCS) { + const methodHandle = closure.methodHandle; + const marshaler1 = closure.argMarshalers[0]!; + const marshaler2 = closure.argMarshalers[1]!; + const resConverter = closure.resConverter!; + const fqn = closure.fullyQualifiedName; + (closure) = null; + return function bindFn_2RA(arg1: any, arg2: any) { + const mark = startMeasure(); + assertRuntimeRunning(); + const sp = Module.stackSave(); + try { + const size = 4; + const args = allocStackFrame(size); + marshaler1(args, arg1); + marshaler2(args, arg2); + + // pre-allocate the promise + let promise = resConverter(args); + + // call C# side + invokeJSExport(methodHandle, args); + + // in case the C# side returned synchronously + promise = endMarshalTaskToJs(args, undefined, promise); + + return promise; + } finally { + if (isRuntimeRunning()) Module.stackRestore(sp); + + endMeasure(mark, MeasuredBlock.callCsFunction, fqn); + } + }; +} + +function bindFn(closure: BindingClosureCS) { + const argsCount = closure.argsCount; + const argMarshalers = closure.argMarshalers; + const resConverter = closure.resConverter; + const methodHandle = closure.methodHandle; + const fqn = closure.fullyQualifiedName; + const isAsync = closure.isAsync; + const isDiscardNoWait = closure.isDiscardNoWait; + return function boundFn(...jsArgs: any[]) { + const mark = startMeasure(); + assertRuntimeRunning(); + const sp = Module.stackSave(); + try { + const size = 2 + argsCount; + const args = allocStackFrame(size); + for (let index = 0; index < argsCount; index++) { + const marshaler = argMarshalers[index]; + if (marshaler) { + const jsArg = jsArgs[index]; + marshaler(args, jsArg); + } + } + let jsResult = undefined; + if (isAsync) { + // pre-allocate the promise + jsResult = resConverter!(args); + } + + // call C# side + if (isAsync) { + invokeJSExport(methodHandle, args); + // in case the C# side returned synchronously + jsResult = endMarshalTaskToJs(args, undefined, jsResult); + } else if (isDiscardNoWait) { + // call C# side, fire and forget + invokeJSExport(methodHandle, args); + } else { + invokeJSExport(methodHandle, args); + if (resConverter) { + jsResult = resConverter(args); + } + } + return jsResult; + } finally { + if (isRuntimeRunning()) Module.stackRestore(sp); + + endMeasure(mark, MeasuredBlock.callCsFunction, fqn); + } + }; +} + +function walkExportsToSeFunction(assembly: string, namespace: string, classname: string, methodname: string, signatureHash: number, fn: Function): void { + const parts = `${namespace}.${classname}`.replace(/\+/g, ".").replace(/\//g, ".").split("."); + let scope: any = undefined; + let assemblyScope = exportsByAssembly.get(assembly); + if (!assemblyScope) { + assemblyScope = {}; + exportsByAssembly.set(assembly, assemblyScope); + exportsByAssembly.set(assembly + ".dll", assemblyScope); + } + scope = assemblyScope; + for (let i = 0; i < parts.length; i++) { + const part = parts[i]; + if (part != "") { + let newscope = scope[part]; + if (typeof newscope === "undefined") { + newscope = {}; + scope[part] = newscope; + } + dotnetAssert.check(newscope, () => `${part} not found while looking up ${classname}`); + scope = newscope; + } + } + + if (!scope[methodname]) { + scope[methodname] = fn; + } + scope[`${methodname}.${signatureHash}`] = fn; +} 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..4ff7ff69ff3af0 --- /dev/null +++ b/src/native/libs/System.Runtime.InteropServices.JavaScript.Native/interop/invoke-js.ts @@ -0,0 +1,359 @@ +// 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 { BindingClosureJS, 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"; +import { wrapAsCancelablePromise } from "./cancelable-promise"; + +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: BindingClosureJS = { + 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: BindingClosureJS) { + 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: BindingClosureJS) { + 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: BindingClosureJS) { + 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: BindingClosureJS) { + 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: BindingClosureJS) { + 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); +} + +export function setProperty(self: any, name: string, value: any): void { + dotnetAssert.check(self, "Null reference"); + self[name] = value; +} + +export function getProperty(self: any, name: string): any { + dotnetAssert.check(self, "Null reference"); + return self[name]; +} + +export function hasProperty(self: any, name: string): boolean { + dotnetAssert.check(self, "Null reference"); + return name in self; +} + +export function getTypeOfProperty(self: any, name: string): string { + dotnetAssert.check(self, "Null reference"); + return typeof self[name]; +} + +export function getGlobalThis(): any { + return globalThis; +} + +export function getDotnetInstance(): any { + return dotnetApi; +} + +export function dynamicImport(module_name: string, module_url: string): Promise { + assertJsInterop(); + dotnetAssert.check(module_name && typeof module_name === "string", "module_name must be string"); + dotnetAssert.check(module_url && typeof module_url === "string", "module_url must be string"); + let promise = importedModulesPromises.get(module_name); + const newPromise = !promise; + if (newPromise) { + dotnetLogger.debug(() => `importing ES6 module '${module_name}' from '${module_url}'`); + promise = import(/*! webpackIgnore: true */module_url); + importedModulesPromises.set(module_name, promise); + } + + return wrapAsCancelablePromise(async function register() { + const module = await promise; + if (newPromise) { + importedModules.set(module_name, module); + dotnetLogger.debug(() => `imported ES6 module '${module_name}' from '${module_url}'`); + } + return module; + }); +} 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..84028574cba352 --- /dev/null +++ b/src/native/libs/System.Runtime.InteropServices.JavaScript.Native/interop/managed-exports.ts @@ -0,0 +1,160 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +import type { JSMarshalerArguments, GCHandle, MarshalerToCs, MarshalerToJs, CSFnHandle } from "./types"; + +import { dotnetAssert, dotnetInteropJSExports, Module } from "./cross-module"; +import { allocStackFrame, getArg, isArgsException, setArgType, setGcHandle } from "./marshal"; +import { marshalExceptionToCs, marshalStringToCs } from "./marshal-to-cs"; +import { beginMarshalTaskToJs, endMarshalTaskToJs, marshalExceptionToJs, marshalInt32ToJs, marshalStringToJs } from "./marshal-to-js"; +import { assertJsInterop, assertRuntimeRunning, isRuntimeRunning } from "./utils"; +import { MarshalerType } from "./types"; + +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); + } +} + +export function releaseJsOwnedObjectByGcHandle(gcHandle: GCHandle) { + dotnetAssert.check(gcHandle, "Must be valid gcHandle"); + assertRuntimeRunning(); + const sp = Module.stackSave(); + try { + const size = 3; + const args = allocStackFrame(size); + const arg1 = getArg(args, 2); + setArgType(arg1, MarshalerType.Object); + setGcHandle(arg1, gcHandle); + // this must stay synchronous for freeGcvHandle sake, to not use-after-free + // also on JSWebWorker, because the message could arrive after the worker is terminated and the GCHandle of JSProxyContext is already freed + dotnetInteropJSExports.SystemInteropJS_ReleaseJSOwnedObjectByGCHandle(args); + } finally { + if (isRuntimeRunning()) Module.stackRestore(sp); + + } +} + +export function callDelegate(callbackGcHandle: GCHandle, arg1Js: any, arg2Js: any, arg3Js: any, resConverter?: MarshalerToJs, arg1Converter?: MarshalerToCs, arg2Converter?: MarshalerToCs, arg3Converter?: MarshalerToCs) { + assertRuntimeRunning(); + + const sp = Module.stackSave(); + try { + const size = 6; + const args = allocStackFrame(size); + + const arg1 = getArg(args, 2); + setArgType(arg1, MarshalerType.Object); + setGcHandle(arg1, callbackGcHandle); + // payload arg numbers are shifted by one, the real first is a gcHandle of the callback + + if (arg1Converter) { + const arg2 = getArg(args, 3); + arg1Converter(arg2, arg1Js); + } + if (arg2Converter) { + const arg3 = getArg(args, 4); + arg2Converter(arg3, arg2Js); + } + if (arg3Converter) { + const arg4 = getArg(args, 5); + arg3Converter(arg4, arg3Js); + } + + dotnetInteropJSExports.SystemInteropJS_CallDelegate(args); + if (isArgsException(args)) { + const exc = getArg(args, 0); + throw marshalExceptionToJs(exc); + } + + if (resConverter) { + const res = getArg(args, 1); + return resConverter(res); + } + } finally { + if (isRuntimeRunning()) Module.stackRestore(sp); + + } +} + +export function completeTask(holderGcHandle: GCHandle, error?: any, data?: any, resConverter?: MarshalerToCs) { + assertRuntimeRunning(); + const sp = Module.stackSave(); + try { + const size = 5; + const args = allocStackFrame(size); + const arg1 = getArg(args, 2); + setArgType(arg1, MarshalerType.Object); + setGcHandle(arg1, holderGcHandle); + const arg2 = getArg(args, 3); + if (error) { + marshalExceptionToCs(arg2, error); + } else { + setArgType(arg2, MarshalerType.None); + const arg3 = getArg(args, 4); + dotnetAssert.check(resConverter, "resConverter missing"); + resConverter(arg3, data); + } + dotnetInteropJSExports.SystemInteropJS_CompleteTask(args); + } finally { + if (isRuntimeRunning()) Module.stackRestore(sp); + + } +} + +export function bindAssemblyExports(assemblyName: string): Promise { + assertRuntimeRunning(); + const sp = Module.stackSave(); + try { + const size = 3; + const args = allocStackFrame(size); + const res = getArg(args, 1); + const arg1 = getArg(args, 2); + marshalStringToCs(arg1, assemblyName); + + // because this is async, we could pre-allocate the promise + let promise = beginMarshalTaskToJs(res, MarshalerType.TaskPreCreated); + + dotnetInteropJSExports.SystemInteropJS_BindAssemblyExports(args); + if (isArgsException(args)) { + // TODO free pre-created promise + const exc = getArg(args, 0); + throw marshalExceptionToJs(exc); + } + + // in case the C# side returned synchronously + promise = endMarshalTaskToJs(args, marshalInt32ToJs, promise); + + if (promise === null || promise === undefined) { + promise = Promise.resolve(); + } + return promise; + } finally { + // synchronously + if (isRuntimeRunning()) Module.stackRestore(sp); + } +} + +export function invokeJSExport(methodHandle: CSFnHandle, args: JSMarshalerArguments): void { + assertJsInterop(); + // TODO methodHandle + dotnetInteropJSExports.SystemInteropJS_CallJSExport(methodHandle, args); + if (isArgsException(args)) { + const exc = getArg(args, 0); + throw marshalExceptionToJs(exc); + } +} 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..01eb3677452bfb --- /dev/null +++ b/src/native/libs/System.Runtime.InteropServices.JavaScript.Native/interop/marshal-to-cs.ts @@ -0,0 +1,510 @@ +// 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); + } +} + +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)); +} + +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..1f34f47b027846 --- /dev/null +++ b/src/native/libs/System.Runtime.InteropServices.JavaScript.Native/interop/marshal-to-js.ts @@ -0,0 +1,552 @@ +// 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; +} + +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; +} + +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..3f5071730a7612 --- /dev/null +++ b/src/native/libs/System.Runtime.InteropServices.JavaScript.Native/interop/marshal.ts @@ -0,0 +1,331 @@ +// 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, + managedThreadTID: 0 as any as PThreadPtr, +}; + +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.getHeapU32(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..3028918a9026ff --- /dev/null +++ b/src/native/libs/System.Runtime.InteropServices.JavaScript.Native/interop/marshaled-types.ts @@ -0,0 +1,168 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +import { dotnetApi, dotnetAssert } from "./cross-module"; + +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 471f9b3259c553..6f257a7a2dfbd4 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,8 +19,167 @@ 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 CSFnHandle = { + __brand: "CSFnHandle" +} +export interface JSFunctionSignature extends NativePointer { + __brand: "JSFunctionSignatures" +} + 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 BindingClosureJS = { + fn: Function, + fqn: string, + isDisposed: boolean, + argsCount: number, + argMarshalers: (BoundMarshalerToJs)[], + resConverter: BoundMarshalerToCs | undefined, + hasCleanup: boolean, + isDiscardNoWait: boolean, + isAsync: boolean, + argCleanup: (Function | undefined)[] +} + +export type BindingClosureCS = { + fullyQualifiedName: string, + argsCount: number, + methodHandle: CSFnHandle, + argMarshalers: (BoundMarshalerToCs)[], + resConverter: BoundMarshalerToJs | undefined, + isAsync: boolean, + isDiscardNoWait: boolean, + isDisposed: boolean, +} + + +// 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/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..5967d0936ed5b0 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,14 @@ const exports = {}; libInteropJavaScriptNative(exports); - let commonDeps = ["$DOTNET"]; + let commonDeps = ["$DOTNET", + "SystemInteropJS_GetManagedStackTrace", + "SystemInteropJS_CallDelegate", + "SystemInteropJS_CompleteTask", + "SystemInteropJS_ReleaseJSOwnedObjectByGCHandle", + "SystemInteropJS_BindAssemblyExports", + "SystemInteropJS_CallJSExport" + ]; 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..d1c7409d46d9d6 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,13 @@ // The .NET Foundation licenses this file to you under the MIT license. import { } from "../../Common/JavaScript/cross-linked"; +import type { CSFnHandle, JSMarshalerArguments } from "../interop/types"; +declare global { + export function _SystemInteropJS_GetManagedStackTrace(args: JSMarshalerArguments): void; + export function _SystemInteropJS_CallDelegate(args: JSMarshalerArguments): void; + export function _SystemInteropJS_CompleteTask(args: JSMarshalerArguments): void; + export function _SystemInteropJS_ReleaseJSOwnedObjectByGCHandle(args: JSMarshalerArguments): void; + export function _SystemInteropJS_BindAssemblyExports(args: JSMarshalerArguments): void; + export function _SystemInteropJS_CallJSExport(methodHandle: CSFnHandle, 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..573531baacdb93 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,52 @@ // 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), + SystemInteropJS_CallDelegate: (args) => _SystemInteropJS_CallDelegate(args), + SystemInteropJS_CompleteTask: (args) => _SystemInteropJS_CompleteTask(args), + SystemInteropJS_ReleaseJSOwnedObjectByGCHandle: (args) => _SystemInteropJS_ReleaseJSOwnedObjectByGCHandle(args), + SystemInteropJS_BindAssemblyExports: (args) => _SystemInteropJS_BindAssemblyExports(args), + SystemInteropJS_CallJSExport: (methodHandle, args) => _SystemInteropJS_CallJSExport(methodHandle, args), }); - // eslint-disable-next-line @typescript-eslint/no-unused-vars function interopJavaScriptExportsToTable(map: InteropJavaScriptExports): InteropJavaScriptExportsTable { // keep in sync with interopJavaScriptExportsFromTable() return [ + map.SystemInteropJS_GetManagedStackTrace, + map.SystemInteropJS_CallDelegate, + map.SystemInteropJS_CompleteTask, + map.SystemInteropJS_ReleaseJSOwnedObjectByGCHandle, + map.SystemInteropJS_BindAssemblyExports, + map.SystemInteropJS_CallJSExport, ]; } dotnetUpdateInternals(internals, dotnetUpdateInternalsSubscriber);