diff --git a/.gitignore b/.gitignore index 71943be3a350db..1bbf3bb104201f 100644 --- a/.gitignore +++ b/.gitignore @@ -187,10 +187,6 @@ node_modules/ *.metaproj *.metaproj.tmp bin.localpkg/ -src/mono/wasm/runtime/dotnet.d.ts.sha256 -src/mono/wasm/runtime/dotnet-legacy.d.ts.sha256 - -src/mono/sample/wasm/browser-nextjs/public/ # RIA/Silverlight projects Generated_Code/ diff --git a/src/libraries/Common/src/Interop/Browser/Interop.Runtime.cs b/src/libraries/Common/src/Interop/Browser/Interop.Runtime.cs index e22b5edf99bf12..fc627a32e1fa0f 100644 --- a/src/libraries/Common/src/Interop/Browser/Interop.Runtime.cs +++ b/src/libraries/Common/src/Interop/Browser/Interop.Runtime.cs @@ -34,7 +34,7 @@ internal static unsafe partial class Runtime #if FEATURE_WASM_THREADS [MethodImpl(MethodImplOptions.InternalCall)] - public static extern void InstallWebWorkerInterop(); + public static extern void InstallWebWorkerInterop(IntPtr proxyContextGCHandle); [MethodImpl(MethodImplOptions.InternalCall)] public static extern void UninstallWebWorkerInterop(); #endif diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/System.Net.Http.Functional.Tests.csproj b/src/libraries/System.Net.Http/tests/FunctionalTests/System.Net.Http.Functional.Tests.csproj index 2451c867a9489d..773d73a39a0943 100644 --- a/src/libraries/System.Net.Http/tests/FunctionalTests/System.Net.Http.Functional.Tests.csproj +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/System.Net.Http.Functional.Tests.csproj @@ -16,6 +16,8 @@ $([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) $(DefineConstants);TargetsWindows $(DefineConstants);TARGETS_BROWSER + + <_XUnitBackgroundExec>false diff --git a/src/libraries/System.Net.WebSockets.Client/tests/System.Net.WebSockets.Client.Tests.csproj b/src/libraries/System.Net.WebSockets.Client/tests/System.Net.WebSockets.Client.Tests.csproj index b797fcf1894599..54be463b694e12 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/System.Net.WebSockets.Client.Tests.csproj +++ b/src/libraries/System.Net.WebSockets.Client/tests/System.Net.WebSockets.Client.Tests.csproj @@ -17,6 +17,9 @@ 01:15:00 + + + <_XUnitBackgroundExec>false diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/gen/JSImportGenerator/Marshaling/PrimitiveJSGenerator.cs b/src/libraries/System.Runtime.InteropServices.JavaScript/gen/JSImportGenerator/Marshaling/PrimitiveJSGenerator.cs index d3de4818346979..7690aa4fe43c88 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/gen/JSImportGenerator/Marshaling/PrimitiveJSGenerator.cs +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/gen/JSImportGenerator/Marshaling/PrimitiveJSGenerator.cs @@ -21,6 +21,7 @@ public PrimitiveJSGenerator(MarshalerType marshalerType) { } + // TODO order parameters in such way that affinity capturing parameters are emitted first public override IEnumerable Generate(TypePositionInfo info, StubCodeContext context) { string argName = context.GetAdditionalIdentifier(info, "js_arg"); 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 0f4f0fd6d41844..1b8664dc435917 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 @@ -46,6 +46,7 @@ + diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/CancelablePromise.cs b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/CancelablePromise.cs index bc34b5f9fc27a3..240199d470238f 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/CancelablePromise.cs +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/CancelablePromise.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. +using System.Threading; using System.Threading.Tasks; namespace System.Runtime.InteropServices.JavaScript @@ -21,13 +22,24 @@ public static void CancelPromise(Task promise) JSHostImplementation.PromiseHolder? holder = promise.AsyncState as JSHostImplementation.PromiseHolder; if (holder == null) throw new InvalidOperationException("Expected Task converted from JS Promise"); - -#if FEATURE_WASM_THREADS - holder.SynchronizationContext!.Send(static (JSHostImplementation.PromiseHolder holder) => +#if !FEATURE_WASM_THREADS + if (holder.IsDisposed) { -#endif + return; + } _CancelPromise(holder.GCHandle); -#if FEATURE_WASM_THREADS +#else + holder.ProxyContext.SynchronizationContext.Post(static (object? h) => + { + var holder = (JSHostImplementation.PromiseHolder)h!; + lock (holder.ProxyContext) + { + if (holder.IsDisposed) + { + return; + } + } + _CancelPromise(holder.GCHandle); }, holder); #endif } @@ -42,15 +54,27 @@ public static void CancelPromise(Task promise, Action callback, T state) JSHostImplementation.PromiseHolder? holder = promise.AsyncState as JSHostImplementation.PromiseHolder; if (holder == null) throw new InvalidOperationException("Expected Task converted from JS Promise"); - -#if FEATURE_WASM_THREADS - holder.SynchronizationContext!.Send((JSHostImplementation.PromiseHolder holder) => +#if !FEATURE_WASM_THREADS + if (holder.IsDisposed) { -#endif + return; + } + _CancelPromise(holder.GCHandle); + callback.Invoke(state); +#else + holder.ProxyContext.SynchronizationContext.Post(_ => + { + lock (holder.ProxyContext) + { + if (holder.IsDisposed) + { + return; + } + } + _CancelPromise(holder.GCHandle); callback.Invoke(state); -#if FEATURE_WASM_THREADS - }, holder); + }, null); #endif } } diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Interop/JavaScriptExports.cs b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Interop/JavaScriptExports.cs index 1233115fab0728..87676e9699cd97 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Interop/JavaScriptExports.cs +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Interop/JavaScriptExports.cs @@ -26,6 +26,11 @@ public static void CallEntrypoint(JSMarshalerArgument* arguments_buffer) ref JSMarshalerArgument arg_2 = ref arguments_buffer[3]; // initialized and set by caller try { +#if FEATURE_WASM_THREADS + // when we arrive here, we are on the thread which owns the proxies + arg_exc.AssertCurrentThreadContext(); +#endif + arg_1.ToManaged(out IntPtr entrypointPtr); if (entrypointPtr == IntPtr.Zero) { @@ -103,6 +108,10 @@ public static void LoadLazyAssembly(JSMarshalerArgument* arguments_buffer) ref JSMarshalerArgument arg_2 = ref arguments_buffer[3]; try { +#if FEATURE_WASM_THREADS + // when we arrive here, we are on the thread which owns the proxies + arg_exc.AssertCurrentThreadContext(); +#endif arg_1.ToManaged(out byte[]? dllBytes); arg_2.ToManaged(out byte[]? pdbBytes); @@ -121,6 +130,10 @@ public static void LoadSatelliteAssembly(JSMarshalerArgument* arguments_buffer) ref JSMarshalerArgument arg_1 = ref arguments_buffer[2]; try { +#if FEATURE_WASM_THREADS + // when we arrive here, we are on the thread which owns the proxies + arg_exc.AssertCurrentThreadContext(); +#endif arg_1.ToManaged(out byte[]? dllBytes); if (dllBytes != null) @@ -140,32 +153,12 @@ public static void ReleaseJSOwnedObjectByGCHandle(JSMarshalerArgument* arguments { ref JSMarshalerArgument arg_exc = ref arguments_buffer[0]; // initialized by caller in alloc_stack_frame() ref JSMarshalerArgument arg_1 = ref arguments_buffer[2]; // initialized and set by caller + try { - var gcHandle = arg_1.slot.GCHandle; - if (IsGCVHandle(gcHandle)) - { - if (ThreadJsOwnedHolders.Remove(gcHandle, out PromiseHolder? holder)) - { - holder.GCHandle = IntPtr.Zero; - holder.Callback!(null); - } - } - else - { - GCHandle handle = (GCHandle)gcHandle; - var target = handle.Target!; - if (target is PromiseHolder holder) - { - holder.GCHandle = IntPtr.Zero; - holder.Callback!(null); - } - else - { - ThreadJsOwnedObjects.Remove(target); - } - handle.Free(); - } + // when we arrive here, we are on the thread which owns the proxies + var ctx = arg_exc.AssertCurrentThreadContext(); + ctx.ReleaseJSOwnedObjectByGCHandle(arg_1.slot.GCHandle); } catch (Exception ex) { @@ -185,6 +178,11 @@ public static void CallDelegate(JSMarshalerArgument* arguments_buffer) // arg_4 set by JS caller when there are arguments try { +#if FEATURE_WASM_THREADS + // when we arrive here, we are on the thread which owns the proxies + arg_exc.AssertCurrentThreadContext(); +#endif + GCHandle callback_gc_handle = (GCHandle)arg_1.slot.GCHandle; if (callback_gc_handle.Target is ToManagedCallback callback) { @@ -210,34 +208,34 @@ public static void CompleteTask(JSMarshalerArgument* arguments_buffer) ref JSMarshalerArgument arg_1 = ref arguments_buffer[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 { - var holderGCHandle = arg_1.slot.GCHandle; - if (IsGCVHandle(holderGCHandle)) + // when we arrive here, we are on the thread which owns the proxies + var ctx = arg_exc.AssertCurrentThreadContext(); + var holder = ctx.GetPromiseHolder(arg_1.slot.GCHandle); + +#if FEATURE_WASM_THREADS + lock (ctx) { - if (ThreadJsOwnedHolders.Remove(holderGCHandle, out PromiseHolder? holder)) + if (holder.Callback == null) { - holder.GCHandle = IntPtr.Zero; - // arg_2, arg_3 are processed by the callback - holder.Callback!(arguments_buffer); + holder.CallbackReady = new ManualResetEventSlim(false); } } - else + if (holder.CallbackReady != null) { - GCHandle handle = (GCHandle)holderGCHandle; - var target = handle.Target!; - if (target is PromiseHolder holder) - { - holder.GCHandle = IntPtr.Zero; - // arg_2, arg_3 are processed by the callback - holder.Callback!(arguments_buffer); - } - else - { - ThreadJsOwnedObjects.Remove(target); - } - handle.Free(); +#pragma warning disable CA1416 // Validate platform compatibility + holder.CallbackReady?.Wait(); +#pragma warning restore CA1416 // Validate platform compatibility } +#endif + var callback = holder.Callback!; + ctx.ReleasePromiseHolder(arg_1.slot.GCHandle); + + // arg_2, arg_3 are processed by the callback + // JSProxyContext.PopOperation() is called by the callback + callback!(arguments_buffer); } catch (Exception ex) { @@ -254,6 +252,9 @@ public static void GetManagedStackTrace(JSMarshalerArgument* arguments_buffer) ref JSMarshalerArgument arg_1 = ref arguments_buffer[2];// initialized and set by caller try { + // when we arrive here, we are on the thread which owns the proxies + arg_exc.AssertCurrentThreadContext(); + GCHandle exception_gc_handle = (GCHandle)arg_1.slot.GCHandle; if (exception_gc_handle.Target is Exception exception) { @@ -275,17 +276,10 @@ public static void GetManagedStackTrace(JSMarshalerArgument* arguments_buffer) // this is here temporarily, until JSWebWorker becomes public API [DynamicDependency(DynamicallyAccessedMemberTypes.NonPublicMethods, "System.Runtime.InteropServices.JavaScript.JSWebWorker", "System.Runtime.InteropServices.JavaScript")] // the marshaled signature is: - // void InstallSynchronizationContext() - public static void InstallSynchronizationContext (JSMarshalerArgument* arguments_buffer) { - ref JSMarshalerArgument arg_exc = ref arguments_buffer[0]; // initialized by caller in alloc_stack_frame() - try - { - InstallWebWorkerInterop(true); - } - catch (Exception ex) - { - arg_exc.ToJS(ex); - } + // void InstallMainSynchronizationContext() + public static void InstallMainSynchronizationContext() + { + InstallWebWorkerInterop(true); } #endif diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Interop/JavaScriptImports.cs b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Interop/JavaScriptImports.cs index 4ebb8a772e236b..667fed536adaf4 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Interop/JavaScriptImports.cs +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Interop/JavaScriptImports.cs @@ -7,19 +7,6 @@ namespace System.Runtime.InteropServices.JavaScript { internal static unsafe partial class JavaScriptImports { - public static void ResolveOrRejectPromise(Span arguments) - { - fixed (JSMarshalerArgument* ptr = arguments) - { - Interop.Runtime.ResolveOrRejectPromise(ptr); - ref JSMarshalerArgument exceptionArg = ref arguments[0]; - if (exceptionArg.slot.Type != MarshalerType.None) - { - JSHostImplementation.ThrowException(ref exceptionArg); - } - } - } - #if !DISABLE_LEGACY_JS_INTEROP #region legacy diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Interop/LegacyExports.cs b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Interop/LegacyExports.cs index bb4b017ef743fb..928d30f74f513c 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Interop/LegacyExports.cs +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Interop/LegacyExports.cs @@ -32,17 +32,10 @@ internal static void PreventTrimming() public static void GetCSOwnedObjectByJSHandleRef(nint jsHandle, int shouldAddInflight, out JSObject? result) { - if (JSHostImplementation.ThreadCsOwnedObjects.TryGetValue(jsHandle, out WeakReference? reference)) - { - reference.TryGetTarget(out JSObject? jsObject); - if (shouldAddInflight != 0) - { - jsObject?.AddInFlight(); - } - result = jsObject; - return; - } - result = null; +#if FEATURE_WASM_THREADS + LegacyHostImplementation.ThrowIfLegacyWorkerThread(); +#endif + result = JSProxyContext.MainThreadContext.GetCSOwnedObjectByJSHandle(jsHandle, shouldAddInflight); } public static IntPtr GetCSOwnedObjectJSHandleRef(in JSObject jsObject, int shouldAddInflight) @@ -71,32 +64,7 @@ public static void CreateCSOwnedProxyRef(nint jsHandle, LegacyHostImplementation #if FEATURE_WASM_THREADS LegacyHostImplementation.ThrowIfLegacyWorkerThread(); #endif - - JSObject? res = null; - - if (!JSHostImplementation.ThreadCsOwnedObjects.TryGetValue(jsHandle, out WeakReference? reference) || - !reference.TryGetTarget(out res) || - res.IsDisposed) - { -#pragma warning disable CS0612 // Type or member is obsolete - res = mappedType switch - { - LegacyHostImplementation.MappedType.JSObject => new JSObject(jsHandle), - LegacyHostImplementation.MappedType.Array => new Array(jsHandle), - LegacyHostImplementation.MappedType.ArrayBuffer => new ArrayBuffer(jsHandle), - LegacyHostImplementation.MappedType.DataView => new DataView(jsHandle), - LegacyHostImplementation.MappedType.Function => new Function(jsHandle), - LegacyHostImplementation.MappedType.Uint8Array => new Uint8Array(jsHandle), - _ => throw new ArgumentOutOfRangeException(nameof(mappedType)) - }; -#pragma warning restore CS0612 // Type or member is obsolete - JSHostImplementation.ThreadCsOwnedObjects[jsHandle] = new WeakReference(res, trackResurrection: true); - } - if (shouldAddInflight != 0) - { - res.AddInFlight(); - } - jsObject = res; + jsObject = JSProxyContext.MainThreadContext.CreateCSOwnedProxy(jsHandle, mappedType, shouldAddInflight); } public static void GetJSOwnedObjectByGCHandleRef(int gcHandle, out object result) @@ -107,7 +75,7 @@ public static void GetJSOwnedObjectByGCHandleRef(int gcHandle, out object result public static IntPtr GetJSOwnedObjectGCHandleRef(in object obj) { - return JSHostImplementation.GetJSOwnedObjectGCHandle(obj, GCHandleType.Normal); + return JSProxyContext.MainThreadContext.GetJSOwnedObjectGCHandle(obj, GCHandleType.Normal); } public static IntPtr CreateTaskSource() diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSException.cs b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSException.cs index e623ef19238224..1a9e2278d57caa 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSException.cs +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSException.cs @@ -47,10 +47,10 @@ public override string? StackTrace } #if FEATURE_WASM_THREADS - var currentTID = JSSynchronizationContext.CurrentJSSynchronizationContext?.TargetTID; - if (jsException.OwnerTID != currentTID) + if (!jsException.ProxyContext.IsCurrentThread()) { - return bs; + // if we are on another thread, it would be too expensive and risky to obtain lazy stack trace. + return bs + Environment.NewLine + "... omitted JavaScript stack trace from another thread."; } #endif string? jsStackTrace = jsException.GetPropertyAsString("stack"); diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSFunctionBinding.cs b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSFunctionBinding.cs index 17a81b059f817b..25c0b74d4a80a0 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSFunctionBinding.cs +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSFunctionBinding.cs @@ -30,9 +30,6 @@ internal JSFunctionBinding() { } internal static volatile uint nextImportHandle = 1; internal int ImportHandle; internal bool IsAsync; -#if FEATURE_WASM_THREADS - internal bool IsThreadCaptured; -#endif [StructLayout(LayoutKind.Sequential, Pack = 4)] internal struct JSBindingHeader @@ -177,7 +174,7 @@ public static JSFunctionBinding BindJSFunction(string functionName, string modul if (RuntimeInformation.OSArchitecture != Architecture.Wasm) throw new PlatformNotSupportedException(); - return BindJSFunctionImpl(functionName, moduleName, signatures); + return BindJSImportImpl(functionName, moduleName, signatures); } /// @@ -220,15 +217,19 @@ internal static unsafe void InvokeJSFunction(JSObject jsFunction, Span arguments) { - if (signature.IsAsync) - { - // pre-allocate the result handle and Task #if FEATURE_WASM_THREADS - JSSynchronizationContext.AssertWebWorkerContext(); - var holder = new JSHostImplementation.PromiseHolder(JSSynchronizationContext.CurrentJSSynchronizationContext!); + var targetContext = JSProxyContext.SealJSImportCapturing(); + JSProxyContext.AssertIsInteropThread(); + arguments[0].slot.ContextHandle = targetContext.ContextHandle; + arguments[1].slot.ContextHandle = targetContext.ContextHandle; #else - var holder = new JSHostImplementation.PromiseHolder(); + var targetContext = JSProxyContext.MainThreadContext; #endif + + if (signature.IsAsync) + { + // pre-allocate the result handle and Task + var holder = new JSHostImplementation.PromiseHolder(targetContext); arguments[1].slot.Type = MarshalerType.TaskPreCreated; arguments[1].slot.GCHandle = holder.GCHandle; } @@ -253,10 +254,10 @@ internal static unsafe void InvokeJSImportImpl(JSFunctionBinding signature, Span } } - internal static unsafe JSFunctionBinding BindJSFunctionImpl(string functionName, string moduleName, ReadOnlySpan signatures) + internal static unsafe JSFunctionBinding BindJSImportImpl(string functionName, string moduleName, ReadOnlySpan signatures) { #if FEATURE_WASM_THREADS - JSSynchronizationContext.AssertWebWorkerContext(); + JSProxyContext.AssertIsInteropThread(); #endif var signature = JSHostImplementation.GetMethodSignature(signatures, functionName, moduleName); @@ -284,5 +285,19 @@ internal static unsafe JSFunctionBinding BindManagedFunctionImpl(string fullyQua return signature; } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static unsafe void ResolveOrRejectPromise(Span arguments) + { + fixed (JSMarshalerArgument* ptr = arguments) + { + Interop.Runtime.ResolveOrRejectPromise(ptr); + ref JSMarshalerArgument exceptionArg = ref arguments[0]; + if (exceptionArg.slot.Type != MarshalerType.None) + { + JSHostImplementation.ThrowException(ref exceptionArg); + } + } + } } } diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSHost.cs b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSHost.cs index 89a7eb8a0246e2..c1ec22aecdd7b4 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSHost.cs +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSHost.cs @@ -22,7 +22,7 @@ public static JSObject GlobalThis get { #if FEATURE_WASM_THREADS - JSSynchronizationContext.AssertWebWorkerContext(); + JSProxyContext.AssertIsInteropThread(); #endif return JavaScriptImports.GetGlobalThis(); } @@ -36,7 +36,7 @@ public static JSObject DotnetInstance get { #if FEATURE_WASM_THREADS - JSSynchronizationContext.AssertWebWorkerContext(); + JSProxyContext.AssertIsInteropThread(); #endif return JavaScriptImports.GetDotnetInstance(); } @@ -54,7 +54,7 @@ public static JSObject DotnetInstance public static Task ImportAsync(string moduleName, string moduleUrl, CancellationToken cancellationToken = default) { #if FEATURE_WASM_THREADS - JSSynchronizationContext.AssertWebWorkerContext(); + JSProxyContext.AssertIsInteropThread(); #endif return JSHostImplementation.ImportAsync(moduleName, moduleUrl, cancellationToken); } @@ -65,7 +65,7 @@ public static SynchronizationContext CurrentOrMainJSSynchronizationContext get { #if FEATURE_WASM_THREADS - return JSSynchronizationContext.CurrentJSSynchronizationContext ?? JSSynchronizationContext.MainJSSynchronizationContext!; + return (JSProxyContext.ExecutionContext ?? JSProxyContext.MainThreadContext).SynchronizationContext; #else return null!; #endif diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSHostImplementation.Types.cs b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSHostImplementation.Types.cs index a8b496237fd8ed..8d4dd3e6a3e877 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSHostImplementation.Types.cs +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSHostImplementation.Types.cs @@ -12,34 +12,24 @@ internal static partial class JSHostImplementation public sealed class PromiseHolder { - public nint GCHandle; // could be also virtual GCVHandle + public readonly nint GCHandle; // could be also virtual GCVHandle public ToManagedCallback? Callback; + public JSProxyContext ProxyContext; + public bool IsDisposed; #if FEATURE_WASM_THREADS - // the JavaScript object could only exist on the single web worker and can't migrate to other workers - internal JSSynchronizationContext SynchronizationContext; + public ManualResetEventSlim? CallbackReady; #endif -#if FEATURE_WASM_THREADS - // TODO possibly unify signature with non-MT and pass null - public PromiseHolder(JSSynchronizationContext targetContext) - { - GCHandle = (IntPtr)InteropServices.GCHandle.Alloc(this, GCHandleType.Normal); - SynchronizationContext = targetContext; - } -#else - public PromiseHolder() + public PromiseHolder(JSProxyContext targetContext) { GCHandle = (IntPtr)InteropServices.GCHandle.Alloc(this, GCHandleType.Normal); + ProxyContext = targetContext; } -#endif - public PromiseHolder(nint gcvHandle) + public PromiseHolder(JSProxyContext targetContext, nint gcvHandle) { GCHandle = gcvHandle; -#if FEATURE_WASM_THREADS - JSSynchronizationContext.AssertWebWorkerContext(); - SynchronizationContext = JSSynchronizationContext.CurrentJSSynchronizationContext!; -#endif + ProxyContext = targetContext; } } 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 b2c0504e4d4f39..877940b6bdacb1 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 @@ -1,7 +1,6 @@ // 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.CodeAnalysis; using System.IO; using System.Reflection; @@ -16,115 +15,6 @@ internal static partial class JSHostImplementation { private const string TaskGetResultName = "get_Result"; private static MethodInfo? s_taskGetResultMethodInfo; - // we use this to maintain identity of JSHandle for a JSObject proxy -#if FEATURE_WASM_THREADS - [ThreadStatic] -#endif - private static Dictionary>? s_csOwnedObjects; - - public static Dictionary> ThreadCsOwnedObjects - { - get - { - s_csOwnedObjects ??= new(); - return s_csOwnedObjects; - } - } - - // we use this to maintain identity of GCHandle for a managed object -#if FEATURE_WASM_THREADS - [ThreadStatic] -#endif - private static Dictionary? s_jsOwnedObjects; - - public static Dictionary ThreadJsOwnedObjects - { - get - { - s_jsOwnedObjects ??= new Dictionary(ReferenceEqualityComparer.Instance); - return s_jsOwnedObjects; - } - } - - // this is similar to GCHandle, but the GCVHandle is allocated on JS side and this keeps the C# proxy alive -#if FEATURE_WASM_THREADS - [ThreadStatic] -#endif - private static Dictionary? s_jsOwnedHolders; - - public static Dictionary ThreadJsOwnedHolders - { - get - { - s_jsOwnedHolders ??= new Dictionary(); - return s_jsOwnedHolders; - } - } - - // JSVHandle is like JSHandle, but it's not tracked and allocated by the JS side - // It's used when we need to create JSHandle-like identity ahead of time, before calling JS. - // they have negative values, so that they don't collide with JSHandles. -#if FEATURE_WASM_THREADS - [ThreadStatic] -#endif - public static nint NextJSVHandle; - -#if FEATURE_WASM_THREADS - [ThreadStatic] -#endif - private static List? s_JSVHandleFreeList; - public static List JSVHandleFreeList - { - get - { - s_JSVHandleFreeList ??= new(); - return s_JSVHandleFreeList; - } - } - - public static nint AllocJSVHandle() - { -#if FEATURE_WASM_THREADS - // TODO, when Task is passed to JSImport as parameter, it could be sent from another thread (in the future) - // and so we need to use JSVHandleFreeList of the target thread - JSSynchronizationContext.AssertWebWorkerContext(); -#endif - - if (JSVHandleFreeList.Count > 0) - { - var jsvHandle = JSVHandleFreeList[JSVHandleFreeList.Count]; - JSVHandleFreeList.RemoveAt(JSVHandleFreeList.Count - 1); - return jsvHandle; - } - if (NextJSVHandle == IntPtr.Zero) - { - NextJSVHandle = -2; - } - return NextJSVHandle--; - } - - public static void FreeJSVHandle(nint jsvHandle) - { - JSVHandleFreeList.Add(jsvHandle); - } - - public static bool IsGCVHandle(nint gcHandle) - { - return gcHandle < -1; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void ReleaseCSOwnedObject(nint jsHandle) - { - if (jsHandle != IntPtr.Zero) - { -#if FEATURE_WASM_THREADS - JSSynchronizationContext.AssertWebWorkerContext(); -#endif - ThreadCsOwnedObjects.Remove(jsHandle); - Interop.Runtime.ReleaseCSOwnedObject(jsHandle); - } - } public static bool GetTaskResultDynamic(Task task, out object? value) { @@ -143,30 +33,6 @@ public static bool GetTaskResultDynamic(Task task, out object? value) throw new InvalidOperationException(); } - // A JSOwnedObject is a managed object with its lifetime controlled by javascript. - // The managed side maintains a strong reference to the object, while the JS side - // maintains a weak reference and notifies the managed side if the JS wrapper object - // has been reclaimed by the JS GC. At that point, the managed side will release its - // strong references, allowing the managed object to be collected. - // This ensures that things like delegates and promises will never 'go away' while JS - // is expecting to be able to invoke or await them. - public static IntPtr GetJSOwnedObjectGCHandle(object obj, GCHandleType handleType = GCHandleType.Normal) - { - if (obj == null) - { - return IntPtr.Zero; - } - - IntPtr gcHandle; - if (ThreadJsOwnedObjects.TryGetValue(obj, out gcHandle)) - { - return gcHandle; - } - - IntPtr result = (IntPtr)GCHandle.Alloc(obj, handleType); - ThreadJsOwnedObjects[obj] = result; - return result; - } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static RuntimeMethodHandle GetMethodHandleFromIntPtr(IntPtr ptr) @@ -282,7 +148,6 @@ public static unsafe JSFunctionBinding GetMethodSignature(ReadOnlySpan 0 && (type.Type == MarshalerType.JSObject || type.Type == MarshalerType.JSException)) - { - signature.IsThreadCaptured = true; - } -#endif } signature.IsAsync = types[0]._signatureType.Type == MarshalerType.Task; @@ -330,24 +189,7 @@ public static unsafe void FreeMethodSignatureBuffer(JSFunctionBinding signature) signature.Sigs = null; } - public static JSObject CreateCSOwnedProxy(nint jsHandle) - { -#if FEATURE_WASM_THREADS - JSSynchronizationContext.AssertWebWorkerContext(); -#endif - JSObject? res; - - if (!ThreadCsOwnedObjects.TryGetValue(jsHandle, out WeakReference? reference) || - !reference.TryGetTarget(out res) || - res.IsDisposed) - { - res = new JSObject(jsHandle); - ThreadCsOwnedObjects[jsHandle] = new WeakReference(res, trackResurrection: true); - } - return res; - } - - [Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "It's always part of the single compilation (and trimming) unit.")] + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "It's always part of the single compilation (and trimming) unit.")] public static void LoadLazyAssembly(byte[] dllBytes, byte[]? pdbBytes) { if (pdbBytes == null) @@ -356,7 +198,7 @@ public static void LoadLazyAssembly(byte[] dllBytes, byte[]? pdbBytes) AssemblyLoadContext.Default.LoadFromStream(new MemoryStream(dllBytes), new MemoryStream(pdbBytes)); } - [Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "It's always part of the single compilation (and trimming) unit.")] + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "It's always part of the single compilation (and trimming) unit.")] public static void LoadSatelliteAssembly(byte[] dllBytes) { AssemblyLoadContext.Default.LoadFromStream(new MemoryStream(dllBytes)); @@ -365,92 +207,33 @@ public static void LoadSatelliteAssembly(byte[] dllBytes) #if FEATURE_WASM_THREADS public static void InstallWebWorkerInterop(bool isMainThread) { - Interop.Runtime.InstallWebWorkerInterop(); - var currentTID = GetNativeThreadId(); - var ctx = JSSynchronizationContext.CurrentJSSynchronizationContext; - if (ctx == null) - { - ctx = new JSSynchronizationContext(Thread.CurrentThread, currentTID); - ctx.previousSynchronizationContext = SynchronizationContext.Current; - JSSynchronizationContext.CurrentJSSynchronizationContext = ctx; - SynchronizationContext.SetSynchronizationContext(ctx); - if (isMainThread) - { - JSSynchronizationContext.MainJSSynchronizationContext = ctx; - } - } - else if (ctx.TargetTID != currentTID) + var ctx = new JSSynchronizationContext(isMainThread); + ctx.previousSynchronizationContext = SynchronizationContext.Current; + SynchronizationContext.SetSynchronizationContext(ctx); + + var proxyContext = ctx.ProxyContext; + JSProxyContext.CurrentThreadContext = proxyContext; + JSProxyContext.ExecutionContext = proxyContext; + if (isMainThread) { - Environment.FailFast($"JSSynchronizationContext.Install has wrong native thread id {ctx.TargetTID} != {currentTID}"); + JSProxyContext.MainThreadContext = proxyContext; } + ctx.AwaitNewData(); + + Interop.Runtime.InstallWebWorkerInterop(proxyContext.ContextHandle); } public static void UninstallWebWorkerInterop() { - var ctx = JSSynchronizationContext.CurrentJSSynchronizationContext; - var uninstallJSSynchronizationContext = ctx != null; - if (uninstallJSSynchronizationContext) - { - try - { - foreach (var jsObjectWeak in ThreadCsOwnedObjects.Values) - { - if (jsObjectWeak.TryGetTarget(out var jso)) - { - jso.Dispose(); - } - } - SynchronizationContext.SetSynchronizationContext(ctx!.previousSynchronizationContext); - JSSynchronizationContext.CurrentJSSynchronizationContext = null; - ctx.isDisposed = true; - } - catch (Exception ex) - { - Environment.FailFast($"Unexpected error in UninstallWebWorkerInterop, ManagedThreadId: {Thread.CurrentThread.ManagedThreadId}. " + ex); - } - } - else + var ctx = JSProxyContext.CurrentThreadContext; + if (ctx == null) throw new InvalidOperationException(); + var syncContext = ctx.SynchronizationContext; + if (SynchronizationContext.Current == syncContext) { - if (ThreadCsOwnedObjects.Count > 0) - { - Environment.FailFast($"There should be no JSObjects proxies on this thread, ManagedThreadId: {Thread.CurrentThread.ManagedThreadId}"); - } - if (ThreadJsOwnedObjects.Count > 0) - { - Environment.FailFast($"There should be no JS proxies of managed objects on this thread, ManagedThreadId: {Thread.CurrentThread.ManagedThreadId}"); - } + SynchronizationContext.SetSynchronizationContext(syncContext.previousSynchronizationContext); } - - Interop.Runtime.UninstallWebWorkerInterop(); - - if (uninstallJSSynchronizationContext) - { - try - { - foreach (var gch in ThreadJsOwnedObjects.Values) - { - GCHandle gcHandle = (GCHandle)gch; - gcHandle.Free(); - } - foreach (var holder in ThreadJsOwnedHolders.Values) - { - unsafe - { - holder.Callback!.Invoke(null); - } - } - } - catch (Exception ex) - { - Environment.FailFast($"Unexpected error in UninstallWebWorkerInterop, ManagedThreadId: {Thread.CurrentThread.ManagedThreadId}. " + ex); - } - } - - ThreadCsOwnedObjects.Clear(); - ThreadJsOwnedObjects.Clear(); - JSVHandleFreeList.Clear(); - NextJSVHandle = IntPtr.Zero; + ctx.Dispose(); } [UnsafeAccessor(UnsafeAccessorKind.Field, Name = "external_eventloop")] @@ -460,15 +243,6 @@ public static void SetHasExternalEventLoop(Thread thread) { GetThreadExternalEventloop(thread) = true; } - - [UnsafeAccessor(UnsafeAccessorKind.Field, Name = "thread_id")] - private static extern ref long GetThreadNativeThreadId(Thread @this); - - public static IntPtr GetNativeThreadId() - { - return (int)GetThreadNativeThreadId(Thread.CurrentThread); - } - #endif } diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSMarshalerArgument.cs b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSMarshalerArgument.cs index 38bc3e23ffc10a..fedb96a751662c 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSMarshalerArgument.cs +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSMarshalerArgument.cs @@ -3,7 +3,9 @@ using System.ComponentModel; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices.JavaScript; using System.Runtime.Versioning; +using System.Threading; namespace System.Runtime.InteropServices.JavaScript { @@ -18,7 +20,7 @@ public partial struct JSMarshalerArgument { internal JSMarshalerArgumentImpl slot; - [StructLayout(LayoutKind.Explicit, Pack = 16, Size = 16)] + [StructLayout(LayoutKind.Explicit, Pack = 32, Size = 32)] internal struct JSMarshalerArgumentImpl { [FieldOffset(0)] @@ -55,6 +57,9 @@ internal struct JSMarshalerArgumentImpl internal MarshalerType Type; [FieldOffset(13)] internal MarshalerType ElementType; + + [FieldOffset(16)] + internal IntPtr ContextHandle; } /// @@ -64,6 +69,98 @@ internal struct JSMarshalerArgumentImpl public unsafe void Initialize() { slot.Type = MarshalerType.None; +#if FEATURE_WASM_THREADS + // we know that this is at the start of some JSImport call, but we don't know yet what would be the target thread + // also this is called multiple times + JSProxyContext.JSImportWithUnknownContext(); + slot.ContextHandle = IntPtr.Zero; +#endif + } + +#if FEATURE_WASM_THREADS + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal unsafe void InitializeWithContext(JSProxyContext knownProxyContext) + { + slot.Type = MarshalerType.None; + slot.ContextHandle = knownProxyContext.ContextHandle; + } +#endif + // this is always called from ToManaged() marshaler +#pragma warning disable CA1822 // Mark members as static + internal JSProxyContext ToManagedContext +#pragma warning restore CA1822 // Mark members as static + { + get + { +#if !FEATURE_WASM_THREADS + return JSProxyContext.MainThreadContext; +#else + // ContextHandle always has to be set + // during JSImport, this is marshaling result/exception and it would be set by: + // - InvokeJSImport implementation + // - ActionJS.InvokeJS + // - ResolveVoidPromise/ResolvePromise/RejectPromise + // during JSExport, this is marshaling parameters and it would be set by: + // - alloc_stack_frame + // - set_js_handle/set_gc_handle + var proxyContextGCHandle = (GCHandle)slot.ContextHandle; + if (proxyContextGCHandle == default) + { + Environment.FailFast($"ContextHandle not set, ManagedThreadId: {Environment.CurrentManagedThreadId}. {Environment.NewLine} {Environment.StackTrace}"); + } + var argumentContext = (JSProxyContext)proxyContextGCHandle.Target!; + return argumentContext; +#endif + } + } + + // this is always called from ToJS() marshaler +#pragma warning disable CA1822 // Mark members as static + internal JSProxyContext ToJSContext +#pragma warning restore CA1822 // Mark members as static + { + get + { +#if !FEATURE_WASM_THREADS + return JSProxyContext.MainThreadContext; +#else + if (JSProxyContext.CapturingState == JSProxyContext.JSImportOperationState.JSImportParams) + { + // we are called from ToJS, during JSImport + // we need to check for captured or default context + return JSProxyContext.CurrentOperationContext; + } + // ContextHandle must be set be set by JS side of JSExport, and we are marshaling result of JSExport + var proxyContextGCHandle = slot.ContextHandle; + if (proxyContextGCHandle == IntPtr.Zero) + { + Environment.FailFast($"ContextHandle not set, ManagedThreadId: {Environment.CurrentManagedThreadId}. {Environment.NewLine} {Environment.StackTrace}"); + } + var argumentContext = (JSProxyContext)((GCHandle)proxyContextGCHandle).Target!; + return argumentContext; +#endif + } + } + + // make sure that we are on a thread with JS interop and that it matches the target of the argument +#pragma warning disable CA1822 // Mark members as static + internal JSProxyContext AssertCurrentThreadContext() +#pragma warning restore CA1822 // Mark members as static + { +#if !FEATURE_WASM_THREADS + return JSProxyContext.MainThreadContext; +#else + var currentThreadContext = JSProxyContext.CurrentThreadContext; + if (currentThreadContext == null) + { + Environment.FailFast($"Must be called on same thread with JS interop, ManagedThreadId: {Environment.CurrentManagedThreadId}. {Environment.NewLine} {Environment.StackTrace}"); + } + if (slot.ContextHandle != currentThreadContext.ContextHandle) + { + Environment.FailFast($"Must be called on same thread which created the stack frame, ManagedThreadId: {Environment.CurrentManagedThreadId}. {Environment.NewLine} {Environment.StackTrace}"); + } + return currentThreadContext; +#endif } } } diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSObject.References.cs b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSObject.References.cs index 82fe397b7ec473..cc56bad524a7c9 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSObject.References.cs +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSObject.References.cs @@ -10,53 +10,37 @@ namespace System.Runtime.InteropServices.JavaScript public partial class JSObject { internal nint JSHandle; - -#if FEATURE_WASM_THREADS - private readonly object _thisLock = new object(); - private SynchronizationContext? m_SynchronizationContext; -#endif + internal JSProxyContext ProxyContext; public SynchronizationContext SynchronizationContext { get { #if FEATURE_WASM_THREADS - return m_SynchronizationContext!; + return ProxyContext.SynchronizationContext; #else throw new PlatformNotSupportedException(); #endif } } -#if FEATURE_WASM_THREADS - // the JavaScript object could only exist on the single web worker and can't migrate to other workers - internal nint OwnerTID; -#endif #if !DISABLE_LEGACY_JS_INTEROP internal GCHandle? InFlight; internal int InFlightCounter; #endif - private bool _isDisposed; + internal bool _isDisposed; - internal JSObject(IntPtr jsHandle) + internal JSObject(IntPtr jsHandle, JSProxyContext ctx) { + ProxyContext = ctx; JSHandle = jsHandle; -#if FEATURE_WASM_THREADS - var ctx = JSSynchronizationContext.CurrentJSSynchronizationContext; - if (ctx == null) - { - Environment.FailFast("Missing CurrentJSSynchronizationContext"); - } - m_SynchronizationContext = ctx; - OwnerTID = ctx!.TargetTID; -#endif } #if !DISABLE_LEGACY_JS_INTEROP internal void AddInFlight() { ObjectDisposedException.ThrowIf(IsDisposed, this); - lock (this) + lock (ProxyContext) { InFlightCounter++; if (InFlightCounter == 1) @@ -72,7 +56,7 @@ internal void AddInFlight() // we only want JSObject to be disposed (from GC finalizer) once there is no in-flight reference and also no natural C# reference internal void ReleaseInFlight() { - lock (this) + lock (ProxyContext) { Debug.Assert(InFlightCounter != 0, "InFlightCounter != 0"); @@ -94,19 +78,17 @@ internal static void AssertThreadAffinity(object value) { return; } - JSSynchronizationContext.AssertWebWorkerContext(); - var currentTID = JSSynchronizationContext.CurrentJSSynchronizationContext!.TargetTID; if (value is JSObject jsObject) { - if (jsObject.OwnerTID != currentTID) + if (!jsObject.ProxyContext.IsCurrentThread()) { throw new InvalidOperationException("The JavaScript object can be used only on the thread where it was created."); } } else if (value is JSException jsException) { - if (jsException.jsException != null && jsException.jsException.OwnerTID != currentTID) + if (jsException.jsException != null && !jsException.jsException.ProxyContext.IsCurrentThread()) { throw new InvalidOperationException("The JavaScript object can be used only on the thread where it was created."); } @@ -123,43 +105,24 @@ internal static void AssertThreadAffinity(object value) /// public override string ToString() => $"(js-obj js '{JSHandle}')"; - internal void DisposeLocal() - { - JSHostImplementation.ThreadCsOwnedObjects.Remove(JSHandle); - _isDisposed = true; - JSHandle = IntPtr.Zero; - } - - private void DisposeThis() + internal void DisposeImpl(bool skipJsCleanup = false) { if (!_isDisposed) { #if FEATURE_WASM_THREADS - if (SynchronizationContext == SynchronizationContext.Current) + if (ProxyContext.IsCurrentThread()) { - lock (_thisLock) - { - JSHostImplementation.ReleaseCSOwnedObject(JSHandle); - _isDisposed = true; - JSHandle = IntPtr.Zero; - m_SynchronizationContext = null; - } //lock + JSProxyContext.ReleaseCSOwnedObject(this, skipJsCleanup); return; } - SynchronizationContext.Post(static (object? s) => + ProxyContext.SynchronizationContext.Post(static (object? s) => { - var self = (JSObject)s!; - lock (self._thisLock) - { - JSHostImplementation.ReleaseCSOwnedObject(self.JSHandle); - self._isDisposed = true; - self.JSHandle = IntPtr.Zero; - self.m_SynchronizationContext = null; - } //lock - }, this); + var x = ((JSObject self, bool skipJS))s!; + JSProxyContext.ReleaseCSOwnedObject(x.self, x.skipJS); + }, (this, skipJsCleanup)); #else - JSHostImplementation.ReleaseCSOwnedObject(JSHandle); + JSProxyContext.ReleaseCSOwnedObject(this, skipJsCleanup); _isDisposed = true; JSHandle = IntPtr.Zero; #endif @@ -168,7 +131,7 @@ private void DisposeThis() ~JSObject() { - DisposeThis(); + DisposeImpl(); } /// @@ -176,8 +139,7 @@ private void DisposeThis() /// public void Dispose() { - DisposeThis(); - GC.SuppressFinalize(this); + DisposeImpl(); } } } 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 new file mode 100644 index 00000000000000..be59f8cf717c07 --- /dev/null +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSProxyContext.cs @@ -0,0 +1,596 @@ +// 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.Runtime.CompilerServices; +using System.Threading; +using static System.Runtime.InteropServices.JavaScript.JSHostImplementation; + +namespace System.Runtime.InteropServices.JavaScript +{ + internal sealed class JSProxyContext : IDisposable + { + private bool _isDisposed; + + // we use this to maintain identity of JSHandle for a JSObject proxy + private readonly Dictionary> ThreadCsOwnedObjects = new(); + // we use this to maintain identity of GCHandle for a managed object + private readonly Dictionary ThreadJsOwnedObjects = new(ReferenceEqualityComparer.Instance); + // this is similar to GCHandle, but the GCVHandle is allocated on JS side and this keeps the C# proxy alive + private readonly Dictionary ThreadJsOwnedHolders = new(); + // JSVHandle is like JSHandle, but it's not tracked and allocated by the JS side + // It's used when we need to create JSHandle-like identity ahead of time, before calling JS. + // they have negative values, so that they don't collide with JSHandles. + private nint NextJSVHandle = -2; + private readonly List JSVHandleFreeList = new(); + +#if !FEATURE_WASM_THREADS + private JSProxyContext() + { + } +#else + public nint ContextHandle; + public nint NativeTID; + public int ManagedTID; + public bool IsMainThread; + public JSSynchronizationContext SynchronizationContext; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool IsCurrentThread() + { + return ManagedTID == Thread.CurrentThread.ManagedThreadId; + } + + [UnsafeAccessor(UnsafeAccessorKind.Field, Name = "thread_id")] + private static extern ref long GetThreadNativeThreadId(Thread @this); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static IntPtr GetNativeThreadId() + { + return (int)GetThreadNativeThreadId(Thread.CurrentThread); + } + + public JSProxyContext(bool isMainThread, JSSynchronizationContext synchronizationContext) + { + SynchronizationContext = synchronizationContext; + NativeTID = GetNativeThreadId(); + ManagedTID = Thread.CurrentThread.ManagedThreadId; + IsMainThread = isMainThread; + ContextHandle = (nint)GCHandle.Alloc(this, GCHandleType.Normal); + } +#endif + + #region Current operation context + +#if !FEATURE_WASM_THREADS + public static readonly JSProxyContext MainThreadContext = new(); + public static JSProxyContext CurrentThreadContext => MainThreadContext; + public static JSProxyContext CurrentOperationContext => MainThreadContext; + public static JSProxyContext PushOperationWithCurrentThreadContext() + { + // in single threaded build we don't have to keep stack of operations and the context/thread is always the same + return MainThreadContext; + } +#else + + // Context of the main thread + private static JSProxyContext? _MainThreadContext; + public static JSProxyContext MainThreadContext + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => _MainThreadContext!; + set => _MainThreadContext = value; + } + + public enum JSImportOperationState + { + None, + JSImportParams, + } + + [ThreadStatic] + private static JSProxyContext? _CapturedOperationContext; + [ThreadStatic] + private static JSImportOperationState _CapturingState; + + public static JSImportOperationState CapturingState => _CapturingState; + + // there will be call to JS from JSImport generated code, but we don't know which target thread yet + public static void JSImportWithUnknownContext() + { + // it would be ideal to assert here, that we arrived here with JSImportOperationState.None + // but any exception during JSImportOperationState.JSImportParams phase could make this state un-balanced + // typically this would be exception which is validating the marshaled value + // manually re-setting _CapturingState on each throw site would be possible, but fragile + // luckily, we always reset it here before any new JSImport call + // so the code which could interact with _CapturedOperationContext value will receive fresh values + _CapturingState = JSImportOperationState.JSImportParams; + _CapturedOperationContext = null; + } + + // there will be no capture during following call to JS + public static void JSImportNoCapture() + { + _CapturingState = JSImportOperationState.None; + _CapturedOperationContext = null; + } + + // we are at the end of marshaling of the JSImport parameters + public static JSProxyContext SealJSImportCapturing() + { + if (_CapturingState != JSImportOperationState.JSImportParams) + { + Environment.FailFast($"Method only allowed during JSImport capturing phase, ManagedThreadId: {Environment.CurrentManagedThreadId}. {Environment.NewLine} {Environment.StackTrace}"); + } + _CapturingState = JSImportOperationState.None; + var capturedOperationContext = _CapturedOperationContext; + _CapturedOperationContext = null; + + if (capturedOperationContext != null) + { + return capturedOperationContext; + } + // it could happen that we are in operation, in which we didn't capture target thread/context + var executionContext = ExecutionContext; + if (executionContext != null) + { + // we could will call JS on the current thread (or child task), if it has the JS interop installed + return executionContext; + } + // otherwise we will call JS on the main thread, which always has JS interop + return MainThreadContext; + } + + // this is called only during marshaling (in) parameters of JSImport, which have existing ProxyContext (thread affinity) + // together with CurrentOperationContext is will validate that all parameters of the call have same context/affinity + public static void CaptureContextFromParameter(JSProxyContext parameterContext) + { + if (_CapturingState != JSImportOperationState.JSImportParams) + { + Environment.FailFast($"Method only allowed during JSImport capturing phase, ManagedThreadId: {Environment.CurrentManagedThreadId}. {Environment.NewLine} {Environment.StackTrace}"); + } + + var capturedContext = _CapturedOperationContext; + + if (capturedContext == null) + { + _CapturedOperationContext = capturedContext; + } + else if (parameterContext != capturedContext) + { + _CapturedOperationContext = null; + _CapturingState = JSImportOperationState.None; + throw new InvalidOperationException("All JSObject proxies need to have same thread affinity. See https://aka.ms/dotnet-JS-interop-threads"); + } + } + + // Context flowing from parent thread into child tasks. + // Could be null on threads which don't have JS interop, like managed thread pool threads. Unless they inherit it from the current Task + // TODO flow it also with ExecutionContext to child threads ? + private static readonly AsyncLocal _currentThreadContext = new AsyncLocal(); + public static JSProxyContext? ExecutionContext + { + get => _currentThreadContext.Value; + set => _currentThreadContext.Value = value; + } + + [ThreadStatic] + public static JSProxyContext? CurrentThreadContext; + + // This is context to dispatch into. In order of preference + // - captured context by arguments of current/pending JSImport call + // - current thread context, for calls from JSWebWorker threads with the interop installed + // - main thread, for calls from any other thread, like managed thread pool or `new Thread` + public static JSProxyContext CurrentOperationContext + { + get + { + if (_CapturingState != JSImportOperationState.JSImportParams) + { + Environment.FailFast($"Method only allowed during JSImport capturing phase, ManagedThreadId: {Environment.CurrentManagedThreadId}. {Environment.NewLine} {Environment.StackTrace}"); + } + var capturedOperationContext = _CapturedOperationContext; + if (capturedOperationContext != null) + { + return capturedOperationContext; + } + // it could happen that we are in operation, in which we didn't capture target thread/context + var executionContext = ExecutionContext; + if (executionContext != null) + { + // capture this fallback for validation of all other parameters + _CapturedOperationContext = executionContext; + + // we could will call JS on the current thread (or child task), if it has the JS interop installed + return executionContext; + } + + // otherwise we will call JS on the main thread, which always has JS interop + var mainThreadContext = MainThreadContext; + + // capture this fallback for validation of all other parameters + // such validation could fail if Task is marshaled earlier than JSObject and uses different target context + _CapturedOperationContext = mainThreadContext; + + return mainThreadContext; + } + } + +#endif + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static JSProxyContext AssertIsInteropThread() + { +#if FEATURE_WASM_THREADS + var ctx = CurrentThreadContext; + if (ctx == null) + { + throw new InvalidOperationException($"Please use dedicated worker for working with JavaScript interop, ManagedThreadId:{Thread.CurrentThread.ManagedThreadId}. See https://aka.ms/dotnet-JS-interop-threads"); + } + if (ctx._isDisposed) + { + ObjectDisposedException.ThrowIf(ctx._isDisposed, ctx); + } + return ctx; +#else + return MainThreadContext; +#endif + } + + #endregion + + #region Handles + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsJSVHandle(nint jsHandle) + { + return jsHandle < -1; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsGCVHandle(nint gcHandle) + { + return gcHandle < -1; + } + + public nint AllocJSVHandle() + { + lock (this) + { + if (JSVHandleFreeList.Count > 0) + { + var jsvHandle = JSVHandleFreeList[JSVHandleFreeList.Count - 1]; + JSVHandleFreeList.RemoveAt(JSVHandleFreeList.Count - 1); + return jsvHandle; + } + if (NextJSVHandle == IntPtr.Zero) + { + NextJSVHandle = -2; + } + return NextJSVHandle--; + } + } + + public void FreeJSVHandle(nint jsvHandle) + { + lock (this) + { + JSVHandleFreeList.Add(jsvHandle); + } + } + + // A JSOwnedObject is a managed object with its lifetime controlled by javascript. + // The managed side maintains a strong reference to the object, while the JS side + // maintains a weak reference and notifies the managed side if the JS wrapper object + // has been reclaimed by the JS GC. At that point, the managed side will release its + // strong references, allowing the managed object to be collected. + // This ensures that things like delegates and promises will never 'go away' while JS + // is expecting to be able to invoke or await them. + public IntPtr GetJSOwnedObjectGCHandle(object obj, GCHandleType handleType = GCHandleType.Normal) + { + if (obj == null) + { + return IntPtr.Zero; + } + + lock (this) + { + if (ThreadJsOwnedObjects.TryGetValue(obj, out IntPtr gcHandle)) + { + return gcHandle; + } + + IntPtr result = (IntPtr)GCHandle.Alloc(obj, handleType); + ThreadJsOwnedObjects[obj] = result; + return result; + } + } + + public PromiseHolder CreatePromiseHolder() + { + lock (this) + { + return new PromiseHolder(this); + } + } + + public PromiseHolder GetPromiseHolder(nint gcHandle) + { + lock (this) + { + PromiseHolder? holder; + if (IsGCVHandle(gcHandle)) + { + if (!ThreadJsOwnedHolders.TryGetValue(gcHandle, out holder)) + { + holder = new PromiseHolder(this, gcHandle); + ThreadJsOwnedHolders.Add(gcHandle, holder); + } + } + else + { + holder = (PromiseHolder)((GCHandle)gcHandle).Target!; + } + return holder; + } + } + + public unsafe void ReleasePromiseHolder(nint holderGCHandle) + { + lock (this) + { + PromiseHolder? holder; + if (IsGCVHandle(holderGCHandle)) + { + if (!ThreadJsOwnedHolders.Remove(holderGCHandle, out holder)) + { + throw new InvalidOperationException("ReleasePromiseHolder expected PromiseHolder " + holderGCHandle); + } + } + else + { + GCHandle handle = (GCHandle)holderGCHandle; + var target = handle.Target!; + if (target is PromiseHolder holder2) + { + holder = holder2; + } + else + { + throw new InvalidOperationException("ReleasePromiseHolder expected PromiseHolder" + holderGCHandle); + } + handle.Free(); + } + holder.IsDisposed = true; + } + } + + public unsafe void ReleaseJSOwnedObjectByGCHandle(nint gcHandle) + { + ToManagedCallback? holderCallback = null; + lock (this) + { + PromiseHolder? holder = null; + if (IsGCVHandle(gcHandle)) + { + if (!ThreadJsOwnedHolders.Remove(gcHandle, out holder)) + { + throw new InvalidOperationException("ReleaseJSOwnedObjectByGCHandle expected in ThreadJsOwnedHolders"); + } + } + else + { + GCHandle handle = (GCHandle)gcHandle; + var target = handle.Target!; + if (target is PromiseHolder holder2) + { + holder = holder2; + } + else + { + if (!ThreadJsOwnedObjects.Remove(target)) + { + throw new InvalidOperationException("ReleaseJSOwnedObjectByGCHandle expected in ThreadJsOwnedObjects"); + } + } + handle.Free(); + } + if (holder != null) + { + holderCallback = holder.Callback; + holder.IsDisposed = true; + } + } + holderCallback?.Invoke(null); + } + + public JSObject CreateCSOwnedProxy(nint jsHandle) + { + lock (this) + { + JSObject? res; + if (!ThreadCsOwnedObjects.TryGetValue(jsHandle, out WeakReference? reference) || + !reference.TryGetTarget(out res) || + res.IsDisposed) + { + res = new JSObject(jsHandle, this); + ThreadCsOwnedObjects[jsHandle] = new WeakReference(res, trackResurrection: true); + } + return res; + } + } + + public static void ReleaseCSOwnedObject(JSObject proxy, bool skipJS) + { + if (proxy.IsDisposed) + { + return; + } + var ctx = proxy.ProxyContext; +#if FEATURE_WASM_THREADS + if (!ctx.IsCurrentThread()) + { + throw new InvalidOperationException($"ReleaseCSOwnedObject has to run on the thread with same affinity as the proxy. ManagedThreadId: {Environment.CurrentManagedThreadId} JSHandle: {proxy.JSHandle}"); + } +#endif + lock (ctx) + { + if (proxy.IsDisposed) + { + return; + } + proxy._isDisposed = true; + GC.SuppressFinalize(proxy); + var jsHandle = proxy.JSHandle; + if (!ctx.ThreadCsOwnedObjects.Remove(jsHandle)) + { + Environment.FailFast($"ReleaseCSOwnedObject expected to find registration for JSHandle: {jsHandle}, ManagedThreadId: {Environment.CurrentManagedThreadId}. {Environment.NewLine} {Environment.StackTrace}"); + }; + if (!skipJS) + { + Interop.Runtime.ReleaseCSOwnedObject(jsHandle); + } + if (IsJSVHandle(jsHandle)) + { + ctx.FreeJSVHandle(jsHandle); + } + } + } + + #endregion + + #region Legacy + + // legacy + public void RegisterCSOwnedObject(JSObject proxy) + { + lock (this) + { + ThreadCsOwnedObjects[(int)proxy.JSHandle] = new WeakReference(proxy, trackResurrection: true); + } + } + + // legacy + public JSObject? GetCSOwnedObjectByJSHandle(nint jsHandle, int shouldAddInflight) + { + lock (this) + { + if (ThreadCsOwnedObjects.TryGetValue(jsHandle, out WeakReference? reference)) + { + reference.TryGetTarget(out JSObject? jsObject); + if (shouldAddInflight != 0) + { + jsObject?.AddInFlight(); + } + return jsObject; + } + } + return null; + } + + // legacy + public JSObject CreateCSOwnedProxy(nint jsHandle, LegacyHostImplementation.MappedType mappedType, int shouldAddInflight) + { + lock (this) + { + JSObject? res = null; + if (!ThreadCsOwnedObjects.TryGetValue(jsHandle, out WeakReference? reference) || + !reference.TryGetTarget(out res) || + res.IsDisposed) + { +#pragma warning disable CS0612 // Type or member is obsolete + res = mappedType switch + { + LegacyHostImplementation.MappedType.JSObject => new JSObject(jsHandle, JSProxyContext.MainThreadContext), + LegacyHostImplementation.MappedType.Array => new Array(jsHandle), + LegacyHostImplementation.MappedType.ArrayBuffer => new ArrayBuffer(jsHandle), + LegacyHostImplementation.MappedType.DataView => new DataView(jsHandle), + LegacyHostImplementation.MappedType.Function => new Function(jsHandle), + LegacyHostImplementation.MappedType.Uint8Array => new Uint8Array(jsHandle), + _ => throw new ArgumentOutOfRangeException(nameof(mappedType)) + }; +#pragma warning restore CS0612 // Type or member is obsolete + ThreadCsOwnedObjects[jsHandle] = new WeakReference(res, trackResurrection: true); + } + if (shouldAddInflight != 0) + { + res.AddInFlight(); + } + return res; + } + } + + #endregion + + #region Dispose + + private void Dispose(bool disposing) + { + lock (this) + { + if (!_isDisposed) + { +#if FEATURE_WASM_THREADS + if (!IsCurrentThread()) + { + Environment.FailFast($"JSProxyContext must be disposed on the thread which owns it, ManagedThreadId: {Environment.CurrentManagedThreadId}. {Environment.NewLine} {Environment.StackTrace}"); + } + ((GCHandle)ContextHandle).Free(); +#endif + + List> copy = new(ThreadCsOwnedObjects.Values); + foreach (var jsObjectWeak in copy) + { + if (jsObjectWeak.TryGetTarget(out var jso)) + { + jso.Dispose(); + } + } + +#if FEATURE_WASM_THREADS + Interop.Runtime.UninstallWebWorkerInterop(); +#endif + + foreach (var gch in ThreadJsOwnedObjects.Values) + { + GCHandle gcHandle = (GCHandle)gch; + gcHandle.Free(); + } + foreach (var holder in ThreadJsOwnedHolders.Values) + { + unsafe + { + holder.Callback!.Invoke(null); + } + } + + ThreadCsOwnedObjects.Clear(); + ThreadJsOwnedObjects.Clear(); + JSVHandleFreeList.Clear(); + NextJSVHandle = IntPtr.Zero; + + if (disposing) + { +#if FEATURE_WASM_THREADS + SynchronizationContext.Dispose(); +#endif + } + _isDisposed = true; + } + } + } + + ~JSProxyContext() + { + Dispose(disposing: false); + } + + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + #endregion + } +} diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSSynchronizationContext.cs b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSSynchronizationContext.cs index 639906274d399f..d04fa731a49aa4 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSSynchronizationContext.cs +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSSynchronizationContext.cs @@ -6,9 +6,8 @@ using System.Threading; using System.Threading.Channels; using System.Runtime.CompilerServices; -using WorkItemQueueType = System.Threading.Channels.Channel; -using static System.Runtime.InteropServices.JavaScript.JSHostImplementation; using System.Collections.Generic; +using WorkItemQueueType = System.Threading.Channels.Channel; namespace System.Runtime.InteropServices.JavaScript { @@ -21,17 +20,12 @@ namespace System.Runtime.InteropServices.JavaScript /// internal sealed class JSSynchronizationContext : SynchronizationContext { + internal readonly JSProxyContext ProxyContext; private readonly Action _DataIsAvailable;// don't allocate Action on each call to UnsafeOnCompleted - public readonly Thread TargetThread; - public readonly IntPtr TargetTID; private readonly WorkItemQueueType Queue; - internal static JSSynchronizationContext? MainJSSynchronizationContext; - - [ThreadStatic] - internal static JSSynchronizationContext? CurrentJSSynchronizationContext; internal SynchronizationContext? previousSynchronizationContext; - internal bool isDisposed; + internal bool _isDisposed; internal readonly struct WorkItem { @@ -47,42 +41,33 @@ public WorkItem(SendOrPostCallback callback, object? data, ManualResetEventSlim? } } - internal JSSynchronizationContext(Thread targetThread, IntPtr targetThreadId) - : this( - targetThread, targetThreadId, - Channel.CreateUnbounded( - new UnboundedChannelOptions { SingleWriter = false, SingleReader = true, AllowSynchronousContinuations = true } - ) - ) + public JSSynchronizationContext(bool isMainThread) { + ProxyContext = new JSProxyContext(isMainThread, this); + Queue = Channel.CreateUnbounded(new UnboundedChannelOptions { SingleWriter = false, SingleReader = true, AllowSynchronousContinuations = true }); + _DataIsAvailable = DataIsAvailable; } - internal static void AssertWebWorkerContext() - { -#if FEATURE_WASM_THREADS - if (CurrentJSSynchronizationContext == null) - { - throw new InvalidOperationException("Please use dedicated worker for working with JavaScript interop. See https://aka.ms/dotnet-JS-interop-threads"); - } -#endif - } - - private JSSynchronizationContext(Thread targetThread, IntPtr targetTID, WorkItemQueueType queue) + internal JSSynchronizationContext(JSProxyContext proxyContext, WorkItemQueueType queue, Action dataIsAvailable) { - TargetThread = targetThread; - TargetTID = targetTID; + ProxyContext = proxyContext; Queue = queue; - _DataIsAvailable = DataIsAvailable; + _DataIsAvailable = dataIsAvailable; } public override SynchronizationContext CreateCopy() { - return new JSSynchronizationContext(TargetThread, TargetTID, Queue); + return new JSSynchronizationContext(ProxyContext, Queue, _DataIsAvailable); } internal void AwaitNewData() { - ObjectDisposedException.ThrowIf(isDisposed, this); + if (_isDisposed) + { + // FIXME: there could be abandoned work, but here we have no way how to propagate the failure + // ObjectDisposedException.ThrowIf(_isDisposed, this); + return; + } var vt = Queue.Reader.WaitToReadAsync(); if (vt.IsCompleted) @@ -103,16 +88,16 @@ private unsafe void DataIsAvailable() { // While we COULD pump here, we don't want to. We want the pump to happen on the next event loop turn. // Otherwise we could get a chain where a pump generates a new work item and that makes us pump again, forever. - TargetThreadScheduleBackgroundJob(TargetTID, (void*)(delegate* unmanaged[Cdecl])&BackgroundJobHandler); + TargetThreadScheduleBackgroundJob(ProxyContext.NativeTID, (void*)(delegate* unmanaged[Cdecl])&BackgroundJobHandler); } public override void Post(SendOrPostCallback d, object? state) { - ObjectDisposedException.ThrowIf(isDisposed, this); + ObjectDisposedException.ThrowIf(_isDisposed, this); var workItem = new WorkItem(d, state, null); if (!Queue.Writer.TryWrite(workItem)) - Environment.FailFast("JSSynchronizationContext.Post failed"); + Environment.FailFast($"JSSynchronizationContext.Post failed, ManagedThreadId: {Environment.CurrentManagedThreadId}. {Environment.NewLine} {Environment.StackTrace}"); } // This path can only run when threading is enabled @@ -120,9 +105,9 @@ public override void Post(SendOrPostCallback d, object? state) public override void Send(SendOrPostCallback d, object? state) { - ObjectDisposedException.ThrowIf(isDisposed, this); + ObjectDisposedException.ThrowIf(_isDisposed, this); - if (Thread.CurrentThread == TargetThread) + if (ProxyContext.IsCurrentThread()) { d(state); return; @@ -132,7 +117,7 @@ public override void Send(SendOrPostCallback d, object? state) { var workItem = new WorkItem(d, state, signal); if (!Queue.Writer.TryWrite(workItem)) - Environment.FailFast("JSSynchronizationContext.Send failed"); + Environment.FailFast($"JSSynchronizationContext.Send failed, ManagedThreadId: {Environment.CurrentManagedThreadId}. {Environment.NewLine} {Environment.StackTrace}"); signal.Wait(); } @@ -147,14 +132,16 @@ public override void Send(SendOrPostCallback d, object? state) // this callback will arrive on the target thread, called from mono_background_exec private static void BackgroundJobHandler() { - CurrentJSSynchronizationContext!.Pump(); + var ctx = JSProxyContext.AssertIsInteropThread(); + ctx.SynchronizationContext.Pump(); } private void Pump() { - if (isDisposed) + if (_isDisposed) { // FIXME: there could be abandoned work, but here we have no way how to propagate the failure + // ObjectDisposedException.ThrowIf(_isDisposed, this); return; } try @@ -181,9 +168,28 @@ private void Pump() finally { // If an item throws, we want to ensure that the next pump gets scheduled appropriately regardless. - if(!isDisposed) AwaitNewData(); + if (!_isDisposed) AwaitNewData(); } } + + private void Dispose(bool disposing) + { + if (!_isDisposed) + { + if (disposing) + { + Queue.Writer.Complete(); + } + previousSynchronizationContext = null; + _isDisposed = true; + } + } + + internal void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } } } diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSWebWorker.cs b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSWebWorker.cs index 7b2cf935bacb40..9cc19c7406b96a 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSWebWorker.cs +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSWebWorker.cs @@ -32,15 +32,19 @@ public static Task RunAsync(Func body) public static async Task RunAsync(Func> body, CancellationToken cancellationToken) { - // TODO remove main thread condition later - if (Thread.CurrentThread.ManagedThreadId == 1) await JavaScriptImports.ThreadAvailable().ConfigureAwait(false); + if (JSProxyContext.MainThreadContext.IsCurrentThread()) + { + await JavaScriptImports.ThreadAvailable().ConfigureAwait(false); + } return await RunAsyncImpl(body, cancellationToken).ConfigureAwait(false); } public static async Task RunAsync(Func body, CancellationToken cancellationToken) { - // TODO remove main thread condition later - if (Thread.CurrentThread.ManagedThreadId == 1) await JavaScriptImports.ThreadAvailable().ConfigureAwait(false); + if (JSProxyContext.MainThreadContext.IsCurrentThread()) + { + await JavaScriptImports.ThreadAvailable().ConfigureAwait(false); + } await RunAsyncImpl(body, cancellationToken).ConfigureAwait(false); } diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Legacy/Array.cs b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Legacy/Array.cs index 29ada438164ccf..7ce50a8e741f28 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Legacy/Array.cs +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Legacy/Array.cs @@ -16,19 +16,19 @@ public class Array : JSObject /// /// Parameters. public Array(params object[] _params) - : base(JavaScriptImports.CreateCSOwnedObject(nameof(Array), _params)) + : base(JavaScriptImports.CreateCSOwnedObject(nameof(Array), _params), JSProxyContext.MainThreadContext) { #if FEATURE_WASM_THREADS LegacyHostImplementation.ThrowIfLegacyWorkerThread(); #endif - LegacyHostImplementation.RegisterCSOwnedObject(this); + JSProxyContext.MainThreadContext.RegisterCSOwnedObject(this); } /// /// Initializes a new instance of the Array/> class. /// /// Js handle. - internal Array(IntPtr jsHandle) : base(jsHandle) + internal Array(IntPtr jsHandle) : base(jsHandle, JSProxyContext.MainThreadContext) { } /// diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Legacy/ArrayBuffer.cs b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Legacy/ArrayBuffer.cs index 39db0563089d0d..92e78a14fe1b27 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Legacy/ArrayBuffer.cs +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Legacy/ArrayBuffer.cs @@ -11,19 +11,19 @@ public class ArrayBuffer : JSObject /// /// Length. public ArrayBuffer(int length) - : base(JavaScriptImports.CreateCSOwnedObject(nameof(ArrayBuffer), new object[] { length })) + : base(JavaScriptImports.CreateCSOwnedObject(nameof(ArrayBuffer), new object[] { length }), JSProxyContext.MainThreadContext) { #if FEATURE_WASM_THREADS LegacyHostImplementation.ThrowIfLegacyWorkerThread(); #endif - LegacyHostImplementation.RegisterCSOwnedObject(this); + JSProxyContext.MainThreadContext.RegisterCSOwnedObject(this); } /// /// Initializes a new instance of the JavaScript Core ArrayBuffer class. /// /// Js handle. - internal ArrayBuffer(IntPtr jsHandle) : base(jsHandle) + internal ArrayBuffer(IntPtr jsHandle) : base(jsHandle, JSProxyContext.MainThreadContext) { } /// diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Legacy/DataView.cs b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Legacy/DataView.cs index 3d3300ec5c5039..1046fa8f99fa45 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Legacy/DataView.cs +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Legacy/DataView.cs @@ -15,12 +15,12 @@ public class DataView : JSObject /// /// ArrayBuffer to use as the storage backing the new DataView object. public DataView(ArrayBuffer buffer) - : base(JavaScriptImports.CreateCSOwnedObject(nameof(DataView), new object[] { buffer })) + : base(JavaScriptImports.CreateCSOwnedObject(nameof(DataView), new object[] { buffer }), JSProxyContext.MainThreadContext) { #if FEATURE_WASM_THREADS LegacyHostImplementation.ThrowIfLegacyWorkerThread(); #endif - LegacyHostImplementation.RegisterCSOwnedObject(this); + JSProxyContext.MainThreadContext.RegisterCSOwnedObject(this); } /// @@ -29,12 +29,12 @@ public DataView(ArrayBuffer buffer) /// ArrayBuffer to use as the storage backing the new DataView object. /// The offset, in bytes, to the first byte in the above buffer for the new view to reference. If unspecified, the buffer view starts with the first byte. public DataView(ArrayBuffer buffer, int byteOffset) - : base(JavaScriptImports.CreateCSOwnedObject(nameof(DataView), new object[] { buffer, byteOffset })) + : base(JavaScriptImports.CreateCSOwnedObject(nameof(DataView), new object[] { buffer, byteOffset }), JSProxyContext.MainThreadContext) { #if FEATURE_WASM_THREADS LegacyHostImplementation.ThrowIfLegacyWorkerThread(); #endif - LegacyHostImplementation.RegisterCSOwnedObject(this); + JSProxyContext.MainThreadContext.RegisterCSOwnedObject(this); } /// @@ -44,19 +44,19 @@ public DataView(ArrayBuffer buffer, int byteOffset) /// The offset, in bytes, to the first byte in the above buffer for the new view to reference. If unspecified, the buffer view starts with the first byte. /// The number of elements in the byte array. If unspecified, the view's length will match the buffer's length. public DataView(ArrayBuffer buffer, int byteOffset, int byteLength) - : base(JavaScriptImports.CreateCSOwnedObject(nameof(DataView), new object[] { buffer, byteOffset, byteLength })) + : base(JavaScriptImports.CreateCSOwnedObject(nameof(DataView), new object[] { buffer, byteOffset, byteLength }), JSProxyContext.MainThreadContext) { #if FEATURE_WASM_THREADS LegacyHostImplementation.ThrowIfLegacyWorkerThread(); #endif - LegacyHostImplementation.RegisterCSOwnedObject(this); + JSProxyContext.MainThreadContext.RegisterCSOwnedObject(this); } /// /// Initializes a new instance of the DataView class. /// /// Js handle. - internal DataView(IntPtr jsHandle) : base(jsHandle) + internal DataView(IntPtr jsHandle) : base(jsHandle, JSProxyContext.MainThreadContext) { } /// diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Legacy/Function.cs b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Legacy/Function.cs index f87fb94c01610a..ebce83d01417ad 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Legacy/Function.cs +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Legacy/Function.cs @@ -16,15 +16,15 @@ namespace System.Runtime.InteropServices.JavaScript public class Function : JSObject { public Function(params object[] args) - : base(JavaScriptImports.CreateCSOwnedObject(nameof(Function), args)) + : base(JavaScriptImports.CreateCSOwnedObject(nameof(Function), args), JSProxyContext.MainThreadContext) { #if FEATURE_WASM_THREADS LegacyHostImplementation.ThrowIfLegacyWorkerThread(); #endif - LegacyHostImplementation.RegisterCSOwnedObject(this); + JSProxyContext.MainThreadContext.RegisterCSOwnedObject(this); } - internal Function(IntPtr jsHandle) : base(jsHandle) + internal Function(IntPtr jsHandle) : base(jsHandle, JSProxyContext.MainThreadContext) { } /// diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Legacy/LegacyHostImplementation.cs b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Legacy/LegacyHostImplementation.cs index f726c26262b283..8e6242017979f2 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Legacy/LegacyHostImplementation.cs +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Legacy/LegacyHostImplementation.cs @@ -18,12 +18,6 @@ public static void ReleaseInFlight(object obj) jsObj?.ReleaseInFlight(); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void RegisterCSOwnedObject(JSObject proxy) - { - JSHostImplementation.ThreadCsOwnedObjects[(int)proxy.JSHandle] = new WeakReference(proxy, trackResurrection: true); - } - public static MarshalType GetMarshalTypeFromType(Type type) { if (type is null) diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Legacy/Uint8Array.cs b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Legacy/Uint8Array.cs index b80f7c31ad64b4..1cea722c1cc623 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Legacy/Uint8Array.cs +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Legacy/Uint8Array.cs @@ -9,24 +9,24 @@ namespace System.Runtime.InteropServices.JavaScript public sealed class Uint8Array : JSObject { public Uint8Array(int length) - : base(JavaScriptImports.CreateCSOwnedObject(nameof(Uint8Array), new object[] { length })) + : base(JavaScriptImports.CreateCSOwnedObject(nameof(Uint8Array), new object[] { length }), JSProxyContext.MainThreadContext) { #if FEATURE_WASM_THREADS LegacyHostImplementation.ThrowIfLegacyWorkerThread(); #endif - LegacyHostImplementation.RegisterCSOwnedObject(this); + JSProxyContext.MainThreadContext.RegisterCSOwnedObject(this); } public Uint8Array(ArrayBuffer buffer) - : base(JavaScriptImports.CreateCSOwnedObject(nameof(Uint8Array), new object[] { buffer })) + : base(JavaScriptImports.CreateCSOwnedObject(nameof(Uint8Array), new object[] { buffer }), JSProxyContext.MainThreadContext) { #if FEATURE_WASM_THREADS LegacyHostImplementation.ThrowIfLegacyWorkerThread(); #endif - LegacyHostImplementation.RegisterCSOwnedObject(this); + JSProxyContext.MainThreadContext.RegisterCSOwnedObject(this); } - internal Uint8Array(IntPtr jsHandle) : base(jsHandle) + internal Uint8Array(IntPtr jsHandle) : base(jsHandle, JSProxyContext.MainThreadContext) { } public int Length diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Marshaling/JSMarshalerArgument.Byte.cs b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Marshaling/JSMarshalerArgument.Byte.cs index 1ce1dc2da6a7e2..5392fca48fae8b 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Marshaling/JSMarshalerArgument.Byte.cs +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Marshaling/JSMarshalerArgument.Byte.cs @@ -133,7 +133,8 @@ public unsafe void ToJS(ArraySegment value) return; } slot.Type = MarshalerType.ArraySegment; - slot.GCHandle = JSHostImplementation.GetJSOwnedObjectGCHandle(value.Array, GCHandleType.Pinned); + var ctx = ToJSContext; + slot.GCHandle = ctx.GetJSOwnedObjectGCHandle(value.Array, GCHandleType.Pinned); var refPtr = (IntPtr)Unsafe.AsPointer(ref MemoryMarshal.GetArrayDataReference(value.Array)); slot.IntPtrValue = refPtr + value.Offset; slot.Length = value.Count; diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Marshaling/JSMarshalerArgument.Double.cs b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Marshaling/JSMarshalerArgument.Double.cs index 9589ea0f42f7ab..9b7f48ed4b3acd 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Marshaling/JSMarshalerArgument.Double.cs +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Marshaling/JSMarshalerArgument.Double.cs @@ -135,7 +135,8 @@ public unsafe void ToJS(ArraySegment value) return; } slot.Type = MarshalerType.ArraySegment; - slot.GCHandle = JSHostImplementation.GetJSOwnedObjectGCHandle(value.Array, GCHandleType.Pinned); + var ctx = ToJSContext; + slot.GCHandle = ctx.GetJSOwnedObjectGCHandle(value.Array, GCHandleType.Pinned); var refPtr = (IntPtr)Unsafe.AsPointer(ref MemoryMarshal.GetArrayDataReference(value.Array)); slot.IntPtrValue = refPtr + (value.Offset * sizeof(double)); slot.Length = value.Count; diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Marshaling/JSMarshalerArgument.Exception.cs b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Marshaling/JSMarshalerArgument.Exception.cs index e19a3f428c8a23..f89c4a669818c7 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Marshaling/JSMarshalerArgument.Exception.cs +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Marshaling/JSMarshalerArgument.Exception.cs @@ -33,7 +33,8 @@ public unsafe void ToManaged(out Exception? value) if (slot.JSHandle != IntPtr.Zero) { // this is JSException round-trip - jsException = JSHostImplementation.CreateCSOwnedProxy(slot.JSHandle); + var ctx = ToManagedContext; + jsException = ctx.CreateCSOwnedProxy(slot.JSHandle); } string? message; @@ -65,11 +66,21 @@ public unsafe void ToJS(Exception? value) var jse = cpy as JSException; if (jse != null && jse.jsException != null) { + ObjectDisposedException.ThrowIf(jse.jsException.IsDisposed, value); #if FEATURE_WASM_THREADS JSObject.AssertThreadAffinity(value); + var ctx = jse.jsException.ProxyContext; + if (JSProxyContext.CapturingState == JSProxyContext.JSImportOperationState.JSImportParams) + { + JSProxyContext.CaptureContextFromParameter(ctx); + slot.ContextHandle = ctx.ContextHandle; + } + else if (slot.ContextHandle != ctx.ContextHandle) + { + Environment.FailFast($"ContextHandle mismatch, ManagedThreadId: {Environment.CurrentManagedThreadId}. {Environment.NewLine} {Environment.StackTrace}"); + } #endif // this is JSException roundtrip - ObjectDisposedException.ThrowIf(jse.jsException.IsDisposed, value); slot.Type = MarshalerType.JSException; slot.JSHandle = jse.jsException.JSHandle; } @@ -77,7 +88,9 @@ public unsafe void ToJS(Exception? value) { ToJS(cpy.Message); slot.Type = MarshalerType.Exception; - slot.GCHandle = JSHostImplementation.GetJSOwnedObjectGCHandle(cpy); + + var ctx = ToJSContext; + slot.GCHandle = ctx.GetJSOwnedObjectGCHandle(cpy); } } } diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Marshaling/JSMarshalerArgument.Func.cs b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Marshaling/JSMarshalerArgument.Func.cs index ca2c0bab7bdff8..ff3183c78ed397 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Marshaling/JSMarshalerArgument.Func.cs +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Marshaling/JSMarshalerArgument.Func.cs @@ -9,9 +9,9 @@ private sealed class ActionJS { private JSObject JSObject; - public ActionJS(IntPtr jsHandle) + public ActionJS(JSObject holder) { - JSObject = JSHostImplementation.CreateCSOwnedProxy(jsHandle); + JSObject = holder; } public void InvokeJS() @@ -26,8 +26,14 @@ public void InvokeJS() Span arguments = stackalloc JSMarshalerArgument[4]; ref JSMarshalerArgument args_exception = ref arguments[0]; ref JSMarshalerArgument args_return = ref arguments[1]; +#if FEATURE_WASM_THREADS + args_exception.InitializeWithContext(JSObject.ProxyContext); + args_return.InitializeWithContext(JSObject.ProxyContext); + JSProxyContext.JSImportNoCapture(); +#else args_exception.Initialize(); args_return.Initialize(); +#endif JSFunctionBinding.InvokeJSFunction(JSObject, arguments); } @@ -39,9 +45,9 @@ private sealed class ActionJS private ArgumentToJSCallback Arg1Marshaler; private JSObject JSObject; - public ActionJS(IntPtr jsHandle, ArgumentToJSCallback arg1Marshaler) + public ActionJS(JSObject holder, ArgumentToJSCallback arg1Marshaler) { - JSObject = JSHostImplementation.CreateCSOwnedProxy(jsHandle); + JSObject = holder; Arg1Marshaler = arg1Marshaler; } @@ -56,10 +62,18 @@ public void InvokeJS(T arg1) ref JSMarshalerArgument args_return = ref arguments[1]; ref JSMarshalerArgument args_arg1 = ref arguments[2]; +#if FEATURE_WASM_THREADS + args_exception.InitializeWithContext(JSObject.ProxyContext); + args_return.InitializeWithContext(JSObject.ProxyContext); + args_arg1.InitializeWithContext(JSObject.ProxyContext); + JSProxyContext.JSImportNoCapture(); +#else args_exception.Initialize(); args_return.Initialize(); +#endif Arg1Marshaler(ref args_arg1, arg1); + JSFunctionBinding.InvokeJSFunction(JSObject, arguments); } } @@ -70,9 +84,9 @@ private sealed class ActionJS private ArgumentToJSCallback Arg2Marshaler; private JSObject JSObject; - public ActionJS(IntPtr jsHandle, ArgumentToJSCallback arg1Marshaler, ArgumentToJSCallback arg2Marshaler) + public ActionJS(JSObject holder, ArgumentToJSCallback arg1Marshaler, ArgumentToJSCallback arg2Marshaler) { - JSObject = JSHostImplementation.CreateCSOwnedProxy(jsHandle); + JSObject = holder; Arg1Marshaler = arg1Marshaler; Arg2Marshaler = arg2Marshaler; } @@ -89,11 +103,20 @@ public void InvokeJS(T1 arg1, T2 arg2) ref JSMarshalerArgument args_arg1 = ref arguments[2]; ref JSMarshalerArgument args_arg2 = ref arguments[3]; +#if FEATURE_WASM_THREADS + args_exception.InitializeWithContext(JSObject.ProxyContext); + args_return.InitializeWithContext(JSObject.ProxyContext); + args_arg1.InitializeWithContext(JSObject.ProxyContext); + args_arg2.InitializeWithContext(JSObject.ProxyContext); + JSProxyContext.JSImportNoCapture(); +#else args_exception.Initialize(); args_return.Initialize(); +#endif Arg1Marshaler(ref args_arg1, arg1); Arg2Marshaler(ref args_arg2, arg2); + JSFunctionBinding.InvokeJSFunction(JSObject, arguments); } } @@ -105,9 +128,9 @@ private sealed class ActionJS private ArgumentToJSCallback Arg3Marshaler; private JSObject JSObject; - public ActionJS(IntPtr jsHandle, ArgumentToJSCallback arg1Marshaler, ArgumentToJSCallback arg2Marshaler, ArgumentToJSCallback arg3Marshaler) + public ActionJS(JSObject holder, ArgumentToJSCallback arg1Marshaler, ArgumentToJSCallback arg2Marshaler, ArgumentToJSCallback arg3Marshaler) { - JSObject = JSHostImplementation.CreateCSOwnedProxy(jsHandle); + JSObject = holder; Arg1Marshaler = arg1Marshaler; Arg2Marshaler = arg2Marshaler; Arg3Marshaler = arg3Marshaler; @@ -126,12 +149,22 @@ public void InvokeJS(T1 arg1, T2 arg2, T3 arg3) ref JSMarshalerArgument args_arg2 = ref arguments[3]; ref JSMarshalerArgument args_arg3 = ref arguments[4]; +#if FEATURE_WASM_THREADS + args_exception.InitializeWithContext(JSObject.ProxyContext); + args_return.InitializeWithContext(JSObject.ProxyContext); + args_arg1.InitializeWithContext(JSObject.ProxyContext); + args_arg2.InitializeWithContext(JSObject.ProxyContext); + args_arg3.InitializeWithContext(JSObject.ProxyContext); + JSProxyContext.JSImportNoCapture(); +#else args_exception.Initialize(); args_return.Initialize(); +#endif Arg1Marshaler(ref args_arg1, arg1); Arg2Marshaler(ref args_arg2, arg2); Arg3Marshaler(ref args_arg3, arg3); + JSFunctionBinding.InvokeJSFunction(JSObject, arguments); } } @@ -149,7 +182,9 @@ public unsafe void ToManaged(out Action? value) return; } - value = new ActionJS(slot.JSHandle).InvokeJS; + var ctx = ToManagedContext; + var holder = ctx.CreateCSOwnedProxy(slot.JSHandle); + value = new ActionJS(holder).InvokeJS; } /// @@ -167,7 +202,9 @@ public unsafe void ToManaged(out Action? value, ArgumentToJSCallback ar return; } - value = new ActionJS(slot.JSHandle, arg1Marshaler).InvokeJS; + var ctx = ToManagedContext; + var holder = ctx.CreateCSOwnedProxy(slot.JSHandle); + value = new ActionJS(holder, arg1Marshaler).InvokeJS; } /// @@ -187,7 +224,9 @@ public unsafe void ToManaged(out Action? value, ArgumentToJSCall return; } - value = new ActionJS(slot.JSHandle, arg1Marshaler, arg2Marshaler).InvokeJS; + var ctx = ToManagedContext; + var holder = ctx.CreateCSOwnedProxy(slot.JSHandle); + value = new ActionJS(holder, arg1Marshaler, arg2Marshaler).InvokeJS; } /// @@ -209,7 +248,9 @@ public unsafe void ToManaged(out Action? value, Argument return; } - value = new ActionJS(slot.JSHandle, arg1Marshaler, arg2Marshaler, arg3Marshaler).InvokeJS; + var ctx = ToManagedContext; + var holder = ctx.CreateCSOwnedProxy(slot.JSHandle); + value = new ActionJS(holder, arg1Marshaler, arg2Marshaler, arg3Marshaler).InvokeJS; } private sealed class FuncJS @@ -217,9 +258,9 @@ private sealed class FuncJS private JSObject JSObject; private ArgumentToManagedCallback ResMarshaler; - public FuncJS(IntPtr jsHandle, ArgumentToManagedCallback resMarshaler) + public FuncJS(JSObject holder, ArgumentToManagedCallback resMarshaler) { - JSObject = JSHostImplementation.CreateCSOwnedProxy(jsHandle); + JSObject = holder; ResMarshaler = resMarshaler; } @@ -235,12 +276,19 @@ public TResult InvokeJS() Span arguments = stackalloc JSMarshalerArgument[4]; ref JSMarshalerArgument args_exception = ref arguments[0]; ref JSMarshalerArgument args_return = ref arguments[1]; +#if FEATURE_WASM_THREADS + args_exception.InitializeWithContext(JSObject.ProxyContext); + args_return.InitializeWithContext(JSObject.ProxyContext); + JSProxyContext.JSImportNoCapture(); +#else args_exception.Initialize(); args_return.Initialize(); +#endif JSFunctionBinding.InvokeJSFunction(JSObject, arguments); ResMarshaler(ref args_return, out TResult res); + return res; } @@ -252,9 +300,9 @@ private sealed class FuncJS private ArgumentToManagedCallback ResMarshaler; private JSObject JSObject; - public FuncJS(IntPtr jsHandle, ArgumentToJSCallback arg1Marshaler, ArgumentToManagedCallback resMarshaler) + public FuncJS(JSObject holder, ArgumentToJSCallback arg1Marshaler, ArgumentToManagedCallback resMarshaler) { - JSObject = JSHostImplementation.CreateCSOwnedProxy(jsHandle); + JSObject = holder; Arg1Marshaler = arg1Marshaler; ResMarshaler = resMarshaler; } @@ -270,8 +318,15 @@ public TResult InvokeJS(T arg1) ref JSMarshalerArgument args_return = ref arguments[1]; ref JSMarshalerArgument args_arg1 = ref arguments[2]; +#if FEATURE_WASM_THREADS + args_exception.InitializeWithContext(JSObject.ProxyContext); + args_return.InitializeWithContext(JSObject.ProxyContext); + args_arg1.InitializeWithContext(JSObject.ProxyContext); + JSProxyContext.JSImportNoCapture(); +#else args_exception.Initialize(); args_return.Initialize(); +#endif Arg1Marshaler(ref args_arg1, arg1); JSFunctionBinding.InvokeJSFunction(JSObject, arguments); @@ -288,9 +343,9 @@ private sealed class FuncJS private ArgumentToManagedCallback ResMarshaler; private JSObject JSObject; - public FuncJS(IntPtr jsHandle, ArgumentToJSCallback arg1Marshaler, ArgumentToJSCallback arg2Marshaler, ArgumentToManagedCallback resMarshaler) + public FuncJS(JSObject holder, ArgumentToJSCallback arg1Marshaler, ArgumentToJSCallback arg2Marshaler, ArgumentToManagedCallback resMarshaler) { - JSObject = JSHostImplementation.CreateCSOwnedProxy(jsHandle); + JSObject = holder; Arg1Marshaler = arg1Marshaler; Arg2Marshaler = arg2Marshaler; ResMarshaler = resMarshaler; @@ -308,8 +363,16 @@ public TResult InvokeJS(T1 arg1, T2 arg2) ref JSMarshalerArgument args_arg1 = ref arguments[2]; ref JSMarshalerArgument args_arg2 = ref arguments[3]; +#if FEATURE_WASM_THREADS + args_exception.InitializeWithContext(JSObject.ProxyContext); + args_return.InitializeWithContext(JSObject.ProxyContext); + args_arg1.InitializeWithContext(JSObject.ProxyContext); + args_arg2.InitializeWithContext(JSObject.ProxyContext); + JSProxyContext.JSImportNoCapture(); +#else args_exception.Initialize(); args_return.Initialize(); +#endif Arg1Marshaler(ref args_arg1, arg1); Arg2Marshaler(ref args_arg2, arg2); @@ -328,9 +391,9 @@ private sealed class FuncJS private ArgumentToManagedCallback ResMarshaler; private JSObject JSObject; - public FuncJS(IntPtr jsHandle, ArgumentToJSCallback arg1Marshaler, ArgumentToJSCallback arg2Marshaler, ArgumentToJSCallback arg3Marshaler, ArgumentToManagedCallback resMarshaler) + public FuncJS(JSObject holder, ArgumentToJSCallback arg1Marshaler, ArgumentToJSCallback arg2Marshaler, ArgumentToJSCallback arg3Marshaler, ArgumentToManagedCallback resMarshaler) { - JSObject = JSHostImplementation.CreateCSOwnedProxy(jsHandle); + JSObject = holder; Arg1Marshaler = arg1Marshaler; Arg2Marshaler = arg2Marshaler; Arg3Marshaler = arg3Marshaler; @@ -350,15 +413,24 @@ public TResult InvokeJS(T1 arg1, T2 arg2, T3 arg3) ref JSMarshalerArgument args_arg2 = ref arguments[3]; ref JSMarshalerArgument args_arg3 = ref arguments[4]; +#if FEATURE_WASM_THREADS + args_exception.InitializeWithContext(JSObject.ProxyContext); + args_return.InitializeWithContext(JSObject.ProxyContext); + args_arg1.InitializeWithContext(JSObject.ProxyContext); + args_arg2.InitializeWithContext(JSObject.ProxyContext); + args_arg3.InitializeWithContext(JSObject.ProxyContext); + JSProxyContext.JSImportNoCapture(); +#else args_exception.Initialize(); args_return.Initialize(); +#endif Arg1Marshaler(ref args_arg1, arg1); Arg2Marshaler(ref args_arg2, arg2); Arg3Marshaler(ref args_arg3, arg3); JSFunctionBinding.InvokeJSFunction(JSObject, arguments); - ResMarshaler(ref args_return, out TResult res); + return res; } } @@ -378,7 +450,9 @@ public unsafe void ToManaged(out Func? value, ArgumentToManage return; } - value = new FuncJS(slot.JSHandle, resMarshaler).InvokeJS; + var ctx = ToManagedContext; + var holder = ctx.CreateCSOwnedProxy(slot.JSHandle); + value = new FuncJS(holder, resMarshaler).InvokeJS; } /// @@ -398,7 +472,9 @@ public unsafe void ToManaged(out Func? value, ArgumentTo return; } - value = new FuncJS(slot.JSHandle, arg1Marshaler, resMarshaler).InvokeJS; + var ctx = ToManagedContext; + var holder = ctx.CreateCSOwnedProxy(slot.JSHandle); + value = new FuncJS(holder, arg1Marshaler, resMarshaler).InvokeJS; } @@ -421,7 +497,9 @@ public unsafe void ToManaged(out Func? value, return; } - value = new FuncJS(slot.JSHandle, arg1Marshaler, arg2Marshaler, resMarshaler).InvokeJS; + var ctx = ToManagedContext; + var holder = ctx.CreateCSOwnedProxy(slot.JSHandle); + value = new FuncJS(holder, arg1Marshaler, arg2Marshaler, resMarshaler).InvokeJS; } /// @@ -444,8 +522,9 @@ public unsafe void ToManaged(out Func? value = null; return; } - - value = new FuncJS(slot.JSHandle, arg1Marshaler, arg2Marshaler, arg3Marshaler, resMarshaler).InvokeJS; + var ctx = ToManagedContext; + var holder = ctx.CreateCSOwnedProxy(slot.JSHandle); + value = new FuncJS(holder, arg1Marshaler, arg2Marshaler, arg3Marshaler, resMarshaler).InvokeJS; } /// @@ -463,7 +542,8 @@ public unsafe void ToJS(Action value) // eventual exception is handled by C# caller }; slot.Type = MarshalerType.Function; - slot.GCHandle = JSHostImplementation.GetJSOwnedObjectGCHandle(cb); + var ctx = ToJSContext; + slot.GCHandle = ctx.GetJSOwnedObjectGCHandle(cb); } /// @@ -484,7 +564,8 @@ public unsafe void ToJS(Action value, ArgumentToManagedCallback arg1Mar // eventual exception is handled by C# caller }; slot.Type = MarshalerType.Action; - slot.GCHandle = JSHostImplementation.GetJSOwnedObjectGCHandle(cb); + var ctx = ToJSContext; + slot.GCHandle = ctx.GetJSOwnedObjectGCHandle(cb); } /// @@ -509,7 +590,8 @@ public unsafe void ToJS(Action value, ArgumentToManagedCallback< // eventual exception is handled by C# caller }; slot.Type = MarshalerType.Action; - slot.GCHandle = JSHostImplementation.GetJSOwnedObjectGCHandle(cb); + var ctx = ToJSContext; + slot.GCHandle = ctx.GetJSOwnedObjectGCHandle(cb); } /// @@ -538,7 +620,8 @@ public unsafe void ToJS(Action value, ArgumentToManagedC // eventual exception is handled by C# caller }; slot.Type = MarshalerType.Action; - slot.GCHandle = JSHostImplementation.GetJSOwnedObjectGCHandle(cb); + var ctx = ToJSContext; + slot.GCHandle = ctx.GetJSOwnedObjectGCHandle(cb); } /// @@ -559,7 +642,8 @@ public unsafe void ToJS(Func value, ArgumentToJSCallback @@ -584,7 +668,8 @@ public unsafe void ToJS(Func value, ArgumentToManagedCal // eventual exception is handled by C# caller }; slot.Type = MarshalerType.Function; - slot.GCHandle = JSHostImplementation.GetJSOwnedObjectGCHandle(cb); + var ctx = ToJSContext; + slot.GCHandle = ctx.GetJSOwnedObjectGCHandle(cb); } /// @@ -613,7 +698,8 @@ public unsafe void ToJS(Func value, ArgumentTo // eventual exception is handled by C# caller }; slot.Type = MarshalerType.Function; - slot.GCHandle = JSHostImplementation.GetJSOwnedObjectGCHandle(cb); + var ctx = ToJSContext; + slot.GCHandle = ctx.GetJSOwnedObjectGCHandle(cb); } /// @@ -646,7 +732,8 @@ public unsafe void ToJS(Func value, Ar // eventual exception is handled by C# caller }; slot.Type = MarshalerType.Function; - slot.GCHandle = JSHostImplementation.GetJSOwnedObjectGCHandle(cb); + var ctx = ToJSContext; + slot.GCHandle = ctx.GetJSOwnedObjectGCHandle(cb); } } } diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Marshaling/JSMarshalerArgument.Int32.cs b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Marshaling/JSMarshalerArgument.Int32.cs index 0f384d1bf75a03..501484af3ab4fa 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Marshaling/JSMarshalerArgument.Int32.cs +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Marshaling/JSMarshalerArgument.Int32.cs @@ -132,8 +132,9 @@ public unsafe void ToJS(ArraySegment value) slot.Type = MarshalerType.None; return; } + var ctx = ToJSContext; slot.Type = MarshalerType.ArraySegment; - slot.GCHandle = JSHostImplementation.GetJSOwnedObjectGCHandle(value.Array, GCHandleType.Pinned); + slot.GCHandle = ctx.GetJSOwnedObjectGCHandle(value.Array, GCHandleType.Pinned); var refPtr = (IntPtr)Unsafe.AsPointer(ref MemoryMarshal.GetArrayDataReference(value.Array)); slot.IntPtrValue = refPtr + (value.Offset * sizeof(int)); slot.Length = value.Count; diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Marshaling/JSMarshalerArgument.JSObject.cs b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Marshaling/JSMarshalerArgument.JSObject.cs index a7220c8934a6d9..3c41ec2fccc564 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Marshaling/JSMarshalerArgument.JSObject.cs +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Marshaling/JSMarshalerArgument.JSObject.cs @@ -20,7 +20,8 @@ public unsafe void ToManaged(out JSObject? value) value = null; return; } - value = JSHostImplementation.CreateCSOwnedProxy(slot.JSHandle); + var ctx = ToManagedContext; + value = ctx.CreateCSOwnedProxy(slot.JSHandle); } /// @@ -34,13 +35,25 @@ public void ToJS(JSObject? value) if (value == null) { slot.Type = MarshalerType.None; + // Note: when null JSObject is passed as argument, it can't be used to capture the target thread in JSProxyContext.CapturedInstance + // in case there is no other argument to capture it from, the call will be dispatched according to JSProxyContext.Default } else { + ObjectDisposedException.ThrowIf(value.IsDisposed, value); #if FEATURE_WASM_THREADS JSObject.AssertThreadAffinity(value); + var ctx = value.ProxyContext; + if (JSProxyContext.CapturingState == JSProxyContext.JSImportOperationState.JSImportParams) + { + JSProxyContext.CaptureContextFromParameter(ctx); + slot.ContextHandle = ctx.ContextHandle; + } + else if (slot.ContextHandle != ctx.ContextHandle) + { + Environment.FailFast($"ContextHandle mismatch, ManagedThreadId: {Environment.CurrentManagedThreadId}. {Environment.NewLine} {Environment.StackTrace}"); + } #endif - ObjectDisposedException.ThrowIf(value.IsDisposed, value); slot.Type = MarshalerType.JSObject; slot.JSHandle = value.JSHandle; } diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Marshaling/JSMarshalerArgument.Object.cs b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Marshaling/JSMarshalerArgument.Object.cs index cf3d7e45856660..75fa4d6aa2f0ed 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Marshaling/JSMarshalerArgument.Object.cs +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Marshaling/JSMarshalerArgument.Object.cs @@ -317,7 +317,8 @@ public void ToJS(object? value) else { slot.Type = MarshalerType.Object; - slot.GCHandle = JSHostImplementation.GetJSOwnedObjectGCHandle(value); + var ctx = ToJSContext; + slot.GCHandle = ctx.GetJSOwnedObjectGCHandle(value); } } diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Marshaling/JSMarshalerArgument.Task.cs b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Marshaling/JSMarshalerArgument.Task.cs index bf721e40b4ab0f..51bfa81989133c 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Marshaling/JSMarshalerArgument.Task.cs +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Marshaling/JSMarshalerArgument.Task.cs @@ -4,6 +4,8 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Threading.Tasks; +using System.ComponentModel; +using System.Threading; using static System.Runtime.InteropServices.JavaScript.JSHostImplementation; namespace System.Runtime.InteropServices.JavaScript @@ -17,7 +19,7 @@ public partial struct JSMarshalerArgument /// Type of the marshaled value. /// The low-level argument representation. /// The value to be marshaled. - [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + [EditorBrowsableAttribute(EditorBrowsableState.Never)] public delegate void ArgumentToManagedCallback(ref JSMarshalerArgument arg, out T value); /// @@ -27,7 +29,7 @@ public partial struct JSMarshalerArgument /// Type of the marshaled value. /// The low-level argument representation. /// The value to be marshaled. - [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + [EditorBrowsableAttribute(EditorBrowsableState.Never)] public delegate void ArgumentToJSCallback(ref JSMarshalerArgument arg, T value); /// @@ -43,30 +45,38 @@ public unsafe void ToManaged(out Task? value) value = null; return; } - PromiseHolder holder = GetPromiseHolder(slot.GCHandle); - TaskCompletionSource tcs = new TaskCompletionSource(holder); - ToManagedCallback callback = (JSMarshalerArgument* arguments_buffer) => + var ctx = ToManagedContext; + lock (ctx) { - if (arguments_buffer == null) + PromiseHolder holder = ctx.GetPromiseHolder(slot.GCHandle); + TaskCompletionSource tcs = new TaskCompletionSource(holder); + ToManagedCallback callback = (JSMarshalerArgument* arguments_buffer) => { - tcs.TrySetException(new TaskCanceledException("WebWorker which is origin of the Promise is being terminated.")); - return; - } - ref JSMarshalerArgument arg_2 = ref arguments_buffer[3]; // set by caller when this is SetException call - // arg_3 set by caller when this is SetResult call, un-used here - if (arg_2.slot.Type != MarshalerType.None) - { - arg_2.ToManaged(out Exception? fail); - tcs.SetException(fail!); - } - else - { - tcs.SetResult(); - } - // eventual exception is handled by caller - }; - holder.Callback = callback; - value = tcs.Task; + if (arguments_buffer == null) + { + tcs.TrySetException(new TaskCanceledException("WebWorker which is origin of the Promise is being terminated.")); + return; + } + ref JSMarshalerArgument arg_2 = ref arguments_buffer[3]; // set by caller when this is SetException call + // arg_3 set by caller when this is SetResult call, un-used here + if (arg_2.slot.Type != MarshalerType.None) + { + arg_2.ToManaged(out Exception? fail); + tcs.SetException(fail!); + } + else + { + tcs.SetResult(); + } + // eventual exception is handled by caller + }; + holder.Callback = callback; + value = tcs.Task; +#if FEATURE_WASM_THREADS + // if the other thread created it, signal that it's ready + holder.CallbackReady?.Set(); +#endif + } } /// @@ -74,8 +84,8 @@ public unsafe void ToManaged(out Task? value) /// It's used by JSImport code generator and should not be used by developers in source code. /// /// The value to be marshaled. - /// The generated callback which marshals the result value of the . - /// Type of marshaled result of the . + /// The generated callback which marshals the result value of the . + /// Type of marshaled result of the . public unsafe void ToManaged(out Task? value, ArgumentToManagedCallback marshaler) { // there is no nice way in JS how to check that JS promise is already resolved, to send MarshalerType.TaskRejected, MarshalerType.TaskResolved @@ -84,53 +94,44 @@ public unsafe void ToManaged(out Task? value, ArgumentToManagedCallback value = null; return; } - PromiseHolder holder = GetPromiseHolder(slot.GCHandle); - TaskCompletionSource tcs = new TaskCompletionSource(holder); - ToManagedCallback callback = (JSMarshalerArgument* arguments_buffer) => + var ctx = ToManagedContext; + lock (ctx) { - if (arguments_buffer == null) - { - tcs.TrySetException(new TaskCanceledException("WebWorker which is origin of the Promise is being terminated.")); - return; - } - - ref JSMarshalerArgument arg_2 = ref arguments_buffer[3]; // set by caller when this is SetException call - ref JSMarshalerArgument arg_3 = ref arguments_buffer[4]; // set by caller when this is SetResult call - if (arg_2.slot.Type != MarshalerType.None) - { - arg_2.ToManaged(out Exception? fail); - if (fail == null) throw new InvalidOperationException(SR.FailedToMarshalException); - tcs.SetException(fail); - } - else + var holder = ctx.GetPromiseHolder(slot.GCHandle); + TaskCompletionSource tcs = new TaskCompletionSource(holder); + ToManagedCallback callback = (JSMarshalerArgument* arguments_buffer) => { - marshaler(ref arg_3, out T result); - tcs.SetResult(result); - } - // eventual exception is handled by caller - }; - holder.Callback = callback; - value = tcs.Task; - } + if (arguments_buffer == null) + { + tcs.TrySetException(new TaskCanceledException("WebWorker which is origin of the Promise is being terminated.")); + return; + } - // TODO unregister and collect pending PromiseHolder also when no C# is awaiting ? - private static PromiseHolder GetPromiseHolder(nint gcHandle) - { - PromiseHolder holder; - if (IsGCVHandle(gcHandle)) - { - // this path should only happen when the Promise is passed as argument of JSExport - holder = new PromiseHolder(gcHandle); - // TODO for MT this must hit the ThreadJsOwnedHolders in the correct thread - ThreadJsOwnedHolders.Add(gcHandle, holder); - } - else - { - holder = (PromiseHolder)((GCHandle)gcHandle).Target!; + ref JSMarshalerArgument arg_2 = ref arguments_buffer[3]; // set by caller when this is SetException call + ref JSMarshalerArgument arg_3 = ref arguments_buffer[4]; // set by caller when this is SetResult call + if (arg_2.slot.Type != MarshalerType.None) + { + arg_2.ToManaged(out Exception? fail); + if (fail == null) throw new InvalidOperationException(SR.FailedToMarshalException); + tcs.SetException(fail); + } + else + { + marshaler(ref arg_3, out T result); + tcs.SetResult(result); + } + // eventual exception is handled by caller + }; + holder.Callback = callback; + value = tcs.Task; +#if FEATURE_WASM_THREADS + // if the other thread created it, signal that it's ready + holder.CallbackReady?.Set(); +#endif } - return holder; } + internal void ToJSDynamic(Task? value) { Task? task = value; @@ -167,10 +168,12 @@ internal void ToJSDynamic(Task? value) } } + var ctx = ToJSContext; + if (slot.Type != MarshalerType.TaskPreCreated) { // this path should only happen when the Task is passed as argument of JSImport - slot.JSHandle = AllocJSVHandle(); + slot.JSHandle = ctx.AllocJSVHandle(); slot.Type = MarshalerType.Task; } else @@ -179,7 +182,7 @@ internal void ToJSDynamic(Task? value) // promise and handle is pre-allocated in slot.JSHandle } - var taskHolder = new JSObject(slot.JSHandle); + var taskHolder = ctx.CreateCSOwnedProxy(slot.JSHandle); #if FEATURE_WASM_THREADS task.ContinueWith(Complete, taskHolder, TaskScheduler.FromCurrentSynchronizationContext()); @@ -245,10 +248,12 @@ public void ToJS(Task? value) } } + var ctx = ToJSContext; + if (slot.Type != MarshalerType.TaskPreCreated) { // this path should only happen when the Task is passed as argument of JSImport - slot.JSHandle = AllocJSVHandle(); + slot.JSHandle = ctx.AllocJSVHandle(); slot.Type = MarshalerType.Task; } else @@ -257,7 +262,7 @@ public void ToJS(Task? value) // promise and handle is pre-allocated in slot.JSHandle } - var taskHolder = new JSObject(slot.JSHandle); + var taskHolder = ctx.CreateCSOwnedProxy(slot.JSHandle); #if FEATURE_WASM_THREADS task.ContinueWith(Complete, taskHolder, TaskScheduler.FromCurrentSynchronizationContext()); @@ -316,10 +321,11 @@ public void ToJS(Task? value, ArgumentToJSCallback marshaler) } } + var ctx = ToJSContext; if (slot.Type != MarshalerType.TaskPreCreated) { // this path should only happen when the Task is passed as argument of JSImport - slot.JSHandle = AllocJSVHandle(); + slot.JSHandle = ctx.AllocJSVHandle(); slot.Type = MarshalerType.Task; } else @@ -328,7 +334,7 @@ public void ToJS(Task? value, ArgumentToJSCallback marshaler) // promise and handle is pre-allocated in slot.JSHandle } - var taskHolder = new JSObject(slot.JSHandle); + var taskHolder = ctx.CreateCSOwnedProxy(slot.JSHandle); #if FEATURE_WASM_THREADS task.ContinueWith(Complete, new HolderAndMarshaler(taskHolder, marshaler), TaskScheduler.FromCurrentSynchronizationContext()); @@ -357,14 +363,26 @@ private static void RejectPromise(JSObject holder, Exception ex) { holder.AssertNotDisposed(); +#if FEATURE_WASM_THREADS + JSObject.AssertThreadAffinity(holder); +#endif + Span args = stackalloc JSMarshalerArgument[4]; ref JSMarshalerArgument exc = ref args[0]; ref JSMarshalerArgument res = ref args[1]; ref JSMarshalerArgument arg_handle = ref args[2]; ref JSMarshalerArgument arg_value = ref args[3]; +#if FEATURE_WASM_THREADS + exc.InitializeWithContext(holder.ProxyContext); + res.InitializeWithContext(holder.ProxyContext); + arg_value.InitializeWithContext(holder.ProxyContext); + arg_handle.InitializeWithContext(holder.ProxyContext); + JSProxyContext.JSImportNoCapture(); +#else exc.Initialize(); res.Initialize(); +#endif // should update existing promise arg_handle.slot.Type = MarshalerType.TaskRejected; @@ -373,14 +391,24 @@ private static void RejectPromise(JSObject holder, Exception ex) // should fail it with exception arg_value.ToJS(ex); - JavaScriptImports.ResolveOrRejectPromise(args); + // we can free the JSHandle here and the holder.resolve_or_reject will do the rest + holder.DisposeImpl(skipJsCleanup: true); - holder.DisposeLocal(); +#if !FEATURE_WASM_THREADS + // order of operations with DisposeImpl matters + JSFunctionBinding.ResolveOrRejectPromise(args); +#else + // order of operations with DisposeImpl matters + JSFunctionBinding.ResolveOrRejectPromise(args); +#endif } private static void ResolveVoidPromise(JSObject holder) { holder.AssertNotDisposed(); +#if FEATURE_WASM_THREADS + JSObject.AssertThreadAffinity(holder); +#endif Span args = stackalloc JSMarshalerArgument[4]; ref JSMarshalerArgument exc = ref args[0]; @@ -388,8 +416,16 @@ private static void ResolveVoidPromise(JSObject holder) ref JSMarshalerArgument arg_handle = ref args[2]; ref JSMarshalerArgument arg_value = ref args[3]; +#if FEATURE_WASM_THREADS + exc.InitializeWithContext(holder.ProxyContext); + res.InitializeWithContext(holder.ProxyContext); + arg_value.InitializeWithContext(holder.ProxyContext); + arg_handle.InitializeWithContext(holder.ProxyContext); + JSProxyContext.JSImportNoCapture(); +#else exc.Initialize(); res.Initialize(); +#endif // should update existing promise arg_handle.slot.Type = MarshalerType.TaskResolved; @@ -397,14 +433,24 @@ private static void ResolveVoidPromise(JSObject holder) arg_value.slot.Type = MarshalerType.Void; - JavaScriptImports.ResolveOrRejectPromise(args); + // we can free the JSHandle here and the holder.resolve_or_reject will do the rest + holder.DisposeImpl(skipJsCleanup: true); - holder.DisposeLocal(); +#if !FEATURE_WASM_THREADS + // order of operations with DisposeImpl matters + JSFunctionBinding.ResolveOrRejectPromise(args); +#else + // order of operations with DisposeImpl matters + JSFunctionBinding.ResolveOrRejectPromise(args); +#endif } private static void ResolvePromise(JSObject holder, T value, ArgumentToJSCallback marshaler) { holder.AssertNotDisposed(); +#if FEATURE_WASM_THREADS + JSObject.AssertThreadAffinity(holder); +#endif Span args = stackalloc JSMarshalerArgument[4]; ref JSMarshalerArgument exc = ref args[0]; @@ -412,8 +458,16 @@ private static void ResolvePromise(JSObject holder, T value, ArgumentToJSCall ref JSMarshalerArgument arg_handle = ref args[2]; ref JSMarshalerArgument arg_value = ref args[3]; +#if FEATURE_WASM_THREADS + exc.InitializeWithContext(holder.ProxyContext); + res.InitializeWithContext(holder.ProxyContext); + arg_value.InitializeWithContext(holder.ProxyContext); + arg_handle.InitializeWithContext(holder.ProxyContext); + JSProxyContext.JSImportNoCapture(); +#else exc.Initialize(); res.Initialize(); +#endif // should update existing promise arg_handle.slot.Type = MarshalerType.TaskResolved; @@ -422,9 +476,16 @@ private static void ResolvePromise(JSObject holder, T value, ArgumentToJSCall // and resolve it with value marshaler(ref arg_value, value); - JavaScriptImports.ResolveOrRejectPromise(args); + // we can free the JSHandle here and the holder.resolve_or_reject will do the rest + holder.DisposeImpl(skipJsCleanup: true); - holder.DisposeLocal(); +#if !FEATURE_WASM_THREADS + // order of operations with DisposeImpl matters + JSFunctionBinding.ResolveOrRejectPromise(args); +#else + // order of operations with DisposeImpl matters + JSFunctionBinding.ResolveOrRejectPromise(args); +#endif } } } 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 c4d5494f93718f..8d6cf7aaf28095 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 @@ -23,5 +23,6 @@ + diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/JSImportExportTest.cs b/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/JSImportExportTest.cs index 314c3e190c335d..9fabc04697af81 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/JSImportExportTest.cs +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/JSImportExportTest.cs @@ -7,6 +7,7 @@ using System.Threading.Tasks; using System.Threading; using Xunit; +using System.Diagnostics.CodeAnalysis; #pragma warning disable xUnit1026 // Theory methods should use all of their parameters namespace System.Runtime.InteropServices.JavaScript.Tests @@ -16,7 +17,7 @@ public class JSImportExportTest : IAsyncLifetime [Fact] public unsafe void StructSize() { - Assert.Equal(16, sizeof(JSMarshalerArgument)); + Assert.Equal(32, sizeof(JSMarshalerArgument)); } [Fact] @@ -378,7 +379,7 @@ static void dummyDelegateA() [Theory] [MemberData(nameof(MarshalObjectArrayCasesThrow))] - public unsafe void JsImportObjectArrayThrows(object[]? expected) + public void JsImportObjectArrayThrows(object[]? expected) { Assert.Throws(() => JavaScriptTestHelper.echo1_ObjectArray(expected)); } @@ -1959,15 +1960,15 @@ public void JsImportMath() #endregion - private void JsExportTest(T value + private void JsExportTest<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] T>(T value , Func invoke, string echoName, string jsType, string? jsClass = null) { T res; res = invoke(value, echoName); - Assert.Equal(value, res); + Assert.Equal(value, res); } - private void JsImportTest(T value + private void JsImportTest<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] T>(T value , Action store1 , Func retrieve1 , Func echo1 diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/JavaScriptTestHelper.cs b/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/JavaScriptTestHelper.cs index 26227d1f00549c..559b9b4ff88e87 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/JavaScriptTestHelper.cs +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/JavaScriptTestHelper.cs @@ -999,19 +999,20 @@ public static async Task InitializeAsync() { if (_module == null) { - // Log("JavaScriptTestHelper.mjs importing"); - _module = await JSHost.ImportAsync("JavaScriptTestHelper", "../JavaScriptTestHelper.mjs"); - await Setup(); - // Log("JavaScriptTestHelper.mjs imported"); + _module = await JSHost.ImportAsync("JavaScriptTestHelper", "../JavaScriptTestHelper.mjs"); ; + await Setup(); ; } + var p = echopromise_String("aaa"); + await p; + // this gives browser chance to serve UI thread event loop before every test await Task.Yield(); } public static Task DisposeAsync() { - _module.Dispose(); + _module?.Dispose(); _module = null; return Task.CompletedTask; } 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 624af0df0fe9f1..baec1cb231c2b8 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 @@ -367,7 +367,15 @@ export function backback(arg1, arg2, arg3) { // console.log('backback A') return (brg1, brg2) => { // console.log('backback B') - return arg1(brg1 + arg2, brg2 + arg3); + try { + var res = arg1(brg1 + arg2, brg2 + arg3); + // console.log('backback C') + return res + } + catch (e) { + // console.log('backback E ' + e) + throw e; + } } } diff --git a/src/mono/browser/.gitignore b/src/mono/browser/.gitignore index 752927a4aeefbf..6047d67b80823f 100644 --- a/src/mono/browser/.gitignore +++ b/src/mono/browser/.gitignore @@ -1,3 +1,6 @@ !Makefile .stamp-wasm-install-and-select* emsdk + +runtime/dotnet.d.ts.sha256 +runtime/dotnet-legacy.d.ts.sha256 diff --git a/src/mono/browser/runtime/corebindings.c b/src/mono/browser/runtime/corebindings.c index 00fed13f2d0a54..8657cd261459f4 100644 --- a/src/mono/browser/runtime/corebindings.c +++ b/src/mono/browser/runtime/corebindings.c @@ -42,7 +42,7 @@ extern void* mono_wasm_invoke_js_blazor (MonoString **exceptionMessage, void *ca #endif /* DISABLE_LEGACY_JS_INTEROP */ #ifndef DISABLE_THREADS -extern void mono_wasm_install_js_worker_interop (); +extern void mono_wasm_install_js_worker_interop (int context_gc_handle); extern void mono_wasm_uninstall_js_worker_interop (); #endif /* DISABLE_THREADS */ diff --git a/src/mono/browser/runtime/cwraps.ts b/src/mono/browser/runtime/cwraps.ts index e76f78e4fc285f..0a3f2e0894cd1d 100644 --- a/src/mono/browser/runtime/cwraps.ts +++ b/src/mono/browser/runtime/cwraps.ts @@ -88,6 +88,7 @@ const fn_signatures: SigLine[] = [ [true, "mono_wasm_profiler_init_browser", "void", ["number"]], [false, "mono_wasm_exec_regression", "number", ["number", "string"]], [false, "mono_wasm_invoke_method_bound", "number", ["number", "number", "number"]], + [false, "mono_wasm_invoke_method_raw", "number", ["number", "number"]], [true, "mono_wasm_write_managed_pointer_unsafe", "void", ["number", "number"]], [true, "mono_wasm_copy_managed_pointer", "void", ["number", "number"]], [true, "mono_wasm_i52_to_f64", "number", ["number", "number"]], @@ -229,6 +230,7 @@ export interface t_Cwraps { mono_wasm_set_main_args(argc: number, argv: VoidPtr): void; mono_wasm_exec_regression(verbose_level: number, image: string): number; mono_wasm_invoke_method_bound(method: MonoMethod, args: JSMarshalerArguments, fail: MonoStringRef): number; + mono_wasm_invoke_method_raw(method: MonoMethod, fail: MonoStringRef): number; mono_wasm_write_managed_pointer_unsafe(destination: VoidPtr | MonoObjectRef, pointer: ManagedPointer): void; mono_wasm_copy_managed_pointer(destination: VoidPtr | MonoObjectRef, source: VoidPtr | MonoObjectRef): void; mono_wasm_i52_to_f64(source: VoidPtr, error: Int32Ptr): number; diff --git a/src/mono/browser/runtime/driver.c b/src/mono/browser/runtime/driver.c index 860140d9cf3273..a48e77894018e6 100644 --- a/src/mono/browser/runtime/driver.c +++ b/src/mono/browser/runtime/driver.c @@ -322,6 +322,27 @@ mono_wasm_invoke_method_bound (MonoMethod *method, void* args /*JSMarshalerArgum return is_err; } +EMSCRIPTEN_KEEPALIVE int +mono_wasm_invoke_method_raw (MonoMethod *method, MonoString **out_exc) +{ + PVOLATILE(MonoObject) temp_exc = NULL; + + int is_err = 0; + + MONO_ENTER_GC_UNSAFE; + mono_runtime_invoke (method, NULL, NULL, (MonoObject **)&temp_exc); + + if (temp_exc && out_exc) { + PVOLATILE(MonoObject) exc2 = NULL; + store_volatile((MonoObject**)out_exc, (MonoObject*)mono_object_to_string ((MonoObject*)temp_exc, (MonoObject **)&exc2)); + if (exc2) + store_volatile((MonoObject**)out_exc, (MonoObject*)mono_string_new (root_domain, "Exception Double Fault")); + is_err = 1; + } + MONO_EXIT_GC_UNSAFE; + return is_err; +} + EMSCRIPTEN_KEEPALIVE MonoMethod* mono_wasm_assembly_get_entry_point (MonoAssembly *assembly, int auto_insert_breakpoint) { diff --git a/src/mono/browser/runtime/gc-handles.ts b/src/mono/browser/runtime/gc-handles.ts index feacccce2a5f0a..6d95160ada062c 100644 --- a/src/mono/browser/runtime/gc-handles.ts +++ b/src/mono/browser/runtime/gc-handles.ts @@ -98,6 +98,7 @@ export function register_with_jsv_handle(js_obj: any, jsv_handle: JSHandle) { } } +// note: in MT, this is called from locked JSProxyContext. Don't call anything that would need locking. export function mono_wasm_release_cs_owned_object(js_handle: JSHandle): void { let obj: any; if (is_js_handle(js_handle)) { @@ -108,6 +109,7 @@ export function mono_wasm_release_cs_owned_object(js_handle: JSHandle): void { else if (is_jsv_handle(js_handle)) { obj = _cs_owned_objects_by_jsv_handle[0 - js_handle]; _cs_owned_objects_by_jsv_handle[0 - js_handle] = undefined; + // see free list in JSProxyContext.FreeJSVHandle } mono_assert(obj !== undefined && obj !== null, "ObjectDisposedException"); if (typeof obj[cs_owned_js_handle_symbol] !== "undefined") { diff --git a/src/mono/browser/runtime/http.ts b/src/mono/browser/runtime/http.ts index cdb60bd862ab08..23f8b20e3e4be6 100644 --- a/src/mono/browser/runtime/http.ts +++ b/src/mono/browser/runtime/http.ts @@ -4,6 +4,7 @@ import { wrap_as_cancelable_promise } from "./cancelable-promise"; import { ENVIRONMENT_IS_NODE, Module, loaderHelpers, mono_assert } from "./globals"; import { MemoryViewType, Span } from "./marshal"; +import { assert_synchronization_context } from "./pthreads/shared"; import type { VoidPtr } from "./types/emscripten"; import { ControllablePromise } from "./types/internal"; @@ -112,6 +113,7 @@ export function http_wasm_fetch_bytes(url: string, header_names: string[], heade export function http_wasm_fetch(url: string, header_names: string[], header_values: string[], option_names: string[], option_values: any[], abort_controller: AbortController, body: Uint8Array | ReadableStream | null): ControllablePromise { verifyEnvironment(); + assert_synchronization_context(); mono_assert(url && typeof url === "string", "expected url string"); mono_assert(header_names && header_values && Array.isArray(header_names) && Array.isArray(header_values) && header_names.length === header_values.length, "expected headerNames and headerValues arrays"); mono_assert(option_names && option_values && Array.isArray(option_names) && Array.isArray(option_values) && option_names.length === option_values.length, "expected headerNames and headerValues arrays"); diff --git a/src/mono/browser/runtime/invoke-cs.ts b/src/mono/browser/runtime/invoke-cs.ts index 17e9cf181b5a16..46324a66175598 100644 --- a/src/mono/browser/runtime/invoke-cs.ts +++ b/src/mono/browser/runtime/invoke-cs.ts @@ -9,7 +9,7 @@ import { bind_arg_marshal_to_cs } from "./marshal-to-cs"; import { marshal_exception_to_js, bind_arg_marshal_to_js, end_marshal_task_to_js } from "./marshal-to-js"; import { get_arg, get_sig, get_signature_argument_count, is_args_exception, - bound_cs_function_symbol, get_signature_version, alloc_stack_frame, get_signature_type, + bound_cs_function_symbol, get_signature_version, alloc_stack_frame, get_signature_type, set_args_context, } from "./marshal"; import { mono_wasm_new_external_root, mono_wasm_new_root } from "./roots"; import { monoStringToString } from "./strings"; @@ -356,8 +356,9 @@ export function invoke_method_and_handle_exception(method: MonoMethod, args: JSM assert_bindings(); const fail_root = mono_wasm_new_root(); try { + set_args_context(args); const fail = cwraps.mono_wasm_invoke_method_bound(method, args, fail_root.address); - if (fail) throw new Error("ERR24: Unexpected error: " + monoStringToString(fail_root)); + if (fail) runtimeHelpers.abort("ERR24: Unexpected error: " + monoStringToString(fail_root)); if (is_args_exception(args)) { const exc = get_arg(args, 0); throw marshal_exception_to_js(exc); @@ -368,6 +369,18 @@ export function invoke_method_and_handle_exception(method: MonoMethod, args: JSM } } +export function invoke_method_raw(method: MonoMethod): void { + assert_bindings(); + const fail_root = mono_wasm_new_root(); + try { + const fail = cwraps.mono_wasm_invoke_method_raw(method, fail_root.address); + if (fail) runtimeHelpers.abort("ERR24: Unexpected error: " + monoStringToString(fail_root)); + } + finally { + fail_root.release(); + } +} + export const exportsByAssembly: Map = new Map(); function _walk_exports_to_set_function(assembly: string, namespace: string, classname: string, methodname: string, signature_hash: number, fn: Function): void { const parts = `${namespace}.${classname}`.replace(/\//g, ".").split("."); diff --git a/src/mono/browser/runtime/managed-exports.ts b/src/mono/browser/runtime/managed-exports.ts index 7e1347617aab75..fff5deb4bb9092 100644 --- a/src/mono/browser/runtime/managed-exports.ts +++ b/src/mono/browser/runtime/managed-exports.ts @@ -7,7 +7,7 @@ import { GCHandle, MarshalerToCs, MarshalerToJs, MarshalerType, MonoMethod } fro import cwraps from "./cwraps"; import { runtimeHelpers, Module, loaderHelpers, mono_assert } from "./globals"; import { alloc_stack_frame, get_arg, set_arg_type, set_gc_handle } from "./marshal"; -import { invoke_method_and_handle_exception } from "./invoke-cs"; +import { invoke_method_and_handle_exception, invoke_method_raw } from "./invoke-cs"; import { marshal_array_to_cs, marshal_array_to_cs_impl, marshal_exception_to_cs, marshal_intptr_to_cs } from "./marshal-to-cs"; import { marshal_int32_to_js, end_marshal_task_to_js, marshal_string_to_js, begin_marshal_task_to_js } from "./marshal-to-js"; import { do_not_force_dispose } from "./gc-handles"; @@ -24,8 +24,8 @@ export function init_managed_exports(): void { if (!runtimeHelpers.runtime_interop_exports_class) throw "Can't find " + runtimeHelpers.runtime_interop_namespace + "." + runtimeHelpers.runtime_interop_exports_classname + " class"; - const install_sync_context = MonoWasmThreads ? get_method("InstallSynchronizationContext") : undefined; - mono_assert(!MonoWasmThreads || install_sync_context, "Can't find InstallSynchronizationContext method"); + const install_main_synchronization_context = MonoWasmThreads ? get_method("InstallMainSynchronizationContext") : undefined; + mono_assert(!MonoWasmThreads || install_main_synchronization_context, "Can't find InstallMainSynchronizationContext method"); const call_entry_point = get_method("CallEntrypoint"); mono_assert(call_entry_point, "Can't find CallEntrypoint method"); const release_js_owned_object_by_gc_handle_method = get_method("ReleaseJSOwnedObjectByGCHandle"); @@ -188,17 +188,8 @@ export function init_managed_exports(): void { Module.stackRestore(sp); } }; - - if (MonoWasmThreads && install_sync_context) { - runtimeHelpers.javaScriptExports.install_synchronization_context = () => { - const sp = Module.stackSave(); - try { - const args = alloc_stack_frame(2); - invoke_method_and_handle_exception(install_sync_context, args); - } finally { - Module.stackRestore(sp); - } - }; + if (MonoWasmThreads && install_main_synchronization_context) { + runtimeHelpers.javaScriptExports.install_main_synchronization_context = () => invoke_method_raw(install_main_synchronization_context); } } diff --git a/src/mono/browser/runtime/marshal-to-cs.ts b/src/mono/browser/runtime/marshal-to-cs.ts index 6765b95b07e1d7..c0dc159eaa8fa0 100644 --- a/src/mono/browser/runtime/marshal-to-cs.ts +++ b/src/mono/browser/runtime/marshal-to-cs.ts @@ -337,10 +337,14 @@ function _marshal_task_to_cs(arg: JSMarshalerArgument, value: Promise, _?: } try { mono_assert(!holder.isDisposed, "This promise can't be propagated to managed code, because the Task was already freed."); - if (MonoWasmThreads) + if (MonoWasmThreads) { settleUnsettledPromise(); + } + // we can unregister the GC handle on JS side + teardown_managed_proxy(holder, gc_handle, 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 runtimeHelpers.javaScriptExports.complete_task(gc_handle, null, data, res_converter || _marshal_cs_object_to_cs); - teardown_managed_proxy(holder, gc_handle, true); // this holds holder alive for finalizer, until the promise is freed, (holding promise instead would not work) } catch (ex) { runtimeHelpers.abort(ex); @@ -352,10 +356,13 @@ function _marshal_task_to_cs(arg: JSMarshalerArgument, value: Promise, _?: } try { mono_assert(!holder.isDisposed, "This promise can't be propagated to managed code, because the Task was already freed."); - if (MonoWasmThreads) + if (MonoWasmThreads) { settleUnsettledPromise(); + } + // we can unregister the GC handle on JS side + teardown_managed_proxy(holder, gc_handle, true); + // order of operations with teardown_managed_proxy matters runtimeHelpers.javaScriptExports.complete_task(gc_handle, reason, null, undefined); - teardown_managed_proxy(holder, gc_handle, true); // this holds holder alive for finalizer, until the promise is freed } catch (ex) { runtimeHelpers.abort(ex); diff --git a/src/mono/browser/runtime/marshal-to-js.ts b/src/mono/browser/runtime/marshal-to-js.ts index 0053102be61e33..0aa489b3b83c35 100644 --- a/src/mono/browser/runtime/marshal-to-js.ts +++ b/src/mono/browser/runtime/marshal-to-js.ts @@ -265,7 +265,7 @@ export function end_marshal_task_to_js(args: JSMarshalerArguments, res_converter } // otherwise drop the eagerPromise's handle - const js_handle = get_arg_js_handle(res); + const js_handle = mono_wasm_get_js_handle(eagerPromise); mono_wasm_release_cs_owned_object(js_handle); // get the synchronous result diff --git a/src/mono/browser/runtime/marshal.ts b/src/mono/browser/runtime/marshal.ts index a788512e9d567d..bf4c5145badd47 100644 --- a/src/mono/browser/runtime/marshal.ts +++ b/src/mono/browser/runtime/marshal.ts @@ -18,7 +18,7 @@ export const bound_js_function_symbol = Symbol.for("wasm bound_js_function"); export const imported_js_function_symbol = Symbol.for("wasm imported_js_function"); export const proxy_debug_symbol = Symbol.for("wasm proxy_debug"); -export const JavaScriptMarshalerArgSize = 16; +export const JavaScriptMarshalerArgSize = 32; export const JSMarshalerTypeSize = 32; export const JSMarshalerSignatureHeaderSize = 4 * 8; // without Exception and Result @@ -26,6 +26,7 @@ export function alloc_stack_frame(size: number): JSMarshalerArguments { const bytes = JavaScriptMarshalerArgSize * size; const args = Module.stackAlloc(bytes) as any; _zero_region(args, bytes); + set_args_context(args); return args; } @@ -40,6 +41,15 @@ export function is_args_exception(args: JSMarshalerArguments): boolean { return exceptionType !== MarshalerType.None; } +export function set_args_context(args: JSMarshalerArguments): void { + if (!MonoWasmThreads) return; + mono_assert(args, "Null args"); + const exc = get_arg(args, 0); + const res = get_arg(args, 1); + set_arg_proxy_context(exc); + set_arg_proxy_context(res); +} + export function get_sig(signature: JSFunctionSignature, index: number): JSMarshalerType { mono_assert(signature, "Null signatures"); return signature + (index * JSMarshalerTypeSize) + JSMarshalerSignatureHeaderSize; @@ -252,9 +262,16 @@ export function get_arg_js_handle(arg: JSMarshalerArgument): JSHandle { return getI32(arg + 4); } +export function set_arg_proxy_context(arg: JSMarshalerArgument): void { + if (!MonoWasmThreads) return; + mono_assert(arg, "Null arg"); + setI32(arg + 16, runtimeHelpers.proxy_context_gc_handle); +} + export function set_js_handle(arg: JSMarshalerArgument, jsHandle: JSHandle): void { mono_assert(arg, "Null arg"); setI32(arg + 4, jsHandle); + set_arg_proxy_context(arg); } export function get_arg_gc_handle(arg: JSMarshalerArgument): GCHandle { @@ -265,6 +282,7 @@ export function get_arg_gc_handle(arg: JSMarshalerArgument): GCHandle { export function set_gc_handle(arg: JSMarshalerArgument, gcHandle: GCHandle): void { mono_assert(arg, "Null arg"); setI32(arg + 4, gcHandle); + set_arg_proxy_context(arg); } export function get_string_root(arg: JSMarshalerArgument): WasmRoot { @@ -331,7 +349,7 @@ export class ManagedError extends Error implements IDisposable { if (this.managed_stack) { return this.managed_stack; } - if (loaderHelpers.is_runtime_running() && (!MonoWasmThreads || runtimeHelpers.jsSynchronizationContextInstalled)) { + if (loaderHelpers.is_runtime_running() && (!MonoWasmThreads || runtimeHelpers.proxy_context_gc_handle)) { const gc_handle = (this)[js_owned_gc_handle_symbol]; if (gc_handle !== GCHandleNull) { const managed_stack = runtimeHelpers.javaScriptExports.get_managed_stack_trace(gc_handle); diff --git a/src/mono/browser/runtime/pthreads/shared/index.ts b/src/mono/browser/runtime/pthreads/shared/index.ts index af991aa7b3fe3b..499ff3f129c0da 100644 --- a/src/mono/browser/runtime/pthreads/shared/index.ts +++ b/src/mono/browser/runtime/pthreads/shared/index.ts @@ -11,6 +11,7 @@ import { mono_log_debug } from "../../logging"; import { bindings_init } from "../../startup"; import { forceDisposeProxies } from "../../gc-handles"; import { pthread_self } from "../worker"; +import { GCHandle, GCHandleNull } from "../../types/internal"; export interface PThreadInfo { readonly pthreadId: pthreadPtr; @@ -166,11 +167,11 @@ export function isMonoWorkerMessagePreload(message: MonoWorkerMessage): message return false; } -export function mono_wasm_install_js_worker_interop(): void { +export function mono_wasm_install_js_worker_interop(context_gc_handle: GCHandle): void { if (!MonoWasmThreads) return; bindings_init(); - if (!runtimeHelpers.jsSynchronizationContextInstalled) { - runtimeHelpers.jsSynchronizationContextInstalled = true; + if (!runtimeHelpers.proxy_context_gc_handle) { + runtimeHelpers.proxy_context_gc_handle = context_gc_handle; mono_log_debug("Installed JSSynchronizationContext"); } Module.runtimeKeepalivePush(); @@ -184,19 +185,19 @@ export function mono_wasm_install_js_worker_interop(): void { export function mono_wasm_uninstall_js_worker_interop(): void { if (!MonoWasmThreads) return; mono_assert(runtimeHelpers.mono_wasm_bindings_is_ready, "JS interop is not installed on this worker."); - mono_assert(runtimeHelpers.jsSynchronizationContextInstalled, "JSSynchronizationContext is not installed on this worker."); + mono_assert(runtimeHelpers.proxy_context_gc_handle, "JSSynchronizationContext is not installed on this worker."); forceDisposeProxies(true, runtimeHelpers.diagnosticTracing); Module.runtimeKeepalivePop(); - runtimeHelpers.jsSynchronizationContextInstalled = false; + runtimeHelpers.proxy_context_gc_handle = GCHandleNull; runtimeHelpers.mono_wasm_bindings_is_ready = false; set_thread_info(pthread_self ? pthread_self.pthreadId : 0, true, false, false); } export function assert_synchronization_context(): void { if (MonoWasmThreads) { - mono_assert(runtimeHelpers.jsSynchronizationContextInstalled, "Please use dedicated worker for working with JavaScript interop. See https://github.com/dotnet/runtime/blob/main/src/mono/wasm/threads.md#JS-interop-on-dedicated-threads"); + mono_assert(runtimeHelpers.proxy_context_gc_handle, "Please use dedicated worker for working with JavaScript interop. See https://github.com/dotnet/runtime/blob/main/src/mono/wasm/threads.md#JS-interop-on-dedicated-threads"); } } diff --git a/src/mono/browser/runtime/startup.ts b/src/mono/browser/runtime/startup.ts index 556dd158c9700c..6ec998ccaabebf 100644 --- a/src/mono/browser/runtime/startup.ts +++ b/src/mono/browser/runtime/startup.ts @@ -289,8 +289,7 @@ async function onRuntimeInitializedAsync(userOnRuntimeInitialized: () => void) { } if (MonoWasmThreads) { - runtimeHelpers.javaScriptExports.install_synchronization_context(); - runtimeHelpers.jsSynchronizationContextInstalled = true; + runtimeHelpers.javaScriptExports.install_main_synchronization_context(); } if (!runtimeHelpers.mono_wasm_runtime_is_ready) mono_wasm_runtime_ready(); diff --git a/src/mono/browser/runtime/types/internal.ts b/src/mono/browser/runtime/types/internal.ts index 4c086350182a69..6e7b649a56c6b6 100644 --- a/src/mono/browser/runtime/types/internal.ts +++ b/src/mono/browser/runtime/types/internal.ts @@ -194,7 +194,7 @@ export type RuntimeHelpers = { getMemory(): WebAssembly.Memory, getWasmIndirectFunctionTable(): WebAssembly.Table, runtimeReady: boolean, - jsSynchronizationContextInstalled: boolean, + proxy_context_gc_handle: GCHandle, cspPolicy: boolean, allAssetsInMemory: PromiseAndController, @@ -348,8 +348,8 @@ export interface JavaScriptExports { // the marshaled signature is: Task? CallEntrypoint(MonoMethod* entrypointPtr, string[] args) call_entry_point(entry_point: MonoMethod, args?: string[]): Promise; - // the marshaled signature is: void InstallSynchronizationContext() - install_synchronization_context(): void; + // the marshaled signature is: void InstallMainSynchronizationContext() + install_main_synchronization_context(): void; // the marshaled signature is: string GetManagedStackTrace(GCHandle exception) get_managed_stack_trace(exception_gc_handle: GCHandle): string | null diff --git a/src/mono/browser/runtime/web-socket.ts b/src/mono/browser/runtime/web-socket.ts index 9c43849f39534c..41b3ca22994281 100644 --- a/src/mono/browser/runtime/web-socket.ts +++ b/src/mono/browser/runtime/web-socket.ts @@ -13,6 +13,7 @@ import { mono_log_warn } from "./logging"; import { viewOrCopy, utf8ToStringRelaxed, stringToUTF8 } from "./strings"; import { IDisposable } from "./marshal"; import { wrap_as_cancelable } from "./cancelable-promise"; +import { assert_synchronization_context } from "./pthreads/shared"; const wasm_ws_pending_send_buffer = Symbol.for("wasm ws_pending_send_buffer"); const wasm_ws_pending_send_buffer_offset = Symbol.for("wasm ws_pending_send_buffer_offset"); @@ -44,6 +45,7 @@ function verifyEnvironment() { export function ws_wasm_create(uri: string, sub_protocols: string[] | null, receive_status_ptr: VoidPtr, onClosed: (code: number, reason: string) => void): WebSocketExtension { verifyEnvironment(); + assert_synchronization_context(); mono_assert(uri && typeof uri === "string", () => `ERR12: Invalid uri ${typeof uri}`); mono_assert(typeof onClosed === "function", () => `ERR12: Invalid onClosed ${typeof onClosed}`); diff --git a/src/mono/sample/wasm/browser-nextjs/.gitignore b/src/mono/sample/wasm/browser-nextjs/.gitignore index 20fccdd4b84d99..36a31210aea2d3 100644 --- a/src/mono/sample/wasm/browser-nextjs/.gitignore +++ b/src/mono/sample/wasm/browser-nextjs/.gitignore @@ -28,3 +28,5 @@ yarn-error.log* .env.development.local .env.test.local .env.production.local + +public \ No newline at end of file