From eea5332209aee768ce48b6d8c150116a34f048dc Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Wed, 13 Dec 2023 12:20:57 +0100 Subject: [PATCH 01/23] wip --- ...tem.Runtime.InteropServices.JavaScript.sln | 13 +- .../Marshaling/PrimitiveJSGenerator.cs | 1 + ....Runtime.InteropServices.JavaScript.csproj | 1 + .../JavaScript/CancelablePromise.cs | 22 +- .../JavaScript/Interop/JavaScriptExports.cs | 122 ++--- .../JavaScript/Interop/LegacyExports.cs | 44 +- .../InteropServices/JavaScript/JSException.cs | 5 +- .../JavaScript/JSFunctionBinding.cs | 16 +- .../InteropServices/JavaScript/JSHost.cs | 8 +- .../JavaScript/JSHostImplementation.Types.cs | 26 +- .../JavaScript/JSHostImplementation.cs | 255 +--------- .../JavaScript/JSMarshalerArgument.cs | 3 + .../JavaScript/JSObject.References.cs | 64 ++- .../JavaScript/JSProxyContext.cs | 466 ++++++++++++++++++ .../JavaScript/JSSynchronizationContext.cs | 87 ++-- .../JavaScript/Legacy/Array.cs | 6 +- .../JavaScript/Legacy/ArrayBuffer.cs | 6 +- .../JavaScript/Legacy/DataView.cs | 14 +- .../JavaScript/Legacy/Function.cs | 6 +- .../Legacy/LegacyHostImplementation.cs | 6 - .../JavaScript/Legacy/Uint8Array.cs | 10 +- .../Marshaling/JSMarshalerArgument.Byte.cs | 2 +- .../Marshaling/JSMarshalerArgument.Double.cs | 2 +- .../JSMarshalerArgument.Exception.cs | 13 +- .../Marshaling/JSMarshalerArgument.Func.cs | 32 +- .../Marshaling/JSMarshalerArgument.Int32.cs | 2 +- .../JSMarshalerArgument.JSObject.cs | 12 +- .../Marshaling/JSMarshalerArgument.Object.cs | 2 +- .../Marshaling/JSMarshalerArgument.Task.cs | 43 +- src/mono/wasm/runtime/cwraps.ts | 2 + src/mono/wasm/runtime/driver.c | 21 + src/mono/wasm/runtime/gc-handles.ts | 1 + src/mono/wasm/runtime/http.ts | 2 + src/mono/wasm/runtime/invoke-cs.ts | 16 + src/mono/wasm/runtime/managed-exports.ts | 24 +- src/mono/wasm/runtime/startup.ts | 2 +- src/mono/wasm/runtime/types/internal.ts | 7 +- src/mono/wasm/runtime/web-socket.ts | 2 + 38 files changed, 807 insertions(+), 559 deletions(-) create mode 100644 src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSProxyContext.cs diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/System.Runtime.InteropServices.JavaScript.sln b/src/libraries/System.Runtime.InteropServices.JavaScript/System.Runtime.InteropServices.JavaScript.sln index 73c85be0abf1e..b4cd9dd113c39 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/System.Runtime.InteropServices.JavaScript.sln +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/System.Runtime.InteropServices.JavaScript.sln @@ -1,4 +1,8 @@ -Microsoft Visual Studio Solution File, Format Version 12.00 + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.8.34004.107 +MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestUtilities", "..\Common\tests\TestUtilities\TestUtilities.csproj", "{ED86AB26-1CFB-457D-BF87-B7A0D8FAF272}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Collections", "..\System.Collections\ref\System.Collections.csproj", "{8B1D80E9-AE0D-4E3C-9F91-E6862B49AEF3}" @@ -51,6 +55,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ref", "ref", "{32CDDDCD-531 EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tools", "tools", "{F92020A9-28BE-4398-86FF-5CFE44C94882}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Net.WebSockets.Client", "..\System.Net.WebSockets.Client\src\System.Net.WebSockets.Client.csproj", "{8B0D5A81-3E35-4391-8FB5-A44ABA356A07}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -129,6 +135,10 @@ Global {008873D5-9028-4FF3-8354-71F713748625}.Debug|Any CPU.Build.0 = Debug|Any CPU {008873D5-9028-4FF3-8354-71F713748625}.Release|Any CPU.ActiveCfg = Release|Any CPU {008873D5-9028-4FF3-8354-71F713748625}.Release|Any CPU.Build.0 = Release|Any CPU + {8B0D5A81-3E35-4391-8FB5-A44ABA356A07}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8B0D5A81-3E35-4391-8FB5-A44ABA356A07}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8B0D5A81-3E35-4391-8FB5-A44ABA356A07}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8B0D5A81-3E35-4391-8FB5-A44ABA356A07}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -155,6 +165,7 @@ Global {39B30F44-B141-44E9-B7A7-B1A9EDB1A61C} = {F92020A9-28BE-4398-86FF-5CFE44C94882} {D819DEB6-6F6B-484B-9F0F-BDBCEDC83A1A} = {F92020A9-28BE-4398-86FF-5CFE44C94882} {32CDDDCD-5319-494C-AB41-42B87C467F04} = {F92020A9-28BE-4398-86FF-5CFE44C94882} + {8B0D5A81-3E35-4391-8FB5-A44ABA356A07} = {67F3A00A-AE6C-434C-927D-E5D38DE2DA2C} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {3FE64246-4AFA-424A-AE5D-7007E20451B5} 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 d3de481834697..7690aa4fe43c8 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 0f4f0fd6d4184..1b8664dc43591 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 bc34b5f9fc27a..23be0db96bdc7 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 @@ -23,10 +24,17 @@ public static void CancelPromise(Task promise) #if FEATURE_WASM_THREADS - holder.SynchronizationContext!.Send(static (JSHostImplementation.PromiseHolder holder) => + if (holder.ProxyContext == JSProxyContext.CurrentInstance) { + _CancelPromise(holder.GCHandle); + return; + } + + holder.ProxyContext.SynchronizationContext.Post(static (object? h) => + { + var holder = (JSHostImplementation.PromiseHolder)h!; #endif - _CancelPromise(holder.GCHandle); + _CancelPromise(holder.GCHandle); #if FEATURE_WASM_THREADS }, holder); #endif @@ -44,8 +52,16 @@ public static void CancelPromise(Task promise, Action callback, T state) #if FEATURE_WASM_THREADS - holder.SynchronizationContext!.Send((JSHostImplementation.PromiseHolder holder) => + if (holder.ProxyContext == JSProxyContext.CurrentInstance) + { + _CancelPromise(holder.GCHandle); + callback.Invoke(state); + return; + } + + holder.ProxyContext.SynchronizationContext.Post((object? h) => { + var holder = (JSHostImplementation.PromiseHolder)h!; #endif _CancelPromise(holder.GCHandle); callback.Invoke(state); 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 1233115fab072..9e93377d88925 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 could assume that all proxies are owned by calling thread + JSProxyContext.CapturedInstance = JSProxyContext.AssertCurrentContext(); +#endif + arg_1.ToManaged(out IntPtr entrypointPtr); if (entrypointPtr == IntPtr.Zero) { @@ -94,6 +99,12 @@ public static void CallEntrypoint(JSMarshalerArgument* arguments_buffer) arg_exc.ToJS(ex); } +#if FEATURE_WASM_THREADS + finally + { + JSProxyContext.CapturedInstance = null; + } +#endif } public static void LoadLazyAssembly(JSMarshalerArgument* arguments_buffer) @@ -140,37 +151,23 @@ 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 assume that all proxies are owned by calling thread + var ctx = JSProxyContext.CapturedInstance = JSProxyContext.AssertCurrentContext(); + ctx.ReleaseJSOwnedObjectByGCHandle(arg_1.slot.GCHandle); } catch (Exception ex) { arg_exc.ToJS(ex); } +#if FEATURE_WASM_THREADS + finally + { + JSProxyContext.CapturedInstance = null; + } +#endif } // the marshaled signature is: @@ -185,6 +182,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 could assume that all proxies are owned by calling thread + JSProxyContext.CapturedInstance = JSProxyContext.AssertCurrentContext(); +#endif + GCHandle callback_gc_handle = (GCHandle)arg_1.slot.GCHandle; if (callback_gc_handle.Target is ToManagedCallback callback) { @@ -200,6 +202,12 @@ public static void CallDelegate(JSMarshalerArgument* arguments_buffer) { arg_exc.ToJS(ex); } +#if FEATURE_WASM_THREADS + finally + { + JSProxyContext.CapturedInstance = null; + } +#endif } // the marshaled signature is: @@ -210,39 +218,28 @@ 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 could assume that all proxies are owned by calling thread + var ctx = JSProxyContext.CapturedInstance = JSProxyContext.AssertCurrentContext(); + var holder = ctx.ReleasePromiseHolder(arg_1.slot.GCHandle); + if (holder != null) { - if (ThreadJsOwnedHolders.Remove(holderGCHandle, out PromiseHolder? holder)) - { - holder.GCHandle = IntPtr.Zero; - // arg_2, arg_3 are processed by the callback - holder.Callback!(arguments_buffer); - } - } - else - { - 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(); + // arg_2, arg_3 are processed by the callback + holder.Callback!(arguments_buffer); } } catch (Exception ex) { arg_exc.ToJS(ex); } +#if FEATURE_WASM_THREADS + finally + { + JSProxyContext.CapturedInstance = null; + } +#endif } // the marshaled signature is: @@ -254,6 +251,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 could assume that all proxies are owned by calling thread + JSProxyContext.CapturedInstance = JSProxyContext.AssertCurrentContext(); + GCHandle exception_gc_handle = (GCHandle)arg_1.slot.GCHandle; if (exception_gc_handle.Target is Exception exception) { @@ -268,24 +268,28 @@ public static void GetManagedStackTrace(JSMarshalerArgument* arguments_buffer) { arg_exc.ToJS(ex); } +#if FEATURE_WASM_THREADS + finally + { + JSProxyContext.CapturedInstance = null; + } +#endif } #if FEATURE_WASM_THREADS + public static void CaptureProxyContext() + { + JSProxyContext.CapturedInstance = JSProxyContext.AssertCurrentContext(); + } + // 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/LegacyExports.cs b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Interop/LegacyExports.cs index bb4b017ef743f..883091383536d 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.MainInstance.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.MainInstance.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.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 e623ef1923822..0e7e7e5f7c2c2 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,9 +47,10 @@ public override string? StackTrace } #if FEATURE_WASM_THREADS - var currentTID = JSSynchronizationContext.CurrentJSSynchronizationContext?.TargetTID; - if (jsException.OwnerTID != currentTID) + var ctx = JSProxyContext.CurrentInstance; + if (ctx == null || jsException.ProxyContext != ctx) { + // if we are on another thread, it would be too expensive and risky to obtain lazy stack trace. return bs; } #endif 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 17a81b059f817..37f0c070629ae 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 @@ -220,15 +217,14 @@ internal static unsafe void InvokeJSFunction(JSObject jsFunction, Span arguments) { +#if FEATURE_WASM_THREADS + JSProxyContext.AssertCurrentContext(); +#endif + if (signature.IsAsync) { // pre-allocate the result handle and Task -#if FEATURE_WASM_THREADS - JSSynchronizationContext.AssertWebWorkerContext(); - var holder = new JSHostImplementation.PromiseHolder(JSSynchronizationContext.CurrentJSSynchronizationContext!); -#else - var holder = new JSHostImplementation.PromiseHolder(); -#endif + var holder = new JSHostImplementation.PromiseHolder(JSProxyContext.DefaultInstance); arguments[1].slot.Type = MarshalerType.TaskPreCreated; arguments[1].slot.GCHandle = holder.GCHandle; } @@ -256,7 +252,7 @@ internal static unsafe void InvokeJSImportImpl(JSFunctionBinding signature, Span internal static unsafe JSFunctionBinding BindJSFunctionImpl(string functionName, string moduleName, ReadOnlySpan signatures) { #if FEATURE_WASM_THREADS - JSSynchronizationContext.AssertWebWorkerContext(); + JSProxyContext.AssertCurrentContext(); #endif var signature = JSHostImplementation.GetMethodSignature(signatures, functionName, moduleName); 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 89a7eb8a0246e..dc165faf7c024 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.AssertCurrentContext(); #endif return JavaScriptImports.GetGlobalThis(); } @@ -36,7 +36,7 @@ public static JSObject DotnetInstance get { #if FEATURE_WASM_THREADS - JSSynchronizationContext.AssertWebWorkerContext(); + JSProxyContext.AssertCurrentContext(); #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.AssertCurrentContext(); #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.CurrentInstance ?? JSProxyContext.MainInstance).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 a8b496237fd8e..b97c8751a57ab 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 @@ -14,32 +14,18 @@ public sealed class PromiseHolder { public nint GCHandle; // could be also virtual GCVHandle public ToManagedCallback? Callback; -#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; -#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() + internal JSProxyContext ProxyContext; + + 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 b2c0504e4d4f3..e5813998ec2a3 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,18 @@ 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); + JSProxyContext.CurrentInstance = ctx.ProxyContext; + if (isMainThread) { - Environment.FailFast($"JSSynchronizationContext.Install has wrong native thread id {ctx.TargetTID} != {currentTID}"); + JSProxyContext.MainInstance = ctx.ProxyContext; } ctx.AwaitNewData(); } 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 - { - 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}"); - } - } - - 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; + JSProxyContext.CurrentInstance?.Dispose(); } [UnsafeAccessor(UnsafeAccessorKind.Field, Name = "external_eventloop")] @@ -460,15 +228,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 38bc3e23ffc10..3c09b0679b9c8 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 @@ -64,6 +64,9 @@ internal struct JSMarshalerArgumentImpl public unsafe void Initialize() { slot.Type = MarshalerType.None; +#if FEATURE_WASM_THREADS + JSProxyContext.CapturedInstance = null; +#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 82fe397b7ec47..e9f771e210b38 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,10 +10,10 @@ namespace System.Runtime.InteropServices.JavaScript public partial class JSObject { internal nint JSHandle; + internal JSProxyContext ProxyContext; #if FEATURE_WASM_THREADS - private readonly object _thisLock = new object(); - private SynchronizationContext? m_SynchronizationContext; + private readonly object _lockObject = new object(); #endif public SynchronizationContext SynchronizationContext @@ -21,42 +21,30 @@ 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 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 (_lockObject) { InFlightCounter++; if (InFlightCounter == 1) @@ -72,7 +60,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 (_lockObject) { Debug.Assert(InFlightCounter != 0, "InFlightCounter != 0"); @@ -94,19 +82,19 @@ internal static void AssertThreadAffinity(object value) { return; } - JSSynchronizationContext.AssertWebWorkerContext(); - var currentTID = JSSynchronizationContext.CurrentJSSynchronizationContext!.TargetTID; + + var currentContext = JSProxyContext.AssertCurrentContext(); if (value is JSObject jsObject) { - if (jsObject.OwnerTID != currentTID) + if (jsObject.ProxyContext != currentContext) { 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 != currentContext) { throw new InvalidOperationException("The JavaScript object can be used only on the thread where it was created."); } @@ -123,11 +111,15 @@ internal static void AssertThreadAffinity(object value) /// public override string ToString() => $"(js-obj js '{JSHandle}')"; + // when we know that JS side already freed the handle internal void DisposeLocal() { - JSHostImplementation.ThreadCsOwnedObjects.Remove(JSHandle); - _isDisposed = true; - JSHandle = IntPtr.Zero; + lock (_lockObject) + { + ProxyContext.RemoveCSOwnedObject(JSHandle); + _isDisposed = true; + JSHandle = IntPtr.Zero; + } } private void DisposeThis() @@ -135,31 +127,33 @@ private void DisposeThis() if (!_isDisposed) { #if FEATURE_WASM_THREADS - if (SynchronizationContext == SynchronizationContext.Current) + if (ProxyContext == JSProxyContext.CurrentInstance) { - lock (_thisLock) + lock (_lockObject) { - JSHostImplementation.ReleaseCSOwnedObject(JSHandle); + if (_isDisposed) + { + return; + } + ProxyContext.ReleaseCSOwnedObject(JSHandle); _isDisposed = true; JSHandle = IntPtr.Zero; - m_SynchronizationContext = null; } //lock return; } - SynchronizationContext.Post(static (object? s) => + ProxyContext.SynchronizationContext.Post(static (object? s) => { var self = (JSObject)s!; - lock (self._thisLock) + lock (self._lockObject) { - JSHostImplementation.ReleaseCSOwnedObject(self.JSHandle); + self.ProxyContext.ReleaseCSOwnedObject(self.JSHandle); self._isDisposed = true; self.JSHandle = IntPtr.Zero; - self.m_SynchronizationContext = null; } //lock }, this); #else - JSHostImplementation.ReleaseCSOwnedObject(JSHandle); + ProxyContext.ReleaseCSOwnedObject(JSHandle); _isDisposed = true; JSHandle = IntPtr.Zero; #endif 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 0000000000000..13b07e9b77055 --- /dev/null +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSProxyContext.cs @@ -0,0 +1,466 @@ +// 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 _disposedValue; + + // 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 + public static readonly JSProxyContext DefaultInstance = new(); + public static JSProxyContext CurrentInstance => DefaultInstance; + public static JSProxyContext CapturedInstance + { + get => MainInstance!; + set { } + } + public static JSProxyContext MainInstance => DefaultInstance; +#else + public nint TargetTID; + public int ThreadId; + public bool IsMainThread; + public JSSynchronizationContext SynchronizationContext; + + [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); + } + + // Context of the main thread + private static JSProxyContext? _MainInstance; + public static JSProxyContext MainInstance + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => _MainInstance!; + set => _MainInstance = value; + } + + // Context captured from parameters of the current operation. + // Most of the time, this matches the current thread. + // When we pass `JSObject` instance to `JSImport`, it could have thread affinity different than the current thread. + // We will use that parameter to capture the target thread to call into. + [ThreadStatic] + public static JSProxyContext? CapturedInstance; + + // Context of the current thread. Could be null on threads which don't have JS interop, like managed thread pool threads. + [ThreadStatic] + public static JSProxyContext? CurrentInstance; + + // 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 DefaultInstance + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + // until we set CapturedInstance in the generated code of JSImport/JSExport, we need to fallback to CurrentInstance to find target thread + // because maybe it was not captured by the other parameters yet + // after capture is solid, we could drop DefaultInstance and use CapturedInstance instead + // TODO: sort generated ToJS() calls to make the capture first + return CapturedInstance ?? CurrentInstance ?? MainInstance; + } + } + + public JSProxyContext(bool isMainThread, JSSynchronizationContext synchronizationContext) + { + SynchronizationContext = synchronizationContext; + Interop.Runtime.InstallWebWorkerInterop(); + TargetTID = GetNativeThreadId(); + ThreadId = Thread.CurrentThread.ManagedThreadId; + IsMainThread = isMainThread; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool IsTargetThread() + { + return ThreadId == Thread.CurrentThread.ManagedThreadId; + } +#endif + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static JSProxyContext AssertCurrentContext() + { +#if FEATURE_WASM_THREADS + var ctx = CurrentInstance; + if (ctx == null || ctx._disposedValue) + { + throw new InvalidOperationException($"Please use dedicated worker for working with JavaScript interop, ManagedThreadId:{Thread.CurrentThread.ManagedThreadId}. See https://aka.ms/dotnet-JS-interop-threads"); + } + return ctx; +#else + return MainInstance; +#endif + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static JSProxyContext AssertCapturedContext() + { +#if FEATURE_WASM_THREADS + var ctx = CapturedInstance; + if (ctx == null || ctx._disposedValue) + { + Environment.FailFast($"CapturedInstance was not set. ManagedThreadId:{Thread.CurrentThread.ManagedThreadId}"); + } + return ctx; +#else + return MainInstance; +#endif + } + + [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]; + 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 static IntPtr GetJSOwnedObjectGCHandle(object obj, GCHandleType handleType = GCHandleType.Normal) + { + if (obj == null) + { + return IntPtr.Zero; + } + + var ctx = JSProxyContext.DefaultInstance; + lock (ctx) + { + if (ctx.ThreadJsOwnedObjects.TryGetValue(obj, out IntPtr gcHandle)) + { + return gcHandle; + } + + IntPtr result = (IntPtr)GCHandle.Alloc(obj, handleType); + ctx.ThreadJsOwnedObjects[obj] = result; + return result; + } + } + + // TODO unregister and collect pending PromiseHolder also when no C# is awaiting ? + public static PromiseHolder GetPromiseHolder(nint gcHandle) + { + PromiseHolder holder; + if (IsGCVHandle(gcHandle)) + { + // this path should only happen when the Promise is passed as argument of JSExport + var ctx = AssertCapturedContext(); + lock (ctx) + { + holder = new PromiseHolder(ctx, gcHandle); + ctx.ThreadJsOwnedHolders.Add(gcHandle, holder); + } + } + else + { + holder = (PromiseHolder)((GCHandle)gcHandle).Target!; + } + return holder; + } + + public PromiseHolder? ReleasePromiseHolder(nint holderGCHandle) + { + PromiseHolder? holder = null; + lock (this) + { + if (IsGCVHandle(holderGCHandle)) + { + if (ThreadJsOwnedHolders.Remove(holderGCHandle, out holder)) + { + holder.GCHandle = IntPtr.Zero; + } + } + else + { + GCHandle handle = (GCHandle)holderGCHandle; + var target = handle.Target!; + if (target is PromiseHolder holder2) + { + holder = holder2; + holder.GCHandle = IntPtr.Zero; + } + else + { + JSProxyContext.CurrentInstance!.ThreadJsOwnedObjects.Remove(target); + } + handle.Free(); + } + } + return holder; + } + + 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 void RemoveCSOwnedObject(nint jsHandle) + { + lock (this) + { + ThreadCsOwnedObjects.Remove(jsHandle); + } + } + + public void ReleaseCSOwnedObject(nint jsHandle) + { + if (jsHandle != IntPtr.Zero) + { + // TODO could this be called from wrong thread ? + lock (this) + { +#if FEATURE_WASM_THREADS && DEBUG + if (ThreadCsOwnedObjects.Remove(jsHandle, out WeakReference? weak)) + { + if (weak.TryGetTarget(out JSObject? obj) && obj.ProxyContext != this) + { + Environment.FailFast("ReleaseCSOwnedObject must be called on the thread that JSObject belongs into."); + } + }; +#else + ThreadCsOwnedObjects.Remove(jsHandle); +#endif + Interop.Runtime.ReleaseCSOwnedObject(jsHandle); + if (IsJSVHandle(jsHandle)) + { + FreeJSVHandle(jsHandle); + } + } + } + } + + public unsafe void ReleaseJSOwnedObjectByGCHandle(nint gcHandle) + { + PromiseHolder? holder = null; + lock (this) + { + if (IsGCVHandle(gcHandle)) + { + if (ThreadJsOwnedHolders.Remove(gcHandle, out holder)) + { + holder.GCHandle = IntPtr.Zero; + } + } + else + { + GCHandle handle = (GCHandle)gcHandle; + var target = handle.Target!; + if (target is PromiseHolder holder2) + { + holder = holder2; + holder.GCHandle = IntPtr.Zero; + } + else + { + ThreadJsOwnedObjects.Remove(target); + } + handle.Free(); + } + } + if (holder != null) + { + holder.Callback!(null); + } + } + + #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.MainInstance), + 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 (!_disposedValue) + { +#if FEATURE_WASM_THREADS + if (!IsTargetThread()) Environment.FailFast($"JSProxyContext must be disposed on the thread which owns it."); +#endif + + // TODO: free unmanaged resources (unmanaged objects) and override finalizer + foreach (var jsObjectWeak in ThreadCsOwnedObjects.Values) + { + 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); + } + } + + if (disposing) + { + ThreadCsOwnedObjects.Clear(); + ThreadJsOwnedObjects.Clear(); + JSVHandleFreeList.Clear(); + NextJSVHandle = IntPtr.Zero; +#if FEATURE_WASM_THREADS + SynchronizationContext.Dispose(); +#endif + } + _disposedValue = 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 639906274d399..64560043db4be 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,30 @@ 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 } - ) - ) - { - } - - 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(bool isMainThread) { - TargetThread = targetThread; - TargetTID = targetTID; - Queue = queue; + ProxyContext = new JSProxyContext(isMainThread, this); + Queue = Channel.CreateUnbounded(new UnboundedChannelOptions { SingleWriter = false, SingleReader = true, AllowSynchronousContinuations = true }); _DataIsAvailable = DataIsAvailable; + + previousSynchronizationContext = Current; + SetSynchronizationContext(this); } public override SynchronizationContext CreateCopy() { - return new JSSynchronizationContext(TargetThread, TargetTID, Queue); + // child thread will inherit this JSSynchronizationContext + return this; } 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,12 +85,12 @@ 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.TargetTID, (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)) @@ -120,9 +102,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.IsTargetThread()) { d(state); return; @@ -147,14 +129,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.AssertCurrentContext(); + 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 +165,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(); + } + SetSynchronizationContext(previousSynchronizationContext); + _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/Legacy/Array.cs b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Legacy/Array.cs index 29ada438164cc..771d3f6ff3787 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.MainInstance) { #if FEATURE_WASM_THREADS LegacyHostImplementation.ThrowIfLegacyWorkerThread(); #endif - LegacyHostImplementation.RegisterCSOwnedObject(this); + JSProxyContext.MainInstance.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.MainInstance) { } /// 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 39db0563089d0..a5c7b78fe1c9b 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.MainInstance) { #if FEATURE_WASM_THREADS LegacyHostImplementation.ThrowIfLegacyWorkerThread(); #endif - LegacyHostImplementation.RegisterCSOwnedObject(this); + JSProxyContext.MainInstance.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.MainInstance) { } /// 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 3d3300ec5c503..94c836323692d 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.MainInstance) { #if FEATURE_WASM_THREADS LegacyHostImplementation.ThrowIfLegacyWorkerThread(); #endif - LegacyHostImplementation.RegisterCSOwnedObject(this); + JSProxyContext.MainInstance.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.MainInstance) { #if FEATURE_WASM_THREADS LegacyHostImplementation.ThrowIfLegacyWorkerThread(); #endif - LegacyHostImplementation.RegisterCSOwnedObject(this); + JSProxyContext.MainInstance.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.MainInstance) { #if FEATURE_WASM_THREADS LegacyHostImplementation.ThrowIfLegacyWorkerThread(); #endif - LegacyHostImplementation.RegisterCSOwnedObject(this); + JSProxyContext.MainInstance.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.MainInstance) { } /// 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 f87fb94c01610..2e336594264e7 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.MainInstance) { #if FEATURE_WASM_THREADS LegacyHostImplementation.ThrowIfLegacyWorkerThread(); #endif - LegacyHostImplementation.RegisterCSOwnedObject(this); + JSProxyContext.MainInstance.RegisterCSOwnedObject(this); } - internal Function(IntPtr jsHandle) : base(jsHandle) + internal Function(IntPtr jsHandle) : base(jsHandle, JSProxyContext.MainInstance) { } /// 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 f726c26262b28..8e6242017979f 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 b80f7c31ad64b..af87e0a25834d 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.MainInstance) { #if FEATURE_WASM_THREADS LegacyHostImplementation.ThrowIfLegacyWorkerThread(); #endif - LegacyHostImplementation.RegisterCSOwnedObject(this); + JSProxyContext.MainInstance.RegisterCSOwnedObject(this); } public Uint8Array(ArrayBuffer buffer) - : base(JavaScriptImports.CreateCSOwnedObject(nameof(Uint8Array), new object[] { buffer })) + : base(JavaScriptImports.CreateCSOwnedObject(nameof(Uint8Array), new object[] { buffer }), JSProxyContext.MainInstance) { #if FEATURE_WASM_THREADS LegacyHostImplementation.ThrowIfLegacyWorkerThread(); #endif - LegacyHostImplementation.RegisterCSOwnedObject(this); + JSProxyContext.MainInstance.RegisterCSOwnedObject(this); } - internal Uint8Array(IntPtr jsHandle) : base(jsHandle) + internal Uint8Array(IntPtr jsHandle) : base(jsHandle, JSProxyContext.MainInstance) { } 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 1ce1dc2da6a7e..4962d5e88552a 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,7 @@ public unsafe void ToJS(ArraySegment value) return; } slot.Type = MarshalerType.ArraySegment; - slot.GCHandle = JSHostImplementation.GetJSOwnedObjectGCHandle(value.Array, GCHandleType.Pinned); + slot.GCHandle = JSProxyContext.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 9589ea0f42f7a..248721692d505 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,7 @@ public unsafe void ToJS(ArraySegment value) return; } slot.Type = MarshalerType.ArraySegment; - slot.GCHandle = JSHostImplementation.GetJSOwnedObjectGCHandle(value.Array, GCHandleType.Pinned); + slot.GCHandle = JSProxyContext.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 e19a3f428c8a2..8dd5ef813d23f 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,7 @@ public unsafe void ToManaged(out Exception? value) if (slot.JSHandle != IntPtr.Zero) { // this is JSException round-trip - jsException = JSHostImplementation.CreateCSOwnedProxy(slot.JSHandle); + jsException = JSProxyContext.DefaultInstance.CreateCSOwnedProxy(slot.JSHandle); } string? message; @@ -72,12 +72,21 @@ public unsafe void ToJS(Exception? value) ObjectDisposedException.ThrowIf(jse.jsException.IsDisposed, value); slot.Type = MarshalerType.JSException; slot.JSHandle = jse.jsException.JSHandle; +#if FEATURE_WASM_THREADS + var parameterContext = jse.jsException.ProxyContext; + var capturedContext = JSProxyContext.CapturedInstance; + if (capturedContext != null && parameterContext != capturedContext) + { + throw new InvalidOperationException("All JSObject proxies need to have same thread affinity"); + } + JSProxyContext.CapturedInstance = parameterContext; +#endif } else { ToJS(cpy.Message); slot.Type = MarshalerType.Exception; - slot.GCHandle = JSHostImplementation.GetJSOwnedObjectGCHandle(cpy); + slot.GCHandle = JSProxyContext.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 ca2c0bab7bdff..2cc0e15ad9f71 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 @@ -11,7 +11,7 @@ private sealed class ActionJS public ActionJS(IntPtr jsHandle) { - JSObject = JSHostImplementation.CreateCSOwnedProxy(jsHandle); + JSObject = JSProxyContext.DefaultInstance.CreateCSOwnedProxy(jsHandle); } public void InvokeJS() @@ -41,7 +41,7 @@ private sealed class ActionJS public ActionJS(IntPtr jsHandle, ArgumentToJSCallback arg1Marshaler) { - JSObject = JSHostImplementation.CreateCSOwnedProxy(jsHandle); + JSObject = JSProxyContext.DefaultInstance.CreateCSOwnedProxy(jsHandle); Arg1Marshaler = arg1Marshaler; } @@ -72,7 +72,7 @@ private sealed class ActionJS public ActionJS(IntPtr jsHandle, ArgumentToJSCallback arg1Marshaler, ArgumentToJSCallback arg2Marshaler) { - JSObject = JSHostImplementation.CreateCSOwnedProxy(jsHandle); + JSObject = JSProxyContext.DefaultInstance.CreateCSOwnedProxy(jsHandle); Arg1Marshaler = arg1Marshaler; Arg2Marshaler = arg2Marshaler; } @@ -107,7 +107,7 @@ private sealed class ActionJS public ActionJS(IntPtr jsHandle, ArgumentToJSCallback arg1Marshaler, ArgumentToJSCallback arg2Marshaler, ArgumentToJSCallback arg3Marshaler) { - JSObject = JSHostImplementation.CreateCSOwnedProxy(jsHandle); + JSObject = JSProxyContext.DefaultInstance.CreateCSOwnedProxy(jsHandle); Arg1Marshaler = arg1Marshaler; Arg2Marshaler = arg2Marshaler; Arg3Marshaler = arg3Marshaler; @@ -219,7 +219,7 @@ private sealed class FuncJS public FuncJS(IntPtr jsHandle, ArgumentToManagedCallback resMarshaler) { - JSObject = JSHostImplementation.CreateCSOwnedProxy(jsHandle); + JSObject = JSProxyContext.DefaultInstance.CreateCSOwnedProxy(jsHandle); ResMarshaler = resMarshaler; } @@ -254,7 +254,7 @@ private sealed class FuncJS public FuncJS(IntPtr jsHandle, ArgumentToJSCallback arg1Marshaler, ArgumentToManagedCallback resMarshaler) { - JSObject = JSHostImplementation.CreateCSOwnedProxy(jsHandle); + JSObject = JSProxyContext.DefaultInstance.CreateCSOwnedProxy(jsHandle); Arg1Marshaler = arg1Marshaler; ResMarshaler = resMarshaler; } @@ -290,7 +290,7 @@ private sealed class FuncJS public FuncJS(IntPtr jsHandle, ArgumentToJSCallback arg1Marshaler, ArgumentToJSCallback arg2Marshaler, ArgumentToManagedCallback resMarshaler) { - JSObject = JSHostImplementation.CreateCSOwnedProxy(jsHandle); + JSObject = JSProxyContext.DefaultInstance.CreateCSOwnedProxy(jsHandle); Arg1Marshaler = arg1Marshaler; Arg2Marshaler = arg2Marshaler; ResMarshaler = resMarshaler; @@ -330,7 +330,7 @@ private sealed class FuncJS public FuncJS(IntPtr jsHandle, ArgumentToJSCallback arg1Marshaler, ArgumentToJSCallback arg2Marshaler, ArgumentToJSCallback arg3Marshaler, ArgumentToManagedCallback resMarshaler) { - JSObject = JSHostImplementation.CreateCSOwnedProxy(jsHandle); + JSObject = JSProxyContext.DefaultInstance.CreateCSOwnedProxy(jsHandle); Arg1Marshaler = arg1Marshaler; Arg2Marshaler = arg2Marshaler; Arg3Marshaler = arg3Marshaler; @@ -463,7 +463,7 @@ public unsafe void ToJS(Action value) // eventual exception is handled by C# caller }; slot.Type = MarshalerType.Function; - slot.GCHandle = JSHostImplementation.GetJSOwnedObjectGCHandle(cb); + slot.GCHandle = JSProxyContext.GetJSOwnedObjectGCHandle(cb); } /// @@ -484,7 +484,7 @@ public unsafe void ToJS(Action value, ArgumentToManagedCallback arg1Mar // eventual exception is handled by C# caller }; slot.Type = MarshalerType.Action; - slot.GCHandle = JSHostImplementation.GetJSOwnedObjectGCHandle(cb); + slot.GCHandle = JSProxyContext.GetJSOwnedObjectGCHandle(cb); } /// @@ -509,7 +509,7 @@ public unsafe void ToJS(Action value, ArgumentToManagedCallback< // eventual exception is handled by C# caller }; slot.Type = MarshalerType.Action; - slot.GCHandle = JSHostImplementation.GetJSOwnedObjectGCHandle(cb); + slot.GCHandle = JSProxyContext.GetJSOwnedObjectGCHandle(cb); } /// @@ -538,7 +538,7 @@ public unsafe void ToJS(Action value, ArgumentToManagedC // eventual exception is handled by C# caller }; slot.Type = MarshalerType.Action; - slot.GCHandle = JSHostImplementation.GetJSOwnedObjectGCHandle(cb); + slot.GCHandle = JSProxyContext.GetJSOwnedObjectGCHandle(cb); } /// @@ -559,7 +559,7 @@ public unsafe void ToJS(Func value, ArgumentToJSCallback @@ -584,7 +584,7 @@ public unsafe void ToJS(Func value, ArgumentToManagedCal // eventual exception is handled by C# caller }; slot.Type = MarshalerType.Function; - slot.GCHandle = JSHostImplementation.GetJSOwnedObjectGCHandle(cb); + slot.GCHandle = JSProxyContext.GetJSOwnedObjectGCHandle(cb); } /// @@ -613,7 +613,7 @@ public unsafe void ToJS(Func value, ArgumentTo // eventual exception is handled by C# caller }; slot.Type = MarshalerType.Function; - slot.GCHandle = JSHostImplementation.GetJSOwnedObjectGCHandle(cb); + slot.GCHandle = JSProxyContext.GetJSOwnedObjectGCHandle(cb); } /// @@ -646,7 +646,7 @@ public unsafe void ToJS(Func value, Ar // eventual exception is handled by C# caller }; slot.Type = MarshalerType.Function; - slot.GCHandle = JSHostImplementation.GetJSOwnedObjectGCHandle(cb); + slot.GCHandle = JSProxyContext.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 0f384d1bf75a0..018d96abef94d 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 @@ -133,7 +133,7 @@ public unsafe void ToJS(ArraySegment value) return; } slot.Type = MarshalerType.ArraySegment; - slot.GCHandle = JSHostImplementation.GetJSOwnedObjectGCHandle(value.Array, GCHandleType.Pinned); + slot.GCHandle = JSProxyContext.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 a7220c8934a6d..f3e7fd001c86a 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,7 @@ public unsafe void ToManaged(out JSObject? value) value = null; return; } - value = JSHostImplementation.CreateCSOwnedProxy(slot.JSHandle); + value = JSProxyContext.DefaultInstance.CreateCSOwnedProxy(slot.JSHandle); } /// @@ -34,11 +34,19 @@ 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 { #if FEATURE_WASM_THREADS - JSObject.AssertThreadAffinity(value); + var parameterContext = value.ProxyContext; + var capturedContext = JSProxyContext.CapturedInstance; + if (capturedContext != null && parameterContext != capturedContext) + { + throw new InvalidOperationException("All JSObject proxies need to have same thread affinity"); + } + JSProxyContext.CapturedInstance = parameterContext; #endif ObjectDisposedException.ThrowIf(value.IsDisposed, value); slot.Type = MarshalerType.JSObject; 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 cf3d7e4585666..66026655d8e20 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,7 @@ public void ToJS(object? value) else { slot.Type = MarshalerType.Object; - slot.GCHandle = JSHostImplementation.GetJSOwnedObjectGCHandle(value); + slot.GCHandle = JSProxyContext.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 bf721e40b4ab0..2c04194b32b73 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,7 +45,7 @@ public unsafe void ToManaged(out Task? value) value = null; return; } - PromiseHolder holder = GetPromiseHolder(slot.GCHandle); + PromiseHolder holder = JSProxyContext.GetPromiseHolder(slot.GCHandle); TaskCompletionSource tcs = new TaskCompletionSource(holder); ToManagedCallback callback = (JSMarshalerArgument* arguments_buffer) => { @@ -74,8 +76,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,7 +86,7 @@ public unsafe void ToManaged(out Task? value, ArgumentToManagedCallback value = null; return; } - PromiseHolder holder = GetPromiseHolder(slot.GCHandle); + PromiseHolder holder = JSProxyContext.GetPromiseHolder(slot.GCHandle); TaskCompletionSource tcs = new TaskCompletionSource(holder); ToManagedCallback callback = (JSMarshalerArgument* arguments_buffer) => { @@ -113,23 +115,6 @@ public unsafe void ToManaged(out Task? value, ArgumentToManagedCallback value = tcs.Task; } - // 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!; - } - return holder; - } internal void ToJSDynamic(Task? value) { @@ -170,7 +155,7 @@ internal void ToJSDynamic(Task? value) if (slot.Type != MarshalerType.TaskPreCreated) { // this path should only happen when the Task is passed as argument of JSImport - slot.JSHandle = AllocJSVHandle(); + slot.JSHandle = JSProxyContext.DefaultInstance.AllocJSVHandle(); slot.Type = MarshalerType.Task; } else @@ -179,7 +164,7 @@ internal void ToJSDynamic(Task? value) // promise and handle is pre-allocated in slot.JSHandle } - var taskHolder = new JSObject(slot.JSHandle); + var taskHolder = new JSObject(slot.JSHandle, JSProxyContext.DefaultInstance); #if FEATURE_WASM_THREADS task.ContinueWith(Complete, taskHolder, TaskScheduler.FromCurrentSynchronizationContext()); @@ -248,7 +233,7 @@ public void ToJS(Task? value) if (slot.Type != MarshalerType.TaskPreCreated) { // this path should only happen when the Task is passed as argument of JSImport - slot.JSHandle = AllocJSVHandle(); + slot.JSHandle = JSProxyContext.DefaultInstance.AllocJSVHandle(); slot.Type = MarshalerType.Task; } else @@ -257,7 +242,7 @@ public void ToJS(Task? value) // promise and handle is pre-allocated in slot.JSHandle } - var taskHolder = new JSObject(slot.JSHandle); + var taskHolder = new JSObject(slot.JSHandle, JSProxyContext.DefaultInstance); #if FEATURE_WASM_THREADS task.ContinueWith(Complete, taskHolder, TaskScheduler.FromCurrentSynchronizationContext()); @@ -319,7 +304,7 @@ public void ToJS(Task? value, ArgumentToJSCallback marshaler) if (slot.Type != MarshalerType.TaskPreCreated) { // this path should only happen when the Task is passed as argument of JSImport - slot.JSHandle = AllocJSVHandle(); + slot.JSHandle = JSProxyContext.DefaultInstance.AllocJSVHandle(); slot.Type = MarshalerType.Task; } else @@ -328,7 +313,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 = new JSObject(slot.JSHandle, JSProxyContext.DefaultInstance); #if FEATURE_WASM_THREADS task.ContinueWith(Complete, new HolderAndMarshaler(taskHolder, marshaler), TaskScheduler.FromCurrentSynchronizationContext()); diff --git a/src/mono/wasm/runtime/cwraps.ts b/src/mono/wasm/runtime/cwraps.ts index e76f78e4fc285..0a3f2e0894cd1 100644 --- a/src/mono/wasm/runtime/cwraps.ts +++ b/src/mono/wasm/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/wasm/runtime/driver.c b/src/mono/wasm/runtime/driver.c index 860140d9cf327..a48e77894018e 100644 --- a/src/mono/wasm/runtime/driver.c +++ b/src/mono/wasm/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/wasm/runtime/gc-handles.ts b/src/mono/wasm/runtime/gc-handles.ts index feacccce2a5f0..0a59dca3d1dfd 100644 --- a/src/mono/wasm/runtime/gc-handles.ts +++ b/src/mono/wasm/runtime/gc-handles.ts @@ -108,6 +108,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 JSHostImplementation.FreeJSVHandle->JSProxyContext } mono_assert(obj !== undefined && obj !== null, "ObjectDisposedException"); if (typeof obj[cs_owned_js_handle_symbol] !== "undefined") { diff --git a/src/mono/wasm/runtime/http.ts b/src/mono/wasm/runtime/http.ts index cdb60bd862ab0..23f8b20e3e4be 100644 --- a/src/mono/wasm/runtime/http.ts +++ b/src/mono/wasm/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/wasm/runtime/invoke-cs.ts b/src/mono/wasm/runtime/invoke-cs.ts index 17e9cf181b5a1..bb4a837b2e578 100644 --- a/src/mono/wasm/runtime/invoke-cs.ts +++ b/src/mono/wasm/runtime/invoke-cs.ts @@ -356,6 +356,10 @@ export function invoke_method_and_handle_exception(method: MonoMethod, args: JSM assert_bindings(); const fail_root = mono_wasm_new_root(); try { + if (MonoWasmThreads) { + // TODO in future, the generated code of JSExport could do this + runtimeHelpers.javaScriptExports.capture_proxy_context(); + } 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 (is_args_exception(args)) { @@ -368,6 +372,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) throw new Error("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/wasm/runtime/managed-exports.ts b/src/mono/wasm/runtime/managed-exports.ts index 7e1347617aab7..cd103fb1a937d 100644 --- a/src/mono/wasm/runtime/managed-exports.ts +++ b/src/mono/wasm/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,10 @@ 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 capture_proxy_context = MonoWasmThreads ? get_method("CaptureProxyContext") : undefined; + mono_assert(!MonoWasmThreads || capture_proxy_context, "Can't find CaptureProxyContext 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 +190,11 @@ 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 && capture_proxy_context) { + runtimeHelpers.javaScriptExports.capture_proxy_context = () => invoke_method_raw(capture_proxy_context); + } + if (MonoWasmThreads && install_main_synchronization_context) { + runtimeHelpers.javaScriptExports.install_main_synchronization_context = () => invoke_method_raw(install_main_synchronization_context); } } diff --git a/src/mono/wasm/runtime/startup.ts b/src/mono/wasm/runtime/startup.ts index 556dd158c9700..52a451acd273d 100644 --- a/src/mono/wasm/runtime/startup.ts +++ b/src/mono/wasm/runtime/startup.ts @@ -289,7 +289,7 @@ async function onRuntimeInitializedAsync(userOnRuntimeInitialized: () => void) { } if (MonoWasmThreads) { - runtimeHelpers.javaScriptExports.install_synchronization_context(); + runtimeHelpers.javaScriptExports.install_main_synchronization_context(); runtimeHelpers.jsSynchronizationContextInstalled = true; } diff --git a/src/mono/wasm/runtime/types/internal.ts b/src/mono/wasm/runtime/types/internal.ts index 4c086350182a6..6c711b6e2263e 100644 --- a/src/mono/wasm/runtime/types/internal.ts +++ b/src/mono/wasm/runtime/types/internal.ts @@ -348,8 +348,11 @@ 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: void InstallMainSynchronizationContext() + capture_proxy_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/wasm/runtime/web-socket.ts b/src/mono/wasm/runtime/web-socket.ts index 9c43849f39534..41b3ca2299428 100644 --- a/src/mono/wasm/runtime/web-socket.ts +++ b/src/mono/wasm/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}`); From bc29323d9a2d055b89a9117cf56a0d84e083c35b Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Wed, 13 Dec 2023 13:15:07 +0100 Subject: [PATCH 02/23] fix --- .../JavaScript/JSObject.References.cs | 4 ++++ .../JavaScript/JSProxyContext.cs | 20 ++----------------- 2 files changed, 6 insertions(+), 18 deletions(-) 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 e9f771e210b38..77bb551076e37 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 @@ -44,7 +44,9 @@ internal JSObject(IntPtr jsHandle, JSProxyContext ctx) internal void AddInFlight() { ObjectDisposedException.ThrowIf(IsDisposed, this); +#if FEATURE_WASM_THREADS lock (_lockObject) +#endif { InFlightCounter++; if (InFlightCounter == 1) @@ -60,7 +62,9 @@ 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() { +#if FEATURE_WASM_THREADS lock (_lockObject) +#endif { Debug.Assert(InFlightCounter != 0, "InFlightCounter != 0"); diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSProxyContext.cs b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSProxyContext.cs index 13b07e9b77055..285d5aa3f5df4 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSProxyContext.cs +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSProxyContext.cs @@ -116,21 +116,6 @@ public static JSProxyContext AssertCurrentContext() #endif } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static JSProxyContext AssertCapturedContext() - { -#if FEATURE_WASM_THREADS - var ctx = CapturedInstance; - if (ctx == null || ctx._disposedValue) - { - Environment.FailFast($"CapturedInstance was not set. ManagedThreadId:{Thread.CurrentThread.ManagedThreadId}"); - } - return ctx; -#else - return MainInstance; -#endif - } - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool IsJSVHandle(nint jsHandle) { @@ -203,8 +188,7 @@ public static PromiseHolder GetPromiseHolder(nint gcHandle) PromiseHolder holder; if (IsGCVHandle(gcHandle)) { - // this path should only happen when the Promise is passed as argument of JSExport - var ctx = AssertCapturedContext(); + var ctx = JSProxyContext.DefaultInstance; lock (ctx) { holder = new PromiseHolder(ctx, gcHandle); @@ -241,7 +225,7 @@ public static PromiseHolder GetPromiseHolder(nint gcHandle) } else { - JSProxyContext.CurrentInstance!.ThreadJsOwnedObjects.Remove(target); + ThreadJsOwnedObjects.Remove(target); } handle.Free(); } From 672781f92040d42cc921af8dcd8ba7c367513f58 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Wed, 13 Dec 2023 13:53:06 +0100 Subject: [PATCH 03/23] wip --- src/mono/wasm/runtime/marshal-to-js.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mono/wasm/runtime/marshal-to-js.ts b/src/mono/wasm/runtime/marshal-to-js.ts index 0053102be61e3..0aa489b3b83c3 100644 --- a/src/mono/wasm/runtime/marshal-to-js.ts +++ b/src/mono/wasm/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 From b34f6ad9659d9da6b943d80175efa10bbb56b7a6 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Wed, 13 Dec 2023 13:56:16 +0100 Subject: [PATCH 04/23] feedback --- .../Runtime/InteropServices/JavaScript/JSWebWorker.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) 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 7b2cf935bacb4..459ac6f4b2ea7 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 @@ -33,14 +33,20 @@ 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 (Thread.CurrentThread.ManagedThreadId == 1) + { + 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 (Thread.CurrentThread.ManagedThreadId == 1) + { + await JavaScriptImports.ThreadAvailable().ConfigureAwait(false); + } await RunAsyncImpl(body, cancellationToken).ConfigureAwait(false); } From 7529572792f5df90566b5df4ccfa3802c56c083d Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Wed, 13 Dec 2023 14:59:49 +0100 Subject: [PATCH 05/23] fix --- .../InteropServices/JavaScript/JSObject.References.cs | 6 ++++++ 1 file changed, 6 insertions(+) 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 77bb551076e37..19c06812a5fec 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 @@ -118,7 +118,9 @@ internal static void AssertThreadAffinity(object value) // when we know that JS side already freed the handle internal void DisposeLocal() { +#if FEATURE_WASM_THREADS lock (_lockObject) +#endif { ProxyContext.RemoveCSOwnedObject(JSHandle); _isDisposed = true; @@ -151,6 +153,10 @@ private void DisposeThis() var self = (JSObject)s!; lock (self._lockObject) { + if (self._isDisposed) + { + return; + } self.ProxyContext.ReleaseCSOwnedObject(self.JSHandle); self._isDisposed = true; self.JSHandle = IntPtr.Zero; From 39da1eb6f6e51511968951eab37b61e4f69905d5 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Wed, 13 Dec 2023 16:00:30 +0100 Subject: [PATCH 06/23] more --- .../JavaScript/Interop/LegacyExports.cs | 2 +- .../JavaScript/JSProxyContext.cs | 18 ++++++++---------- .../Marshaling/JSMarshalerArgument.Byte.cs | 2 +- .../Marshaling/JSMarshalerArgument.Double.cs | 2 +- .../JSMarshalerArgument.Exception.cs | 2 +- .../Marshaling/JSMarshalerArgument.Func.cs | 16 ++++++++-------- .../Marshaling/JSMarshalerArgument.Int32.cs | 2 +- .../Marshaling/JSMarshalerArgument.Object.cs | 2 +- .../Marshaling/JSMarshalerArgument.Task.cs | 4 ++-- 9 files changed, 24 insertions(+), 26 deletions(-) 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 883091383536d..b6315eaecb5d3 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 @@ -75,7 +75,7 @@ public static void GetJSOwnedObjectByGCHandleRef(int gcHandle, out object result public static IntPtr GetJSOwnedObjectGCHandleRef(in object obj) { - return JSProxyContext.GetJSOwnedObjectGCHandle(obj, GCHandleType.Normal); + return JSProxyContext.MainInstance.GetJSOwnedObjectGCHandle(obj, GCHandleType.Normal); } public static IntPtr CreateTaskSource() diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSProxyContext.cs b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSProxyContext.cs index 285d5aa3f5df4..b4c5c2e9b15f3 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSProxyContext.cs +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSProxyContext.cs @@ -161,38 +161,36 @@ public void FreeJSVHandle(nint jsvHandle) // 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) + public IntPtr GetJSOwnedObjectGCHandle(object obj, GCHandleType handleType = GCHandleType.Normal) { if (obj == null) { return IntPtr.Zero; } - var ctx = JSProxyContext.DefaultInstance; - lock (ctx) + lock (this) { - if (ctx.ThreadJsOwnedObjects.TryGetValue(obj, out IntPtr gcHandle)) + if (ThreadJsOwnedObjects.TryGetValue(obj, out IntPtr gcHandle)) { return gcHandle; } IntPtr result = (IntPtr)GCHandle.Alloc(obj, handleType); - ctx.ThreadJsOwnedObjects[obj] = result; + ThreadJsOwnedObjects[obj] = result; return result; } } // TODO unregister and collect pending PromiseHolder also when no C# is awaiting ? - public static PromiseHolder GetPromiseHolder(nint gcHandle) + public PromiseHolder GetPromiseHolder(nint gcHandle) { PromiseHolder holder; if (IsGCVHandle(gcHandle)) { - var ctx = JSProxyContext.DefaultInstance; - lock (ctx) + lock (this) { - holder = new PromiseHolder(ctx, gcHandle); - ctx.ThreadJsOwnedHolders.Add(gcHandle, holder); + holder = new PromiseHolder(this, gcHandle); + ThreadJsOwnedHolders.Add(gcHandle, holder); } } else 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 4962d5e88552a..13f07360c697b 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,7 @@ public unsafe void ToJS(ArraySegment value) return; } slot.Type = MarshalerType.ArraySegment; - slot.GCHandle = JSProxyContext.GetJSOwnedObjectGCHandle(value.Array, GCHandleType.Pinned); + slot.GCHandle = JSProxyContext.DefaultInstance.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 248721692d505..6f4ea67359da8 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,7 @@ public unsafe void ToJS(ArraySegment value) return; } slot.Type = MarshalerType.ArraySegment; - slot.GCHandle = JSProxyContext.GetJSOwnedObjectGCHandle(value.Array, GCHandleType.Pinned); + slot.GCHandle = JSProxyContext.DefaultInstance.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 8dd5ef813d23f..84b389875d48b 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 @@ -86,7 +86,7 @@ public unsafe void ToJS(Exception? value) { ToJS(cpy.Message); slot.Type = MarshalerType.Exception; - slot.GCHandle = JSProxyContext.GetJSOwnedObjectGCHandle(cpy); + slot.GCHandle = JSProxyContext.DefaultInstance.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 2cc0e15ad9f71..d921237daecac 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 @@ -463,7 +463,7 @@ public unsafe void ToJS(Action value) // eventual exception is handled by C# caller }; slot.Type = MarshalerType.Function; - slot.GCHandle = JSProxyContext.GetJSOwnedObjectGCHandle(cb); + slot.GCHandle = JSProxyContext.DefaultInstance.GetJSOwnedObjectGCHandle(cb); } /// @@ -484,7 +484,7 @@ public unsafe void ToJS(Action value, ArgumentToManagedCallback arg1Mar // eventual exception is handled by C# caller }; slot.Type = MarshalerType.Action; - slot.GCHandle = JSProxyContext.GetJSOwnedObjectGCHandle(cb); + slot.GCHandle = JSProxyContext.DefaultInstance.GetJSOwnedObjectGCHandle(cb); } /// @@ -509,7 +509,7 @@ public unsafe void ToJS(Action value, ArgumentToManagedCallback< // eventual exception is handled by C# caller }; slot.Type = MarshalerType.Action; - slot.GCHandle = JSProxyContext.GetJSOwnedObjectGCHandle(cb); + slot.GCHandle = JSProxyContext.DefaultInstance.GetJSOwnedObjectGCHandle(cb); } /// @@ -538,7 +538,7 @@ public unsafe void ToJS(Action value, ArgumentToManagedC // eventual exception is handled by C# caller }; slot.Type = MarshalerType.Action; - slot.GCHandle = JSProxyContext.GetJSOwnedObjectGCHandle(cb); + slot.GCHandle = JSProxyContext.DefaultInstance.GetJSOwnedObjectGCHandle(cb); } /// @@ -559,7 +559,7 @@ public unsafe void ToJS(Func value, ArgumentToJSCallback @@ -584,7 +584,7 @@ public unsafe void ToJS(Func value, ArgumentToManagedCal // eventual exception is handled by C# caller }; slot.Type = MarshalerType.Function; - slot.GCHandle = JSProxyContext.GetJSOwnedObjectGCHandle(cb); + slot.GCHandle = JSProxyContext.DefaultInstance.GetJSOwnedObjectGCHandle(cb); } /// @@ -613,7 +613,7 @@ public unsafe void ToJS(Func value, ArgumentTo // eventual exception is handled by C# caller }; slot.Type = MarshalerType.Function; - slot.GCHandle = JSProxyContext.GetJSOwnedObjectGCHandle(cb); + slot.GCHandle = JSProxyContext.DefaultInstance.GetJSOwnedObjectGCHandle(cb); } /// @@ -646,7 +646,7 @@ public unsafe void ToJS(Func value, Ar // eventual exception is handled by C# caller }; slot.Type = MarshalerType.Function; - slot.GCHandle = JSProxyContext.GetJSOwnedObjectGCHandle(cb); + slot.GCHandle = JSProxyContext.DefaultInstance.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 018d96abef94d..8fe19a83d8c09 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 @@ -133,7 +133,7 @@ public unsafe void ToJS(ArraySegment value) return; } slot.Type = MarshalerType.ArraySegment; - slot.GCHandle = JSProxyContext.GetJSOwnedObjectGCHandle(value.Array, GCHandleType.Pinned); + slot.GCHandle = JSProxyContext.DefaultInstance.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.Object.cs b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Marshaling/JSMarshalerArgument.Object.cs index 66026655d8e20..93624128ac31e 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,7 @@ public void ToJS(object? value) else { slot.Type = MarshalerType.Object; - slot.GCHandle = JSProxyContext.GetJSOwnedObjectGCHandle(value); + slot.GCHandle = JSProxyContext.DefaultInstance.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 2c04194b32b73..a0bbfa7ba23a5 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 @@ -45,7 +45,7 @@ public unsafe void ToManaged(out Task? value) value = null; return; } - PromiseHolder holder = JSProxyContext.GetPromiseHolder(slot.GCHandle); + PromiseHolder holder = JSProxyContext.DefaultInstance.GetPromiseHolder(slot.GCHandle); TaskCompletionSource tcs = new TaskCompletionSource(holder); ToManagedCallback callback = (JSMarshalerArgument* arguments_buffer) => { @@ -86,7 +86,7 @@ public unsafe void ToManaged(out Task? value, ArgumentToManagedCallback value = null; return; } - PromiseHolder holder = JSProxyContext.GetPromiseHolder(slot.GCHandle); + PromiseHolder holder = JSProxyContext.DefaultInstance.GetPromiseHolder(slot.GCHandle); TaskCompletionSource tcs = new TaskCompletionSource(holder); ToManagedCallback callback = (JSMarshalerArgument* arguments_buffer) => { From a5505d378268363daa28380b8c268e222920bbb1 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Wed, 13 Dec 2023 16:44:46 +0100 Subject: [PATCH 07/23] more --- .../JavaScript/JSProxyContext.cs | 68 +++++++++---------- src/mono/wasm/runtime/marshal-to-cs.ts | 10 +-- 2 files changed, 40 insertions(+), 38 deletions(-) diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSProxyContext.cs b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSProxyContext.cs index b4c5c2e9b15f3..94744a67da049 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSProxyContext.cs +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSProxyContext.cs @@ -200,6 +200,40 @@ public PromiseHolder GetPromiseHolder(nint gcHandle) return holder; } + public unsafe void ReleaseJSOwnedObjectByGCHandle(nint gcHandle) + { + PromiseHolder? holder = null; + lock (this) + { + if (IsGCVHandle(gcHandle)) + { + if (ThreadJsOwnedHolders.Remove(gcHandle, out holder)) + { + holder.GCHandle = IntPtr.Zero; + } + } + else + { + GCHandle handle = (GCHandle)gcHandle; + var target = handle.Target!; + if (target is PromiseHolder holder2) + { + holder = holder2; + holder.GCHandle = IntPtr.Zero; + } + else + { + ThreadJsOwnedObjects.Remove(target); + } + handle.Free(); + } + } + if (holder != null) + { + holder.Callback!(null); + } + } + public PromiseHolder? ReleasePromiseHolder(nint holderGCHandle) { PromiseHolder? holder = null; @@ -282,40 +316,6 @@ public void ReleaseCSOwnedObject(nint jsHandle) } } - public unsafe void ReleaseJSOwnedObjectByGCHandle(nint gcHandle) - { - PromiseHolder? holder = null; - lock (this) - { - if (IsGCVHandle(gcHandle)) - { - if (ThreadJsOwnedHolders.Remove(gcHandle, out holder)) - { - holder.GCHandle = IntPtr.Zero; - } - } - else - { - GCHandle handle = (GCHandle)gcHandle; - var target = handle.Target!; - if (target is PromiseHolder holder2) - { - holder = holder2; - holder.GCHandle = IntPtr.Zero; - } - else - { - ThreadJsOwnedObjects.Remove(target); - } - handle.Free(); - } - } - if (holder != null) - { - holder.Callback!(null); - } - } - #region Legacy // legacy diff --git a/src/mono/wasm/runtime/marshal-to-cs.ts b/src/mono/wasm/runtime/marshal-to-cs.ts index 6765b95b07e1d..fe50773d84310 100644 --- a/src/mono/wasm/runtime/marshal-to-cs.ts +++ b/src/mono/wasm/runtime/marshal-to-cs.ts @@ -337,10 +337,11 @@ 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(); - 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) + runtimeHelpers.javaScriptExports.complete_task(gc_handle, null, data, res_converter || _marshal_cs_object_to_cs); } catch (ex) { runtimeHelpers.abort(ex); @@ -352,10 +353,11 @@ 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(); - 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 + runtimeHelpers.javaScriptExports.complete_task(gc_handle, reason, null, undefined); } catch (ex) { runtimeHelpers.abort(ex); From 76c1cf24e70472c9579c1c4f6f2334e325940def Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Thu, 14 Dec 2023 13:43:01 +0100 Subject: [PATCH 08/23] - feedback about JSObject._lockObject - feedback about IsCurrentThread - feedback about JavaScript.sln - more comments - fix operation order for DisposeImpl(true)+ResolveOrRejectPromise --- ...tem.Runtime.InteropServices.JavaScript.sln | 13 +--- .../JavaScript/JSObject.References.cs | 60 ++++--------------- .../JavaScript/JSProxyContext.cs | 49 ++++++++------- .../JavaScript/JSSynchronizationContext.cs | 2 +- .../Marshaling/JSMarshalerArgument.Task.cs | 18 ++++-- src/mono/wasm/runtime/gc-handles.ts | 2 +- src/mono/wasm/runtime/marshal-to-cs.ts | 9 ++- 7 files changed, 56 insertions(+), 97 deletions(-) diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/System.Runtime.InteropServices.JavaScript.sln b/src/libraries/System.Runtime.InteropServices.JavaScript/System.Runtime.InteropServices.JavaScript.sln index b4cd9dd113c39..73c85be0abf1e 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/System.Runtime.InteropServices.JavaScript.sln +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/System.Runtime.InteropServices.JavaScript.sln @@ -1,8 +1,4 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.8.34004.107 -MinimumVisualStudioVersion = 10.0.40219.1 +Microsoft Visual Studio Solution File, Format Version 12.00 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestUtilities", "..\Common\tests\TestUtilities\TestUtilities.csproj", "{ED86AB26-1CFB-457D-BF87-B7A0D8FAF272}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Collections", "..\System.Collections\ref\System.Collections.csproj", "{8B1D80E9-AE0D-4E3C-9F91-E6862B49AEF3}" @@ -55,8 +51,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ref", "ref", "{32CDDDCD-531 EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tools", "tools", "{F92020A9-28BE-4398-86FF-5CFE44C94882}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Net.WebSockets.Client", "..\System.Net.WebSockets.Client\src\System.Net.WebSockets.Client.csproj", "{8B0D5A81-3E35-4391-8FB5-A44ABA356A07}" -EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -135,10 +129,6 @@ Global {008873D5-9028-4FF3-8354-71F713748625}.Debug|Any CPU.Build.0 = Debug|Any CPU {008873D5-9028-4FF3-8354-71F713748625}.Release|Any CPU.ActiveCfg = Release|Any CPU {008873D5-9028-4FF3-8354-71F713748625}.Release|Any CPU.Build.0 = Release|Any CPU - {8B0D5A81-3E35-4391-8FB5-A44ABA356A07}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {8B0D5A81-3E35-4391-8FB5-A44ABA356A07}.Debug|Any CPU.Build.0 = Debug|Any CPU - {8B0D5A81-3E35-4391-8FB5-A44ABA356A07}.Release|Any CPU.ActiveCfg = Release|Any CPU - {8B0D5A81-3E35-4391-8FB5-A44ABA356A07}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -165,7 +155,6 @@ Global {39B30F44-B141-44E9-B7A7-B1A9EDB1A61C} = {F92020A9-28BE-4398-86FF-5CFE44C94882} {D819DEB6-6F6B-484B-9F0F-BDBCEDC83A1A} = {F92020A9-28BE-4398-86FF-5CFE44C94882} {32CDDDCD-5319-494C-AB41-42B87C467F04} = {F92020A9-28BE-4398-86FF-5CFE44C94882} - {8B0D5A81-3E35-4391-8FB5-A44ABA356A07} = {67F3A00A-AE6C-434C-927D-E5D38DE2DA2C} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {3FE64246-4AFA-424A-AE5D-7007E20451B5} 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 19c06812a5fec..884482c360bd9 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 @@ -12,10 +12,6 @@ public partial class JSObject internal nint JSHandle; internal JSProxyContext ProxyContext; -#if FEATURE_WASM_THREADS - private readonly object _lockObject = new object(); -#endif - public SynchronizationContext SynchronizationContext { get @@ -32,7 +28,7 @@ public SynchronizationContext SynchronizationContext internal GCHandle? InFlight; internal int InFlightCounter; #endif - private bool _isDisposed; + internal bool _isDisposed; internal JSObject(IntPtr jsHandle, JSProxyContext ctx) { @@ -44,9 +40,7 @@ internal JSObject(IntPtr jsHandle, JSProxyContext ctx) internal void AddInFlight() { ObjectDisposedException.ThrowIf(IsDisposed, this); -#if FEATURE_WASM_THREADS - lock (_lockObject) -#endif + lock (ProxyContext) { InFlightCounter++; if (InFlightCounter == 1) @@ -62,9 +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() { -#if FEATURE_WASM_THREADS - lock (_lockObject) -#endif + lock (ProxyContext) { Debug.Assert(InFlightCounter != 0, "InFlightCounter != 0"); @@ -115,53 +107,22 @@ internal static void AssertThreadAffinity(object value) /// public override string ToString() => $"(js-obj js '{JSHandle}')"; - // when we know that JS side already freed the handle - internal void DisposeLocal() - { -#if FEATURE_WASM_THREADS - lock (_lockObject) -#endif - { - ProxyContext.RemoveCSOwnedObject(JSHandle); - _isDisposed = true; - JSHandle = IntPtr.Zero; - } - } - - private void DisposeThis() + internal void DisposeImpl(bool skipJS) { if (!_isDisposed) { #if FEATURE_WASM_THREADS if (ProxyContext == JSProxyContext.CurrentInstance) { - lock (_lockObject) - { - if (_isDisposed) - { - return; - } - ProxyContext.ReleaseCSOwnedObject(JSHandle); - _isDisposed = true; - JSHandle = IntPtr.Zero; - } //lock + JSProxyContext.ReleaseCSOwnedObject(this, skipJS); return; } ProxyContext.SynchronizationContext.Post(static (object? s) => { - var self = (JSObject)s!; - lock (self._lockObject) - { - if (self._isDisposed) - { - return; - } - self.ProxyContext.ReleaseCSOwnedObject(self.JSHandle); - self._isDisposed = true; - self.JSHandle = IntPtr.Zero; - } //lock - }, this); + var x = ((JSObject self, bool skipJS))s!; + JSProxyContext.ReleaseCSOwnedObject(x.self, x.skipJS); + }, (this, skipJS)); #else ProxyContext.ReleaseCSOwnedObject(JSHandle); _isDisposed = true; @@ -172,7 +133,7 @@ private void DisposeThis() ~JSObject() { - DisposeThis(); + DisposeImpl(false); } /// @@ -180,8 +141,7 @@ private void DisposeThis() /// public void Dispose() { - DisposeThis(); - GC.SuppressFinalize(this); + DisposeImpl(false); } } } diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSProxyContext.cs b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSProxyContext.cs index 94744a67da049..d12de5bf95729 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSProxyContext.cs +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSProxyContext.cs @@ -95,7 +95,7 @@ public JSProxyContext(bool isMainThread, JSSynchronizationContext synchronizatio } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool IsTargetThread() + public bool IsCurrentThread() { return ThreadId == Thread.CurrentThread.ManagedThreadId; } @@ -281,36 +281,35 @@ public JSObject CreateCSOwnedProxy(nint jsHandle) } } - public void RemoveCSOwnedObject(nint jsHandle) + public static void ReleaseCSOwnedObject(JSObject proxy, bool skipJS) { - lock (this) + if (!proxy.IsDisposed) { - ThreadCsOwnedObjects.Remove(jsHandle); - } - } - - public void ReleaseCSOwnedObject(nint jsHandle) - { - if (jsHandle != IntPtr.Zero) - { - // TODO could this be called from wrong thread ? - lock (this) + var ctx = proxy.ProxyContext; + if (!ctx.IsCurrentThread()) + { + Environment.FailFast("ReleaseCSOwnedObject has to run on the thread with same affinity as the proxy"); + } + lock (ctx) { -#if FEATURE_WASM_THREADS && DEBUG - if (ThreadCsOwnedObjects.Remove(jsHandle, out WeakReference? weak)) + if (proxy.IsDisposed) { - if (weak.TryGetTarget(out JSObject? obj) && obj.ProxyContext != this) - { - Environment.FailFast("ReleaseCSOwnedObject must be called on the thread that JSObject belongs into."); - } + return; + } + proxy._isDisposed = true; + GC.SuppressFinalize(proxy); + var jsHandle = proxy.JSHandle; + if (ctx.ThreadCsOwnedObjects.Remove(jsHandle)) + { + Environment.FailFast("ReleaseCSOwnedObject expected to find registration"); }; -#else - ThreadCsOwnedObjects.Remove(jsHandle); -#endif - Interop.Runtime.ReleaseCSOwnedObject(jsHandle); + if (!skipJS) + { + Interop.Runtime.ReleaseCSOwnedObject(jsHandle); + } if (IsJSVHandle(jsHandle)) { - FreeJSVHandle(jsHandle); + ctx.FreeJSVHandle(jsHandle); } } } @@ -388,7 +387,7 @@ private void Dispose(bool disposing) if (!_disposedValue) { #if FEATURE_WASM_THREADS - if (!IsTargetThread()) Environment.FailFast($"JSProxyContext must be disposed on the thread which owns it."); + if (!IsCurrentThread()) Environment.FailFast($"JSProxyContext must be disposed on the thread which owns it."); #endif // TODO: free unmanaged resources (unmanaged objects) and override finalizer 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 64560043db4be..8d97370184942 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 @@ -104,7 +104,7 @@ public override void Send(SendOrPostCallback d, object? state) { ObjectDisposedException.ThrowIf(_isDisposed, this); - if (ProxyContext.IsTargetThread()) + if (ProxyContext.IsCurrentThread()) { d(state); return; 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 a0bbfa7ba23a5..c8771c2f930eb 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 @@ -358,9 +358,11 @@ 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(true); - holder.DisposeLocal(); + // order of operations with DisposeImpl matters + JavaScriptImports.ResolveOrRejectPromise(args); } private static void ResolveVoidPromise(JSObject holder) @@ -382,9 +384,11 @@ 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(true); - holder.DisposeLocal(); + // order of operations with DisposeImpl matters + JavaScriptImports.ResolveOrRejectPromise(args); } private static void ResolvePromise(JSObject holder, T value, ArgumentToJSCallback marshaler) @@ -407,9 +411,11 @@ 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(true); - holder.DisposeLocal(); + // order of operations with DisposeImpl matters + JavaScriptImports.ResolveOrRejectPromise(args); } } } diff --git a/src/mono/wasm/runtime/gc-handles.ts b/src/mono/wasm/runtime/gc-handles.ts index 0a59dca3d1dfd..ff0727c4769c9 100644 --- a/src/mono/wasm/runtime/gc-handles.ts +++ b/src/mono/wasm/runtime/gc-handles.ts @@ -108,7 +108,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 JSHostImplementation.FreeJSVHandle->JSProxyContext + // 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/wasm/runtime/marshal-to-cs.ts b/src/mono/wasm/runtime/marshal-to-cs.ts index fe50773d84310..c0dc159eaa8fa 100644 --- a/src/mono/wasm/runtime/marshal-to-cs.ts +++ b/src/mono/wasm/runtime/marshal-to-cs.ts @@ -340,7 +340,10 @@ function _marshal_task_to_cs(arg: JSMarshalerArgument, value: Promise, _?: if (MonoWasmThreads) { settleUnsettledPromise(); } - teardown_managed_proxy(holder, gc_handle, true); // this holds holder alive for finalizer, until the promise is freed, (holding promise instead would not work) + // 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); } catch (ex) { @@ -356,7 +359,9 @@ function _marshal_task_to_cs(arg: JSMarshalerArgument, value: Promise, _?: if (MonoWasmThreads) { settleUnsettledPromise(); } - teardown_managed_proxy(holder, gc_handle, true); // this holds holder alive for finalizer, until the promise is freed + // 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); } catch (ex) { From c08c14313927f022b536fb4cef8bad7f9503b555 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Thu, 14 Dec 2023 22:21:11 +0100 Subject: [PATCH 09/23] - feedback about push/pop operation frame - feedback for JSProxyContext.CurrentThreadContext, MainThreadContext - feedback for JSProxyContext.CurrentOperationContext - feedback about lazy exception message --- .../CompatibilitySuppressions.WasmThreads.xml | 6 + .../JavaScript/CancelablePromise.cs | 4 +- .../JavaScript/Interop/JavaScriptExports.cs | 58 +++- .../JavaScript/Interop/LegacyExports.cs | 6 +- .../InteropServices/JavaScript/JSException.cs | 4 +- .../JavaScript/JSFunctionBinding.cs | 5 +- .../InteropServices/JavaScript/JSHost.cs | 9 +- .../JavaScript/JSHostImplementation.cs | 9 +- .../JavaScript/JSMarshalerArgument.cs | 4 +- .../JavaScript/JSObject.References.cs | 2 +- .../JavaScript/JSProxyContext.cs | 296 +++++++++++++----- .../JavaScript/JSSynchronizationContext.cs | 2 +- .../JavaScript/Legacy/Array.cs | 6 +- .../JavaScript/Legacy/ArrayBuffer.cs | 6 +- .../JavaScript/Legacy/DataView.cs | 14 +- .../JavaScript/Legacy/Function.cs | 6 +- .../JavaScript/Legacy/Uint8Array.cs | 10 +- .../Marshaling/JSMarshalerArgument.Byte.cs | 2 +- .../Marshaling/JSMarshalerArgument.Double.cs | 2 +- .../JSMarshalerArgument.Exception.cs | 14 +- .../Marshaling/JSMarshalerArgument.Func.cs | 68 +++- .../Marshaling/JSMarshalerArgument.Int32.cs | 2 +- .../JSMarshalerArgument.JSObject.cs | 10 +- .../Marshaling/JSMarshalerArgument.Object.cs | 2 +- .../Marshaling/JSMarshalerArgument.Task.cs | 53 +++- ...me.InteropServices.JavaScript.Tests.csproj | 1 + .../JavaScript/JSImportExportTest.cs | 19 +- .../JavaScript/JavaScriptTestHelper.cs | 13 +- src/mono/wasm/runtime/invoke-cs.ts | 14 +- src/mono/wasm/runtime/managed-exports.ts | 27 +- src/mono/wasm/runtime/types/internal.ts | 5 +- 31 files changed, 485 insertions(+), 194 deletions(-) diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/src/CompatibilitySuppressions.WasmThreads.xml b/src/libraries/System.Runtime.InteropServices.JavaScript/src/CompatibilitySuppressions.WasmThreads.xml index dfc469c8a165e..30d3e2e189d46 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/src/CompatibilitySuppressions.WasmThreads.xml +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/src/CompatibilitySuppressions.WasmThreads.xml @@ -6,4 +6,10 @@ ref/net9.0/System.Runtime.InteropServices.JavaScript.dll runtimes/browser/lib/net9.0/System.Runtime.InteropServices.JavaScript.dll + + CP0002 + M:System.Runtime.InteropServices.JavaScript.JSHost.AssertOperationStack(System.Int32) + ref/net9.0/System.Runtime.InteropServices.JavaScript.dll + runtimes/browser/lib/net9.0/System.Runtime.InteropServices.JavaScript.dll + 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 23be0db96bdc7..8fc2e7543b8bf 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 @@ -24,7 +24,7 @@ public static void CancelPromise(Task promise) #if FEATURE_WASM_THREADS - if (holder.ProxyContext == JSProxyContext.CurrentInstance) + if (holder.ProxyContext == JSProxyContext.CurrentThreadContext) { _CancelPromise(holder.GCHandle); return; @@ -52,7 +52,7 @@ public static void CancelPromise(Task promise, Action callback, T state) #if FEATURE_WASM_THREADS - if (holder.ProxyContext == JSProxyContext.CurrentInstance) + if (holder.ProxyContext == JSProxyContext.CurrentThreadContext) { _CancelPromise(holder.GCHandle); callback.Invoke(state); 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 9e93377d88925..1667841a7614a 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 @@ -28,7 +28,7 @@ public static void CallEntrypoint(JSMarshalerArgument* arguments_buffer) { #if FEATURE_WASM_THREADS // when we arrive here, we could assume that all proxies are owned by calling thread - JSProxyContext.CapturedInstance = JSProxyContext.AssertCurrentContext(); + JSProxyContext.PushOperationWithCurrentThreadContext(); #endif arg_1.ToManaged(out IntPtr entrypointPtr); @@ -102,7 +102,7 @@ public static void CallEntrypoint(JSMarshalerArgument* arguments_buffer) #if FEATURE_WASM_THREADS finally { - JSProxyContext.CapturedInstance = null; + JSProxyContext.PopOperation(); } #endif } @@ -114,6 +114,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 could assume that all proxies are owned by calling thread + JSProxyContext.PushOperationWithCurrentThreadContext(); +#endif arg_1.ToManaged(out byte[]? dllBytes); arg_2.ToManaged(out byte[]? pdbBytes); @@ -124,6 +128,12 @@ public static void LoadLazyAssembly(JSMarshalerArgument* arguments_buffer) { arg_exc.ToJS(ex); } +#if FEATURE_WASM_THREADS + finally + { + JSProxyContext.PopOperation(); + } +#endif } public static void LoadSatelliteAssembly(JSMarshalerArgument* arguments_buffer) @@ -132,6 +142,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 could assume that all proxies are owned by calling thread + JSProxyContext.PushOperationWithCurrentThreadContext(); +#endif arg_1.ToManaged(out byte[]? dllBytes); if (dllBytes != null) @@ -141,6 +155,12 @@ public static void LoadSatelliteAssembly(JSMarshalerArgument* arguments_buffer) { arg_exc.ToJS(ex); } +#if FEATURE_WASM_THREADS + finally + { + JSProxyContext.PopOperation(); + } +#endif } // The JS layer invokes this method when the JS wrapper for a JS owned object @@ -155,7 +175,7 @@ public static void ReleaseJSOwnedObjectByGCHandle(JSMarshalerArgument* arguments try { // when we arrive here, we assume that all proxies are owned by calling thread - var ctx = JSProxyContext.CapturedInstance = JSProxyContext.AssertCurrentContext(); + var ctx = JSProxyContext.PushOperationWithCurrentThreadContext(); ctx.ReleaseJSOwnedObjectByGCHandle(arg_1.slot.GCHandle); } catch (Exception ex) @@ -165,7 +185,7 @@ public static void ReleaseJSOwnedObjectByGCHandle(JSMarshalerArgument* arguments #if FEATURE_WASM_THREADS finally { - JSProxyContext.CapturedInstance = null; + JSProxyContext.PopOperation(); } #endif } @@ -184,7 +204,7 @@ public static void CallDelegate(JSMarshalerArgument* arguments_buffer) { #if FEATURE_WASM_THREADS // when we arrive here, we could assume that all proxies are owned by calling thread - JSProxyContext.CapturedInstance = JSProxyContext.AssertCurrentContext(); + JSProxyContext.PushOperationWithCurrentThreadContext(); #endif GCHandle callback_gc_handle = (GCHandle)arg_1.slot.GCHandle; @@ -205,7 +225,7 @@ public static void CallDelegate(JSMarshalerArgument* arguments_buffer) #if FEATURE_WASM_THREADS finally { - JSProxyContext.CapturedInstance = null; + JSProxyContext.PopOperation(); } #endif } @@ -222,7 +242,8 @@ public static void CompleteTask(JSMarshalerArgument* arguments_buffer) try { // when we arrive here, we could assume that all proxies are owned by calling thread - var ctx = JSProxyContext.CapturedInstance = JSProxyContext.AssertCurrentContext(); + var ctx = JSProxyContext.PushOperationWithCurrentThreadContext(); + var holder = ctx.ReleasePromiseHolder(arg_1.slot.GCHandle); if (holder != null) { @@ -232,14 +253,14 @@ public static void CompleteTask(JSMarshalerArgument* arguments_buffer) } catch (Exception ex) { +#if FEATURE_WASM_THREADS + var ctx = JSProxyContext.PushOperationWithCurrentThreadContext(); +#endif arg_exc.ToJS(ex); - } #if FEATURE_WASM_THREADS - finally - { - JSProxyContext.CapturedInstance = null; - } + JSProxyContext.PopOperation(); #endif + } } // the marshaled signature is: @@ -252,7 +273,7 @@ public static void GetManagedStackTrace(JSMarshalerArgument* arguments_buffer) try { // when we arrive here, we could assume that all proxies are owned by calling thread - JSProxyContext.CapturedInstance = JSProxyContext.AssertCurrentContext(); + JSProxyContext.PushOperationWithCurrentThreadContext(); GCHandle exception_gc_handle = (GCHandle)arg_1.slot.GCHandle; if (exception_gc_handle.Target is Exception exception) @@ -271,16 +292,21 @@ public static void GetManagedStackTrace(JSMarshalerArgument* arguments_buffer) #if FEATURE_WASM_THREADS finally { - JSProxyContext.CapturedInstance = null; + JSProxyContext.PopOperation(); } #endif } #if FEATURE_WASM_THREADS - public static void CaptureProxyContext() + public static void PushOperation() + { + JSProxyContext.PushOperationWithCurrentThreadContext(); + } + + public static void PopOperation() { - JSProxyContext.CapturedInstance = JSProxyContext.AssertCurrentContext(); + JSProxyContext.PopOperation(); } // this is here temporarily, until JSWebWorker becomes public API 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 b6315eaecb5d3..928d30f74f513 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 @@ -35,7 +35,7 @@ public static void GetCSOwnedObjectByJSHandleRef(nint jsHandle, int shouldAddInf #if FEATURE_WASM_THREADS LegacyHostImplementation.ThrowIfLegacyWorkerThread(); #endif - result = JSProxyContext.MainInstance.GetCSOwnedObjectByJSHandle(jsHandle, shouldAddInflight); + result = JSProxyContext.MainThreadContext.GetCSOwnedObjectByJSHandle(jsHandle, shouldAddInflight); } public static IntPtr GetCSOwnedObjectJSHandleRef(in JSObject jsObject, int shouldAddInflight) @@ -64,7 +64,7 @@ public static void CreateCSOwnedProxyRef(nint jsHandle, LegacyHostImplementation #if FEATURE_WASM_THREADS LegacyHostImplementation.ThrowIfLegacyWorkerThread(); #endif - jsObject = JSProxyContext.MainInstance.CreateCSOwnedProxy(jsHandle, mappedType, shouldAddInflight); + jsObject = JSProxyContext.MainThreadContext.CreateCSOwnedProxy(jsHandle, mappedType, shouldAddInflight); } public static void GetJSOwnedObjectByGCHandleRef(int gcHandle, out object result) @@ -75,7 +75,7 @@ public static void GetJSOwnedObjectByGCHandleRef(int gcHandle, out object result public static IntPtr GetJSOwnedObjectGCHandleRef(in object obj) { - return JSProxyContext.MainInstance.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 0e7e7e5f7c2c2..a7e90f8fba133 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,11 +47,11 @@ public override string? StackTrace } #if FEATURE_WASM_THREADS - var ctx = JSProxyContext.CurrentInstance; + var ctx = JSProxyContext.CurrentThreadContext; if (ctx == null || jsException.ProxyContext != ctx) { // if we are on another thread, it would be too expensive and risky to obtain lazy stack trace. - return bs; + 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 37f0c070629ae..61a91f6848e6e 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 @@ -224,7 +224,7 @@ internal static unsafe void InvokeJSImportImpl(JSFunctionBinding signature, Span if (signature.IsAsync) { // pre-allocate the result handle and Task - var holder = new JSHostImplementation.PromiseHolder(JSProxyContext.DefaultInstance); + var holder = new JSHostImplementation.PromiseHolder(JSProxyContext.CurrentOperationContext); arguments[1].slot.Type = MarshalerType.TaskPreCreated; arguments[1].slot.GCHandle = holder.GCHandle; } @@ -247,6 +247,9 @@ internal static unsafe void InvokeJSImportImpl(JSFunctionBinding signature, Span holderHandle.Free(); } } +#if FEATURE_WASM_THREADS + JSProxyContext.SchedulePopOperation(); +#endif } internal static unsafe JSFunctionBinding BindJSFunctionImpl(string functionName, string moduleName, ReadOnlySpan signatures) 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 dc165faf7c024..81afc1188ebb1 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 @@ -65,11 +65,18 @@ public static SynchronizationContext CurrentOrMainJSSynchronizationContext get { #if FEATURE_WASM_THREADS - return (JSProxyContext.CurrentInstance ?? JSProxyContext.MainInstance).SynchronizationContext; + return (JSProxyContext.CurrentThreadContext ?? JSProxyContext.MainThreadContext).SynchronizationContext; #else return null!; #endif } } + +#if FEATURE_WASM_THREADS + public static void AssertOperationStack(int expected) + { + JSProxyContext.AssertOperationStack(expected); + } +#endif } } 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 e5813998ec2a3..0b24e420fc720 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 @@ -77,6 +77,9 @@ public static void ThrowException(ref JSMarshalerArgument arg) { arg.ToManaged(out Exception? ex); +#if FEATURE_WASM_THREADS + JSProxyContext.PopOperation(); +#endif if (ex != null) { throw ex; @@ -208,17 +211,17 @@ public static void LoadSatelliteAssembly(byte[] dllBytes) public static void InstallWebWorkerInterop(bool isMainThread) { var ctx = new JSSynchronizationContext(isMainThread); - JSProxyContext.CurrentInstance = ctx.ProxyContext; + JSProxyContext.CurrentThreadContext = ctx.ProxyContext; if (isMainThread) { - JSProxyContext.MainInstance = ctx.ProxyContext; + JSProxyContext.MainThreadContext = ctx.ProxyContext; } ctx.AwaitNewData(); } public static void UninstallWebWorkerInterop() { - JSProxyContext.CurrentInstance?.Dispose(); + JSProxyContext.CurrentThreadContext?.Dispose(); } [UnsafeAccessor(UnsafeAccessorKind.Field, Name = "external_eventloop")] 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 3c09b0679b9c8..ec5bddffdf5de 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 @@ -65,7 +65,9 @@ public unsafe void Initialize() { slot.Type = MarshalerType.None; #if FEATURE_WASM_THREADS - JSProxyContext.CapturedInstance = null; + // 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.PushOperationUnknowContext(); #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 884482c360bd9..7d77688d96c90 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 @@ -112,7 +112,7 @@ internal void DisposeImpl(bool skipJS) if (!_isDisposed) { #if FEATURE_WASM_THREADS - if (ProxyContext == JSProxyContext.CurrentInstance) + if (ProxyContext == JSProxyContext.CurrentThreadContext) { JSProxyContext.ReleaseCSOwnedObject(this, skipJS); return; diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSProxyContext.cs b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSProxyContext.cs index d12de5bf95729..152758d56b4e7 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSProxyContext.cs +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSProxyContext.cs @@ -10,7 +10,7 @@ namespace System.Runtime.InteropServices.JavaScript { internal sealed class JSProxyContext : IDisposable { - private bool _disposedValue; + private bool _isDisposed; // we use this to maintain identity of JSHandle for a JSObject proxy private readonly Dictionary> ThreadCsOwnedObjects = new(); @@ -24,21 +24,18 @@ internal sealed class JSProxyContext : IDisposable private nint NextJSVHandle = -2; private readonly List JSVHandleFreeList = new(); -#if !FEATURE_WASM_THREADS - public static readonly JSProxyContext DefaultInstance = new(); - public static JSProxyContext CurrentInstance => DefaultInstance; - public static JSProxyContext CapturedInstance - { - get => MainInstance!; - set { } - } - public static JSProxyContext MainInstance => DefaultInstance; -#else - public nint TargetTID; - public int ThreadId; +#if FEATURE_WASM_THREADS + 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); @@ -47,41 +44,176 @@ public static IntPtr GetNativeThreadId() { return (int)GetThreadNativeThreadId(Thread.CurrentThread); } +#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 PushOperation() + { + // 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? _MainInstance; - public static JSProxyContext MainInstance + private static JSProxyContext? _MainThreadContext; + public static JSProxyContext MainThreadContext { [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => _MainInstance!; - set => _MainInstance = value; + get => _MainThreadContext!; + set => _MainThreadContext = value; } - // Context captured from parameters of the current operation. - // Most of the time, this matches the current thread. - // When we pass `JSObject` instance to `JSImport`, it could have thread affinity different than the current thread. - // We will use that parameter to capture the target thread to call into. - [ThreadStatic] - public static JSProxyContext? CapturedInstance; + private sealed class PendingOperation + { + public JSProxyContext? CapturedContext; + public bool Called; + public bool Multiple; + } - // Context of the current thread. Could be null on threads which don't have JS interop, like managed thread pool threads. [ThreadStatic] - public static JSProxyContext? CurrentInstance; + private static List? _OperationStack; + private static List OperationStack + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + return _OperationStack ??= new(); + } + } + + public static void PushOperationUnknowContext() + { + var stack = OperationStack; + // for SchedulePopOperation() + if (stack.Count > 0 && stack[stack.Count - 1].Called) + { + PopOperation(); + } + // because this is called multiple times for each JSImport + if (stack.Count > 0 && stack[stack.Count - 1].Multiple) + { + return; + } + + stack.Add(new PendingOperation() { Multiple = true }); + } + + public static void PushOperationWithContext(JSProxyContext knownContext) + { + var stack = OperationStack; + // for SchedulePopOperation() + if (stack.Count > 0 && stack[stack.Count - 1].Called) + { + PopOperation(); + } + // because this is called multiple times for each JSImport + if (stack.Count > 0 && stack[stack.Count - 1].Multiple) + { + return; + } + + stack.Add(new PendingOperation() { CapturedContext = knownContext, Multiple = true }); + } + + public static JSProxyContext PushOperationWithCurrentThreadContext() + { + var stack = OperationStack; + // for SchedulePopOperation() + if (stack.Count > 0 && stack[stack.Count - 1].Called) + { + PopOperation(); + } + + var current = AssertCurrentContext(); + stack.Add(new PendingOperation { CapturedContext = current }); + return current; + } + + public static void PopOperation() + { + var stack = OperationStack; + stack.RemoveAt(stack.Count - 1); + } + + // this is here until we change the code generator and the API to Push/Pop the context + // we have no way how to Pop from the after marshaling the result + public static void SchedulePopOperation() + { + var stack = OperationStack; + stack[stack.Count - 1].Called = true; + } + + public static void AssertOperationStack(int expected) + { + var stack = OperationStack; + if (stack.Count > 0 && stack[stack.Count - 1].Called) + { + PopOperation(); + } + var actual = stack.Count; + var multiple = stack.Count > 0 ? stack[stack.Count - 1].Multiple : false; + var called = stack.Count > 0 ? stack[stack.Count - 1].Called : false; + if (expected == 0) + { + stack.Clear(); + } + // TODO Environment.FailFast + if (actual != expected) throw new InvalidOperationException($"Unexpected OperationStack size expected: {expected} actual: {actual} called:{called} multiple:{multiple}"); + } + + // TODO: sort generated ToJS() calls to make the capture context before we need to use it + public static void CaptureContextFromParameter(JSProxyContext parameterContext) + { + var stack = OperationStack; + var pendingOperation = stack[stack.Count - 1]; + var capturedContext = pendingOperation.CapturedContext; + if (capturedContext != null && parameterContext != capturedContext) + { + throw new InvalidOperationException("All JSObject proxies need to have same thread affinity"); + } + pendingOperation.CapturedContext = capturedContext; + } + + // Context of the current thread or async task. Could be null on threads which don't have JS interop, like managed thread pool threads. + // TODO flow it also with ExecutionContext to child threads ? + private static readonly AsyncLocal _currentThreadContext = new AsyncLocal(); + public static JSProxyContext? CurrentThreadContext + { + get => _currentThreadContext.Value; + set => _currentThreadContext.Value = value; + } // 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 DefaultInstance + public static JSProxyContext CurrentOperationContext { [MethodImpl(MethodImplOptions.AggressiveInlining)] get { - // until we set CapturedInstance in the generated code of JSImport/JSExport, we need to fallback to CurrentInstance to find target thread - // because maybe it was not captured by the other parameters yet - // after capture is solid, we could drop DefaultInstance and use CapturedInstance instead - // TODO: sort generated ToJS() calls to make the capture first - return CapturedInstance ?? CurrentInstance ?? MainInstance; + var stack = OperationStack; + if (stack.Count < 1) throw new Exception("CurrentOperationContext could be only used during pending operation."); + var pendingOperation = stack[stack.Count - 1]; + if (pendingOperation.CapturedContext != null) + { + return pendingOperation.CapturedContext; + } + // it could happen that we are in operation, in which we didn't capture target thread/context + var currentThreadContext = CurrentThreadContext; + if (currentThreadContext != null) + { + // we could will call JS on the current thread, if it has the JS interop installed + return currentThreadContext; + } + // otherwise we will call JS on the main thread, which always has JS interop + return MainThreadContext; } } @@ -89,33 +221,36 @@ public JSProxyContext(bool isMainThread, JSSynchronizationContext synchronizatio { SynchronizationContext = synchronizationContext; Interop.Runtime.InstallWebWorkerInterop(); - TargetTID = GetNativeThreadId(); - ThreadId = Thread.CurrentThread.ManagedThreadId; + NativeTID = GetNativeThreadId(); + ManagedTID = Thread.CurrentThread.ManagedThreadId; IsMainThread = isMainThread; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool IsCurrentThread() - { - return ThreadId == Thread.CurrentThread.ManagedThreadId; - } #endif [MethodImpl(MethodImplOptions.AggressiveInlining)] public static JSProxyContext AssertCurrentContext() { #if FEATURE_WASM_THREADS - var ctx = CurrentInstance; - if (ctx == null || ctx._disposedValue) + 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 MainInstance; #endif } + #endregion + + #region Handles + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool IsJSVHandle(nint jsHandle) { @@ -134,7 +269,7 @@ public nint AllocJSVHandle() { if (JSVHandleFreeList.Count > 0) { - var jsvHandle = JSVHandleFreeList[JSVHandleFreeList.Count]; + var jsvHandle = JSVHandleFreeList[JSVHandleFreeList.Count - 1]; JSVHandleFreeList.RemoveAt(JSVHandleFreeList.Count - 1); return jsvHandle; } @@ -211,6 +346,10 @@ public unsafe void ReleaseJSOwnedObjectByGCHandle(nint gcHandle) { holder.GCHandle = IntPtr.Zero; } + else + { + throw new InvalidOperationException("ReleaseJSOwnedObjectByGCHandle expected in ThreadJsOwnedHolders"); + } } else { @@ -223,7 +362,10 @@ public unsafe void ReleaseJSOwnedObjectByGCHandle(nint gcHandle) } else { - ThreadJsOwnedObjects.Remove(target); + if (!ThreadJsOwnedObjects.Remove(target)) + { + throw new InvalidOperationException("ReleaseJSOwnedObjectByGCHandle expected in ThreadJsOwnedObjects"); + } } handle.Free(); } @@ -245,6 +387,10 @@ public unsafe void ReleaseJSOwnedObjectByGCHandle(nint gcHandle) { holder.GCHandle = IntPtr.Zero; } + else + { + throw new InvalidOperationException("ReleasePromiseHolder expected to find handle in ThreadJsOwnedHolders"); + } } else { @@ -257,7 +403,10 @@ public unsafe void ReleaseJSOwnedObjectByGCHandle(nint gcHandle) } else { - ThreadJsOwnedObjects.Remove(target); + if (!ThreadJsOwnedObjects.Remove(target)) + { + throw new InvalidOperationException("ReleasePromiseHolder expected to find handle in ThreadJsOwnedObjects"); + } } handle.Free(); } @@ -283,38 +432,41 @@ public JSObject CreateCSOwnedProxy(nint jsHandle) public static void ReleaseCSOwnedObject(JSObject proxy, bool skipJS) { - if (!proxy.IsDisposed) + if (proxy.IsDisposed) + { + return; + } + var ctx = proxy.ProxyContext; + if (!ctx.IsCurrentThread()) + { + throw new InvalidOperationException("ReleaseCSOwnedObject has to run on the thread with same affinity as the proxy"); + } + lock (ctx) { - var ctx = proxy.ProxyContext; - if (!ctx.IsCurrentThread()) + if (proxy.IsDisposed) { - Environment.FailFast("ReleaseCSOwnedObject has to run on the thread with same affinity as the proxy"); + return; } - lock (ctx) + proxy._isDisposed = true; + GC.SuppressFinalize(proxy); + var jsHandle = proxy.JSHandle; + if (!ctx.ThreadCsOwnedObjects.Remove(jsHandle)) { - 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"); - }; - if (!skipJS) - { - Interop.Runtime.ReleaseCSOwnedObject(jsHandle); - } - if (IsJSVHandle(jsHandle)) - { - ctx.FreeJSVHandle(jsHandle); - } + throw new InvalidOperationException("ReleaseCSOwnedObject expected to find registration for" + jsHandle); + }; + if (!skipJS) + { + Interop.Runtime.ReleaseCSOwnedObject(jsHandle); + } + if (IsJSVHandle(jsHandle)) + { + ctx.FreeJSVHandle(jsHandle); } } } + #endregion + #region Legacy // legacy @@ -357,7 +509,7 @@ public JSObject CreateCSOwnedProxy(nint jsHandle, LegacyHostImplementation.Mappe #pragma warning disable CS0612 // Type or member is obsolete res = mappedType switch { - LegacyHostImplementation.MappedType.JSObject => new JSObject(jsHandle, JSProxyContext.MainInstance), + 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), @@ -384,10 +536,12 @@ private void Dispose(bool disposing) { lock (this) { - if (!_disposedValue) + if (!_isDisposed) { #if FEATURE_WASM_THREADS - if (!IsCurrentThread()) Environment.FailFast($"JSProxyContext must be disposed on the thread which owns it."); + if (!IsCurrentThread()) throw new InvalidOperationException("JSProxyContext must be disposed on the thread which owns it."); + AssertOperationStack(0); + _OperationStack = null; #endif // TODO: free unmanaged resources (unmanaged objects) and override finalizer @@ -426,7 +580,7 @@ private void Dispose(bool disposing) SynchronizationContext.Dispose(); #endif } - _disposedValue = true; + _isDisposed = true; } } } 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 8d97370184942..92d4e39ede7c3 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 @@ -85,7 +85,7 @@ 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(ProxyContext.TargetTID, (void*)(delegate* unmanaged[Cdecl])&BackgroundJobHandler); + TargetThreadScheduleBackgroundJob(ProxyContext.NativeTID, (void*)(delegate* unmanaged[Cdecl])&BackgroundJobHandler); } public override void Post(SendOrPostCallback d, object? state) 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 771d3f6ff3787..7ce50a8e741f2 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), JSProxyContext.MainInstance) + : base(JavaScriptImports.CreateCSOwnedObject(nameof(Array), _params), JSProxyContext.MainThreadContext) { #if FEATURE_WASM_THREADS LegacyHostImplementation.ThrowIfLegacyWorkerThread(); #endif - JSProxyContext.MainInstance.RegisterCSOwnedObject(this); + JSProxyContext.MainThreadContext.RegisterCSOwnedObject(this); } /// /// Initializes a new instance of the Array/> class. /// /// Js handle. - internal Array(IntPtr jsHandle) : base(jsHandle, JSProxyContext.MainInstance) + 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 a5c7b78fe1c9b..92e78a14fe1b2 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 }), JSProxyContext.MainInstance) + : base(JavaScriptImports.CreateCSOwnedObject(nameof(ArrayBuffer), new object[] { length }), JSProxyContext.MainThreadContext) { #if FEATURE_WASM_THREADS LegacyHostImplementation.ThrowIfLegacyWorkerThread(); #endif - JSProxyContext.MainInstance.RegisterCSOwnedObject(this); + JSProxyContext.MainThreadContext.RegisterCSOwnedObject(this); } /// /// Initializes a new instance of the JavaScript Core ArrayBuffer class. /// /// Js handle. - internal ArrayBuffer(IntPtr jsHandle) : base(jsHandle, JSProxyContext.MainInstance) + 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 94c836323692d..1046fa8f99fa4 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 }), JSProxyContext.MainInstance) + : base(JavaScriptImports.CreateCSOwnedObject(nameof(DataView), new object[] { buffer }), JSProxyContext.MainThreadContext) { #if FEATURE_WASM_THREADS LegacyHostImplementation.ThrowIfLegacyWorkerThread(); #endif - JSProxyContext.MainInstance.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 }), JSProxyContext.MainInstance) + : base(JavaScriptImports.CreateCSOwnedObject(nameof(DataView), new object[] { buffer, byteOffset }), JSProxyContext.MainThreadContext) { #if FEATURE_WASM_THREADS LegacyHostImplementation.ThrowIfLegacyWorkerThread(); #endif - JSProxyContext.MainInstance.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 }), JSProxyContext.MainInstance) + : base(JavaScriptImports.CreateCSOwnedObject(nameof(DataView), new object[] { buffer, byteOffset, byteLength }), JSProxyContext.MainThreadContext) { #if FEATURE_WASM_THREADS LegacyHostImplementation.ThrowIfLegacyWorkerThread(); #endif - JSProxyContext.MainInstance.RegisterCSOwnedObject(this); + JSProxyContext.MainThreadContext.RegisterCSOwnedObject(this); } /// /// Initializes a new instance of the DataView class. /// /// Js handle. - internal DataView(IntPtr jsHandle) : base(jsHandle, JSProxyContext.MainInstance) + 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 2e336594264e7..ebce83d01417a 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), JSProxyContext.MainInstance) + : base(JavaScriptImports.CreateCSOwnedObject(nameof(Function), args), JSProxyContext.MainThreadContext) { #if FEATURE_WASM_THREADS LegacyHostImplementation.ThrowIfLegacyWorkerThread(); #endif - JSProxyContext.MainInstance.RegisterCSOwnedObject(this); + JSProxyContext.MainThreadContext.RegisterCSOwnedObject(this); } - internal Function(IntPtr jsHandle) : base(jsHandle, JSProxyContext.MainInstance) + internal Function(IntPtr jsHandle) : base(jsHandle, JSProxyContext.MainThreadContext) { } /// 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 af87e0a25834d..1cea722c1cc62 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 }), JSProxyContext.MainInstance) + : base(JavaScriptImports.CreateCSOwnedObject(nameof(Uint8Array), new object[] { length }), JSProxyContext.MainThreadContext) { #if FEATURE_WASM_THREADS LegacyHostImplementation.ThrowIfLegacyWorkerThread(); #endif - JSProxyContext.MainInstance.RegisterCSOwnedObject(this); + JSProxyContext.MainThreadContext.RegisterCSOwnedObject(this); } public Uint8Array(ArrayBuffer buffer) - : base(JavaScriptImports.CreateCSOwnedObject(nameof(Uint8Array), new object[] { buffer }), JSProxyContext.MainInstance) + : base(JavaScriptImports.CreateCSOwnedObject(nameof(Uint8Array), new object[] { buffer }), JSProxyContext.MainThreadContext) { #if FEATURE_WASM_THREADS LegacyHostImplementation.ThrowIfLegacyWorkerThread(); #endif - JSProxyContext.MainInstance.RegisterCSOwnedObject(this); + JSProxyContext.MainThreadContext.RegisterCSOwnedObject(this); } - internal Uint8Array(IntPtr jsHandle) : base(jsHandle, JSProxyContext.MainInstance) + 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 13f07360c697b..50cc257e59b8a 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,7 @@ public unsafe void ToJS(ArraySegment value) return; } slot.Type = MarshalerType.ArraySegment; - slot.GCHandle = JSProxyContext.DefaultInstance.GetJSOwnedObjectGCHandle(value.Array, GCHandleType.Pinned); + slot.GCHandle = JSProxyContext.CurrentOperationContext.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 6f4ea67359da8..cb88fde7ad715 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,7 @@ public unsafe void ToJS(ArraySegment value) return; } slot.Type = MarshalerType.ArraySegment; - slot.GCHandle = JSProxyContext.DefaultInstance.GetJSOwnedObjectGCHandle(value.Array, GCHandleType.Pinned); + slot.GCHandle = JSProxyContext.CurrentOperationContext.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 84b389875d48b..d1318d27e419c 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,7 @@ public unsafe void ToManaged(out Exception? value) if (slot.JSHandle != IntPtr.Zero) { // this is JSException round-trip - jsException = JSProxyContext.DefaultInstance.CreateCSOwnedProxy(slot.JSHandle); + jsException = JSProxyContext.CurrentOperationContext.CreateCSOwnedProxy(slot.JSHandle); } string? message; @@ -67,26 +67,18 @@ public unsafe void ToJS(Exception? value) { #if FEATURE_WASM_THREADS JSObject.AssertThreadAffinity(value); + JSProxyContext.CaptureContextFromParameter(jse.jsException.ProxyContext); #endif // this is JSException roundtrip ObjectDisposedException.ThrowIf(jse.jsException.IsDisposed, value); slot.Type = MarshalerType.JSException; slot.JSHandle = jse.jsException.JSHandle; -#if FEATURE_WASM_THREADS - var parameterContext = jse.jsException.ProxyContext; - var capturedContext = JSProxyContext.CapturedInstance; - if (capturedContext != null && parameterContext != capturedContext) - { - throw new InvalidOperationException("All JSObject proxies need to have same thread affinity"); - } - JSProxyContext.CapturedInstance = parameterContext; -#endif } else { ToJS(cpy.Message); slot.Type = MarshalerType.Exception; - slot.GCHandle = JSProxyContext.DefaultInstance.GetJSOwnedObjectGCHandle(cpy); + slot.GCHandle = JSProxyContext.CurrentOperationContext.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 d921237daecac..816177417202c 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 @@ -11,7 +11,7 @@ private sealed class ActionJS public ActionJS(IntPtr jsHandle) { - JSObject = JSProxyContext.DefaultInstance.CreateCSOwnedProxy(jsHandle); + JSObject = JSProxyContext.CurrentOperationContext.CreateCSOwnedProxy(jsHandle); } public void InvokeJS() @@ -21,6 +21,7 @@ public void InvokeJS() #if FEATURE_WASM_THREADS JSObject.AssertThreadAffinity(JSObject); + JSProxyContext.PushOperationWithContext(JSObject.ProxyContext); #endif Span arguments = stackalloc JSMarshalerArgument[4]; @@ -30,6 +31,10 @@ public void InvokeJS() args_return.Initialize(); JSFunctionBinding.InvokeJSFunction(JSObject, arguments); + +#if FEATURE_WASM_THREADS + JSProxyContext.PopOperation(); +#endif } } @@ -41,7 +46,7 @@ private sealed class ActionJS public ActionJS(IntPtr jsHandle, ArgumentToJSCallback arg1Marshaler) { - JSObject = JSProxyContext.DefaultInstance.CreateCSOwnedProxy(jsHandle); + JSObject = JSProxyContext.CurrentOperationContext.CreateCSOwnedProxy(jsHandle); Arg1Marshaler = arg1Marshaler; } @@ -49,6 +54,7 @@ public void InvokeJS(T arg1) { #if FEATURE_WASM_THREADS JSObject.AssertThreadAffinity(JSObject); + JSProxyContext.PushOperationWithContext(JSObject.ProxyContext); #endif Span arguments = stackalloc JSMarshalerArgument[4]; @@ -61,6 +67,9 @@ public void InvokeJS(T arg1) Arg1Marshaler(ref args_arg1, arg1); JSFunctionBinding.InvokeJSFunction(JSObject, arguments); +#if FEATURE_WASM_THREADS + JSProxyContext.PopOperation(); +#endif } } @@ -72,7 +81,7 @@ private sealed class ActionJS public ActionJS(IntPtr jsHandle, ArgumentToJSCallback arg1Marshaler, ArgumentToJSCallback arg2Marshaler) { - JSObject = JSProxyContext.DefaultInstance.CreateCSOwnedProxy(jsHandle); + JSObject = JSProxyContext.CurrentOperationContext.CreateCSOwnedProxy(jsHandle); Arg1Marshaler = arg1Marshaler; Arg2Marshaler = arg2Marshaler; } @@ -81,6 +90,7 @@ public void InvokeJS(T1 arg1, T2 arg2) { #if FEATURE_WASM_THREADS JSObject.AssertThreadAffinity(JSObject); + JSProxyContext.PushOperationWithContext(JSObject.ProxyContext); #endif Span arguments = stackalloc JSMarshalerArgument[4]; @@ -95,6 +105,9 @@ public void InvokeJS(T1 arg1, T2 arg2) Arg2Marshaler(ref args_arg2, arg2); JSFunctionBinding.InvokeJSFunction(JSObject, arguments); +#if FEATURE_WASM_THREADS + JSProxyContext.PopOperation(); +#endif } } @@ -107,7 +120,7 @@ private sealed class ActionJS public ActionJS(IntPtr jsHandle, ArgumentToJSCallback arg1Marshaler, ArgumentToJSCallback arg2Marshaler, ArgumentToJSCallback arg3Marshaler) { - JSObject = JSProxyContext.DefaultInstance.CreateCSOwnedProxy(jsHandle); + JSObject = JSProxyContext.CurrentOperationContext.CreateCSOwnedProxy(jsHandle); Arg1Marshaler = arg1Marshaler; Arg2Marshaler = arg2Marshaler; Arg3Marshaler = arg3Marshaler; @@ -117,6 +130,7 @@ public void InvokeJS(T1 arg1, T2 arg2, T3 arg3) { #if FEATURE_WASM_THREADS JSObject.AssertThreadAffinity(JSObject); + JSProxyContext.PushOperationWithContext(JSObject.ProxyContext); #endif Span arguments = stackalloc JSMarshalerArgument[5]; @@ -133,6 +147,9 @@ public void InvokeJS(T1 arg1, T2 arg2, T3 arg3) Arg3Marshaler(ref args_arg3, arg3); JSFunctionBinding.InvokeJSFunction(JSObject, arguments); +#if FEATURE_WASM_THREADS + JSProxyContext.PopOperation(); +#endif } } @@ -219,7 +236,7 @@ private sealed class FuncJS public FuncJS(IntPtr jsHandle, ArgumentToManagedCallback resMarshaler) { - JSObject = JSProxyContext.DefaultInstance.CreateCSOwnedProxy(jsHandle); + JSObject = JSProxyContext.CurrentOperationContext.CreateCSOwnedProxy(jsHandle); ResMarshaler = resMarshaler; } @@ -227,6 +244,7 @@ public TResult InvokeJS() { #if FEATURE_WASM_THREADS JSObject.AssertThreadAffinity(JSObject); + JSProxyContext.PushOperationWithContext(JSObject.ProxyContext); #endif // JSObject (held by this lambda) would be collected by GC after the lambda is collected @@ -241,6 +259,9 @@ public TResult InvokeJS() JSFunctionBinding.InvokeJSFunction(JSObject, arguments); ResMarshaler(ref args_return, out TResult res); +#if FEATURE_WASM_THREADS + JSProxyContext.PopOperation(); +#endif return res; } @@ -254,7 +275,7 @@ private sealed class FuncJS public FuncJS(IntPtr jsHandle, ArgumentToJSCallback arg1Marshaler, ArgumentToManagedCallback resMarshaler) { - JSObject = JSProxyContext.DefaultInstance.CreateCSOwnedProxy(jsHandle); + JSObject = JSProxyContext.CurrentOperationContext.CreateCSOwnedProxy(jsHandle); Arg1Marshaler = arg1Marshaler; ResMarshaler = resMarshaler; } @@ -263,6 +284,7 @@ public TResult InvokeJS(T arg1) { #if FEATURE_WASM_THREADS JSObject.AssertThreadAffinity(JSObject); + JSProxyContext.PushOperationWithContext(JSObject.ProxyContext); #endif Span arguments = stackalloc JSMarshalerArgument[4]; @@ -277,6 +299,9 @@ public TResult InvokeJS(T arg1) JSFunctionBinding.InvokeJSFunction(JSObject, arguments); ResMarshaler(ref args_return, out TResult res); +#if FEATURE_WASM_THREADS + JSProxyContext.PopOperation(); +#endif return res; } } @@ -290,7 +315,7 @@ private sealed class FuncJS public FuncJS(IntPtr jsHandle, ArgumentToJSCallback arg1Marshaler, ArgumentToJSCallback arg2Marshaler, ArgumentToManagedCallback resMarshaler) { - JSObject = JSProxyContext.DefaultInstance.CreateCSOwnedProxy(jsHandle); + JSObject = JSProxyContext.CurrentOperationContext.CreateCSOwnedProxy(jsHandle); Arg1Marshaler = arg1Marshaler; Arg2Marshaler = arg2Marshaler; ResMarshaler = resMarshaler; @@ -300,6 +325,7 @@ public TResult InvokeJS(T1 arg1, T2 arg2) { #if FEATURE_WASM_THREADS JSObject.AssertThreadAffinity(JSObject); + JSProxyContext.PushOperationWithContext(JSObject.ProxyContext); #endif Span arguments = stackalloc JSMarshalerArgument[4]; @@ -316,6 +342,9 @@ public TResult InvokeJS(T1 arg1, T2 arg2) JSFunctionBinding.InvokeJSFunction(JSObject, arguments); ResMarshaler(ref args_return, out TResult res); +#if FEATURE_WASM_THREADS + JSProxyContext.PopOperation(); +#endif return res; } } @@ -330,7 +359,7 @@ private sealed class FuncJS public FuncJS(IntPtr jsHandle, ArgumentToJSCallback arg1Marshaler, ArgumentToJSCallback arg2Marshaler, ArgumentToJSCallback arg3Marshaler, ArgumentToManagedCallback resMarshaler) { - JSObject = JSProxyContext.DefaultInstance.CreateCSOwnedProxy(jsHandle); + JSObject = JSProxyContext.CurrentOperationContext.CreateCSOwnedProxy(jsHandle); Arg1Marshaler = arg1Marshaler; Arg2Marshaler = arg2Marshaler; Arg3Marshaler = arg3Marshaler; @@ -341,6 +370,7 @@ public TResult InvokeJS(T1 arg1, T2 arg2, T3 arg3) { #if FEATURE_WASM_THREADS JSObject.AssertThreadAffinity(JSObject); + JSProxyContext.PushOperationWithContext(JSObject.ProxyContext); #endif Span arguments = stackalloc JSMarshalerArgument[5]; @@ -357,8 +387,12 @@ public TResult InvokeJS(T1 arg1, T2 arg2, T3 arg3) Arg3Marshaler(ref args_arg3, arg3); JSFunctionBinding.InvokeJSFunction(JSObject, arguments); - ResMarshaler(ref args_return, out TResult res); + +#if FEATURE_WASM_THREADS + JSProxyContext.PopOperation(); +#endif + return res; } } @@ -463,7 +497,7 @@ public unsafe void ToJS(Action value) // eventual exception is handled by C# caller }; slot.Type = MarshalerType.Function; - slot.GCHandle = JSProxyContext.DefaultInstance.GetJSOwnedObjectGCHandle(cb); + slot.GCHandle = JSProxyContext.CurrentOperationContext.GetJSOwnedObjectGCHandle(cb); } /// @@ -484,7 +518,7 @@ public unsafe void ToJS(Action value, ArgumentToManagedCallback arg1Mar // eventual exception is handled by C# caller }; slot.Type = MarshalerType.Action; - slot.GCHandle = JSProxyContext.DefaultInstance.GetJSOwnedObjectGCHandle(cb); + slot.GCHandle = JSProxyContext.CurrentOperationContext.GetJSOwnedObjectGCHandle(cb); } /// @@ -509,7 +543,7 @@ public unsafe void ToJS(Action value, ArgumentToManagedCallback< // eventual exception is handled by C# caller }; slot.Type = MarshalerType.Action; - slot.GCHandle = JSProxyContext.DefaultInstance.GetJSOwnedObjectGCHandle(cb); + slot.GCHandle = JSProxyContext.CurrentOperationContext.GetJSOwnedObjectGCHandle(cb); } /// @@ -538,7 +572,7 @@ public unsafe void ToJS(Action value, ArgumentToManagedC // eventual exception is handled by C# caller }; slot.Type = MarshalerType.Action; - slot.GCHandle = JSProxyContext.DefaultInstance.GetJSOwnedObjectGCHandle(cb); + slot.GCHandle = JSProxyContext.CurrentOperationContext.GetJSOwnedObjectGCHandle(cb); } /// @@ -559,7 +593,7 @@ public unsafe void ToJS(Func value, ArgumentToJSCallback @@ -584,7 +618,7 @@ public unsafe void ToJS(Func value, ArgumentToManagedCal // eventual exception is handled by C# caller }; slot.Type = MarshalerType.Function; - slot.GCHandle = JSProxyContext.DefaultInstance.GetJSOwnedObjectGCHandle(cb); + slot.GCHandle = JSProxyContext.CurrentOperationContext.GetJSOwnedObjectGCHandle(cb); } /// @@ -613,7 +647,7 @@ public unsafe void ToJS(Func value, ArgumentTo // eventual exception is handled by C# caller }; slot.Type = MarshalerType.Function; - slot.GCHandle = JSProxyContext.DefaultInstance.GetJSOwnedObjectGCHandle(cb); + slot.GCHandle = JSProxyContext.CurrentOperationContext.GetJSOwnedObjectGCHandle(cb); } /// @@ -646,7 +680,7 @@ public unsafe void ToJS(Func value, Ar // eventual exception is handled by C# caller }; slot.Type = MarshalerType.Function; - slot.GCHandle = JSProxyContext.DefaultInstance.GetJSOwnedObjectGCHandle(cb); + slot.GCHandle = JSProxyContext.CurrentOperationContext.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 8fe19a83d8c09..70878f4af122c 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 @@ -133,7 +133,7 @@ public unsafe void ToJS(ArraySegment value) return; } slot.Type = MarshalerType.ArraySegment; - slot.GCHandle = JSProxyContext.DefaultInstance.GetJSOwnedObjectGCHandle(value.Array, GCHandleType.Pinned); + slot.GCHandle = JSProxyContext.CurrentOperationContext.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 f3e7fd001c86a..9d22dc30400ea 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,7 @@ public unsafe void ToManaged(out JSObject? value) value = null; return; } - value = JSProxyContext.DefaultInstance.CreateCSOwnedProxy(slot.JSHandle); + value = JSProxyContext.CurrentOperationContext.CreateCSOwnedProxy(slot.JSHandle); } /// @@ -40,13 +40,7 @@ public void ToJS(JSObject? value) else { #if FEATURE_WASM_THREADS - var parameterContext = value.ProxyContext; - var capturedContext = JSProxyContext.CapturedInstance; - if (capturedContext != null && parameterContext != capturedContext) - { - throw new InvalidOperationException("All JSObject proxies need to have same thread affinity"); - } - JSProxyContext.CapturedInstance = parameterContext; + JSProxyContext.CaptureContextFromParameter(value.ProxyContext); #endif ObjectDisposedException.ThrowIf(value.IsDisposed, value); slot.Type = MarshalerType.JSObject; 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 93624128ac31e..08ea160f1b0af 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,7 @@ public void ToJS(object? value) else { slot.Type = MarshalerType.Object; - slot.GCHandle = JSProxyContext.DefaultInstance.GetJSOwnedObjectGCHandle(value); + slot.GCHandle = JSProxyContext.CurrentOperationContext.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 c8771c2f930eb..8fa4712eac862 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 @@ -45,7 +45,7 @@ public unsafe void ToManaged(out Task? value) value = null; return; } - PromiseHolder holder = JSProxyContext.DefaultInstance.GetPromiseHolder(slot.GCHandle); + PromiseHolder holder = JSProxyContext.CurrentOperationContext.GetPromiseHolder(slot.GCHandle); TaskCompletionSource tcs = new TaskCompletionSource(holder); ToManagedCallback callback = (JSMarshalerArgument* arguments_buffer) => { @@ -59,10 +59,16 @@ public unsafe void ToManaged(out Task? value) if (arg_2.slot.Type != MarshalerType.None) { arg_2.ToManaged(out Exception? fail); +#if FEATURE_WASM_THREADS + JSProxyContext.PopOperation(); +#endif tcs.SetException(fail!); } else { +#if FEATURE_WASM_THREADS + JSProxyContext.PopOperation(); +#endif tcs.SetResult(); } // eventual exception is handled by caller @@ -86,7 +92,7 @@ public unsafe void ToManaged(out Task? value, ArgumentToManagedCallback value = null; return; } - PromiseHolder holder = JSProxyContext.DefaultInstance.GetPromiseHolder(slot.GCHandle); + PromiseHolder holder = JSProxyContext.CurrentOperationContext.GetPromiseHolder(slot.GCHandle); TaskCompletionSource tcs = new TaskCompletionSource(holder); ToManagedCallback callback = (JSMarshalerArgument* arguments_buffer) => { @@ -101,12 +107,18 @@ public unsafe void ToManaged(out Task? value, ArgumentToManagedCallback if (arg_2.slot.Type != MarshalerType.None) { arg_2.ToManaged(out Exception? fail); +#if FEATURE_WASM_THREADS + JSProxyContext.PopOperation(); +#endif if (fail == null) throw new InvalidOperationException(SR.FailedToMarshalException); tcs.SetException(fail); } else { marshaler(ref arg_3, out T result); +#if FEATURE_WASM_THREADS + JSProxyContext.PopOperation(); +#endif tcs.SetResult(result); } // eventual exception is handled by caller @@ -155,7 +167,7 @@ internal void ToJSDynamic(Task? value) if (slot.Type != MarshalerType.TaskPreCreated) { // this path should only happen when the Task is passed as argument of JSImport - slot.JSHandle = JSProxyContext.DefaultInstance.AllocJSVHandle(); + slot.JSHandle = JSProxyContext.CurrentOperationContext.AllocJSVHandle(); slot.Type = MarshalerType.Task; } else @@ -164,7 +176,7 @@ internal void ToJSDynamic(Task? value) // promise and handle is pre-allocated in slot.JSHandle } - var taskHolder = new JSObject(slot.JSHandle, JSProxyContext.DefaultInstance); + var taskHolder = JSProxyContext.CurrentOperationContext.CreateCSOwnedProxy(slot.JSHandle); #if FEATURE_WASM_THREADS task.ContinueWith(Complete, taskHolder, TaskScheduler.FromCurrentSynchronizationContext()); @@ -233,7 +245,7 @@ public void ToJS(Task? value) if (slot.Type != MarshalerType.TaskPreCreated) { // this path should only happen when the Task is passed as argument of JSImport - slot.JSHandle = JSProxyContext.DefaultInstance.AllocJSVHandle(); + slot.JSHandle = JSProxyContext.CurrentOperationContext.AllocJSVHandle(); slot.Type = MarshalerType.Task; } else @@ -242,7 +254,7 @@ public void ToJS(Task? value) // promise and handle is pre-allocated in slot.JSHandle } - var taskHolder = new JSObject(slot.JSHandle, JSProxyContext.DefaultInstance); + var taskHolder = JSProxyContext.CurrentOperationContext.CreateCSOwnedProxy(slot.JSHandle); #if FEATURE_WASM_THREADS task.ContinueWith(Complete, taskHolder, TaskScheduler.FromCurrentSynchronizationContext()); @@ -304,7 +316,7 @@ public void ToJS(Task? value, ArgumentToJSCallback marshaler) if (slot.Type != MarshalerType.TaskPreCreated) { // this path should only happen when the Task is passed as argument of JSImport - slot.JSHandle = JSProxyContext.DefaultInstance.AllocJSVHandle(); + slot.JSHandle = JSProxyContext.CurrentOperationContext.AllocJSVHandle(); slot.Type = MarshalerType.Task; } else @@ -313,7 +325,7 @@ public void ToJS(Task? value, ArgumentToJSCallback marshaler) // promise and handle is pre-allocated in slot.JSHandle } - var taskHolder = new JSObject(slot.JSHandle, JSProxyContext.DefaultInstance); + var taskHolder = JSProxyContext.CurrentOperationContext.CreateCSOwnedProxy(slot.JSHandle); #if FEATURE_WASM_THREADS task.ContinueWith(Complete, new HolderAndMarshaler(taskHolder, marshaler), TaskScheduler.FromCurrentSynchronizationContext()); @@ -342,6 +354,11 @@ private static void RejectPromise(JSObject holder, Exception ex) { holder.AssertNotDisposed(); +#if FEATURE_WASM_THREADS + JSObject.AssertThreadAffinity(holder); + JSProxyContext.PushOperationWithContext(holder.ProxyContext); +#endif + Span args = stackalloc JSMarshalerArgument[4]; ref JSMarshalerArgument exc = ref args[0]; ref JSMarshalerArgument res = ref args[1]; @@ -363,11 +380,19 @@ private static void RejectPromise(JSObject holder, Exception ex) // order of operations with DisposeImpl matters JavaScriptImports.ResolveOrRejectPromise(args); + +#if FEATURE_WASM_THREADS + JSProxyContext.PopOperation(); +#endif } private static void ResolveVoidPromise(JSObject holder) { holder.AssertNotDisposed(); +#if FEATURE_WASM_THREADS + JSObject.AssertThreadAffinity(holder); + JSProxyContext.PushOperationWithContext(holder.ProxyContext); +#endif Span args = stackalloc JSMarshalerArgument[4]; ref JSMarshalerArgument exc = ref args[0]; @@ -389,11 +414,19 @@ private static void ResolveVoidPromise(JSObject holder) // order of operations with DisposeImpl matters JavaScriptImports.ResolveOrRejectPromise(args); + +#if FEATURE_WASM_THREADS + JSProxyContext.PopOperation(); +#endif } private static void ResolvePromise(JSObject holder, T value, ArgumentToJSCallback marshaler) { holder.AssertNotDisposed(); +#if FEATURE_WASM_THREADS + JSObject.AssertThreadAffinity(holder); + JSProxyContext.PushOperationWithContext(holder.ProxyContext); +#endif Span args = stackalloc JSMarshalerArgument[4]; ref JSMarshalerArgument exc = ref args[0]; @@ -416,6 +449,10 @@ private static void ResolvePromise(JSObject holder, T value, ArgumentToJSCall // order of operations with DisposeImpl matters JavaScriptImports.ResolveOrRejectPromise(args); + +#if FEATURE_WASM_THREADS + JSProxyContext.PopOperation(); +#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 c4d5494f93718..8d6cf7aaf2809 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 314c3e190c335..fa74ae5005be3 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 @@ -378,9 +378,16 @@ static void dummyDelegateA() [Theory] [MemberData(nameof(MarshalObjectArrayCasesThrow))] - public unsafe void JsImportObjectArrayThrows(object[]? expected) + public void JsImportObjectArrayThrows(object[]? expected) { Assert.Throws(() => JavaScriptTestHelper.echo1_ObjectArray(expected)); + // this will make OperationStack unbalanced + // because the exceptions is throwed from ToJs() marshaler before the call to JSFunctionBinding.InvokeJS + // which is expected until we make operation context part of the code generator + JSHost.AssertOperationStack(1); + // any subsequent interop operation will fix that + JavaScriptTestHelper.echo1_Int32(1); + JSHost.AssertOperationStack(0); } [Fact] @@ -1833,6 +1840,7 @@ public void JsImportBackCallback_FunctionIntInt() int called = -1; Func res = JavaScriptTestHelper.backback_FuncIntFuncInt((a) => { + JSHost.AssertOperationStack(0); called = a; return a; }, 42); @@ -1849,6 +1857,7 @@ public void JsImportBackCallback_FunctionIntIntIntInt() int calledB = -1; Func res = JavaScriptTestHelper.backback_FuncIntIntFuncIntInt((a, b) => { + JSHost.AssertOperationStack(0); calledA = a; calledB = b; return a + b; @@ -1868,6 +1877,7 @@ public void JsImportCallback_ActionIntInt() int calledB = -1; JavaScriptTestHelper.back3_ActionIntInt((a, b) => { + JSHost.AssertOperationStack(0); calledA = a; calledB = b; }, 42, 43); @@ -1882,6 +1892,7 @@ public void JsImportCallback_ActionLongLong() long calledB = -1; JavaScriptTestHelper.back3_ActionLongLong((a, b) => { + JSHost.AssertOperationStack(0); calledA = a; calledB = b; }, 42, 43); @@ -1896,6 +1907,7 @@ public void JsImportCallback_ActionIntLong() long calledB = -1; JavaScriptTestHelper.back3_ActionIntLong((a, b) => { + JSHost.AssertOperationStack(0); calledA = a; calledB = b; }, 42, 43); @@ -1910,6 +1922,7 @@ public void JsImportCallback_ActionIntThrow() Exception expected = new Exception("test!!"); Exception actual = Assert.Throws(() => JavaScriptTestHelper.back3_ActionInt((a) => { + JSHost.AssertOperationStack(0); called = a; throw expected; }, 42)); @@ -1923,12 +1936,14 @@ public void JsExportCallback_FunctionIntInt() int called = -1; var chain = JavaScriptTestHelper.invoke1_FuncOfIntInt((int a) => { + JSHost.AssertOperationStack(0); called = a; return a; }, nameof(JavaScriptTestHelper.BackFuncOfIntInt)); Assert.Equal(-1, called); var actual = chain(42); + JSHost.AssertOperationStack(0); Assert.Equal(42, actual); Assert.Equal(42, called); } @@ -1940,12 +1955,14 @@ public void JsExportCallback_FunctionIntIntThrow() var expected = new Exception("test!!"); var chain = JavaScriptTestHelper.invoke1_FuncOfIntInt((int a) => { + JSHost.AssertOperationStack(0); called = a; throw expected; }, nameof(JavaScriptTestHelper.BackFuncOfIntInt)); Assert.Equal(-1, called); var actual = Assert.Throws(() => chain(42)); + JSHost.AssertOperationStack(0); Assert.Equal(42, called); Assert.Same(expected, actual); } 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 26227d1f00549..c5bc51c2e2d05 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 @@ -997,21 +997,24 @@ public static JSObject EchoIJSObject([JSMarshalAs] JSObject arg1) static JSObject _module; public static async Task InitializeAsync() { + JSHost.AssertOperationStack(0); 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(); + JSHost.AssertOperationStack(0); + _module?.Dispose(); _module = null; return Task.CompletedTask; } diff --git a/src/mono/wasm/runtime/invoke-cs.ts b/src/mono/wasm/runtime/invoke-cs.ts index bb4a837b2e578..97fb98486c004 100644 --- a/src/mono/wasm/runtime/invoke-cs.ts +++ b/src/mono/wasm/runtime/invoke-cs.ts @@ -352,16 +352,20 @@ type BindingClosure = { isDisposed: boolean, } -export function invoke_method_and_handle_exception(method: MonoMethod, args: JSMarshalerArguments): void { +export function invoke_method_and_handle_exception(method: MonoMethod, args: JSMarshalerArguments, skipPushOperation?: boolean): void { assert_bindings(); const fail_root = mono_wasm_new_root(); try { - if (MonoWasmThreads) { + if (MonoWasmThreads && !skipPushOperation) { // TODO in future, the generated code of JSExport could do this - runtimeHelpers.javaScriptExports.capture_proxy_context(); + runtimeHelpers.javaScriptExports.push_operation(); } 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 (MonoWasmThreads && !skipPushOperation) { + // TODO in future, the generated code of JSExport could do this + runtimeHelpers.javaScriptExports.pop_operation(); + } if (is_args_exception(args)) { const exc = get_arg(args, 0); throw marshal_exception_to_js(exc); @@ -377,7 +381,7 @@ export function invoke_method_raw(method: MonoMethod): void { const fail_root = mono_wasm_new_root(); try { const fail = cwraps.mono_wasm_invoke_method_raw(method, fail_root.address); - if (fail) throw new Error("ERR24: Unexpected error: " + monoStringToString(fail_root)); + if (fail) runtimeHelpers.abort("ERR24: Unexpected error: " + monoStringToString(fail_root)); } finally { fail_root.release(); diff --git a/src/mono/wasm/runtime/managed-exports.ts b/src/mono/wasm/runtime/managed-exports.ts index cd103fb1a937d..03fc8a0541ed0 100644 --- a/src/mono/wasm/runtime/managed-exports.ts +++ b/src/mono/wasm/runtime/managed-exports.ts @@ -24,8 +24,10 @@ 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 capture_proxy_context = MonoWasmThreads ? get_method("CaptureProxyContext") : undefined; - mono_assert(!MonoWasmThreads || capture_proxy_context, "Can't find CaptureProxyContext method"); + const push_operation = MonoWasmThreads ? get_method("PushOperation") : undefined; + mono_assert(!MonoWasmThreads || push_operation, "Can't find PushOperation method"); + const pop_operation = MonoWasmThreads ? get_method("PopOperation") : undefined; + mono_assert(!MonoWasmThreads || pop_operation, "Can't find PopOperation 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"); @@ -61,7 +63,7 @@ export function init_managed_exports(): void { // because this is async, we could pre-allocate the promise let promise = begin_marshal_task_to_js(res, MarshalerType.TaskPreCreated, marshal_int32_to_js); - invoke_method_and_handle_exception(call_entry_point, args); + invoke_method_and_handle_exception(call_entry_point, args, true); // in case the C# side returned synchronously promise = end_marshal_task_to_js(args, marshal_int32_to_js, promise); @@ -83,7 +85,7 @@ export function init_managed_exports(): void { const arg1 = get_arg(args, 2); set_arg_type(arg1, MarshalerType.Array); marshal_array_to_cs(arg1, dll, MarshalerType.Byte); - invoke_method_and_handle_exception(load_satellite_assembly_method, args); + invoke_method_and_handle_exception(load_satellite_assembly_method, args, true); } finally { Module.stackRestore(sp); } @@ -98,7 +100,7 @@ export function init_managed_exports(): void { set_arg_type(arg2, MarshalerType.Array); marshal_array_to_cs(arg1, dll, MarshalerType.Byte); marshal_array_to_cs(arg2, pdb, MarshalerType.Byte); - invoke_method_and_handle_exception(load_lazy_assembly_method, args); + invoke_method_and_handle_exception(load_lazy_assembly_method, args, true); } finally { Module.stackRestore(sp); } @@ -112,7 +114,7 @@ export function init_managed_exports(): void { const arg1 = get_arg(args, 2); set_arg_type(arg1, MarshalerType.Object); set_gc_handle(arg1, gc_handle); - invoke_method_and_handle_exception(release_js_owned_object_by_gc_handle_method, args); + invoke_method_and_handle_exception(release_js_owned_object_by_gc_handle_method, args, true); } finally { Module.stackRestore(sp); } @@ -134,7 +136,7 @@ export function init_managed_exports(): void { mono_assert(res_converter, "res_converter missing"); res_converter(arg3, data); } - invoke_method_and_handle_exception(complete_task_method, args); + invoke_method_and_handle_exception(complete_task_method, args, true); } finally { Module.stackRestore(sp); } @@ -163,7 +165,7 @@ export function init_managed_exports(): void { arg3_converter(arg4, arg3_js); } - invoke_method_and_handle_exception(call_delegate_method, args); + invoke_method_and_handle_exception(call_delegate_method, args, true); if (res_converter) { const res = get_arg(args, 1); @@ -183,15 +185,18 @@ export function init_managed_exports(): void { set_arg_type(arg1, MarshalerType.Exception); set_gc_handle(arg1, exception_gc_handle); - invoke_method_and_handle_exception(get_managed_stack_trace_method, args); + invoke_method_and_handle_exception(get_managed_stack_trace_method, args, true); const res = get_arg(args, 1); return marshal_string_to_js(res); } finally { Module.stackRestore(sp); } }; - if (MonoWasmThreads && capture_proxy_context) { - runtimeHelpers.javaScriptExports.capture_proxy_context = () => invoke_method_raw(capture_proxy_context); + if (MonoWasmThreads && push_operation) { + runtimeHelpers.javaScriptExports.push_operation = () => invoke_method_raw(push_operation); + } + if (MonoWasmThreads && pop_operation) { + runtimeHelpers.javaScriptExports.pop_operation = () => invoke_method_raw(pop_operation); } if (MonoWasmThreads && install_main_synchronization_context) { runtimeHelpers.javaScriptExports.install_main_synchronization_context = () => invoke_method_raw(install_main_synchronization_context); diff --git a/src/mono/wasm/runtime/types/internal.ts b/src/mono/wasm/runtime/types/internal.ts index 6c711b6e2263e..5397e4530c80e 100644 --- a/src/mono/wasm/runtime/types/internal.ts +++ b/src/mono/wasm/runtime/types/internal.ts @@ -352,7 +352,10 @@ export interface JavaScriptExports { install_main_synchronization_context(): void; // the marshaled signature is: void InstallMainSynchronizationContext() - capture_proxy_context(): void; + push_operation(): void; + + // the marshaled signature is: void InstallMainSynchronizationContext() + pop_operation(): void; // the marshaled signature is: string GetManagedStackTrace(GCHandle exception) get_managed_stack_trace(exception_gc_handle: GCHandle): string | null From 186095fcbe4008964769f7580d079e16343f91d9 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Thu, 14 Dec 2023 23:16:35 +0100 Subject: [PATCH 10/23] feedback --- .../JavaScript/JSHostImplementation.cs | 9 ++++++++- .../JavaScript/JSSynchronizationContext.cs | 15 +++++++++------ 2 files changed, 17 insertions(+), 7 deletions(-) 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 0b24e420fc720..2ba27d982ad6a 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 @@ -216,12 +216,19 @@ public static void InstallWebWorkerInterop(bool isMainThread) { JSProxyContext.MainThreadContext = ctx.ProxyContext; } + ctx.previousSynchronizationContext = SynchronizationContext.Current; + SynchronizationContext.SetSynchronizationContext(ctx); + ctx.AwaitNewData(); } public static void UninstallWebWorkerInterop() { - JSProxyContext.CurrentThreadContext?.Dispose(); + var ctx = JSProxyContext.CurrentThreadContext; + if (ctx == null) throw new InvalidOperationException(); + var syncContext = ctx.SynchronizationContext; + SynchronizationContext.SetSynchronizationContext(syncContext.previousSynchronizationContext); + ctx.Dispose(); } [UnsafeAccessor(UnsafeAccessorKind.Field, Name = "external_eventloop")] 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 92d4e39ede7c3..8e6a743bf41fc 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 @@ -41,20 +41,23 @@ public WorkItem(SendOrPostCallback callback, object? data, ManualResetEventSlim? } } - internal JSSynchronizationContext(bool isMainThread) + public JSSynchronizationContext(bool isMainThread) { ProxyContext = new JSProxyContext(isMainThread, this); Queue = Channel.CreateUnbounded(new UnboundedChannelOptions { SingleWriter = false, SingleReader = true, AllowSynchronousContinuations = true }); _DataIsAvailable = DataIsAvailable; + } - previousSynchronizationContext = Current; - SetSynchronizationContext(this); + internal JSSynchronizationContext(JSProxyContext proxyContext, WorkItemQueueType queue, Action dataIsAvailable) + { + ProxyContext = proxyContext; + Queue = queue; + _DataIsAvailable = dataIsAvailable; } public override SynchronizationContext CreateCopy() { - // child thread will inherit this JSSynchronizationContext - return this; + return new JSSynchronizationContext(ProxyContext, Queue, _DataIsAvailable); } internal void AwaitNewData() @@ -177,7 +180,7 @@ private void Dispose(bool disposing) { Queue.Writer.Complete(); } - SetSynchronizationContext(previousSynchronizationContext); + previousSynchronizationContext = null; _isDisposed = true; } } From 5f2ae2e40e346e44f222d1d1280c4cfbf0b464af Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Thu, 14 Dec 2023 23:27:13 +0100 Subject: [PATCH 11/23] fix tests --- .../InteropServices/JavaScript/JSProxyContext.cs | 4 ---- ...ntime.InteropServices.JavaScript.Tests.csproj | 1 + .../JavaScript/JSImportExportTest.cs | 16 ++++++++-------- .../JavaScript/JavaScriptTestHelper.mjs | 10 +++++++++- 4 files changed, 18 insertions(+), 13 deletions(-) diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSProxyContext.cs b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSProxyContext.cs index 152758d56b4e7..d2341824ee219 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSProxyContext.cs +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSProxyContext.cs @@ -159,10 +159,6 @@ public static void AssertOperationStack(int expected) var actual = stack.Count; var multiple = stack.Count > 0 ? stack[stack.Count - 1].Multiple : false; var called = stack.Count > 0 ? stack[stack.Count - 1].Called : false; - if (expected == 0) - { - stack.Clear(); - } // TODO Environment.FailFast if (actual != expected) throw new InvalidOperationException($"Unexpected OperationStack size expected: {expected} actual: {actual} called:{called} multiple:{multiple}"); } 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 8d6cf7aaf2809..394d2abf22c23 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 @@ -12,6 +12,7 @@ true <_XUnitBackgroundExec>false + false 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 fa74ae5005be3..f3f11c1c59a28 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 @@ -1840,7 +1840,7 @@ public void JsImportBackCallback_FunctionIntInt() int called = -1; Func res = JavaScriptTestHelper.backback_FuncIntFuncInt((a) => { - JSHost.AssertOperationStack(0); + JSHost.AssertOperationStack(2); called = a; return a; }, 42); @@ -1857,7 +1857,7 @@ public void JsImportBackCallback_FunctionIntIntIntInt() int calledB = -1; Func res = JavaScriptTestHelper.backback_FuncIntIntFuncIntInt((a, b) => { - JSHost.AssertOperationStack(0); + JSHost.AssertOperationStack(2); calledA = a; calledB = b; return a + b; @@ -1877,7 +1877,7 @@ public void JsImportCallback_ActionIntInt() int calledB = -1; JavaScriptTestHelper.back3_ActionIntInt((a, b) => { - JSHost.AssertOperationStack(0); + JSHost.AssertOperationStack(2); calledA = a; calledB = b; }, 42, 43); @@ -1892,7 +1892,7 @@ public void JsImportCallback_ActionLongLong() long calledB = -1; JavaScriptTestHelper.back3_ActionLongLong((a, b) => { - JSHost.AssertOperationStack(0); + JSHost.AssertOperationStack(2); calledA = a; calledB = b; }, 42, 43); @@ -1907,7 +1907,7 @@ public void JsImportCallback_ActionIntLong() long calledB = -1; JavaScriptTestHelper.back3_ActionIntLong((a, b) => { - JSHost.AssertOperationStack(0); + JSHost.AssertOperationStack(2); calledA = a; calledB = b; }, 42, 43); @@ -1922,7 +1922,7 @@ public void JsImportCallback_ActionIntThrow() Exception expected = new Exception("test!!"); Exception actual = Assert.Throws(() => JavaScriptTestHelper.back3_ActionInt((a) => { - JSHost.AssertOperationStack(0); + JSHost.AssertOperationStack(2); called = a; throw expected; }, 42)); @@ -1936,7 +1936,7 @@ public void JsExportCallback_FunctionIntInt() int called = -1; var chain = JavaScriptTestHelper.invoke1_FuncOfIntInt((int a) => { - JSHost.AssertOperationStack(0); + JSHost.AssertOperationStack(4); called = a; return a; }, nameof(JavaScriptTestHelper.BackFuncOfIntInt)); @@ -1955,7 +1955,7 @@ public void JsExportCallback_FunctionIntIntThrow() var expected = new Exception("test!!"); var chain = JavaScriptTestHelper.invoke1_FuncOfIntInt((int a) => { - JSHost.AssertOperationStack(0); + JSHost.AssertOperationStack(4); called = a; throw expected; }, nameof(JavaScriptTestHelper.BackFuncOfIntInt)); 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 624af0df0fe9f..baec1cb231c2b 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; + } } } From e8ab8990ea5f9dfd05dc0969498e5260fa3b5444 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Thu, 14 Dec 2023 23:29:15 +0100 Subject: [PATCH 12/23] bad push/pop balance is fatal --- .../Runtime/InteropServices/JavaScript/JSProxyContext.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSProxyContext.cs b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSProxyContext.cs index d2341824ee219..bf9694ae942d2 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSProxyContext.cs +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSProxyContext.cs @@ -159,8 +159,7 @@ public static void AssertOperationStack(int expected) var actual = stack.Count; var multiple = stack.Count > 0 ? stack[stack.Count - 1].Multiple : false; var called = stack.Count > 0 ? stack[stack.Count - 1].Called : false; - // TODO Environment.FailFast - if (actual != expected) throw new InvalidOperationException($"Unexpected OperationStack size expected: {expected} actual: {actual} called:{called} multiple:{multiple}"); + if (actual != expected) Environment.FailFast($"Unexpected OperationStack size expected: {expected} actual: {actual} called:{called} multiple:{multiple}"); } // TODO: sort generated ToJS() calls to make the capture context before we need to use it From 17231ac23f09a28f3bf0d19c304adf1038689b11 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Thu, 14 Dec 2023 23:39:57 +0100 Subject: [PATCH 13/23] fix ST --- .../JavaScript/Interop/JavaScriptExports.cs | 1 + .../JavaScript/JSMarshalerArgument.cs | 2 +- .../JavaScript/JSProxyContext.cs | 35 +++++++++++-------- 3 files changed, 22 insertions(+), 16 deletions(-) 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 1667841a7614a..39cb92a8033bb 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 @@ -248,6 +248,7 @@ public static void CompleteTask(JSMarshalerArgument* arguments_buffer) if (holder != null) { // arg_2, arg_3 are processed by the callback + // JSProxyContext.PopOperation() is called by the callback holder.Callback!(arguments_buffer); } } 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 ec5bddffdf5de..bff02a596b526 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 @@ -67,7 +67,7 @@ public unsafe void Initialize() #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.PushOperationUnknowContext(); + JSProxyContext.PushOperationUnknownContext(); #endif } } diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSProxyContext.cs b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSProxyContext.cs index bf9694ae942d2..ae3c8ce2838ac 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSProxyContext.cs +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSProxyContext.cs @@ -24,7 +24,11 @@ internal sealed class JSProxyContext : IDisposable private nint NextJSVHandle = -2; private readonly List JSVHandleFreeList = new(); -#if FEATURE_WASM_THREADS +#if !FEATURE_WASM_THREADS + private JSProxyContext() + { + } +#else public nint NativeTID; public int ManagedTID; public bool IsMainThread; @@ -44,6 +48,15 @@ public static IntPtr GetNativeThreadId() { return (int)GetThreadNativeThreadId(Thread.CurrentThread); } + + public JSProxyContext(bool isMainThread, JSSynchronizationContext synchronizationContext) + { + SynchronizationContext = synchronizationContext; + Interop.Runtime.InstallWebWorkerInterop(); + NativeTID = GetNativeThreadId(); + ManagedTID = Thread.CurrentThread.ManagedThreadId; + IsMainThread = isMainThread; + } #endif #region Current operation context @@ -52,8 +65,7 @@ public static IntPtr GetNativeThreadId() public static readonly JSProxyContext MainThreadContext = new(); public static JSProxyContext CurrentThreadContext => MainThreadContext; public static JSProxyContext CurrentOperationContext => MainThreadContext; - - public static JSProxyContext PushOperation() + 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; @@ -87,7 +99,7 @@ private static List OperationStack } } - public static void PushOperationUnknowContext() + public static void PushOperationUnknownContext() { var stack = OperationStack; // for SchedulePopOperation() @@ -212,15 +224,6 @@ public static JSProxyContext CurrentOperationContext } } - public JSProxyContext(bool isMainThread, JSSynchronizationContext synchronizationContext) - { - SynchronizationContext = synchronizationContext; - Interop.Runtime.InstallWebWorkerInterop(); - NativeTID = GetNativeThreadId(); - ManagedTID = Thread.CurrentThread.ManagedThreadId; - IsMainThread = isMainThread; - } - #endif [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -238,7 +241,7 @@ public static JSProxyContext AssertCurrentContext() } return ctx; #else - return MainInstance; + return MainThreadContext; #endif } @@ -432,10 +435,12 @@ public static void ReleaseCSOwnedObject(JSObject proxy, bool skipJS) 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"); } +#endif lock (ctx) { if (proxy.IsDisposed) @@ -460,7 +465,7 @@ public static void ReleaseCSOwnedObject(JSObject proxy, bool skipJS) } } - #endregion +#endregion #region Legacy From da755864612449c1fddf961c4a70fb2bf9d73a79 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Fri, 15 Dec 2023 00:04:25 +0100 Subject: [PATCH 14/23] feedback --- .../InteropServices/JavaScript/JSHostImplementation.cs | 5 ++++- .../InteropServices/JavaScript/JSObject.References.cs | 2 +- .../Runtime/InteropServices/JavaScript/JSProxyContext.cs | 5 ++++- src/mono/wasm/runtime/types/internal.ts | 4 ++-- 4 files changed, 11 insertions(+), 5 deletions(-) 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 2ba27d982ad6a..81683719fa454 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 @@ -227,7 +227,10 @@ public static void UninstallWebWorkerInterop() var ctx = JSProxyContext.CurrentThreadContext; if (ctx == null) throw new InvalidOperationException(); var syncContext = ctx.SynchronizationContext; - SynchronizationContext.SetSynchronizationContext(syncContext.previousSynchronizationContext); + if (SynchronizationContext.Current == syncContext) + { + SynchronizationContext.SetSynchronizationContext(syncContext.previousSynchronizationContext); + } ctx.Dispose(); } 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 7d77688d96c90..c5c48c7719d09 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 @@ -124,7 +124,7 @@ internal void DisposeImpl(bool skipJS) JSProxyContext.ReleaseCSOwnedObject(x.self, x.skipJS); }, (this, skipJS)); #else - ProxyContext.ReleaseCSOwnedObject(JSHandle); + JSProxyContext.ReleaseCSOwnedObject(this, skipJS); _isDisposed = true; JSHandle = IntPtr.Zero; #endif diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSProxyContext.cs b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSProxyContext.cs index ae3c8ce2838ac..245d35619b6b5 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSProxyContext.cs +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSProxyContext.cs @@ -150,6 +150,7 @@ public static JSProxyContext PushOperationWithCurrentThreadContext() public static void PopOperation() { var stack = OperationStack; + if (stack.Count < 1) Environment.FailFast("Unbalanced PopOperation");// there is no recovery stack.RemoveAt(stack.Count - 1); } @@ -158,7 +159,9 @@ public static void PopOperation() public static void SchedulePopOperation() { var stack = OperationStack; - stack[stack.Count - 1].Called = true; + var op = stack[stack.Count - 1]; + if (op.Called) Environment.FailFast("SchedulePopOperation called twice"); + op.Called = true; } public static void AssertOperationStack(int expected) diff --git a/src/mono/wasm/runtime/types/internal.ts b/src/mono/wasm/runtime/types/internal.ts index 5397e4530c80e..37f6bdc33fd26 100644 --- a/src/mono/wasm/runtime/types/internal.ts +++ b/src/mono/wasm/runtime/types/internal.ts @@ -351,10 +351,10 @@ export interface JavaScriptExports { // the marshaled signature is: void InstallMainSynchronizationContext() install_main_synchronization_context(): void; - // the marshaled signature is: void InstallMainSynchronizationContext() + // the marshaled signature is: void PushOperation() push_operation(): void; - // the marshaled signature is: void InstallMainSynchronizationContext() + // the marshaled signature is: void PopOperation() pop_operation(): void; // the marshaled signature is: string GetManagedStackTrace(GCHandle exception) From e82babab31166fcd81355ac4a7794e6c14c568a1 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Fri, 15 Dec 2023 11:21:27 +0100 Subject: [PATCH 15/23] fix --- .../JavaScript/JSImportExportTest.cs | 38 +++++++++++-------- .../JavaScript/JavaScriptTestHelper.cs | 4 ++ 2 files changed, 27 insertions(+), 15 deletions(-) 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 f3f11c1c59a28..32a4e8d9b5cc8 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 @@ -384,10 +385,10 @@ public void JsImportObjectArrayThrows(object[]? expected) // this will make OperationStack unbalanced // because the exceptions is throwed from ToJs() marshaler before the call to JSFunctionBinding.InvokeJS // which is expected until we make operation context part of the code generator - JSHost.AssertOperationStack(1); + AssertOperationStack(1); // any subsequent interop operation will fix that JavaScriptTestHelper.echo1_Int32(1); - JSHost.AssertOperationStack(0); + AssertOperationStack(0); } [Fact] @@ -1840,7 +1841,7 @@ public void JsImportBackCallback_FunctionIntInt() int called = -1; Func res = JavaScriptTestHelper.backback_FuncIntFuncInt((a) => { - JSHost.AssertOperationStack(2); + AssertOperationStack(2); called = a; return a; }, 42); @@ -1857,7 +1858,7 @@ public void JsImportBackCallback_FunctionIntIntIntInt() int calledB = -1; Func res = JavaScriptTestHelper.backback_FuncIntIntFuncIntInt((a, b) => { - JSHost.AssertOperationStack(2); + AssertOperationStack(2); calledA = a; calledB = b; return a + b; @@ -1877,7 +1878,7 @@ public void JsImportCallback_ActionIntInt() int calledB = -1; JavaScriptTestHelper.back3_ActionIntInt((a, b) => { - JSHost.AssertOperationStack(2); + AssertOperationStack(2); calledA = a; calledB = b; }, 42, 43); @@ -1892,7 +1893,7 @@ public void JsImportCallback_ActionLongLong() long calledB = -1; JavaScriptTestHelper.back3_ActionLongLong((a, b) => { - JSHost.AssertOperationStack(2); + AssertOperationStack(2); calledA = a; calledB = b; }, 42, 43); @@ -1907,7 +1908,7 @@ public void JsImportCallback_ActionIntLong() long calledB = -1; JavaScriptTestHelper.back3_ActionIntLong((a, b) => { - JSHost.AssertOperationStack(2); + AssertOperationStack(2); calledA = a; calledB = b; }, 42, 43); @@ -1922,7 +1923,7 @@ public void JsImportCallback_ActionIntThrow() Exception expected = new Exception("test!!"); Exception actual = Assert.Throws(() => JavaScriptTestHelper.back3_ActionInt((a) => { - JSHost.AssertOperationStack(2); + AssertOperationStack(2); called = a; throw expected; }, 42)); @@ -1936,14 +1937,14 @@ public void JsExportCallback_FunctionIntInt() int called = -1; var chain = JavaScriptTestHelper.invoke1_FuncOfIntInt((int a) => { - JSHost.AssertOperationStack(4); + AssertOperationStack(4); called = a; return a; }, nameof(JavaScriptTestHelper.BackFuncOfIntInt)); Assert.Equal(-1, called); var actual = chain(42); - JSHost.AssertOperationStack(0); + AssertOperationStack(0); Assert.Equal(42, actual); Assert.Equal(42, called); } @@ -1955,14 +1956,14 @@ public void JsExportCallback_FunctionIntIntThrow() var expected = new Exception("test!!"); var chain = JavaScriptTestHelper.invoke1_FuncOfIntInt((int a) => { - JSHost.AssertOperationStack(4); + AssertOperationStack(4); called = a; throw expected; }, nameof(JavaScriptTestHelper.BackFuncOfIntInt)); Assert.Equal(-1, called); var actual = Assert.Throws(() => chain(42)); - JSHost.AssertOperationStack(0); + AssertOperationStack(0); Assert.Equal(42, called); Assert.Same(expected, actual); } @@ -1976,15 +1977,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 @@ -2139,5 +2140,12 @@ public static DateTimeOffset TrimNano(DateTimeOffset date) { return new DateTime(date.Ticks - (date.Ticks % TimeSpan.TicksPerMillisecond), DateTimeKind.Utc); } + + private static void AssertOperationStack(int cnt) + { +#if FEATURE_WASM_THREADS + JSHost.AssertOperationStack(cnt); +#endif + } } } 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 c5bc51c2e2d05..d6590740d8b69 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 @@ -997,7 +997,9 @@ public static JSObject EchoIJSObject([JSMarshalAs] JSObject arg1) static JSObject _module; public static async Task InitializeAsync() { +#if FEATURE_WASM_THREADS JSHost.AssertOperationStack(0); +#endif if (_module == null) { _module = await JSHost.ImportAsync("JavaScriptTestHelper", "../JavaScriptTestHelper.mjs"); ; @@ -1013,7 +1015,9 @@ public static async Task InitializeAsync() public static Task DisposeAsync() { +#if FEATURE_WASM_THREADS JSHost.AssertOperationStack(0); +#endif _module?.Dispose(); _module = null; return Task.CompletedTask; From 6aeef075ca32afa7d9aadc433147b3168926ce01 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Fri, 15 Dec 2023 12:41:28 +0100 Subject: [PATCH 16/23] thread vs execution context cleanup --- .../JavaScript/CancelablePromise.cs | 4 ++-- .../InteropServices/JavaScript/JSException.cs | 3 +-- .../JavaScript/JSFunctionBinding.cs | 8 ++++---- .../InteropServices/JavaScript/JSHost.cs | 8 ++++---- .../JavaScript/JSHostImplementation.cs | 11 ++++++---- .../JavaScript/JSObject.References.cs | 8 +++----- .../JavaScript/JSProxyContext.cs | 20 +++++++++++-------- .../JavaScript/JSSynchronizationContext.cs | 2 +- 8 files changed, 34 insertions(+), 30 deletions(-) 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 8fc2e7543b8bf..8e447992f4b1d 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 @@ -24,7 +24,7 @@ public static void CancelPromise(Task promise) #if FEATURE_WASM_THREADS - if (holder.ProxyContext == JSProxyContext.CurrentThreadContext) + if (holder.ProxyContext.IsCurrentThread()) { _CancelPromise(holder.GCHandle); return; @@ -52,7 +52,7 @@ public static void CancelPromise(Task promise, Action callback, T state) #if FEATURE_WASM_THREADS - if (holder.ProxyContext == JSProxyContext.CurrentThreadContext) + if (holder.ProxyContext.IsCurrentThread()) { _CancelPromise(holder.GCHandle); callback.Invoke(state); 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 a7e90f8fba133..1a9e2278d57ca 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,8 +47,7 @@ public override string? StackTrace } #if FEATURE_WASM_THREADS - var ctx = JSProxyContext.CurrentThreadContext; - if (ctx == null || jsException.ProxyContext != ctx) + if (!jsException.ProxyContext.IsCurrentThread()) { // 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."; 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 61a91f6848e6e..e227609917673 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 @@ -174,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); } /// @@ -218,7 +218,7 @@ internal static unsafe void InvokeJSFunction(JSObject jsFunction, Span arguments) { #if FEATURE_WASM_THREADS - JSProxyContext.AssertCurrentContext(); + JSProxyContext.AssertIsInteropThread(); #endif if (signature.IsAsync) @@ -252,10 +252,10 @@ internal static unsafe void InvokeJSImportImpl(JSFunctionBinding signature, Span #endif } - 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 - JSProxyContext.AssertCurrentContext(); + JSProxyContext.AssertIsInteropThread(); #endif var signature = JSHostImplementation.GetMethodSignature(signatures, functionName, moduleName); 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 81afc1188ebb1..1464dfe7d437b 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 - JSProxyContext.AssertCurrentContext(); + JSProxyContext.AssertIsInteropThread(); #endif return JavaScriptImports.GetGlobalThis(); } @@ -36,7 +36,7 @@ public static JSObject DotnetInstance get { #if FEATURE_WASM_THREADS - JSProxyContext.AssertCurrentContext(); + 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 - JSProxyContext.AssertCurrentContext(); + JSProxyContext.AssertIsInteropThread(); #endif return JSHostImplementation.ImportAsync(moduleName, moduleUrl, cancellationToken); } @@ -65,7 +65,7 @@ public static SynchronizationContext CurrentOrMainJSSynchronizationContext get { #if FEATURE_WASM_THREADS - return (JSProxyContext.CurrentThreadContext ?? JSProxyContext.MainThreadContext).SynchronizationContext; + 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.cs b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSHostImplementation.cs index 81683719fa454..b7fa6be44f4eb 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 @@ -211,13 +211,16 @@ public static void LoadSatelliteAssembly(byte[] dllBytes) public static void InstallWebWorkerInterop(bool isMainThread) { var ctx = new JSSynchronizationContext(isMainThread); - JSProxyContext.CurrentThreadContext = ctx.ProxyContext; + ctx.previousSynchronizationContext = SynchronizationContext.Current; + SynchronizationContext.SetSynchronizationContext(ctx); + + var proxyContext = ctx.ProxyContext; + JSProxyContext.CurrentThreadContext = proxyContext; + JSProxyContext.ExecutionContext = proxyContext; if (isMainThread) { - JSProxyContext.MainThreadContext = ctx.ProxyContext; + JSProxyContext.MainThreadContext = proxyContext; } - ctx.previousSynchronizationContext = SynchronizationContext.Current; - SynchronizationContext.SetSynchronizationContext(ctx); ctx.AwaitNewData(); } 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 c5c48c7719d09..eff5a97daef2f 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 @@ -79,18 +79,16 @@ internal static void AssertThreadAffinity(object value) return; } - var currentContext = JSProxyContext.AssertCurrentContext(); - if (value is JSObject jsObject) { - if (jsObject.ProxyContext != currentContext) + 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.ProxyContext != currentContext) + 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."); } @@ -112,7 +110,7 @@ internal void DisposeImpl(bool skipJS) if (!_isDisposed) { #if FEATURE_WASM_THREADS - if (ProxyContext == JSProxyContext.CurrentThreadContext) + if (ProxyContext.IsCurrentThread()) { JSProxyContext.ReleaseCSOwnedObject(this, skipJS); return; diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSProxyContext.cs b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSProxyContext.cs index 245d35619b6b5..e93031fb390be 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSProxyContext.cs +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSProxyContext.cs @@ -142,7 +142,7 @@ public static JSProxyContext PushOperationWithCurrentThreadContext() PopOperation(); } - var current = AssertCurrentContext(); + var current = AssertIsInteropThread(); stack.Add(new PendingOperation { CapturedContext = current }); return current; } @@ -190,15 +190,19 @@ public static void CaptureContextFromParameter(JSProxyContext parameterContext) pendingOperation.CapturedContext = capturedContext; } - // Context of the current thread or async task. Could be null on threads which don't have JS interop, like managed thread pool 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? CurrentThreadContext + 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 @@ -216,11 +220,11 @@ public static JSProxyContext CurrentOperationContext return pendingOperation.CapturedContext; } // it could happen that we are in operation, in which we didn't capture target thread/context - var currentThreadContext = CurrentThreadContext; - if (currentThreadContext != null) + var executionContext = ExecutionContext; + if (executionContext != null) { - // we could will call JS on the current thread, if it has the JS interop installed - return currentThreadContext; + // 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; @@ -230,7 +234,7 @@ public static JSProxyContext CurrentOperationContext #endif [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static JSProxyContext AssertCurrentContext() + public static JSProxyContext AssertIsInteropThread() { #if FEATURE_WASM_THREADS var ctx = CurrentThreadContext; 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 8e6a743bf41fc..d685580274889 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 @@ -132,7 +132,7 @@ 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() { - var ctx = JSProxyContext.AssertCurrentContext(); + var ctx = JSProxyContext.AssertIsInteropThread(); ctx.SynchronizationContext.Pump(); } From e3e95176d52e5e1223e34358f12df79231f8a710 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Fri, 15 Dec 2023 13:17:05 +0100 Subject: [PATCH 17/23] more cleanup --- .../JavaScript/JSObject.References.cs | 12 ++++++------ .../InteropServices/JavaScript/JSProxyContext.cs | 1 + .../InteropServices/JavaScript/JSWebWorker.cs | 6 ++---- .../Marshaling/JSMarshalerArgument.Task.cs | 6 +++--- 4 files changed, 12 insertions(+), 13 deletions(-) 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 eff5a97daef2f..cc56bad524a7c 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 @@ -105,14 +105,14 @@ internal static void AssertThreadAffinity(object value) /// public override string ToString() => $"(js-obj js '{JSHandle}')"; - internal void DisposeImpl(bool skipJS) + internal void DisposeImpl(bool skipJsCleanup = false) { if (!_isDisposed) { #if FEATURE_WASM_THREADS if (ProxyContext.IsCurrentThread()) { - JSProxyContext.ReleaseCSOwnedObject(this, skipJS); + JSProxyContext.ReleaseCSOwnedObject(this, skipJsCleanup); return; } @@ -120,9 +120,9 @@ internal void DisposeImpl(bool skipJS) { var x = ((JSObject self, bool skipJS))s!; JSProxyContext.ReleaseCSOwnedObject(x.self, x.skipJS); - }, (this, skipJS)); + }, (this, skipJsCleanup)); #else - JSProxyContext.ReleaseCSOwnedObject(this, skipJS); + JSProxyContext.ReleaseCSOwnedObject(this, skipJsCleanup); _isDisposed = true; JSHandle = IntPtr.Zero; #endif @@ -131,7 +131,7 @@ internal void DisposeImpl(bool skipJS) ~JSObject() { - DisposeImpl(false); + DisposeImpl(); } /// @@ -139,7 +139,7 @@ internal void DisposeImpl(bool skipJS) /// public void Dispose() { - DisposeImpl(false); + 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 index e93031fb390be..d8b8113a1b340 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSProxyContext.cs +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSProxyContext.cs @@ -181,6 +181,7 @@ public static void AssertOperationStack(int expected) public static void CaptureContextFromParameter(JSProxyContext parameterContext) { var stack = OperationStack; + if (stack.Count < 1) Environment.FailFast("CaptureContextFromParameter could be only used during pending operation."); var pendingOperation = stack[stack.Count - 1]; var capturedContext = pendingOperation.CapturedContext; if (capturedContext != null && parameterContext != capturedContext) 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 459ac6f4b2ea7..9cc19c7406b96 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,8 +32,7 @@ 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) + if (JSProxyContext.MainThreadContext.IsCurrentThread()) { await JavaScriptImports.ThreadAvailable().ConfigureAwait(false); } @@ -42,8 +41,7 @@ public static async Task RunAsync(Func> body, CancellationToken ca public static async Task RunAsync(Func body, CancellationToken cancellationToken) { - // TODO remove main thread condition later - if (Thread.CurrentThread.ManagedThreadId == 1) + if (JSProxyContext.MainThreadContext.IsCurrentThread()) { await JavaScriptImports.ThreadAvailable().ConfigureAwait(false); } 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 8fa4712eac862..64ed99b42b6cd 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 @@ -376,7 +376,7 @@ private static void RejectPromise(JSObject holder, Exception ex) arg_value.ToJS(ex); // we can free the JSHandle here and the holder.resolve_or_reject will do the rest - holder.DisposeImpl(true); + holder.DisposeImpl(skipJsCleanup: true); // order of operations with DisposeImpl matters JavaScriptImports.ResolveOrRejectPromise(args); @@ -410,7 +410,7 @@ private static void ResolveVoidPromise(JSObject holder) arg_value.slot.Type = MarshalerType.Void; // we can free the JSHandle here and the holder.resolve_or_reject will do the rest - holder.DisposeImpl(true); + holder.DisposeImpl(skipJsCleanup: true); // order of operations with DisposeImpl matters JavaScriptImports.ResolveOrRejectPromise(args); @@ -445,7 +445,7 @@ private static void ResolvePromise(JSObject holder, T value, ArgumentToJSCall marshaler(ref arg_value, value); // we can free the JSHandle here and the holder.resolve_or_reject will do the rest - holder.DisposeImpl(true); + holder.DisposeImpl(skipJsCleanup: true); // order of operations with DisposeImpl matters JavaScriptImports.ResolveOrRejectPromise(args); From a7f3fbcbd99ffd3f6658be69bd6c03d8fc68fcad Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Sat, 16 Dec 2023 20:52:56 +0100 Subject: [PATCH 18/23] - PromiseHolder thread safe - faster InitializeImpl --- .../JavaScript/Interop/JavaScriptExports.cs | 25 ++- .../JavaScript/Interop/JavaScriptImports.cs | 13 -- .../JavaScript/JSFunctionBinding.cs | 14 ++ .../JavaScript/JSHostImplementation.Types.cs | 5 +- .../JavaScript/JSHostImplementation.cs | 2 + .../JavaScript/JSMarshalerArgument.cs | 8 +- .../JavaScript/JSProxyContext.cs | 73 ++++---- .../Marshaling/JSMarshalerArgument.Func.cs | 32 ++-- .../Marshaling/JSMarshalerArgument.Task.cs | 159 ++++++++++-------- 9 files changed, 195 insertions(+), 136 deletions(-) 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 39cb92a8033bb..ad9ca6488c6a4 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 @@ -243,14 +243,29 @@ public static void CompleteTask(JSMarshalerArgument* arguments_buffer) { // when we arrive here, we could assume that all proxies are owned by calling thread var ctx = JSProxyContext.PushOperationWithCurrentThreadContext(); + var holder = ctx.GetPromiseHolder(arg_1.slot.GCHandle); - var holder = ctx.ReleasePromiseHolder(arg_1.slot.GCHandle); - if (holder != null) +#if FEATURE_WASM_THREADS + lock (ctx) + { + if (holder.Callback == null) + { + holder.CallbackReady = new ManualResetEventSlim(false); + } + } + if (holder.CallbackReady != null) { - // arg_2, arg_3 are processed by the callback - // JSProxyContext.PopOperation() is called by the callback - holder.Callback!(arguments_buffer); +#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) { 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 4ebb8a772e236..667fed536adaf 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/JSFunctionBinding.cs b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSFunctionBinding.cs index e227609917673..25971296ad892 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 @@ -283,5 +283,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/JSHostImplementation.Types.cs b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSHostImplementation.Types.cs index b97c8751a57ab..837b43c983c6f 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 @@ -14,7 +14,10 @@ public sealed class PromiseHolder { public nint GCHandle; // could be also virtual GCVHandle public ToManagedCallback? Callback; - internal JSProxyContext ProxyContext; + public JSProxyContext ProxyContext; +#if FEATURE_WASM_THREADS + public ManualResetEventSlim? CallbackReady; +#endif public PromiseHolder(JSProxyContext 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 b7fa6be44f4eb..f36776628f0ef 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 @@ -223,6 +223,8 @@ public static void InstallWebWorkerInterop(bool isMainThread) } ctx.AwaitNewData(); + + Interop.Runtime.InstallWebWorkerInterop(); } public static void UninstallWebWorkerInterop() 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 bff02a596b526..e1aae8085b20b 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 @@ -63,12 +63,18 @@ internal struct JSMarshalerArgumentImpl [MethodImpl(MethodImplOptions.AggressiveInlining)] public unsafe void Initialize() { - slot.Type = MarshalerType.None; + InitializeImpl(); #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.PushOperationUnknownContext(); #endif } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal unsafe void InitializeImpl() + { + slot.Type = MarshalerType.None; + } } } diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSProxyContext.cs b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSProxyContext.cs index d8b8113a1b340..a26cc57be1f86 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSProxyContext.cs +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSProxyContext.cs @@ -52,7 +52,6 @@ public static IntPtr GetNativeThreadId() public JSProxyContext(bool isMainThread, JSSynchronizationContext synchronizationContext) { SynchronizationContext = synchronizationContext; - Interop.Runtime.InstallWebWorkerInterop(); NativeTID = GetNativeThreadId(); ManagedTID = Thread.CurrentThread.ManagedThreadId; IsMainThread = isMainThread; @@ -322,44 +321,54 @@ public IntPtr GetJSOwnedObjectGCHandle(object obj, GCHandleType handleType = GCH } } - // TODO unregister and collect pending PromiseHolder also when no C# is awaiting ? + public PromiseHolder CreatePromiseHolder() + { + lock (this) + { + return new PromiseHolder(this); + } + } + public PromiseHolder GetPromiseHolder(nint gcHandle) { - PromiseHolder holder; - if (IsGCVHandle(gcHandle)) + lock (this) { - lock (this) + PromiseHolder? holder; + if (IsGCVHandle(gcHandle)) { - holder = new PromiseHolder(this, gcHandle); - ThreadJsOwnedHolders.Add(gcHandle, holder); + if (!ThreadJsOwnedHolders.TryGetValue(gcHandle, out holder)) + { + holder = new PromiseHolder(this, gcHandle); + ThreadJsOwnedHolders.Add(gcHandle, holder); + } } + else + { + holder = (PromiseHolder)((GCHandle)gcHandle).Target!; + } + return holder; } - else - { - holder = (PromiseHolder)((GCHandle)gcHandle).Target!; - } - return holder; } - public unsafe void ReleaseJSOwnedObjectByGCHandle(nint gcHandle) + public unsafe void ReleasePromiseHolder(nint holderGCHandle) { - PromiseHolder? holder = null; lock (this) { - if (IsGCVHandle(gcHandle)) + PromiseHolder? holder; + if (IsGCVHandle(holderGCHandle)) { - if (ThreadJsOwnedHolders.Remove(gcHandle, out holder)) + if (ThreadJsOwnedHolders.Remove(holderGCHandle, out holder)) { holder.GCHandle = IntPtr.Zero; } else { - throw new InvalidOperationException("ReleaseJSOwnedObjectByGCHandle expected in ThreadJsOwnedHolders"); + throw new InvalidOperationException("ReleasePromiseHolder expected PromiseHolder " + holderGCHandle); } } else { - GCHandle handle = (GCHandle)gcHandle; + GCHandle handle = (GCHandle)holderGCHandle; var target = handle.Target!; if (target is PromiseHolder holder2) { @@ -368,39 +377,32 @@ public unsafe void ReleaseJSOwnedObjectByGCHandle(nint gcHandle) } else { - if (!ThreadJsOwnedObjects.Remove(target)) - { - throw new InvalidOperationException("ReleaseJSOwnedObjectByGCHandle expected in ThreadJsOwnedObjects"); - } + throw new InvalidOperationException("ReleasePromiseHolder expected PromiseHolder" + holderGCHandle); } handle.Free(); } } - if (holder != null) - { - holder.Callback!(null); - } } - public PromiseHolder? ReleasePromiseHolder(nint holderGCHandle) + public unsafe void ReleaseJSOwnedObjectByGCHandle(nint gcHandle) { PromiseHolder? holder = null; lock (this) { - if (IsGCVHandle(holderGCHandle)) + if (IsGCVHandle(gcHandle)) { - if (ThreadJsOwnedHolders.Remove(holderGCHandle, out holder)) + if (ThreadJsOwnedHolders.Remove(gcHandle, out holder)) { holder.GCHandle = IntPtr.Zero; } else { - throw new InvalidOperationException("ReleasePromiseHolder expected to find handle in ThreadJsOwnedHolders"); + throw new InvalidOperationException("ReleaseJSOwnedObjectByGCHandle expected in ThreadJsOwnedHolders"); } } else { - GCHandle handle = (GCHandle)holderGCHandle; + GCHandle handle = (GCHandle)gcHandle; var target = handle.Target!; if (target is PromiseHolder holder2) { @@ -411,13 +413,16 @@ public unsafe void ReleaseJSOwnedObjectByGCHandle(nint gcHandle) { if (!ThreadJsOwnedObjects.Remove(target)) { - throw new InvalidOperationException("ReleasePromiseHolder expected to find handle in ThreadJsOwnedObjects"); + throw new InvalidOperationException("ReleaseJSOwnedObjectByGCHandle expected in ThreadJsOwnedObjects"); } } handle.Free(); } } - return holder; + if (holder != null) + { + holder.Callback!(null); + } } public JSObject CreateCSOwnedProxy(nint jsHandle) @@ -473,7 +478,7 @@ public static void ReleaseCSOwnedObject(JSObject proxy, bool skipJS) } } -#endregion + #endregion #region Legacy 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 816177417202c..bda14974b08e3 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 @@ -27,8 +27,8 @@ public void InvokeJS() Span arguments = stackalloc JSMarshalerArgument[4]; ref JSMarshalerArgument args_exception = ref arguments[0]; ref JSMarshalerArgument args_return = ref arguments[1]; - args_exception.Initialize(); - args_return.Initialize(); + args_exception.InitializeImpl(); + args_return.InitializeImpl(); JSFunctionBinding.InvokeJSFunction(JSObject, arguments); @@ -62,8 +62,8 @@ public void InvokeJS(T arg1) ref JSMarshalerArgument args_return = ref arguments[1]; ref JSMarshalerArgument args_arg1 = ref arguments[2]; - args_exception.Initialize(); - args_return.Initialize(); + args_exception.InitializeImpl(); + args_return.InitializeImpl(); Arg1Marshaler(ref args_arg1, arg1); JSFunctionBinding.InvokeJSFunction(JSObject, arguments); @@ -99,8 +99,8 @@ public void InvokeJS(T1 arg1, T2 arg2) ref JSMarshalerArgument args_arg1 = ref arguments[2]; ref JSMarshalerArgument args_arg2 = ref arguments[3]; - args_exception.Initialize(); - args_return.Initialize(); + args_exception.InitializeImpl(); + args_return.InitializeImpl(); Arg1Marshaler(ref args_arg1, arg1); Arg2Marshaler(ref args_arg2, arg2); @@ -140,8 +140,8 @@ public void InvokeJS(T1 arg1, T2 arg2, T3 arg3) ref JSMarshalerArgument args_arg2 = ref arguments[3]; ref JSMarshalerArgument args_arg3 = ref arguments[4]; - args_exception.Initialize(); - args_return.Initialize(); + args_exception.InitializeImpl(); + args_return.InitializeImpl(); Arg1Marshaler(ref args_arg1, arg1); Arg2Marshaler(ref args_arg2, arg2); Arg3Marshaler(ref args_arg3, arg3); @@ -253,8 +253,8 @@ public TResult InvokeJS() Span arguments = stackalloc JSMarshalerArgument[4]; ref JSMarshalerArgument args_exception = ref arguments[0]; ref JSMarshalerArgument args_return = ref arguments[1]; - args_exception.Initialize(); - args_return.Initialize(); + args_exception.InitializeImpl(); + args_return.InitializeImpl(); JSFunctionBinding.InvokeJSFunction(JSObject, arguments); @@ -292,8 +292,8 @@ public TResult InvokeJS(T arg1) ref JSMarshalerArgument args_return = ref arguments[1]; ref JSMarshalerArgument args_arg1 = ref arguments[2]; - args_exception.Initialize(); - args_return.Initialize(); + args_exception.InitializeImpl(); + args_return.InitializeImpl(); Arg1Marshaler(ref args_arg1, arg1); JSFunctionBinding.InvokeJSFunction(JSObject, arguments); @@ -334,8 +334,8 @@ public TResult InvokeJS(T1 arg1, T2 arg2) ref JSMarshalerArgument args_arg1 = ref arguments[2]; ref JSMarshalerArgument args_arg2 = ref arguments[3]; - args_exception.Initialize(); - args_return.Initialize(); + args_exception.InitializeImpl(); + args_return.InitializeImpl(); Arg1Marshaler(ref args_arg1, arg1); Arg2Marshaler(ref args_arg2, arg2); @@ -380,8 +380,8 @@ public TResult InvokeJS(T1 arg1, T2 arg2, T3 arg3) ref JSMarshalerArgument args_arg2 = ref arguments[3]; ref JSMarshalerArgument args_arg3 = ref arguments[4]; - args_exception.Initialize(); - args_return.Initialize(); + args_exception.InitializeImpl(); + args_return.InitializeImpl(); Arg1Marshaler(ref args_arg1, arg1); Arg2Marshaler(ref args_arg2, arg2); Arg3Marshaler(ref args_arg3, arg3); 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 64ed99b42b6cd..bee3a28b231ae 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 @@ -45,36 +45,47 @@ public unsafe void ToManaged(out Task? value) value = null; return; } - PromiseHolder holder = JSProxyContext.CurrentOperationContext.GetPromiseHolder(slot.GCHandle); - TaskCompletionSource tcs = new TaskCompletionSource(holder); - ToManagedCallback callback = (JSMarshalerArgument* arguments_buffer) => + var ctx = JSProxyContext.CurrentOperationContext; + 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); + if (arguments_buffer == null) + { #if FEATURE_WASM_THREADS - JSProxyContext.PopOperation(); + JSProxyContext.PopOperation(); #endif - tcs.SetException(fail!); - } - else - { + 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); #if FEATURE_WASM_THREADS - JSProxyContext.PopOperation(); + JSProxyContext.PopOperation(); #endif - tcs.SetResult(); - } - // eventual exception is handled by caller - }; - holder.Callback = callback; - value = tcs.Task; + tcs.SetException(fail!); + } + else + { +#if FEATURE_WASM_THREADS + JSProxyContext.PopOperation(); +#endif + 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 + } } /// @@ -92,39 +103,50 @@ public unsafe void ToManaged(out Task? value, ArgumentToManagedCallback value = null; return; } - PromiseHolder holder = JSProxyContext.CurrentOperationContext.GetPromiseHolder(slot.GCHandle); - TaskCompletionSource tcs = new TaskCompletionSource(holder); - ToManagedCallback callback = (JSMarshalerArgument* arguments_buffer) => + var ctx = JSProxyContext.CurrentOperationContext; + lock (ctx) { - if (arguments_buffer == null) + var 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; - } + if (arguments_buffer == null) + { +#if FEATURE_WASM_THREADS + JSProxyContext.PopOperation(); +#endif + 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); + 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 FEATURE_WASM_THREADS - JSProxyContext.PopOperation(); + JSProxyContext.PopOperation(); #endif - if (fail == null) throw new InvalidOperationException(SR.FailedToMarshalException); - tcs.SetException(fail); - } - else - { - marshaler(ref arg_3, out T result); + if (fail == null) throw new InvalidOperationException(SR.FailedToMarshalException); + tcs.SetException(fail); + } + else + { + marshaler(ref arg_3, out T result); #if FEATURE_WASM_THREADS - JSProxyContext.PopOperation(); + JSProxyContext.PopOperation(); #endif - tcs.SetResult(result); - } - // eventual exception is handled by caller - }; - holder.Callback = callback; - value = tcs.Task; + 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 + } } @@ -365,8 +387,8 @@ private static void RejectPromise(JSObject holder, Exception ex) ref JSMarshalerArgument arg_handle = ref args[2]; ref JSMarshalerArgument arg_value = ref args[3]; - exc.Initialize(); - res.Initialize(); + exc.InitializeImpl(); + res.InitializeImpl(); // should update existing promise arg_handle.slot.Type = MarshalerType.TaskRejected; @@ -378,10 +400,12 @@ private static void RejectPromise(JSObject holder, Exception ex) // we can free the JSHandle here and the holder.resolve_or_reject will do the rest holder.DisposeImpl(skipJsCleanup: true); +#if !FEATURE_WASM_THREADS // order of operations with DisposeImpl matters - JavaScriptImports.ResolveOrRejectPromise(args); - -#if FEATURE_WASM_THREADS + JSFunctionBinding.ResolveOrRejectPromise(args); +#else + // order of operations with DisposeImpl matters + JSFunctionBinding.ResolveOrRejectPromise(args); JSProxyContext.PopOperation(); #endif } @@ -400,8 +424,8 @@ private static void ResolveVoidPromise(JSObject holder) ref JSMarshalerArgument arg_handle = ref args[2]; ref JSMarshalerArgument arg_value = ref args[3]; - exc.Initialize(); - res.Initialize(); + exc.InitializeImpl(); + res.InitializeImpl(); // should update existing promise arg_handle.slot.Type = MarshalerType.TaskResolved; @@ -412,10 +436,12 @@ private static void ResolveVoidPromise(JSObject holder) // we can free the JSHandle here and the holder.resolve_or_reject will do the rest holder.DisposeImpl(skipJsCleanup: true); +#if !FEATURE_WASM_THREADS // order of operations with DisposeImpl matters - JavaScriptImports.ResolveOrRejectPromise(args); - -#if FEATURE_WASM_THREADS + JSFunctionBinding.ResolveOrRejectPromise(args); +#else + // order of operations with DisposeImpl matters + JSFunctionBinding.ResolveOrRejectPromise(args); JSProxyContext.PopOperation(); #endif } @@ -424,7 +450,6 @@ private static void ResolvePromise(JSObject holder, T value, ArgumentToJSCall { holder.AssertNotDisposed(); #if FEATURE_WASM_THREADS - JSObject.AssertThreadAffinity(holder); JSProxyContext.PushOperationWithContext(holder.ProxyContext); #endif @@ -434,8 +459,8 @@ private static void ResolvePromise(JSObject holder, T value, ArgumentToJSCall ref JSMarshalerArgument arg_handle = ref args[2]; ref JSMarshalerArgument arg_value = ref args[3]; - exc.Initialize(); - res.Initialize(); + exc.InitializeImpl(); + res.InitializeImpl(); // should update existing promise arg_handle.slot.Type = MarshalerType.TaskResolved; @@ -447,10 +472,12 @@ private static void ResolvePromise(JSObject holder, T value, ArgumentToJSCall // we can free the JSHandle here and the holder.resolve_or_reject will do the rest holder.DisposeImpl(skipJsCleanup: true); +#if !FEATURE_WASM_THREADS // order of operations with DisposeImpl matters - JavaScriptImports.ResolveOrRejectPromise(args); - -#if FEATURE_WASM_THREADS + JSFunctionBinding.ResolveOrRejectPromise(args); +#else + // order of operations with DisposeImpl matters + JSFunctionBinding.ResolveOrRejectPromise(args); JSProxyContext.PopOperation(); #endif } From da9a509598bfb68e3bf09a900c50bb9ee4a8f1cd Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Sat, 16 Dec 2023 22:04:03 +0100 Subject: [PATCH 19/23] feedback --- .../JavaScript/JSFunctionBinding.cs | 4 + .../JavaScript/JSHostImplementation.Types.cs | 2 +- .../JavaScript/JSHostImplementation.cs | 1 + .../JavaScript/JSProxyContext.cs | 110 +++++++++--------- .../Marshaling/JSMarshalerArgument.Task.cs | 6 + ...me.InteropServices.JavaScript.Tests.csproj | 1 - 6 files changed, 69 insertions(+), 55 deletions(-) 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 25971296ad892..9940037c9bacd 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 @@ -209,6 +209,7 @@ internal static unsafe void InvokeJSFunction(JSObject jsFunction, Span arg ref JSMarshalerArgument exceptionArg = ref arguments[0]; if (exceptionArg.slot.Type != MarshalerType.None) { + // this will pop interop operation in MT JSHostImplementation.ThrowException(ref exceptionArg); } } 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 837b43c983c6f..67ce3dc5cd4c1 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,7 +12,7 @@ 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; #if FEATURE_WASM_THREADS 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 f36776628f0ef..5c965a8542a06 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 @@ -72,6 +72,7 @@ public static MethodInfo GetTaskResultMethodInfo(Type taskType) throw new InvalidOperationException(); } + /// In multi-threading build, this will pop interop operation [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void ThrowException(ref JSMarshalerArgument arg) { diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSProxyContext.cs b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSProxyContext.cs index a26cc57be1f86..18aea1b3255f1 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSProxyContext.cs +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSProxyContext.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; +using System.Diagnostics; using System.Runtime.CompilerServices; using System.Threading; using static System.Runtime.InteropServices.JavaScript.JSHostImplementation; @@ -83,8 +84,8 @@ public static JSProxyContext MainThreadContext private sealed class PendingOperation { public JSProxyContext? CapturedContext; - public bool Called; - public bool Multiple; + public bool PopScheduled; + public bool SkipAdditionalPushes; } [ThreadStatic] @@ -100,46 +101,34 @@ private static List OperationStack public static void PushOperationUnknownContext() { - var stack = OperationStack; - // for SchedulePopOperation() - if (stack.Count > 0 && stack[stack.Count - 1].Called) - { - PopOperation(); - } + // for SchedulePopOperation + var stack = PopScheduledOperation(); + // because this is called multiple times for each JSImport - if (stack.Count > 0 && stack[stack.Count - 1].Multiple) + if (SkipPushes(stack)) { return; } - stack.Add(new PendingOperation() { Multiple = true }); + stack.Add(new PendingOperation() { SkipAdditionalPushes = true }); } public static void PushOperationWithContext(JSProxyContext knownContext) { - var stack = OperationStack; - // for SchedulePopOperation() - if (stack.Count > 0 && stack[stack.Count - 1].Called) - { - PopOperation(); - } - // because this is called multiple times for each JSImport - if (stack.Count > 0 && stack[stack.Count - 1].Multiple) - { - return; - } + // for SchedulePopOperation + var stack = PopScheduledOperation(); - stack.Add(new PendingOperation() { CapturedContext = knownContext, Multiple = true }); + if (SkipPushes(stack)) Environment.FailFast("PushOperationWithContext should not be used with SkipAdditionalPushes"); + + stack.Add(new PendingOperation() { CapturedContext = knownContext }); } public static JSProxyContext PushOperationWithCurrentThreadContext() { - var stack = OperationStack; - // for SchedulePopOperation() - if (stack.Count > 0 && stack[stack.Count - 1].Called) - { - PopOperation(); - } + // for SchedulePopOperation + var stack = PopScheduledOperation(); + + if (SkipPushes(stack)) Environment.FailFast("PushOperationWithCurrentThreadContext should not be used with SkipAdditionalPushes"); var current = AssertIsInteropThread(); stack.Add(new PendingOperation { CapturedContext = current }); @@ -159,21 +148,44 @@ public static void SchedulePopOperation() { var stack = OperationStack; var op = stack[stack.Count - 1]; - if (op.Called) Environment.FailFast("SchedulePopOperation called twice"); - op.Called = true; + if (op.PopScheduled) Environment.FailFast("SchedulePopOperation called twice"); + op.PopScheduled = true; + op.SkipAdditionalPushes = false; } - public static void AssertOperationStack(int expected) + // for SchedulePopOperation + private static List PopScheduledOperation() { var stack = OperationStack; - if (stack.Count > 0 && stack[stack.Count - 1].Called) + if (stack.Count > 0 && stack[stack.Count - 1].PopScheduled) { PopOperation(); } + Debug.Assert(stack.Count == 0 || !stack[stack.Count - 1].PopScheduled); + return stack; + } + + private static bool SkipPushes(List stack) + { + return stack.Count > 0 && stack[stack.Count - 1].SkipAdditionalPushes; + } + + public static void SealSkipPushes() + { + var stack = OperationStack; + if (stack.Count < 1) Environment.FailFast("Unbalanced PopOperation");// there is no recovery + stack[stack.Count - 1].SkipAdditionalPushes = false; + } + + public static void AssertOperationStack(int expected) + { + // for SchedulePopOperation + var stack = PopScheduledOperation(); + var actual = stack.Count; - var multiple = stack.Count > 0 ? stack[stack.Count - 1].Multiple : false; - var called = stack.Count > 0 ? stack[stack.Count - 1].Called : false; - if (actual != expected) Environment.FailFast($"Unexpected OperationStack size expected: {expected} actual: {actual} called:{called} multiple:{multiple}"); + var skipAdditionalPushes = stack.Count > 0 ? stack[stack.Count - 1].SkipAdditionalPushes : false; + var popScheduled = stack.Count > 0 ? stack[stack.Count - 1].PopScheduled : false; + if (actual != expected) Environment.FailFast($"Unexpected OperationStack size expected: {expected} actual: {actual} popScheduled:{popScheduled} skipAdditionalPushes:{skipAdditionalPushes}"); } // TODO: sort generated ToJS() calls to make the capture context before we need to use it @@ -182,6 +194,7 @@ public static void CaptureContextFromParameter(JSProxyContext parameterContext) var stack = OperationStack; if (stack.Count < 1) Environment.FailFast("CaptureContextFromParameter could be only used during pending operation."); var pendingOperation = stack[stack.Count - 1]; + if (pendingOperation.PopScheduled) Environment.FailFast("CaptureContextFromParameter could be only used during pending operation."); var capturedContext = pendingOperation.CapturedContext; if (capturedContext != null && parameterContext != capturedContext) { @@ -357,11 +370,7 @@ public unsafe void ReleasePromiseHolder(nint holderGCHandle) PromiseHolder? holder; if (IsGCVHandle(holderGCHandle)) { - if (ThreadJsOwnedHolders.Remove(holderGCHandle, out holder)) - { - holder.GCHandle = IntPtr.Zero; - } - else + if (!ThreadJsOwnedHolders.Remove(holderGCHandle, out holder)) { throw new InvalidOperationException("ReleasePromiseHolder expected PromiseHolder " + holderGCHandle); } @@ -373,7 +382,6 @@ public unsafe void ReleasePromiseHolder(nint holderGCHandle) if (target is PromiseHolder holder2) { holder = holder2; - holder.GCHandle = IntPtr.Zero; } else { @@ -391,11 +399,7 @@ public unsafe void ReleaseJSOwnedObjectByGCHandle(nint gcHandle) { if (IsGCVHandle(gcHandle)) { - if (ThreadJsOwnedHolders.Remove(gcHandle, out holder)) - { - holder.GCHandle = IntPtr.Zero; - } - else + if (!ThreadJsOwnedHolders.Remove(gcHandle, out holder)) { throw new InvalidOperationException("ReleaseJSOwnedObjectByGCHandle expected in ThreadJsOwnedHolders"); } @@ -407,7 +411,6 @@ public unsafe void ReleaseJSOwnedObjectByGCHandle(nint gcHandle) if (target is PromiseHolder holder2) { holder = holder2; - holder.GCHandle = IntPtr.Zero; } else { @@ -557,8 +560,8 @@ private void Dispose(bool disposing) _OperationStack = null; #endif - // TODO: free unmanaged resources (unmanaged objects) and override finalizer - foreach (var jsObjectWeak in ThreadCsOwnedObjects.Values) + List> copy = new(ThreadCsOwnedObjects.Values); + foreach (var jsObjectWeak in copy) { if (jsObjectWeak.TryGetTarget(out var jso)) { @@ -583,12 +586,13 @@ private void Dispose(bool disposing) } } + ThreadCsOwnedObjects.Clear(); + ThreadJsOwnedObjects.Clear(); + JSVHandleFreeList.Clear(); + NextJSVHandle = IntPtr.Zero; + if (disposing) { - ThreadCsOwnedObjects.Clear(); - ThreadJsOwnedObjects.Clear(); - JSVHandleFreeList.Clear(); - NextJSVHandle = IntPtr.Zero; #if FEATURE_WASM_THREADS SynchronizationContext.Dispose(); #endif 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 bee3a28b231ae..4c91e9afa9159 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 @@ -55,6 +55,7 @@ public unsafe void ToManaged(out Task? value) if (arguments_buffer == null) { #if FEATURE_WASM_THREADS + // when we arrive here, we are called from CompleteTask, with will PushOperationWithCurrentThreadContext JSProxyContext.PopOperation(); #endif tcs.TrySetException(new TaskCanceledException("WebWorker which is origin of the Promise is being terminated.")); @@ -66,6 +67,7 @@ public unsafe void ToManaged(out Task? value) { arg_2.ToManaged(out Exception? fail); #if FEATURE_WASM_THREADS + // when we arrive here, we are called from CompleteTask, with will PushOperationWithCurrentThreadContext JSProxyContext.PopOperation(); #endif tcs.SetException(fail!); @@ -73,6 +75,7 @@ public unsafe void ToManaged(out Task? value) else { #if FEATURE_WASM_THREADS + // when we arrive here, we are called from CompleteTask, with will PushOperationWithCurrentThreadContext JSProxyContext.PopOperation(); #endif tcs.SetResult(); @@ -113,6 +116,7 @@ public unsafe void ToManaged(out Task? value, ArgumentToManagedCallback if (arguments_buffer == null) { #if FEATURE_WASM_THREADS + // when we arrive here, we are called from CompleteTask, with will PushOperationWithCurrentThreadContext JSProxyContext.PopOperation(); #endif tcs.TrySetException(new TaskCanceledException("WebWorker which is origin of the Promise is being terminated.")); @@ -125,6 +129,7 @@ public unsafe void ToManaged(out Task? value, ArgumentToManagedCallback { arg_2.ToManaged(out Exception? fail); #if FEATURE_WASM_THREADS + // when we arrive here, we are called from CompleteTask, with will PushOperationWithCurrentThreadContext JSProxyContext.PopOperation(); #endif if (fail == null) throw new InvalidOperationException(SR.FailedToMarshalException); @@ -134,6 +139,7 @@ public unsafe void ToManaged(out Task? value, ArgumentToManagedCallback { marshaler(ref arg_3, out T result); #if FEATURE_WASM_THREADS + // when we arrive here, we are called from CompleteTask, with will PushOperationWithCurrentThreadContext JSProxyContext.PopOperation(); #endif tcs.SetResult(result); 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 394d2abf22c23..8d6cf7aaf2809 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 @@ -12,7 +12,6 @@ true <_XUnitBackgroundExec>false - false From f56baa1ee117c25f5cdfd3ce360a73a7f58a9609 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Mon, 18 Dec 2023 18:48:03 +0100 Subject: [PATCH 20/23] remove complexity of PendingOperation maintanance - new JSMarshalerArgumentImpl.ContextHandle field - all calls from JS side will set JSMarshalerArgumentImpl.ContextHandle with current thread context - via set_arg_proxy_context - all calls to JS where runtime calls stackalloc JSMarshalerArgument will set JSMarshalerArgumentImpl.ContextHandle with Context of promise or function affinity - it will use InitializeWithContext, JSImportNoCapture - these are Task and Function callbacks to JS - JSMarshalerArgument.ToManaged always uses new JSMarshalerArgument.ToManagedContext - JSMarshalerArgument.ToManagedContext always uses JSMarshalerArgumentImpl.ContextHandle, no magic - JSMarshalerArgument.ToJS always uses new JSMarshalerArgument.ToJSContext - JSMarshalerArgument.ToJSContext during JSImport uses JSProxyContext.CurrentOperationContext otherwise JSMarshalerArgumentImpl.ContextHandle --- .../src/Interop/Browser/Interop.Runtime.cs | 2 +- .../CompatibilitySuppressions.WasmThreads.xml | 6 - .../JavaScript/Interop/JavaScriptExports.cs | 80 ++----- .../JavaScript/JSFunctionBinding.cs | 14 +- .../InteropServices/JavaScript/JSHost.cs | 7 - .../JavaScript/JSHostImplementation.cs | 6 +- .../JavaScript/JSMarshalerArgument.cs | 92 +++++++- .../JavaScript/JSProxyContext.cs | 186 +++++++-------- .../Marshaling/JSMarshalerArgument.Byte.cs | 3 +- .../Marshaling/JSMarshalerArgument.Double.cs | 3 +- .../JSMarshalerArgument.Exception.cs | 18 +- .../Marshaling/JSMarshalerArgument.Func.cs | 219 +++++++++++------- .../Marshaling/JSMarshalerArgument.Int32.cs | 3 +- .../JSMarshalerArgument.JSObject.cs | 15 +- .../Marshaling/JSMarshalerArgument.Object.cs | 3 +- .../Marshaling/JSMarshalerArgument.Task.cs | 88 +++---- .../JavaScript/JSImportExportTest.cs | 26 +-- .../JavaScript/JavaScriptTestHelper.cs | 6 - src/mono/wasm/runtime/corebindings.c | 2 +- src/mono/wasm/runtime/invoke-cs.ts | 13 +- src/mono/wasm/runtime/managed-exports.ts | 24 +- src/mono/wasm/runtime/marshal.ts | 22 +- .../wasm/runtime/pthreads/shared/index.ts | 13 +- src/mono/wasm/runtime/startup.ts | 1 - src/mono/wasm/runtime/types/internal.ts | 8 +- 25 files changed, 445 insertions(+), 415 deletions(-) diff --git a/src/libraries/Common/src/Interop/Browser/Interop.Runtime.cs b/src/libraries/Common/src/Interop/Browser/Interop.Runtime.cs index e22b5edf99bf1..fc627a32e1fa0 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.Runtime.InteropServices.JavaScript/src/CompatibilitySuppressions.WasmThreads.xml b/src/libraries/System.Runtime.InteropServices.JavaScript/src/CompatibilitySuppressions.WasmThreads.xml index 30d3e2e189d46..dfc469c8a165e 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/src/CompatibilitySuppressions.WasmThreads.xml +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/src/CompatibilitySuppressions.WasmThreads.xml @@ -6,10 +6,4 @@ ref/net9.0/System.Runtime.InteropServices.JavaScript.dll runtimes/browser/lib/net9.0/System.Runtime.InteropServices.JavaScript.dll - - CP0002 - M:System.Runtime.InteropServices.JavaScript.JSHost.AssertOperationStack(System.Int32) - ref/net9.0/System.Runtime.InteropServices.JavaScript.dll - runtimes/browser/lib/net9.0/System.Runtime.InteropServices.JavaScript.dll - 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 ad9ca6488c6a4..87676e9699cd9 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 @@ -27,8 +27,8 @@ public static void CallEntrypoint(JSMarshalerArgument* arguments_buffer) try { #if FEATURE_WASM_THREADS - // when we arrive here, we could assume that all proxies are owned by calling thread - JSProxyContext.PushOperationWithCurrentThreadContext(); + // when we arrive here, we are on the thread which owns the proxies + arg_exc.AssertCurrentThreadContext(); #endif arg_1.ToManaged(out IntPtr entrypointPtr); @@ -99,12 +99,6 @@ public static void CallEntrypoint(JSMarshalerArgument* arguments_buffer) arg_exc.ToJS(ex); } -#if FEATURE_WASM_THREADS - finally - { - JSProxyContext.PopOperation(); - } -#endif } public static void LoadLazyAssembly(JSMarshalerArgument* arguments_buffer) @@ -115,8 +109,8 @@ public static void LoadLazyAssembly(JSMarshalerArgument* arguments_buffer) try { #if FEATURE_WASM_THREADS - // when we arrive here, we could assume that all proxies are owned by calling thread - JSProxyContext.PushOperationWithCurrentThreadContext(); + // 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); @@ -128,12 +122,6 @@ public static void LoadLazyAssembly(JSMarshalerArgument* arguments_buffer) { arg_exc.ToJS(ex); } -#if FEATURE_WASM_THREADS - finally - { - JSProxyContext.PopOperation(); - } -#endif } public static void LoadSatelliteAssembly(JSMarshalerArgument* arguments_buffer) @@ -143,8 +131,8 @@ public static void LoadSatelliteAssembly(JSMarshalerArgument* arguments_buffer) try { #if FEATURE_WASM_THREADS - // when we arrive here, we could assume that all proxies are owned by calling thread - JSProxyContext.PushOperationWithCurrentThreadContext(); + // when we arrive here, we are on the thread which owns the proxies + arg_exc.AssertCurrentThreadContext(); #endif arg_1.ToManaged(out byte[]? dllBytes); @@ -155,12 +143,6 @@ public static void LoadSatelliteAssembly(JSMarshalerArgument* arguments_buffer) { arg_exc.ToJS(ex); } -#if FEATURE_WASM_THREADS - finally - { - JSProxyContext.PopOperation(); - } -#endif } // The JS layer invokes this method when the JS wrapper for a JS owned object @@ -174,20 +156,14 @@ public static void ReleaseJSOwnedObjectByGCHandle(JSMarshalerArgument* arguments try { - // when we arrive here, we assume that all proxies are owned by calling thread - var ctx = JSProxyContext.PushOperationWithCurrentThreadContext(); + // 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) { arg_exc.ToJS(ex); } -#if FEATURE_WASM_THREADS - finally - { - JSProxyContext.PopOperation(); - } -#endif } // the marshaled signature is: @@ -203,8 +179,8 @@ public static void CallDelegate(JSMarshalerArgument* arguments_buffer) try { #if FEATURE_WASM_THREADS - // when we arrive here, we could assume that all proxies are owned by calling thread - JSProxyContext.PushOperationWithCurrentThreadContext(); + // 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; @@ -222,12 +198,6 @@ public static void CallDelegate(JSMarshalerArgument* arguments_buffer) { arg_exc.ToJS(ex); } -#if FEATURE_WASM_THREADS - finally - { - JSProxyContext.PopOperation(); - } -#endif } // the marshaled signature is: @@ -241,8 +211,8 @@ public static void CompleteTask(JSMarshalerArgument* arguments_buffer) try { - // when we arrive here, we could assume that all proxies are owned by calling thread - var ctx = JSProxyContext.PushOperationWithCurrentThreadContext(); + // 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 @@ -269,13 +239,7 @@ public static void CompleteTask(JSMarshalerArgument* arguments_buffer) } catch (Exception ex) { -#if FEATURE_WASM_THREADS - var ctx = JSProxyContext.PushOperationWithCurrentThreadContext(); -#endif arg_exc.ToJS(ex); -#if FEATURE_WASM_THREADS - JSProxyContext.PopOperation(); -#endif } } @@ -288,8 +252,8 @@ 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 could assume that all proxies are owned by calling thread - JSProxyContext.PushOperationWithCurrentThreadContext(); + // 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) @@ -305,26 +269,10 @@ public static void GetManagedStackTrace(JSMarshalerArgument* arguments_buffer) { arg_exc.ToJS(ex); } -#if FEATURE_WASM_THREADS - finally - { - JSProxyContext.PopOperation(); - } -#endif } #if FEATURE_WASM_THREADS - public static void PushOperation() - { - JSProxyContext.PushOperationWithCurrentThreadContext(); - } - - public static void PopOperation() - { - JSProxyContext.PopOperation(); - } - // 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: 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 9940037c9bacd..25c0b74d4a80a 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 @@ -209,7 +209,6 @@ internal static unsafe void InvokeJSFunction(JSObject jsFunction, Span arguments) { #if FEATURE_WASM_THREADS + var targetContext = JSProxyContext.SealJSImportCapturing(); JSProxyContext.AssertIsInteropThread(); - JSProxyContext.SealSkipPushes(); + arguments[0].slot.ContextHandle = targetContext.ContextHandle; + arguments[1].slot.ContextHandle = targetContext.ContextHandle; +#else + var targetContext = JSProxyContext.MainThreadContext; #endif if (signature.IsAsync) { // pre-allocate the result handle and Task - var holder = new JSHostImplementation.PromiseHolder(JSProxyContext.CurrentOperationContext); + var holder = new JSHostImplementation.PromiseHolder(targetContext); arguments[1].slot.Type = MarshalerType.TaskPreCreated; arguments[1].slot.GCHandle = holder.GCHandle; } @@ -237,7 +240,6 @@ internal static unsafe void InvokeJSImportImpl(JSFunctionBinding signature, Span ref JSMarshalerArgument exceptionArg = ref arguments[0]; if (exceptionArg.slot.Type != MarshalerType.None) { - // this will pop interop operation in MT JSHostImplementation.ThrowException(ref exceptionArg); } } @@ -250,9 +252,6 @@ internal static unsafe void InvokeJSImportImpl(JSFunctionBinding signature, Span holderHandle.Free(); } } -#if FEATURE_WASM_THREADS - JSProxyContext.SchedulePopOperation(); -#endif } internal static unsafe JSFunctionBinding BindJSImportImpl(string functionName, string moduleName, ReadOnlySpan signatures) @@ -296,7 +295,6 @@ internal static unsafe void ResolveOrRejectPromise(Span arg ref JSMarshalerArgument exceptionArg = ref arguments[0]; if (exceptionArg.slot.Type != MarshalerType.None) { - // this will pop interop operation in MT 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 1464dfe7d437b..c1ec22aecdd7b 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 @@ -71,12 +71,5 @@ public static SynchronizationContext CurrentOrMainJSSynchronizationContext #endif } } - -#if FEATURE_WASM_THREADS - public static void AssertOperationStack(int expected) - { - JSProxyContext.AssertOperationStack(expected); - } -#endif } } 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 5c965a8542a06..877940b6bdacb 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 @@ -72,15 +72,11 @@ public static MethodInfo GetTaskResultMethodInfo(Type taskType) throw new InvalidOperationException(); } - /// In multi-threading build, this will pop interop operation [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void ThrowException(ref JSMarshalerArgument arg) { arg.ToManaged(out Exception? ex); -#if FEATURE_WASM_THREADS - JSProxyContext.PopOperation(); -#endif if (ex != null) { throw ex; @@ -225,7 +221,7 @@ public static void InstallWebWorkerInterop(bool isMainThread) ctx.AwaitNewData(); - Interop.Runtime.InstallWebWorkerInterop(); + Interop.Runtime.InstallWebWorkerInterop(proxyContext.ContextHandle); } public static void UninstallWebWorkerInterop() 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 e1aae8085b20b..9f94f0ec40dfd 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; } /// @@ -63,18 +68,97 @@ internal struct JSMarshalerArgumentImpl [MethodImpl(MethodImplOptions.AggressiveInlining)] public unsafe void Initialize() { - InitializeImpl(); + 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.PushOperationUnknownContext(); + JSProxyContext.JSImportWithUnknownContext(); + slot.ContextHandle = IntPtr.Zero; #endif } +#if FEATURE_WASM_THREADS [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal unsafe void InitializeImpl() + internal unsafe void InitializeWithContext(JSProxyContext knownProxyContext) { slot.Type = MarshalerType.None; + slot.ContextHandle = knownProxyContext.ContextHandle; + } +#endif + // this is always called from ToManaged() marshaler + internal JSProxyContext ToManagedContext + { + 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) + { + // throw new InvalidOperationException("ContextHandle not set"); + Environment.FailFast("ToManagedContext: ContextHandle not set"); + } + var argumentContext = (JSProxyContext)proxyContextGCHandle.Target!; + return argumentContext; +#endif + } + } + + // this is always called from ToJS() marshaler + internal JSProxyContext ToJSContext + { + 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("ToJSContext: ContextHandle not set"); + //throw new InvalidOperationException("ContextHandle was not set"); + } + 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 + internal JSProxyContext AssertCurrentThreadContext() + { +#if !FEATURE_WASM_THREADS + return JSProxyContext.MainThreadContext; +#else + var currentThreadContext = JSProxyContext.CurrentThreadContext; + if (currentThreadContext == null) + { + // must be called on thread with JS interop + Environment.FailFast(Environment.StackTrace); + } + if (slot.ContextHandle != currentThreadContext.ContextHandle) + { + // must be called on same thread which created the stack frame + Environment.FailFast(Environment.StackTrace); + } + return currentThreadContext; +#endif } } } diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSProxyContext.cs b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSProxyContext.cs index 18aea1b3255f1..5e2eb23f5151f 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSProxyContext.cs +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSProxyContext.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; -using System.Diagnostics; using System.Runtime.CompilerServices; using System.Threading; using static System.Runtime.InteropServices.JavaScript.JSHostImplementation; @@ -30,6 +29,7 @@ private JSProxyContext() { } #else + public nint ContextHandle; public nint NativeTID; public int ManagedTID; public bool IsMainThread; @@ -56,6 +56,7 @@ public JSProxyContext(bool isMainThread, JSSynchronizationContext synchronizatio NativeTID = GetNativeThreadId(); ManagedTID = Thread.CurrentThread.ManagedThreadId; IsMainThread = isMainThread; + ContextHandle = (nint)GCHandle.Alloc(this, GCHandleType.Normal); } #endif @@ -81,126 +82,85 @@ public static JSProxyContext MainThreadContext set => _MainThreadContext = value; } - private sealed class PendingOperation + public enum JSImportOperationState { - public JSProxyContext? CapturedContext; - public bool PopScheduled; - public bool SkipAdditionalPushes; + None, + JSImportParams, } [ThreadStatic] - private static List? _OperationStack; - private static List OperationStack - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get - { - return _OperationStack ??= new(); - } - } - - public static void PushOperationUnknownContext() - { - // for SchedulePopOperation - var stack = PopScheduledOperation(); - - // because this is called multiple times for each JSImport - if (SkipPushes(stack)) - { - return; - } - - stack.Add(new PendingOperation() { SkipAdditionalPushes = true }); - } - - public static void PushOperationWithContext(JSProxyContext knownContext) - { - // for SchedulePopOperation - var stack = PopScheduledOperation(); - - if (SkipPushes(stack)) Environment.FailFast("PushOperationWithContext should not be used with SkipAdditionalPushes"); - - stack.Add(new PendingOperation() { CapturedContext = knownContext }); - } - - public static JSProxyContext PushOperationWithCurrentThreadContext() - { - // for SchedulePopOperation - var stack = PopScheduledOperation(); - - if (SkipPushes(stack)) Environment.FailFast("PushOperationWithCurrentThreadContext should not be used with SkipAdditionalPushes"); - - var current = AssertIsInteropThread(); - stack.Add(new PendingOperation { CapturedContext = current }); - return current; - } + private static JSProxyContext? _CapturedOperationContext; + private static JSImportOperationState _CapturingState; + public static JSImportOperationState CapturingState => _CapturingState; - public static void PopOperation() + // there will be call to JS from JSImport generated code, but we don't know which target thread yet + public static void JSImportWithUnknownContext() { - var stack = OperationStack; - if (stack.Count < 1) Environment.FailFast("Unbalanced PopOperation");// there is no recovery - stack.RemoveAt(stack.Count - 1); + // 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; } - // this is here until we change the code generator and the API to Push/Pop the context - // we have no way how to Pop from the after marshaling the result - public static void SchedulePopOperation() + // there will be no capture during following call to JS + public static void JSImportNoCapture() { - var stack = OperationStack; - var op = stack[stack.Count - 1]; - if (op.PopScheduled) Environment.FailFast("SchedulePopOperation called twice"); - op.PopScheduled = true; - op.SkipAdditionalPushes = false; + _CapturingState = JSImportOperationState.None; + _CapturedOperationContext = null; } - // for SchedulePopOperation - private static List PopScheduledOperation() + // we are at the end of marshaling of the JSImport parameters + public static JSProxyContext SealJSImportCapturing() { - var stack = OperationStack; - if (stack.Count > 0 && stack[stack.Count - 1].PopScheduled) + if (_CapturingState != JSImportOperationState.JSImportParams) { - PopOperation(); + Environment.FailFast("Not in JSImport capturing phase"); + //throw new InvalidOperationException("Not in JSImport capturing phase"); } - Debug.Assert(stack.Count == 0 || !stack[stack.Count - 1].PopScheduled); - return stack; - } + _CapturingState = JSImportOperationState.None; + var capturedOperationContext = _CapturedOperationContext; + _CapturedOperationContext = null; - private static bool SkipPushes(List stack) - { - return stack.Count > 0 && stack[stack.Count - 1].SkipAdditionalPushes; + 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; } - public static void SealSkipPushes() + // 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) { - var stack = OperationStack; - if (stack.Count < 1) Environment.FailFast("Unbalanced PopOperation");// there is no recovery - stack[stack.Count - 1].SkipAdditionalPushes = false; - } + if (_CapturingState != JSImportOperationState.JSImportParams) + { + Environment.FailFast("CaptureContextFromParameter state mismatch"); + } - public static void AssertOperationStack(int expected) - { - // for SchedulePopOperation - var stack = PopScheduledOperation(); + var capturedContext = _CapturedOperationContext; - var actual = stack.Count; - var skipAdditionalPushes = stack.Count > 0 ? stack[stack.Count - 1].SkipAdditionalPushes : false; - var popScheduled = stack.Count > 0 ? stack[stack.Count - 1].PopScheduled : false; - if (actual != expected) Environment.FailFast($"Unexpected OperationStack size expected: {expected} actual: {actual} popScheduled:{popScheduled} skipAdditionalPushes:{skipAdditionalPushes}"); - } - - // TODO: sort generated ToJS() calls to make the capture context before we need to use it - public static void CaptureContextFromParameter(JSProxyContext parameterContext) - { - var stack = OperationStack; - if (stack.Count < 1) Environment.FailFast("CaptureContextFromParameter could be only used during pending operation."); - var pendingOperation = stack[stack.Count - 1]; - if (pendingOperation.PopScheduled) Environment.FailFast("CaptureContextFromParameter could be only used during pending operation."); - var capturedContext = pendingOperation.CapturedContext; - if (capturedContext != null && parameterContext != capturedContext) + 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"); } - pendingOperation.CapturedContext = capturedContext; } // Context flowing from parent thread into child tasks. @@ -222,25 +182,36 @@ public static JSProxyContext? ExecutionContext // - main thread, for calls from any other thread, like managed thread pool or `new Thread` public static JSProxyContext CurrentOperationContext { - [MethodImpl(MethodImplOptions.AggressiveInlining)] get { - var stack = OperationStack; - if (stack.Count < 1) throw new Exception("CurrentOperationContext could be only used during pending operation."); - var pendingOperation = stack[stack.Count - 1]; - if (pendingOperation.CapturedContext != null) + if (_CapturingState != JSImportOperationState.JSImportParams) { - return pendingOperation.CapturedContext; + throw new InvalidOperationException("Not in capturing phase of a JSImport"); + } + 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 - return MainThreadContext; + 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; } } @@ -468,7 +439,7 @@ public static void ReleaseCSOwnedObject(JSObject proxy, bool skipJS) var jsHandle = proxy.JSHandle; if (!ctx.ThreadCsOwnedObjects.Remove(jsHandle)) { - throw new InvalidOperationException("ReleaseCSOwnedObject expected to find registration for" + jsHandle); + throw new InvalidOperationException("ReleaseCSOwnedObject expected to find registration for " + jsHandle); }; if (!skipJS) { @@ -556,8 +527,7 @@ private void Dispose(bool disposing) { #if FEATURE_WASM_THREADS if (!IsCurrentThread()) throw new InvalidOperationException("JSProxyContext must be disposed on the thread which owns it."); - AssertOperationStack(0); - _OperationStack = null; + ((GCHandle)ContextHandle).Free(); #endif List> copy = new(ThreadCsOwnedObjects.Values); 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 50cc257e59b8a..5392fca48fae8 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 = JSProxyContext.CurrentOperationContext.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 cb88fde7ad715..9b7f48ed4b3ac 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 = JSProxyContext.CurrentOperationContext.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 d1318d27e419c..a18830859e4d8 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 = JSProxyContext.CurrentOperationContext.CreateCSOwnedProxy(slot.JSHandle); + var ctx = ToManagedContext; + jsException = ctx.CreateCSOwnedProxy(slot.JSHandle); } string? message; @@ -67,7 +68,16 @@ public unsafe void ToJS(Exception? value) { #if FEATURE_WASM_THREADS JSObject.AssertThreadAffinity(value); - JSProxyContext.CaptureContextFromParameter(jse.jsException.ProxyContext); + 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"); + } #endif // this is JSException roundtrip ObjectDisposedException.ThrowIf(jse.jsException.IsDisposed, value); @@ -78,7 +88,9 @@ public unsafe void ToJS(Exception? value) { ToJS(cpy.Message); slot.Type = MarshalerType.Exception; - slot.GCHandle = JSProxyContext.CurrentOperationContext.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 bda14974b08e3..ff3183c78ed39 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 = JSProxyContext.CurrentOperationContext.CreateCSOwnedProxy(jsHandle); + JSObject = holder; } public void InvokeJS() @@ -21,20 +21,21 @@ public void InvokeJS() #if FEATURE_WASM_THREADS JSObject.AssertThreadAffinity(JSObject); - JSProxyContext.PushOperationWithContext(JSObject.ProxyContext); #endif Span arguments = stackalloc JSMarshalerArgument[4]; ref JSMarshalerArgument args_exception = ref arguments[0]; ref JSMarshalerArgument args_return = ref arguments[1]; - args_exception.InitializeImpl(); - args_return.InitializeImpl(); - - JSFunctionBinding.InvokeJSFunction(JSObject, arguments); - #if FEATURE_WASM_THREADS - JSProxyContext.PopOperation(); + args_exception.InitializeWithContext(JSObject.ProxyContext); + args_return.InitializeWithContext(JSObject.ProxyContext); + JSProxyContext.JSImportNoCapture(); +#else + args_exception.Initialize(); + args_return.Initialize(); #endif + + JSFunctionBinding.InvokeJSFunction(JSObject, arguments); } } @@ -44,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 = JSProxyContext.CurrentOperationContext.CreateCSOwnedProxy(jsHandle); + JSObject = holder; Arg1Marshaler = arg1Marshaler; } @@ -54,7 +55,6 @@ public void InvokeJS(T arg1) { #if FEATURE_WASM_THREADS JSObject.AssertThreadAffinity(JSObject); - JSProxyContext.PushOperationWithContext(JSObject.ProxyContext); #endif Span arguments = stackalloc JSMarshalerArgument[4]; @@ -62,14 +62,19 @@ public void InvokeJS(T arg1) ref JSMarshalerArgument args_return = ref arguments[1]; ref JSMarshalerArgument args_arg1 = ref arguments[2]; - args_exception.InitializeImpl(); - args_return.InitializeImpl(); +#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); -#if FEATURE_WASM_THREADS - JSProxyContext.PopOperation(); -#endif } } @@ -79,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 = JSProxyContext.CurrentOperationContext.CreateCSOwnedProxy(jsHandle); + JSObject = holder; Arg1Marshaler = arg1Marshaler; Arg2Marshaler = arg2Marshaler; } @@ -90,7 +95,6 @@ public void InvokeJS(T1 arg1, T2 arg2) { #if FEATURE_WASM_THREADS JSObject.AssertThreadAffinity(JSObject); - JSProxyContext.PushOperationWithContext(JSObject.ProxyContext); #endif Span arguments = stackalloc JSMarshalerArgument[4]; @@ -99,15 +103,21 @@ public void InvokeJS(T1 arg1, T2 arg2) ref JSMarshalerArgument args_arg1 = ref arguments[2]; ref JSMarshalerArgument args_arg2 = ref arguments[3]; - args_exception.InitializeImpl(); - args_return.InitializeImpl(); +#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); -#if FEATURE_WASM_THREADS - JSProxyContext.PopOperation(); -#endif } } @@ -118,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 = JSProxyContext.CurrentOperationContext.CreateCSOwnedProxy(jsHandle); + JSObject = holder; Arg1Marshaler = arg1Marshaler; Arg2Marshaler = arg2Marshaler; Arg3Marshaler = arg3Marshaler; @@ -130,7 +140,6 @@ public void InvokeJS(T1 arg1, T2 arg2, T3 arg3) { #if FEATURE_WASM_THREADS JSObject.AssertThreadAffinity(JSObject); - JSProxyContext.PushOperationWithContext(JSObject.ProxyContext); #endif Span arguments = stackalloc JSMarshalerArgument[5]; @@ -140,16 +149,23 @@ public void InvokeJS(T1 arg1, T2 arg2, T3 arg3) ref JSMarshalerArgument args_arg2 = ref arguments[3]; ref JSMarshalerArgument args_arg3 = ref arguments[4]; - args_exception.InitializeImpl(); - args_return.InitializeImpl(); +#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); -#if FEATURE_WASM_THREADS - JSProxyContext.PopOperation(); -#endif } } @@ -166,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; } /// @@ -184,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; } /// @@ -204,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; } /// @@ -226,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 @@ -234,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 = JSProxyContext.CurrentOperationContext.CreateCSOwnedProxy(jsHandle); + JSObject = holder; ResMarshaler = resMarshaler; } @@ -244,7 +268,6 @@ public TResult InvokeJS() { #if FEATURE_WASM_THREADS JSObject.AssertThreadAffinity(JSObject); - JSProxyContext.PushOperationWithContext(JSObject.ProxyContext); #endif // JSObject (held by this lambda) would be collected by GC after the lambda is collected @@ -253,15 +276,19 @@ public TResult InvokeJS() Span arguments = stackalloc JSMarshalerArgument[4]; ref JSMarshalerArgument args_exception = ref arguments[0]; ref JSMarshalerArgument args_return = ref arguments[1]; - args_exception.InitializeImpl(); - args_return.InitializeImpl(); +#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); -#if FEATURE_WASM_THREADS - JSProxyContext.PopOperation(); -#endif + return res; } @@ -273,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 = JSProxyContext.CurrentOperationContext.CreateCSOwnedProxy(jsHandle); + JSObject = holder; Arg1Marshaler = arg1Marshaler; ResMarshaler = resMarshaler; } @@ -284,7 +311,6 @@ public TResult InvokeJS(T arg1) { #if FEATURE_WASM_THREADS JSObject.AssertThreadAffinity(JSObject); - JSProxyContext.PushOperationWithContext(JSObject.ProxyContext); #endif Span arguments = stackalloc JSMarshalerArgument[4]; @@ -292,16 +318,20 @@ public TResult InvokeJS(T arg1) ref JSMarshalerArgument args_return = ref arguments[1]; ref JSMarshalerArgument args_arg1 = ref arguments[2]; - args_exception.InitializeImpl(); - args_return.InitializeImpl(); +#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); ResMarshaler(ref args_return, out TResult res); -#if FEATURE_WASM_THREADS - JSProxyContext.PopOperation(); -#endif return res; } } @@ -313,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 = JSProxyContext.CurrentOperationContext.CreateCSOwnedProxy(jsHandle); + JSObject = holder; Arg1Marshaler = arg1Marshaler; Arg2Marshaler = arg2Marshaler; ResMarshaler = resMarshaler; @@ -325,7 +355,6 @@ public TResult InvokeJS(T1 arg1, T2 arg2) { #if FEATURE_WASM_THREADS JSObject.AssertThreadAffinity(JSObject); - JSProxyContext.PushOperationWithContext(JSObject.ProxyContext); #endif Span arguments = stackalloc JSMarshalerArgument[4]; @@ -334,17 +363,22 @@ public TResult InvokeJS(T1 arg1, T2 arg2) ref JSMarshalerArgument args_arg1 = ref arguments[2]; ref JSMarshalerArgument args_arg2 = ref arguments[3]; - args_exception.InitializeImpl(); - args_return.InitializeImpl(); +#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); ResMarshaler(ref args_return, out TResult res); -#if FEATURE_WASM_THREADS - JSProxyContext.PopOperation(); -#endif return res; } } @@ -357,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 = JSProxyContext.CurrentOperationContext.CreateCSOwnedProxy(jsHandle); + JSObject = holder; Arg1Marshaler = arg1Marshaler; Arg2Marshaler = arg2Marshaler; Arg3Marshaler = arg3Marshaler; @@ -370,7 +404,6 @@ public TResult InvokeJS(T1 arg1, T2 arg2, T3 arg3) { #if FEATURE_WASM_THREADS JSObject.AssertThreadAffinity(JSObject); - JSProxyContext.PushOperationWithContext(JSObject.ProxyContext); #endif Span arguments = stackalloc JSMarshalerArgument[5]; @@ -380,8 +413,17 @@ public TResult InvokeJS(T1 arg1, T2 arg2, T3 arg3) ref JSMarshalerArgument args_arg2 = ref arguments[3]; ref JSMarshalerArgument args_arg3 = ref arguments[4]; - args_exception.InitializeImpl(); - args_return.InitializeImpl(); +#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); @@ -389,10 +431,6 @@ public TResult InvokeJS(T1 arg1, T2 arg2, T3 arg3) JSFunctionBinding.InvokeJSFunction(JSObject, arguments); ResMarshaler(ref args_return, out TResult res); -#if FEATURE_WASM_THREADS - JSProxyContext.PopOperation(); -#endif - return res; } } @@ -412,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; } /// @@ -432,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; } @@ -455,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; } /// @@ -478,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; } /// @@ -497,7 +542,8 @@ public unsafe void ToJS(Action value) // eventual exception is handled by C# caller }; slot.Type = MarshalerType.Function; - slot.GCHandle = JSProxyContext.CurrentOperationContext.GetJSOwnedObjectGCHandle(cb); + var ctx = ToJSContext; + slot.GCHandle = ctx.GetJSOwnedObjectGCHandle(cb); } /// @@ -518,7 +564,8 @@ public unsafe void ToJS(Action value, ArgumentToManagedCallback arg1Mar // eventual exception is handled by C# caller }; slot.Type = MarshalerType.Action; - slot.GCHandle = JSProxyContext.CurrentOperationContext.GetJSOwnedObjectGCHandle(cb); + var ctx = ToJSContext; + slot.GCHandle = ctx.GetJSOwnedObjectGCHandle(cb); } /// @@ -543,7 +590,8 @@ public unsafe void ToJS(Action value, ArgumentToManagedCallback< // eventual exception is handled by C# caller }; slot.Type = MarshalerType.Action; - slot.GCHandle = JSProxyContext.CurrentOperationContext.GetJSOwnedObjectGCHandle(cb); + var ctx = ToJSContext; + slot.GCHandle = ctx.GetJSOwnedObjectGCHandle(cb); } /// @@ -572,7 +620,8 @@ public unsafe void ToJS(Action value, ArgumentToManagedC // eventual exception is handled by C# caller }; slot.Type = MarshalerType.Action; - slot.GCHandle = JSProxyContext.CurrentOperationContext.GetJSOwnedObjectGCHandle(cb); + var ctx = ToJSContext; + slot.GCHandle = ctx.GetJSOwnedObjectGCHandle(cb); } /// @@ -593,7 +642,8 @@ public unsafe void ToJS(Func value, ArgumentToJSCallback @@ -618,7 +668,8 @@ public unsafe void ToJS(Func value, ArgumentToManagedCal // eventual exception is handled by C# caller }; slot.Type = MarshalerType.Function; - slot.GCHandle = JSProxyContext.CurrentOperationContext.GetJSOwnedObjectGCHandle(cb); + var ctx = ToJSContext; + slot.GCHandle = ctx.GetJSOwnedObjectGCHandle(cb); } /// @@ -647,7 +698,8 @@ public unsafe void ToJS(Func value, ArgumentTo // eventual exception is handled by C# caller }; slot.Type = MarshalerType.Function; - slot.GCHandle = JSProxyContext.CurrentOperationContext.GetJSOwnedObjectGCHandle(cb); + var ctx = ToJSContext; + slot.GCHandle = ctx.GetJSOwnedObjectGCHandle(cb); } /// @@ -680,7 +732,8 @@ public unsafe void ToJS(Func value, Ar // eventual exception is handled by C# caller }; slot.Type = MarshalerType.Function; - slot.GCHandle = JSProxyContext.CurrentOperationContext.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 70878f4af122c..501484af3ab4f 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 = JSProxyContext.CurrentOperationContext.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 9d22dc30400ea..75a3a03d2bd75 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 = JSProxyContext.CurrentOperationContext.CreateCSOwnedProxy(slot.JSHandle); + var ctx = ToManagedContext; + value = ctx.CreateCSOwnedProxy(slot.JSHandle); } /// @@ -40,7 +41,17 @@ public void ToJS(JSObject? value) else { #if FEATURE_WASM_THREADS - JSProxyContext.CaptureContextFromParameter(value.ProxyContext); + 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"); + } #endif ObjectDisposedException.ThrowIf(value.IsDisposed, value); slot.Type = MarshalerType.JSObject; 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 08ea160f1b0af..75fa4d6aa2f0e 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 = JSProxyContext.CurrentOperationContext.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 4c91e9afa9159..51bfa81989133 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 @@ -45,7 +45,7 @@ public unsafe void ToManaged(out Task? value) value = null; return; } - var ctx = JSProxyContext.CurrentOperationContext; + var ctx = ToManagedContext; lock (ctx) { PromiseHolder holder = ctx.GetPromiseHolder(slot.GCHandle); @@ -54,10 +54,6 @@ public unsafe void ToManaged(out Task? value) { if (arguments_buffer == null) { -#if FEATURE_WASM_THREADS - // when we arrive here, we are called from CompleteTask, with will PushOperationWithCurrentThreadContext - JSProxyContext.PopOperation(); -#endif tcs.TrySetException(new TaskCanceledException("WebWorker which is origin of the Promise is being terminated.")); return; } @@ -66,18 +62,10 @@ public unsafe void ToManaged(out Task? value) if (arg_2.slot.Type != MarshalerType.None) { arg_2.ToManaged(out Exception? fail); -#if FEATURE_WASM_THREADS - // when we arrive here, we are called from CompleteTask, with will PushOperationWithCurrentThreadContext - JSProxyContext.PopOperation(); -#endif tcs.SetException(fail!); } else { -#if FEATURE_WASM_THREADS - // when we arrive here, we are called from CompleteTask, with will PushOperationWithCurrentThreadContext - JSProxyContext.PopOperation(); -#endif tcs.SetResult(); } // eventual exception is handled by caller @@ -106,7 +94,7 @@ public unsafe void ToManaged(out Task? value, ArgumentToManagedCallback value = null; return; } - var ctx = JSProxyContext.CurrentOperationContext; + var ctx = ToManagedContext; lock (ctx) { var holder = ctx.GetPromiseHolder(slot.GCHandle); @@ -115,10 +103,6 @@ public unsafe void ToManaged(out Task? value, ArgumentToManagedCallback { if (arguments_buffer == null) { -#if FEATURE_WASM_THREADS - // when we arrive here, we are called from CompleteTask, with will PushOperationWithCurrentThreadContext - JSProxyContext.PopOperation(); -#endif tcs.TrySetException(new TaskCanceledException("WebWorker which is origin of the Promise is being terminated.")); return; } @@ -128,20 +112,12 @@ public unsafe void ToManaged(out Task? value, ArgumentToManagedCallback if (arg_2.slot.Type != MarshalerType.None) { arg_2.ToManaged(out Exception? fail); -#if FEATURE_WASM_THREADS - // when we arrive here, we are called from CompleteTask, with will PushOperationWithCurrentThreadContext - JSProxyContext.PopOperation(); -#endif if (fail == null) throw new InvalidOperationException(SR.FailedToMarshalException); tcs.SetException(fail); } else { marshaler(ref arg_3, out T result); -#if FEATURE_WASM_THREADS - // when we arrive here, we are called from CompleteTask, with will PushOperationWithCurrentThreadContext - JSProxyContext.PopOperation(); -#endif tcs.SetResult(result); } // eventual exception is handled by caller @@ -192,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 = JSProxyContext.CurrentOperationContext.AllocJSVHandle(); + slot.JSHandle = ctx.AllocJSVHandle(); slot.Type = MarshalerType.Task; } else @@ -204,7 +182,7 @@ internal void ToJSDynamic(Task? value) // promise and handle is pre-allocated in slot.JSHandle } - var taskHolder = JSProxyContext.CurrentOperationContext.CreateCSOwnedProxy(slot.JSHandle); + var taskHolder = ctx.CreateCSOwnedProxy(slot.JSHandle); #if FEATURE_WASM_THREADS task.ContinueWith(Complete, taskHolder, TaskScheduler.FromCurrentSynchronizationContext()); @@ -270,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 = JSProxyContext.CurrentOperationContext.AllocJSVHandle(); + slot.JSHandle = ctx.AllocJSVHandle(); slot.Type = MarshalerType.Task; } else @@ -282,7 +262,7 @@ public void ToJS(Task? value) // promise and handle is pre-allocated in slot.JSHandle } - var taskHolder = JSProxyContext.CurrentOperationContext.CreateCSOwnedProxy(slot.JSHandle); + var taskHolder = ctx.CreateCSOwnedProxy(slot.JSHandle); #if FEATURE_WASM_THREADS task.ContinueWith(Complete, taskHolder, TaskScheduler.FromCurrentSynchronizationContext()); @@ -341,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 = JSProxyContext.CurrentOperationContext.AllocJSVHandle(); + slot.JSHandle = ctx.AllocJSVHandle(); slot.Type = MarshalerType.Task; } else @@ -353,7 +334,7 @@ public void ToJS(Task? value, ArgumentToJSCallback marshaler) // promise and handle is pre-allocated in slot.JSHandle } - var taskHolder = JSProxyContext.CurrentOperationContext.CreateCSOwnedProxy(slot.JSHandle); + var taskHolder = ctx.CreateCSOwnedProxy(slot.JSHandle); #if FEATURE_WASM_THREADS task.ContinueWith(Complete, new HolderAndMarshaler(taskHolder, marshaler), TaskScheduler.FromCurrentSynchronizationContext()); @@ -384,7 +365,6 @@ private static void RejectPromise(JSObject holder, Exception ex) #if FEATURE_WASM_THREADS JSObject.AssertThreadAffinity(holder); - JSProxyContext.PushOperationWithContext(holder.ProxyContext); #endif Span args = stackalloc JSMarshalerArgument[4]; @@ -393,8 +373,16 @@ private static void RejectPromise(JSObject holder, Exception ex) ref JSMarshalerArgument arg_handle = ref args[2]; ref JSMarshalerArgument arg_value = ref args[3]; - exc.InitializeImpl(); - res.InitializeImpl(); +#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; @@ -412,7 +400,6 @@ private static void RejectPromise(JSObject holder, Exception ex) #else // order of operations with DisposeImpl matters JSFunctionBinding.ResolveOrRejectPromise(args); - JSProxyContext.PopOperation(); #endif } @@ -421,7 +408,6 @@ private static void ResolveVoidPromise(JSObject holder) holder.AssertNotDisposed(); #if FEATURE_WASM_THREADS JSObject.AssertThreadAffinity(holder); - JSProxyContext.PushOperationWithContext(holder.ProxyContext); #endif Span args = stackalloc JSMarshalerArgument[4]; @@ -430,8 +416,16 @@ private static void ResolveVoidPromise(JSObject holder) ref JSMarshalerArgument arg_handle = ref args[2]; ref JSMarshalerArgument arg_value = ref args[3]; - exc.InitializeImpl(); - res.InitializeImpl(); +#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; @@ -448,7 +442,6 @@ private static void ResolveVoidPromise(JSObject holder) #else // order of operations with DisposeImpl matters JSFunctionBinding.ResolveOrRejectPromise(args); - JSProxyContext.PopOperation(); #endif } @@ -456,7 +449,7 @@ private static void ResolvePromise(JSObject holder, T value, ArgumentToJSCall { holder.AssertNotDisposed(); #if FEATURE_WASM_THREADS - JSProxyContext.PushOperationWithContext(holder.ProxyContext); + JSObject.AssertThreadAffinity(holder); #endif Span args = stackalloc JSMarshalerArgument[4]; @@ -465,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]; - exc.InitializeImpl(); - res.InitializeImpl(); +#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; @@ -484,7 +485,6 @@ private static void ResolvePromise(JSObject holder, T value, ArgumentToJSCall #else // order of operations with DisposeImpl matters JSFunctionBinding.ResolveOrRejectPromise(args); - JSProxyContext.PopOperation(); #endif } } 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 32a4e8d9b5cc8..9fabc04697af8 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 @@ -17,7 +17,7 @@ public class JSImportExportTest : IAsyncLifetime [Fact] public unsafe void StructSize() { - Assert.Equal(16, sizeof(JSMarshalerArgument)); + Assert.Equal(32, sizeof(JSMarshalerArgument)); } [Fact] @@ -382,13 +382,6 @@ static void dummyDelegateA() public void JsImportObjectArrayThrows(object[]? expected) { Assert.Throws(() => JavaScriptTestHelper.echo1_ObjectArray(expected)); - // this will make OperationStack unbalanced - // because the exceptions is throwed from ToJs() marshaler before the call to JSFunctionBinding.InvokeJS - // which is expected until we make operation context part of the code generator - AssertOperationStack(1); - // any subsequent interop operation will fix that - JavaScriptTestHelper.echo1_Int32(1); - AssertOperationStack(0); } [Fact] @@ -1841,7 +1834,6 @@ public void JsImportBackCallback_FunctionIntInt() int called = -1; Func res = JavaScriptTestHelper.backback_FuncIntFuncInt((a) => { - AssertOperationStack(2); called = a; return a; }, 42); @@ -1858,7 +1850,6 @@ public void JsImportBackCallback_FunctionIntIntIntInt() int calledB = -1; Func res = JavaScriptTestHelper.backback_FuncIntIntFuncIntInt((a, b) => { - AssertOperationStack(2); calledA = a; calledB = b; return a + b; @@ -1878,7 +1869,6 @@ public void JsImportCallback_ActionIntInt() int calledB = -1; JavaScriptTestHelper.back3_ActionIntInt((a, b) => { - AssertOperationStack(2); calledA = a; calledB = b; }, 42, 43); @@ -1893,7 +1883,6 @@ public void JsImportCallback_ActionLongLong() long calledB = -1; JavaScriptTestHelper.back3_ActionLongLong((a, b) => { - AssertOperationStack(2); calledA = a; calledB = b; }, 42, 43); @@ -1908,7 +1897,6 @@ public void JsImportCallback_ActionIntLong() long calledB = -1; JavaScriptTestHelper.back3_ActionIntLong((a, b) => { - AssertOperationStack(2); calledA = a; calledB = b; }, 42, 43); @@ -1923,7 +1911,6 @@ public void JsImportCallback_ActionIntThrow() Exception expected = new Exception("test!!"); Exception actual = Assert.Throws(() => JavaScriptTestHelper.back3_ActionInt((a) => { - AssertOperationStack(2); called = a; throw expected; }, 42)); @@ -1937,14 +1924,12 @@ public void JsExportCallback_FunctionIntInt() int called = -1; var chain = JavaScriptTestHelper.invoke1_FuncOfIntInt((int a) => { - AssertOperationStack(4); called = a; return a; }, nameof(JavaScriptTestHelper.BackFuncOfIntInt)); Assert.Equal(-1, called); var actual = chain(42); - AssertOperationStack(0); Assert.Equal(42, actual); Assert.Equal(42, called); } @@ -1956,14 +1941,12 @@ public void JsExportCallback_FunctionIntIntThrow() var expected = new Exception("test!!"); var chain = JavaScriptTestHelper.invoke1_FuncOfIntInt((int a) => { - AssertOperationStack(4); called = a; throw expected; }, nameof(JavaScriptTestHelper.BackFuncOfIntInt)); Assert.Equal(-1, called); var actual = Assert.Throws(() => chain(42)); - AssertOperationStack(0); Assert.Equal(42, called); Assert.Same(expected, actual); } @@ -2140,12 +2123,5 @@ public static DateTimeOffset TrimNano(DateTimeOffset date) { return new DateTime(date.Ticks - (date.Ticks % TimeSpan.TicksPerMillisecond), DateTimeKind.Utc); } - - private static void AssertOperationStack(int cnt) - { -#if FEATURE_WASM_THREADS - JSHost.AssertOperationStack(cnt); -#endif - } } } 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 d6590740d8b69..559b9b4ff88e8 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 @@ -997,9 +997,6 @@ public static JSObject EchoIJSObject([JSMarshalAs] JSObject arg1) static JSObject _module; public static async Task InitializeAsync() { -#if FEATURE_WASM_THREADS - JSHost.AssertOperationStack(0); -#endif if (_module == null) { _module = await JSHost.ImportAsync("JavaScriptTestHelper", "../JavaScriptTestHelper.mjs"); ; @@ -1015,9 +1012,6 @@ public static async Task InitializeAsync() public static Task DisposeAsync() { -#if FEATURE_WASM_THREADS - JSHost.AssertOperationStack(0); -#endif _module?.Dispose(); _module = null; return Task.CompletedTask; diff --git a/src/mono/wasm/runtime/corebindings.c b/src/mono/wasm/runtime/corebindings.c index 00fed13f2d0a5..8657cd261459f 100644 --- a/src/mono/wasm/runtime/corebindings.c +++ b/src/mono/wasm/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/wasm/runtime/invoke-cs.ts b/src/mono/wasm/runtime/invoke-cs.ts index 97fb98486c004..8787327eeb83b 100644 --- a/src/mono/wasm/runtime/invoke-cs.ts +++ b/src/mono/wasm/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"; @@ -352,20 +352,15 @@ type BindingClosure = { isDisposed: boolean, } -export function invoke_method_and_handle_exception(method: MonoMethod, args: JSMarshalerArguments, skipPushOperation?: boolean): void { +export function invoke_method_and_handle_exception(method: MonoMethod, args: JSMarshalerArguments): void { assert_bindings(); const fail_root = mono_wasm_new_root(); try { - if (MonoWasmThreads && !skipPushOperation) { - // TODO in future, the generated code of JSExport could do this - runtimeHelpers.javaScriptExports.push_operation(); + if (MonoWasmThreads) { + set_args_context(args); } const fail = cwraps.mono_wasm_invoke_method_bound(method, args, fail_root.address); if (fail) runtimeHelpers.abort("ERR24: Unexpected error: " + monoStringToString(fail_root)); - if (MonoWasmThreads && !skipPushOperation) { - // TODO in future, the generated code of JSExport could do this - runtimeHelpers.javaScriptExports.pop_operation(); - } if (is_args_exception(args)) { const exc = get_arg(args, 0); throw marshal_exception_to_js(exc); diff --git a/src/mono/wasm/runtime/managed-exports.ts b/src/mono/wasm/runtime/managed-exports.ts index 03fc8a0541ed0..fff5deb4bb909 100644 --- a/src/mono/wasm/runtime/managed-exports.ts +++ b/src/mono/wasm/runtime/managed-exports.ts @@ -24,10 +24,6 @@ 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 push_operation = MonoWasmThreads ? get_method("PushOperation") : undefined; - mono_assert(!MonoWasmThreads || push_operation, "Can't find PushOperation method"); - const pop_operation = MonoWasmThreads ? get_method("PopOperation") : undefined; - mono_assert(!MonoWasmThreads || pop_operation, "Can't find PopOperation 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"); @@ -63,7 +59,7 @@ export function init_managed_exports(): void { // because this is async, we could pre-allocate the promise let promise = begin_marshal_task_to_js(res, MarshalerType.TaskPreCreated, marshal_int32_to_js); - invoke_method_and_handle_exception(call_entry_point, args, true); + invoke_method_and_handle_exception(call_entry_point, args); // in case the C# side returned synchronously promise = end_marshal_task_to_js(args, marshal_int32_to_js, promise); @@ -85,7 +81,7 @@ export function init_managed_exports(): void { const arg1 = get_arg(args, 2); set_arg_type(arg1, MarshalerType.Array); marshal_array_to_cs(arg1, dll, MarshalerType.Byte); - invoke_method_and_handle_exception(load_satellite_assembly_method, args, true); + invoke_method_and_handle_exception(load_satellite_assembly_method, args); } finally { Module.stackRestore(sp); } @@ -100,7 +96,7 @@ export function init_managed_exports(): void { set_arg_type(arg2, MarshalerType.Array); marshal_array_to_cs(arg1, dll, MarshalerType.Byte); marshal_array_to_cs(arg2, pdb, MarshalerType.Byte); - invoke_method_and_handle_exception(load_lazy_assembly_method, args, true); + invoke_method_and_handle_exception(load_lazy_assembly_method, args); } finally { Module.stackRestore(sp); } @@ -114,7 +110,7 @@ export function init_managed_exports(): void { const arg1 = get_arg(args, 2); set_arg_type(arg1, MarshalerType.Object); set_gc_handle(arg1, gc_handle); - invoke_method_and_handle_exception(release_js_owned_object_by_gc_handle_method, args, true); + invoke_method_and_handle_exception(release_js_owned_object_by_gc_handle_method, args); } finally { Module.stackRestore(sp); } @@ -136,7 +132,7 @@ export function init_managed_exports(): void { mono_assert(res_converter, "res_converter missing"); res_converter(arg3, data); } - invoke_method_and_handle_exception(complete_task_method, args, true); + invoke_method_and_handle_exception(complete_task_method, args); } finally { Module.stackRestore(sp); } @@ -165,7 +161,7 @@ export function init_managed_exports(): void { arg3_converter(arg4, arg3_js); } - invoke_method_and_handle_exception(call_delegate_method, args, true); + invoke_method_and_handle_exception(call_delegate_method, args); if (res_converter) { const res = get_arg(args, 1); @@ -185,19 +181,13 @@ export function init_managed_exports(): void { set_arg_type(arg1, MarshalerType.Exception); set_gc_handle(arg1, exception_gc_handle); - invoke_method_and_handle_exception(get_managed_stack_trace_method, args, true); + invoke_method_and_handle_exception(get_managed_stack_trace_method, args); const res = get_arg(args, 1); return marshal_string_to_js(res); } finally { Module.stackRestore(sp); } }; - if (MonoWasmThreads && push_operation) { - runtimeHelpers.javaScriptExports.push_operation = () => invoke_method_raw(push_operation); - } - if (MonoWasmThreads && pop_operation) { - runtimeHelpers.javaScriptExports.pop_operation = () => invoke_method_raw(pop_operation); - } if (MonoWasmThreads && install_main_synchronization_context) { runtimeHelpers.javaScriptExports.install_main_synchronization_context = () => invoke_method_raw(install_main_synchronization_context); } diff --git a/src/mono/wasm/runtime/marshal.ts b/src/mono/wasm/runtime/marshal.ts index a788512e9d567..c91dbe0749e4b 100644 --- a/src/mono/wasm/runtime/marshal.ts +++ b/src/mono/wasm/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,9 @@ export function alloc_stack_frame(size: number): JSMarshalerArguments { const bytes = JavaScriptMarshalerArgSize * size; const args = Module.stackAlloc(bytes) as any; _zero_region(args, bytes); + if (MonoWasmThreads) { + set_args_context(args); + } return args; } @@ -40,6 +43,14 @@ export function is_args_exception(args: JSMarshalerArguments): boolean { return exceptionType !== MarshalerType.None; } +export function set_args_context(args: JSMarshalerArguments): void { + 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 +263,15 @@ export function get_arg_js_handle(arg: JSMarshalerArgument): JSHandle { return getI32(arg + 4); } +export function set_arg_proxy_context(arg: JSMarshalerArgument): void { + 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); + if (MonoWasmThreads) 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); + if (MonoWasmThreads) 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/wasm/runtime/pthreads/shared/index.ts b/src/mono/wasm/runtime/pthreads/shared/index.ts index af991aa7b3fe3..499ff3f129c0d 100644 --- a/src/mono/wasm/runtime/pthreads/shared/index.ts +++ b/src/mono/wasm/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/wasm/runtime/startup.ts b/src/mono/wasm/runtime/startup.ts index 52a451acd273d..6ec998ccaabeb 100644 --- a/src/mono/wasm/runtime/startup.ts +++ b/src/mono/wasm/runtime/startup.ts @@ -290,7 +290,6 @@ async function onRuntimeInitializedAsync(userOnRuntimeInitialized: () => void) { if (MonoWasmThreads) { runtimeHelpers.javaScriptExports.install_main_synchronization_context(); - runtimeHelpers.jsSynchronizationContextInstalled = true; } if (!runtimeHelpers.mono_wasm_runtime_is_ready) mono_wasm_runtime_ready(); diff --git a/src/mono/wasm/runtime/types/internal.ts b/src/mono/wasm/runtime/types/internal.ts index 37f6bdc33fd26..6e7b649a56c6b 100644 --- a/src/mono/wasm/runtime/types/internal.ts +++ b/src/mono/wasm/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, @@ -351,12 +351,6 @@ export interface JavaScriptExports { // the marshaled signature is: void InstallMainSynchronizationContext() install_main_synchronization_context(): void; - // the marshaled signature is: void PushOperation() - push_operation(): void; - - // the marshaled signature is: void PopOperation() - pop_operation(): void; - // the marshaled signature is: string GetManagedStackTrace(GCHandle exception) get_managed_stack_trace(exception_gc_handle: GCHandle): string | null From ae057286674cc62331b9fda2ef01fc21eb59325f Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Mon, 18 Dec 2023 20:42:14 +0100 Subject: [PATCH 21/23] warnings --- .../InteropServices/JavaScript/JSMarshalerArgument.cs | 6 ++++++ 1 file changed, 6 insertions(+) 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 9f94f0ec40dfd..55c42186f05cf 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 @@ -86,7 +86,9 @@ internal unsafe void InitializeWithContext(JSProxyContext knownProxyContext) } #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 { @@ -114,7 +116,9 @@ internal JSProxyContext ToManagedContext } // 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 { @@ -141,7 +145,9 @@ internal JSProxyContext ToJSContext } // 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; From 9b8e3add0e21be26bbc2b2268375308d2e729ad2 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Tue, 19 Dec 2023 14:51:03 +0100 Subject: [PATCH 22/23] feedback cancellation thread safe Active issue https://github.com/dotnet/runtime/issues/96173 --- .../System.Net.Http.Functional.Tests.csproj | 2 + .../System.Net.WebSockets.Client.Tests.csproj | 3 ++ .../JavaScript/CancelablePromise.cs | 44 +++++++++++-------- .../JavaScript/JSHostImplementation.Types.cs | 1 + .../JavaScript/JSMarshalerArgument.cs | 12 ++--- .../JavaScript/JSProxyContext.cs | 34 ++++++++------ .../JavaScript/JSSynchronizationContext.cs | 4 +- .../JSMarshalerArgument.Exception.cs | 4 +- .../JSMarshalerArgument.JSObject.cs | 4 +- src/mono/wasm/runtime/gc-handles.ts | 1 + src/mono/wasm/runtime/invoke-cs.ts | 4 +- src/mono/wasm/runtime/marshal.ts | 10 ++--- 12 files changed, 70 insertions(+), 53 deletions(-) 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 2451c867a9489..773d73a39a094 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 b797fcf189459..54be463b694e1 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/src/System/Runtime/InteropServices/JavaScript/CancelablePromise.cs b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/CancelablePromise.cs index 8e447992f4b1d..240199d470238 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 @@ -22,20 +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 - if (holder.ProxyContext.IsCurrentThread()) +#if !FEATURE_WASM_THREADS + if (holder.IsDisposed) { - _CancelPromise(holder.GCHandle); return; } - + _CancelPromise(holder.GCHandle); +#else holder.ProxyContext.SynchronizationContext.Post(static (object? h) => { var holder = (JSHostImplementation.PromiseHolder)h!; -#endif + lock (holder.ProxyContext) + { + if (holder.IsDisposed) + { + return; + } + } _CancelPromise(holder.GCHandle); -#if FEATURE_WASM_THREADS }, holder); #endif } @@ -50,23 +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 - if (holder.ProxyContext.IsCurrentThread()) +#if !FEATURE_WASM_THREADS + if (holder.IsDisposed) { - _CancelPromise(holder.GCHandle); - callback.Invoke(state); return; } - - holder.ProxyContext.SynchronizationContext.Post((object? h) => + _CancelPromise(holder.GCHandle); + callback.Invoke(state); +#else + holder.ProxyContext.SynchronizationContext.Post(_ => { - var holder = (JSHostImplementation.PromiseHolder)h!; -#endif + 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/JSHostImplementation.Types.cs b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSHostImplementation.Types.cs index 67ce3dc5cd4c1..8d4dd3e6a3e87 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 @@ -15,6 +15,7 @@ public sealed class PromiseHolder public readonly nint GCHandle; // could be also virtual GCVHandle public ToManagedCallback? Callback; public JSProxyContext ProxyContext; + public bool IsDisposed; #if FEATURE_WASM_THREADS public ManualResetEventSlim? CallbackReady; #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 55c42186f05cf..fedb96a751662 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 @@ -106,8 +106,7 @@ internal JSProxyContext ToManagedContext var proxyContextGCHandle = (GCHandle)slot.ContextHandle; if (proxyContextGCHandle == default) { - // throw new InvalidOperationException("ContextHandle not set"); - Environment.FailFast("ToManagedContext: ContextHandle not set"); + Environment.FailFast($"ContextHandle not set, ManagedThreadId: {Environment.CurrentManagedThreadId}. {Environment.NewLine} {Environment.StackTrace}"); } var argumentContext = (JSProxyContext)proxyContextGCHandle.Target!; return argumentContext; @@ -135,8 +134,7 @@ internal JSProxyContext ToJSContext var proxyContextGCHandle = slot.ContextHandle; if (proxyContextGCHandle == IntPtr.Zero) { - Environment.FailFast("ToJSContext: ContextHandle not set"); - //throw new InvalidOperationException("ContextHandle was not set"); + Environment.FailFast($"ContextHandle not set, ManagedThreadId: {Environment.CurrentManagedThreadId}. {Environment.NewLine} {Environment.StackTrace}"); } var argumentContext = (JSProxyContext)((GCHandle)proxyContextGCHandle).Target!; return argumentContext; @@ -155,13 +153,11 @@ internal JSProxyContext AssertCurrentThreadContext() var currentThreadContext = JSProxyContext.CurrentThreadContext; if (currentThreadContext == null) { - // must be called on thread with JS interop - Environment.FailFast(Environment.StackTrace); + Environment.FailFast($"Must be called on same thread with JS interop, ManagedThreadId: {Environment.CurrentManagedThreadId}. {Environment.NewLine} {Environment.StackTrace}"); } if (slot.ContextHandle != currentThreadContext.ContextHandle) { - // must be called on same thread which created the stack frame - Environment.FailFast(Environment.StackTrace); + 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/JSProxyContext.cs b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSProxyContext.cs index 5e2eb23f5151f..be59f8cf717c0 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSProxyContext.cs +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSProxyContext.cs @@ -90,7 +90,9 @@ public enum JSImportOperationState [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 @@ -118,8 +120,7 @@ public static JSProxyContext SealJSImportCapturing() { if (_CapturingState != JSImportOperationState.JSImportParams) { - Environment.FailFast("Not in JSImport capturing phase"); - //throw new InvalidOperationException("Not in JSImport capturing phase"); + Environment.FailFast($"Method only allowed during JSImport capturing phase, ManagedThreadId: {Environment.CurrentManagedThreadId}. {Environment.NewLine} {Environment.StackTrace}"); } _CapturingState = JSImportOperationState.None; var capturedOperationContext = _CapturedOperationContext; @@ -146,7 +147,7 @@ public static void CaptureContextFromParameter(JSProxyContext parameterContext) { if (_CapturingState != JSImportOperationState.JSImportParams) { - Environment.FailFast("CaptureContextFromParameter state mismatch"); + Environment.FailFast($"Method only allowed during JSImport capturing phase, ManagedThreadId: {Environment.CurrentManagedThreadId}. {Environment.NewLine} {Environment.StackTrace}"); } var capturedContext = _CapturedOperationContext; @@ -159,7 +160,7 @@ public static void CaptureContextFromParameter(JSProxyContext parameterContext) { _CapturedOperationContext = null; _CapturingState = JSImportOperationState.None; - throw new InvalidOperationException("All JSObject proxies need to have same thread affinity"); + throw new InvalidOperationException("All JSObject proxies need to have same thread affinity. See https://aka.ms/dotnet-JS-interop-threads"); } } @@ -186,7 +187,7 @@ public static JSProxyContext CurrentOperationContext { if (_CapturingState != JSImportOperationState.JSImportParams) { - throw new InvalidOperationException("Not in capturing phase of a JSImport"); + Environment.FailFast($"Method only allowed during JSImport capturing phase, ManagedThreadId: {Environment.CurrentManagedThreadId}. {Environment.NewLine} {Environment.StackTrace}"); } var capturedOperationContext = _CapturedOperationContext; if (capturedOperationContext != null) @@ -360,14 +361,16 @@ public unsafe void ReleasePromiseHolder(nint holderGCHandle) } handle.Free(); } + holder.IsDisposed = true; } } public unsafe void ReleaseJSOwnedObjectByGCHandle(nint gcHandle) { - PromiseHolder? holder = null; + ToManagedCallback? holderCallback = null; lock (this) { + PromiseHolder? holder = null; if (IsGCVHandle(gcHandle)) { if (!ThreadJsOwnedHolders.Remove(gcHandle, out holder)) @@ -392,11 +395,13 @@ public unsafe void ReleaseJSOwnedObjectByGCHandle(nint gcHandle) } handle.Free(); } + if (holder != null) + { + holderCallback = holder.Callback; + holder.IsDisposed = true; + } } - if (holder != null) - { - holder.Callback!(null); - } + holderCallback?.Invoke(null); } public JSObject CreateCSOwnedProxy(nint jsHandle) @@ -425,7 +430,7 @@ public static void ReleaseCSOwnedObject(JSObject proxy, bool skipJS) #if FEATURE_WASM_THREADS if (!ctx.IsCurrentThread()) { - throw new InvalidOperationException("ReleaseCSOwnedObject has to run on the thread with same affinity as the proxy"); + 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) @@ -439,7 +444,7 @@ public static void ReleaseCSOwnedObject(JSObject proxy, bool skipJS) var jsHandle = proxy.JSHandle; if (!ctx.ThreadCsOwnedObjects.Remove(jsHandle)) { - throw new InvalidOperationException("ReleaseCSOwnedObject expected to find registration for " + jsHandle); + Environment.FailFast($"ReleaseCSOwnedObject expected to find registration for JSHandle: {jsHandle}, ManagedThreadId: {Environment.CurrentManagedThreadId}. {Environment.NewLine} {Environment.StackTrace}"); }; if (!skipJS) { @@ -526,7 +531,10 @@ private void Dispose(bool disposing) if (!_isDisposed) { #if FEATURE_WASM_THREADS - if (!IsCurrentThread()) throw new InvalidOperationException("JSProxyContext must be disposed on the thread which owns it."); + 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 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 d685580274889..d04fa731a49aa 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 @@ -97,7 +97,7 @@ public override void Post(SendOrPostCallback d, object? state) 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 @@ -117,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(); } 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 a18830859e4d8..f89c4a669818c 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 @@ -66,6 +66,7 @@ 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; @@ -76,11 +77,10 @@ public unsafe void ToJS(Exception? value) } else if (slot.ContextHandle != ctx.ContextHandle) { - Environment.FailFast("ContextHandle mismatch"); + 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; } 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 75a3a03d2bd75..3c41ec2fccc56 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 @@ -40,6 +40,7 @@ public void ToJS(JSObject? value) } else { + ObjectDisposedException.ThrowIf(value.IsDisposed, value); #if FEATURE_WASM_THREADS JSObject.AssertThreadAffinity(value); var ctx = value.ProxyContext; @@ -50,10 +51,9 @@ public void ToJS(JSObject? value) } else if (slot.ContextHandle != ctx.ContextHandle) { - Environment.FailFast("ContextHandle mismatch"); + 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/mono/wasm/runtime/gc-handles.ts b/src/mono/wasm/runtime/gc-handles.ts index ff0727c4769c9..6d95160ada062 100644 --- a/src/mono/wasm/runtime/gc-handles.ts +++ b/src/mono/wasm/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)) { diff --git a/src/mono/wasm/runtime/invoke-cs.ts b/src/mono/wasm/runtime/invoke-cs.ts index 8787327eeb83b..46324a6617559 100644 --- a/src/mono/wasm/runtime/invoke-cs.ts +++ b/src/mono/wasm/runtime/invoke-cs.ts @@ -356,9 +356,7 @@ export function invoke_method_and_handle_exception(method: MonoMethod, args: JSM assert_bindings(); const fail_root = mono_wasm_new_root(); try { - if (MonoWasmThreads) { - set_args_context(args); - } + set_args_context(args); const fail = cwraps.mono_wasm_invoke_method_bound(method, args, fail_root.address); if (fail) runtimeHelpers.abort("ERR24: Unexpected error: " + monoStringToString(fail_root)); if (is_args_exception(args)) { diff --git a/src/mono/wasm/runtime/marshal.ts b/src/mono/wasm/runtime/marshal.ts index c91dbe0749e4b..bf4c5145badd4 100644 --- a/src/mono/wasm/runtime/marshal.ts +++ b/src/mono/wasm/runtime/marshal.ts @@ -26,9 +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); - if (MonoWasmThreads) { - set_args_context(args); - } + set_args_context(args); return args; } @@ -44,6 +42,7 @@ export function is_args_exception(args: JSMarshalerArguments): boolean { } 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); @@ -264,6 +263,7 @@ export function get_arg_js_handle(arg: JSMarshalerArgument): JSHandle { } 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); } @@ -271,7 +271,7 @@ export function set_arg_proxy_context(arg: JSMarshalerArgument): void { export function set_js_handle(arg: JSMarshalerArgument, jsHandle: JSHandle): void { mono_assert(arg, "Null arg"); setI32(arg + 4, jsHandle); - if (MonoWasmThreads) set_arg_proxy_context(arg); + set_arg_proxy_context(arg); } export function get_arg_gc_handle(arg: JSMarshalerArgument): GCHandle { @@ -282,7 +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); - if (MonoWasmThreads) set_arg_proxy_context(arg); + set_arg_proxy_context(arg); } export function get_string_root(arg: JSMarshalerArgument): WasmRoot { From 9ea81185fe38accc449838b31b59402f8a9aced0 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Tue, 19 Dec 2023 17:43:19 +0100 Subject: [PATCH 23/23] fix gitignore --- .gitignore | 4 ---- src/mono/browser/.gitignore | 3 +++ src/mono/sample/wasm/browser-nextjs/.gitignore | 2 ++ 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index 71943be3a350d..1bbf3bb104201 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/mono/browser/.gitignore b/src/mono/browser/.gitignore index 752927a4aeefb..6047d67b80823 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/sample/wasm/browser-nextjs/.gitignore b/src/mono/sample/wasm/browser-nextjs/.gitignore index 20fccdd4b84d9..36a31210aea2d 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