From 3a88b89b025bd1f5addf61ec9a30b54284d056e2 Mon Sep 17 00:00:00 2001 From: Pavel Savara Date: Mon, 24 Jul 2023 22:03:13 +0200 Subject: [PATCH] [browser][mt] Release all proxies of C# and JS objects (#88052) --- .../BrowserWebSockets/BrowserWebSocket.cs | 2 +- .../JavaScript/Interop/JavaScriptExports.cs | 7 +- .../JavaScript/JSFunctionBinding.cs | 6 +- .../Runtime/InteropServices/JavaScript/JSHost.cs | 9 ++ .../JavaScript/JSHostImplementation.cs | 87 ++++++++++-- .../Runtime/InteropServices/JavaScript/JSObject.cs | 3 + .../JavaScript/JSSynchronizationContext.cs | 10 ++ .../Marshaling/JSMarshalerArgument.Task.cs | 11 ++ .../JavaScript/SynchronizationContextExtensions.cs | 2 +- .../InteropServices/JavaScript/WebWorker.cs | 38 ++--- .../JavaScript/JSImportExportTest.cs | 34 +++-- .../JavaScript/JavaScriptTestHelper.cs | 12 ++ .../sample/wasm/browser-threads-minimal/main.js | 3 +- src/mono/wasm/runtime/cancelable-promise.ts | 2 +- src/mono/wasm/runtime/exports-internal.ts | 2 + src/mono/wasm/runtime/exports.ts | 2 + src/mono/wasm/runtime/gc-handles.ts | 153 +++++++++++++++++---- src/mono/wasm/runtime/invoke-cs.ts | 19 ++- src/mono/wasm/runtime/invoke-js.ts | 23 ++-- src/mono/wasm/runtime/loader/exit.ts | 13 +- src/mono/wasm/runtime/loader/run.ts | 13 ++ src/mono/wasm/runtime/managed-exports.ts | 2 + src/mono/wasm/runtime/marshal-to-cs.ts | 32 ++++- src/mono/wasm/runtime/marshal-to-js.ts | 28 +++- src/mono/wasm/runtime/marshal.ts | 1 + src/mono/wasm/runtime/pthreads/shared/index.ts | 2 +- src/mono/wasm/runtime/snapshot.ts | 3 + src/mono/wasm/runtime/startup.ts | 2 + src/mono/wasm/runtime/types/index.ts | 1 + src/mono/wasm/runtime/types/internal.ts | 3 + src/mono/wasm/runtime/weak-ref.ts | 5 +- src/mono/wasm/runtime/web-socket.ts | 8 +- src/mono/wasm/test-main.js | 1 + 33 files changed, 425 insertions(+), 114 deletions(-) diff --git a/src/libraries/System.Net.WebSockets.Client/src/System/Net/WebSockets/BrowserWebSockets/BrowserWebSocket.cs b/src/libraries/System.Net.WebSockets.Client/src/System/Net/WebSockets/BrowserWebSockets/BrowserWebSocket.cs index ded2f5c..121fa85 100644 --- a/src/libraries/System.Net.WebSockets.Client/src/System/Net/WebSockets/BrowserWebSockets/BrowserWebSocket.cs +++ b/src/libraries/System.Net.WebSockets.Client/src/System/Net/WebSockets/BrowserWebSockets/BrowserWebSocket.cs @@ -601,7 +601,7 @@ namespace System.Net.WebSockets FastState = WebSocketState.Aborted; throw new OperationCanceledException(cancellationToken); } - if (ex.Message == "OperationCanceledException") + if (ex.Message == "Error: OperationCanceledException") { FastState = WebSocketState.Aborted; throw new OperationCanceledException("The operation was cancelled.", ex, cancellationToken); 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 e89142b..20c0279 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 @@ -143,11 +143,8 @@ namespace System.Runtime.InteropServices.JavaScript { GCHandle handle = (GCHandle)arg_1.slot.GCHandle; - lock (JSHostImplementation.s_gcHandleFromJSOwnedObject) - { - JSHostImplementation.s_gcHandleFromJSOwnedObject.Remove(handle.Target!); - handle.Free(); - } + JSHostImplementation.ThreadJsOwnedObjects.Remove(handle.Target!); + handle.Free(); } catch (Exception ex) { 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 bec4aff..d5b0036 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 @@ -172,6 +172,7 @@ namespace System.Runtime.InteropServices.JavaScript [MethodImpl(MethodImplOptions.AggressiveInlining)] internal static unsafe void InvokeJSImpl(JSObject jsFunction, Span arguments) { + ObjectDisposedException.ThrowIf(jsFunction.IsDisposed, jsFunction); #if FEATURE_WASM_THREADS JSObject.AssertThreadAffinity(jsFunction); #endif @@ -205,10 +206,7 @@ namespace System.Runtime.InteropServices.JavaScript internal static unsafe JSFunctionBinding BindJSFunctionImpl(string functionName, string moduleName, ReadOnlySpan signatures) { #if FEATURE_WASM_THREADS - if (JSSynchronizationContext.CurrentJSSynchronizationContext == null) - { - throw new InvalidOperationException("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 "); - } + JSSynchronizationContext.AssertWebWorkerContext(); #endif var signature = JSHostImplementation.GetMethodSignature(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 219a3b9..16e3324 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 @@ -21,6 +21,9 @@ namespace System.Runtime.InteropServices.JavaScript { get { +#if FEATURE_WASM_THREADS + JSSynchronizationContext.AssertWebWorkerContext(); +#endif return JavaScriptImports.GetGlobalThis(); } } @@ -32,6 +35,9 @@ namespace System.Runtime.InteropServices.JavaScript { get { +#if FEATURE_WASM_THREADS + JSSynchronizationContext.AssertWebWorkerContext(); +#endif return JavaScriptImports.GetDotnetInstance(); } } @@ -47,6 +53,9 @@ namespace System.Runtime.InteropServices.JavaScript [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Task ImportAsync(string moduleName, string moduleUrl, CancellationToken cancellationToken = default) { +#if FEATURE_WASM_THREADS + JSSynchronizationContext.AssertWebWorkerContext(); +#endif return JSHostImplementation.ImportAsync(moduleName, moduleUrl, cancellationToken); } 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 499c593..94707e6 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 @@ -32,13 +32,28 @@ namespace System.Runtime.InteropServices.JavaScript } // we use this to maintain identity of GCHandle for a managed object - public static Dictionary s_gcHandleFromJSOwnedObject = new Dictionary(ReferenceEqualityComparer.Instance); +#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; + } + } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void ReleaseCSOwnedObject(nint jsHandle) { if (jsHandle != IntPtr.Zero) { +#if FEATURE_WASM_THREADS + JSSynchronizationContext.AssertWebWorkerContext(); +#endif ThreadCsOwnedObjects.Remove((int)jsHandle); Interop.Runtime.ReleaseCSOwnedObject(jsHandle); } @@ -64,19 +79,19 @@ namespace System.Runtime.InteropServices.JavaScript public static IntPtr GetJSOwnedObjectGCHandle(object obj, GCHandleType handleType = GCHandleType.Normal) { if (obj == null) + { return IntPtr.Zero; + } - IntPtr result; - lock (s_gcHandleFromJSOwnedObject) + IntPtr gcHandle; + if (ThreadJsOwnedObjects.TryGetValue(obj, out gcHandle)) { - IntPtr gcHandle; - if (s_gcHandleFromJSOwnedObject.TryGetValue(obj, out gcHandle)) - return gcHandle; - - result = (IntPtr)GCHandle.Alloc(obj, handleType); - s_gcHandleFromJSOwnedObject[obj] = result; - return result; + return gcHandle; } + + IntPtr result = (IntPtr)GCHandle.Alloc(obj, handleType); + ThreadJsOwnedObjects[obj] = result; + return result; } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -188,6 +203,9 @@ namespace System.Runtime.InteropServices.JavaScript public static JSObject CreateCSOwnedProxy(nint jsHandle) { +#if FEATURE_WASM_THREADS + JSSynchronizationContext.AssertWebWorkerContext(); +#endif JSObject? res; if (!ThreadCsOwnedObjects.TryGetValue((int)jsHandle, out WeakReference? reference) || @@ -244,14 +262,55 @@ namespace System.Runtime.InteropServices.JavaScript public static void UninstallWebWorkerInterop() { - var ctx = SynchronizationContext.Current as JSSynchronizationContext; + var ctx = JSSynchronizationContext.CurrentJSSynchronizationContext; var uninstallJSSynchronizationContext = ctx != null; if (uninstallJSSynchronizationContext) { - SynchronizationContext.SetSynchronizationContext(ctx!.previousSynchronizationContext); - JSSynchronizationContext.CurrentJSSynchronizationContext = null; - ctx.isDisposed = true; + try + { + foreach (var jsObjectWeak in ThreadCsOwnedObjects.Values) + { + if (jsObjectWeak.TryGetTarget(out var jso)) + { + jso.Dispose(); + } + } + foreach (var gch in ThreadJsOwnedObjects.Values) + { + GCHandle gcHandle = (GCHandle)gch; + + // if this is pending promise we reject it + if (gcHandle.Target is TaskCallback holder) + { + unsafe + { + holder.Callback!.Invoke(null); + } + } + gcHandle.Free(); + } + 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}"); + } } + ThreadCsOwnedObjects.Clear(); + ThreadJsOwnedObjects.Clear(); Interop.Runtime.UninstallWebWorkerInterop(uninstallJSSynchronizationContext); } diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSObject.cs b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSObject.cs index d53934a..57dc9ab 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSObject.cs +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSObject.cs @@ -27,6 +27,9 @@ namespace System.Runtime.InteropServices.JavaScript public bool HasProperty(string propertyName) { ObjectDisposedException.ThrowIf(IsDisposed, this); +#if FEATURE_WASM_THREADS + JSObject.AssertThreadAffinity(this); +#endif return JavaScriptImports.HasProperty(this, propertyName); } 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 1a807e5..5aabaac 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 @@ -55,6 +55,16 @@ namespace System.Runtime.InteropServices.JavaScript { } + 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 targetThreadId, WorkItemQueueType queue) { TargetThread = targetThread; 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 99844db..e5fa56a 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 @@ -49,6 +49,11 @@ namespace System.Runtime.InteropServices.JavaScript TaskCompletionSource tcs = new TaskCompletionSource(holder); JSHostImplementation.ToManagedCallback callback = (JSMarshalerArgument* arguments_buffer) => { + if (arguments_buffer == null) + { + tcs.TrySetException(new TaskCanceledException("WebWorker which is origin of the Promise is being terminated.")); + return; + } ref JSMarshalerArgument arg_2 = ref arguments_buffer[3]; // set by caller when this is SetException call // arg_3 set by caller when this is SetResult call, un-used here if (arg_2.slot.Type != MarshalerType.None) @@ -88,6 +93,12 @@ namespace System.Runtime.InteropServices.JavaScript TaskCompletionSource tcs = new TaskCompletionSource(holder); JSHostImplementation.ToManagedCallback callback = (JSMarshalerArgument* arguments_buffer) => { + if (arguments_buffer == null) + { + tcs.TrySetException(new TaskCanceledException("WebWorker which is origin of the Promise is being terminated.")); + return; + } + ref JSMarshalerArgument arg_2 = ref arguments_buffer[3]; // set by caller when this is SetException call ref JSMarshalerArgument arg_3 = ref arguments_buffer[4]; // set by caller when this is SetResult call if (arg_2.slot.Type != MarshalerType.None) diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/SynchronizationContextExtensions.cs b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/SynchronizationContextExtensions.cs index b91fe719..bebe252 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/SynchronizationContextExtensions.cs +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/SynchronizationContextExtensions.cs @@ -7,7 +7,7 @@ using System.Threading.Tasks; namespace System.Runtime.InteropServices.JavaScript { /// - /// This is draft for possible public API of SynchronizationContext + /// Extensions of SynchronizationContext which propagate errors and return values /// public static class SynchronizationContextExtension { diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/WebWorker.cs b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/WebWorker.cs index 98a9037..8184ca9 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/WebWorker.cs +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/WebWorker.cs @@ -38,13 +38,13 @@ namespace System.Runtime.InteropServices.JavaScript // the continuation is executed by setTimeout() callback of the thread. res.ContinueWith(t => { - PostWhenDone(parentContext, tcs, res); + SendWhenDone(parentContext, tcs, res); JSHostImplementation.UninstallWebWorkerInterop(); }, childScheduler); } catch (Exception ex) { - PostWhenException(parentContext, tcs, ex); + SendWhenException(parentContext, tcs, ex); } }); @@ -75,13 +75,13 @@ namespace System.Runtime.InteropServices.JavaScript // the continuation is executed by setTimeout() callback of the thread. res.ContinueWith(t => { - PostWhenDone(parentContext, tcs, res); + SendWhenDone(parentContext, tcs, res); JSHostImplementation.UninstallWebWorkerInterop(); }, childScheduler); } catch (Exception ex) { - PostWhenException(parentContext, tcs, ex); + SendWhenException(parentContext, tcs, ex); } }); @@ -109,17 +109,17 @@ namespace System.Runtime.InteropServices.JavaScript try { body(); - PostWhenDone(parentContext, tcs); + SendWhenDone(parentContext, tcs); } catch (Exception ex) { - PostWhenException(parentContext, tcs, ex); + SendWhenException(parentContext, tcs, ex); } JSHostImplementation.UninstallWebWorkerInterop(); } catch (Exception ex) { - PostWhenException(parentContext, tcs, ex); + SendWhenException(parentContext, tcs, ex); } }); @@ -134,7 +134,7 @@ namespace System.Runtime.InteropServices.JavaScript { try { - ctx.Post((_) => tcs.SetCanceled(), null); + ctx.Send((_) => tcs.SetCanceled(), null); } catch (Exception e) { @@ -146,7 +146,7 @@ namespace System.Runtime.InteropServices.JavaScript { try { - ctx.Post((_) => tcs.SetCanceled(), null); + ctx.Send((_) => tcs.SetCanceled(), null); } catch (Exception e) { @@ -154,11 +154,11 @@ namespace System.Runtime.InteropServices.JavaScript } } - private static void PostWhenDone(SynchronizationContext ctx, TaskCompletionSource tcs, Task done) + private static void SendWhenDone(SynchronizationContext ctx, TaskCompletionSource tcs, Task done) { try { - ctx.Post((_) => + ctx.Send((_) => { PropagateCompletion(tcs, done); }, null); @@ -169,11 +169,11 @@ namespace System.Runtime.InteropServices.JavaScript } } - private static void PostWhenDone(SynchronizationContext ctx, TaskCompletionSource tcs) + private static void SendWhenDone(SynchronizationContext ctx, TaskCompletionSource tcs) { try { - ctx.Post((_) => tcs.SetResult(), null); + ctx.Send((_) => tcs.SetResult(), null); } catch (Exception e) { @@ -181,11 +181,11 @@ namespace System.Runtime.InteropServices.JavaScript } } - private static void PostWhenException(SynchronizationContext ctx, TaskCompletionSource tcs, Exception ex) + private static void SendWhenException(SynchronizationContext ctx, TaskCompletionSource tcs, Exception ex) { try { - ctx.Post((_) => tcs.SetException(ex), null); + ctx.Send((_) => tcs.SetException(ex), null); } catch (Exception e) { @@ -193,11 +193,11 @@ namespace System.Runtime.InteropServices.JavaScript } } - private static void PostWhenException(SynchronizationContext ctx, TaskCompletionSource tcs, Exception ex) + private static void SendWhenException(SynchronizationContext ctx, TaskCompletionSource tcs, Exception ex) { try { - ctx.Post((_) => tcs.SetException(ex), null); + ctx.Send((_) => tcs.SetException(ex), null); } catch (Exception e) { @@ -205,11 +205,11 @@ namespace System.Runtime.InteropServices.JavaScript } } - private static void PostWhenDone(SynchronizationContext ctx, TaskCompletionSource tcs, Task done) + private static void SendWhenDone(SynchronizationContext ctx, TaskCompletionSource tcs, Task done) { try { - ctx.Post((_) => + ctx.Send((_) => { PropagateCompletion(tcs, done); }, null); 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 a341f3c..b55bc19 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 @@ -30,6 +30,9 @@ namespace System.Runtime.InteropServices.JavaScript.Tests var instance1 = first.GetPropertyAsJSObject("instance"); var instance2 = second.GetPropertyAsJSObject("instance"); Assert.Same(instance1, instance2); + first.Dispose(); + second.Dispose(); + instance1.Dispose(); } [Fact] @@ -39,20 +42,24 @@ namespace System.Runtime.InteropServices.JavaScript.Tests var exTask = Assert.ThrowsAsync(async () => await JSHost.ImportAsync("JavaScriptTestHelper", "../JavaScriptTestHelper.mjs", cts.Token)); cts.Cancel(); var actualEx2 = await exTask; - Assert.Equal("OperationCanceledException", actualEx2.Message); + Assert.Equal("Error: OperationCanceledException", actualEx2.Message); var actualEx = await Assert.ThrowsAsync(async () => await JSHost.ImportAsync("JavaScriptTestHelper", "../JavaScriptTestHelper.mjs", new CancellationToken(true))); - Assert.Equal("OperationCanceledException", actualEx.Message); + Assert.Equal("Error: OperationCanceledException", actualEx.Message); } [Fact] public unsafe void GlobalThis() { - Assert.Null(JSHost.GlobalThis.GetPropertyAsString("dummy")); - Assert.False(JSHost.GlobalThis.HasProperty("dummy")); - Assert.Equal("undefined", JSHost.GlobalThis.GetTypeOfProperty("dummy")); - Assert.Equal("function", JSHost.GlobalThis.GetTypeOfProperty("Array")); - Assert.NotNull(JSHost.GlobalThis.GetPropertyAsJSObject("javaScriptTestHelper")); + var globalThis = JSHost.GlobalThis; + Assert.Null(globalThis.GetPropertyAsString("dummy")); + Assert.False(globalThis.HasProperty("dummy")); + Assert.Equal("undefined", globalThis.GetTypeOfProperty("dummy")); + Assert.Equal("function", globalThis.GetTypeOfProperty("Array")); + var javaScriptTestHelper = globalThis.GetPropertyAsJSObject("javaScriptTestHelper"); + Assert.NotNull(javaScriptTestHelper); + globalThis.Dispose(); + javaScriptTestHelper.Dispose(); } [Fact] @@ -317,10 +324,10 @@ namespace System.Runtime.InteropServices.JavaScript.Tests Assert.Equal(expected, actual); if (expected != null) for (int i = 0; i < expected.Length; i++) - { - var actualI = JavaScriptTestHelper.store_ObjectArray(expected, i); - Assert.Equal(expected[i], actualI); - } + { + var actualI = JavaScriptTestHelper.store_ObjectArray(expected, i); + Assert.Equal(expected[i], actualI); + } } public static IEnumerable MarshalObjectArrayCasesToDouble() @@ -2101,7 +2108,10 @@ namespace System.Runtime.InteropServices.JavaScript.Tests await JavaScriptTestHelper.InitializeAsync(); } - public Task DisposeAsync() => Task.CompletedTask; + public async Task DisposeAsync() + { + await JavaScriptTestHelper.DisposeAsync(); + } // js Date doesn't have nanosecond precision public static DateTime TrimNano(DateTime date) 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 fea5cc4..f61ab28 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 @@ -991,6 +991,9 @@ namespace System.Runtime.InteropServices.JavaScript.Tests [JSImport("setup", "JavaScriptTestHelper")] internal static partial Task Setup(); + [JSImport("INTERNAL.forceDisposeProxies")] + internal static partial void ForceDisposeProxies(bool disposeMethods, bool verbose); + static JSObject _module; public static async Task InitializeAsync() { @@ -1002,6 +1005,15 @@ namespace System.Runtime.InteropServices.JavaScript.Tests // Log("JavaScriptTestHelper.mjs imported"); } } + + public static Task DisposeAsync() + { + _module.Dispose(); + _module = null; + // you can set verbose: true to see which proxies are left to the GC to collect + ForceDisposeProxies(false, verbose: false); + return Task.CompletedTask; + } } } diff --git a/src/mono/sample/wasm/browser-threads-minimal/main.js b/src/mono/sample/wasm/browser-threads-minimal/main.js index d35c744..7263133 100644 --- a/src/mono/sample/wasm/browser-threads-minimal/main.js +++ b/src/mono/sample/wasm/browser-threads-minimal/main.js @@ -79,7 +79,6 @@ try { let w0 = await exports.Sample.Test.WsClientMain("wss://corefx-net-http11.azurewebsites.net/WebSocket/EchoWebSocket.ashx"); console.log("smoke: WsClientMain done " + w0); - /* ActiveIssue https://github.com/dotnet/runtime/issues/88057 console.log("smoke: running FetchBackground(blurst.txt)"); let s = await exports.Sample.Test.FetchBackground(resolveUrl("./blurst.txt")); console.log("smoke: FetchBackground(blurst.txt) done"); @@ -96,7 +95,7 @@ try { const msg = `Unexpected FetchBackground(missing) result ${s}`; document.getElementById("out").innerHTML = msg; throw new Error(msg); - }*/ + } console.log("smoke: running TaskRunCompute"); const r1 = await exports.Sample.Test.RunBackgroundTaskRunCompute(); diff --git a/src/mono/wasm/runtime/cancelable-promise.ts b/src/mono/wasm/runtime/cancelable-promise.ts index 3b59aef..2fbb648 100644 --- a/src/mono/wasm/runtime/cancelable-promise.ts +++ b/src/mono/wasm/runtime/cancelable-promise.ts @@ -30,6 +30,6 @@ export function mono_wasm_cancel_promise(task_holder_gc_handle: GCHandle): void mono_assert(!!promise, () => `Expected Promise for GCHandle ${task_holder_gc_handle}`); loaderHelpers.assertIsControllablePromise(promise); const promise_control = loaderHelpers.getPromiseController(promise); - promise_control.reject("OperationCanceledException"); + promise_control.reject(new Error("OperationCanceledException")); } diff --git a/src/mono/wasm/runtime/exports-internal.ts b/src/mono/wasm/runtime/exports-internal.ts index 7854b3b..bfd9d53 100644 --- a/src/mono/wasm/runtime/exports-internal.ts +++ b/src/mono/wasm/runtime/exports-internal.ts @@ -15,11 +15,13 @@ import { getOptions, applyOptions } from "./jiterpreter-support"; import { mono_wasm_gc_lock, mono_wasm_gc_unlock } from "./gc-lock"; import { loadLazyAssembly } from "./lazyLoading"; import { loadSatelliteAssemblies } from "./satelliteAssemblies"; +import { forceDisposeProxies } from "./gc-handles"; export function export_internal(): any { return { // tests mono_wasm_exit: (exit_code: number) => { Module.err("early exit " + exit_code); }, + forceDisposeProxies, // with mono_wasm_debugger_log and mono_wasm_trace_logger logging: undefined, diff --git a/src/mono/wasm/runtime/exports.ts b/src/mono/wasm/runtime/exports.ts index e3d1bdc..6ecd3b6 100644 --- a/src/mono/wasm/runtime/exports.ts +++ b/src/mono/wasm/runtime/exports.ts @@ -23,6 +23,7 @@ import { initializeLegacyExports } from "./net6-legacy/globals"; import { mono_log_warn, mono_wasm_stringify_as_error_with_stack } from "./logging"; import { instantiate_asset, instantiate_symbols_asset } from "./assets"; import { jiterpreter_dump_stats } from "./jiterpreter"; +import { forceDisposeProxies } from "./gc-handles"; function initializeExports(globalObjects: GlobalObjects): RuntimeAPI { const module = Module; @@ -45,6 +46,7 @@ function initializeExports(globalObjects: GlobalObjects): RuntimeAPI { instantiate_symbols_asset, instantiate_asset, jiterpreter_dump_stats, + forceDisposeProxies, }); const API = export_api(); diff --git a/src/mono/wasm/runtime/gc-handles.ts b/src/mono/wasm/runtime/gc-handles.ts index c95e0a9..857b737 100644 --- a/src/mono/wasm/runtime/gc-handles.ts +++ b/src/mono/wasm/runtime/gc-handles.ts @@ -1,16 +1,23 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -import { loaderHelpers, runtimeHelpers } from "./globals"; -import { mono_log_warn } from "./logging"; +import MonoWasmThreads from "consts:monoWasmThreads"; +import BuildConfiguration from "consts:configuration"; + +import { loaderHelpers, mono_assert, runtimeHelpers } from "./globals"; +import { fn_wrapper_by_fn_handle } from "./invoke-js"; +import { mono_log_info, mono_log_warn } from "./logging"; +import { bound_cs_function_symbol, imported_js_function_symbol, proxy_debug_symbol } from "./marshal"; import { GCHandle, GCHandleNull, JSHandle, JSHandleDisposed, JSHandleNull } from "./types/internal"; -import { create_weak_ref } from "./weak-ref"; +import { _use_weak_ref, create_weak_ref } from "./weak-ref"; +import { exportsByAssembly } from "./invoke-cs"; const _use_finalization_registry = typeof globalThis.FinalizationRegistry === "function"; let _js_owned_object_registry: FinalizationRegistry; // this is array, not map. We maintain list of gaps in _js_handle_free_list so that it could be as compact as possible -const _cs_owned_objects_by_js_handle: any[] = []; +// 0th element is always null, because JSHandle == 0 is invalid handle. +const _cs_owned_objects_by_js_handle: any[] = [null]; const _js_handle_free_list: JSHandle[] = []; let _next_js_handle = 1; @@ -23,6 +30,7 @@ if (_use_finalization_registry) { export const js_owned_gc_handle_symbol = Symbol.for("wasm js_owned_gc_handle"); export const cs_owned_js_handle_symbol = Symbol.for("wasm cs_owned_js_handle"); +export const do_not_force_dispose = Symbol.for("wasm do_not_force_dispose"); export function mono_wasm_get_jsobj_from_js_handle(js_handle: JSHandle): any { @@ -59,11 +67,6 @@ export function mono_wasm_get_js_handle(js_obj: any): JSHandle { export function mono_wasm_release_cs_owned_object(js_handle: JSHandle): void { const obj = _cs_owned_objects_by_js_handle[js_handle]; if (typeof obj !== "undefined" && obj !== null) { - // if this is the global object then do not - // unregister it. - if (globalThis === obj) - return; - if (typeof obj[cs_owned_js_handle_symbol] !== "undefined") { obj[cs_owned_js_handle_symbol] = undefined; } @@ -133,29 +136,133 @@ export function _lookup_js_owned_object(gc_handle: GCHandle): any { return null; } -export function forceDisposeProxies(dump: boolean): void { +export function assertNoProxies(): void { + if (!MonoWasmThreads) return; + mono_assert(_js_owned_object_table.size === 0, "There should be no proxies on this thread."); + mono_assert(_cs_owned_objects_by_js_handle.length === 1, "There should be no proxies on this thread."); + mono_assert(exportsByAssembly.size === 0, "There should be no exports on this thread."); + mono_assert(fn_wrapper_by_fn_handle.length === 1, "There should be no imports on this thread."); +} + +// when we arrive here, the C# side is already done with the object. +// We don't have to call back to release them. +export function forceDisposeProxies(disposeMethods: boolean, verbose: boolean): void { + let keepSomeCsAlive = false; + let keepSomeJsAlive = false; + + let doneImports = 0; + let doneExports = 0; + let doneGCHandles = 0; + let doneJSHandles = 0; // dispose all proxies to C# objects - const gchandles = [..._js_owned_object_table.keys()]; - for (const gchandle of gchandles) { - const wr = _js_owned_object_table.get(gchandle); + const gc_handles = [..._js_owned_object_table.keys()]; + for (const gc_handle of gc_handles) { + const wr = _js_owned_object_table.get(gc_handle); const obj = wr.deref(); + if (_use_finalization_registry && obj) { + _js_owned_object_registry.unregister(obj); + } + if (obj) { - if (dump) { - mono_log_warn(`Proxy of C# object with GCHandle ${gchandle} was still alive`); + const keepAlive = typeof obj[do_not_force_dispose] === "boolean" && obj[do_not_force_dispose]; + if (verbose) { + const proxy_debug = BuildConfiguration === "Debug" ? obj[proxy_debug_symbol] : undefined; + if (BuildConfiguration === "Debug" && proxy_debug) { + mono_log_warn(`${proxy_debug} ${typeof obj} was still alive. ${keepAlive ? "keeping" : "disposing"}.`); + } else { + mono_log_warn(`Proxy of C# ${typeof obj} with GCHandle ${gc_handle} was still alive. ${keepAlive ? "keeping" : "disposing"}.`); + } + } + if (!keepAlive) { + const promise_control = loaderHelpers.getPromiseController(obj); + if (promise_control) { + promise_control.reject(new Error("WebWorker which is origin of the Task is being terminated.")); + } + if (typeof obj.dispose === "function") { + obj.dispose(); + } + if (obj[js_owned_gc_handle_symbol] === gc_handle) { + obj[js_owned_gc_handle_symbol] = GCHandleNull; + } + if (!_use_weak_ref && wr) wr.dispose(); + doneGCHandles++; + } else { + keepSomeCsAlive = true; } - teardown_managed_proxy(obj, gchandle); } } - // TODO: call C# to iterate and release all in JSHostImplementation.ThreadCsOwnedObjects + if (!keepSomeCsAlive) { + _js_owned_object_table.clear(); + if (_use_finalization_registry) { + _js_owned_object_registry = new globalThis.FinalizationRegistry(_js_owned_object_finalized); + } + } // dispose all proxies to JS objects - for (const js_obj of _cs_owned_objects_by_js_handle) { - if (js_obj) { - const js_handle = js_obj[cs_owned_js_handle_symbol]; - if (js_handle) { - mono_log_warn(`Proxy of JS object with JSHandleandle ${js_handle} was still alive`); - mono_wasm_release_cs_owned_object(js_handle); + for (let js_handle = 0; js_handle < _cs_owned_objects_by_js_handle.length; js_handle++) { + const obj = _cs_owned_objects_by_js_handle[js_handle]; + const keepAlive = obj && typeof obj[do_not_force_dispose] === "boolean" && obj[do_not_force_dispose]; + if (!keepAlive) { + _cs_owned_objects_by_js_handle[js_handle] = undefined; + } + if (obj) { + if (verbose) { + const proxy_debug = BuildConfiguration === "Debug" ? obj[proxy_debug_symbol] : undefined; + if (BuildConfiguration === "Debug" && proxy_debug) { + mono_log_warn(`${proxy_debug} ${typeof obj} was still alive. ${keepAlive ? "keeping" : "disposing"}.`); + } else { + mono_log_warn(`Proxy of JS ${typeof obj} with JSHandle ${js_handle} was still alive. ${keepAlive ? "keeping" : "disposing"}.`); + } + } + if (!keepAlive) { + const promise_control = loaderHelpers.getPromiseController(obj); + if (promise_control) { + promise_control.reject(new Error("WebWorker which is origin of the Task is being terminated.")); + } + if (typeof obj.dispose === "function") { + obj.dispose(); + } + if (obj[cs_owned_js_handle_symbol] === js_handle) { + obj[cs_owned_js_handle_symbol] = undefined; + } + doneJSHandles++; + } else { + keepSomeJsAlive = true; + } + } + } + if (!keepSomeJsAlive) { + _cs_owned_objects_by_js_handle.length = 1; + _next_js_handle = 1; + _js_handle_free_list.length = 0; + } + + if (disposeMethods) { + // dispose all [JSImport] + for (const bound_fn of fn_wrapper_by_fn_handle) { + if (bound_fn) { + const closure = (bound_fn)[imported_js_function_symbol]; + if (closure) { + closure.disposed = true; + doneImports++; + } + } + } + fn_wrapper_by_fn_handle.length = 1; + + // dispose all [JSExport] + const assemblyExports = [...exportsByAssembly.values()]; + for (const assemblyExport of assemblyExports) { + for (const exportName in assemblyExport) { + const bound_fn = assemblyExport[exportName]; + const closure = bound_fn[bound_cs_function_symbol]; + if (closure) { + closure.disposed = true; + doneExports++; + } } } + exportsByAssembly.clear(); } + mono_log_info(`forceDisposeProxies done: ${doneImports} imports, ${doneExports} exports, ${doneGCHandles} GCHandles, ${doneJSHandles} JSHandles.`); } \ No newline at end of file diff --git a/src/mono/wasm/runtime/invoke-cs.ts b/src/mono/wasm/runtime/invoke-cs.ts index a7a8bd1..b38393a 100644 --- a/src/mono/wasm/runtime/invoke-cs.ts +++ b/src/mono/wasm/runtime/invoke-cs.ts @@ -76,6 +76,7 @@ export function mono_wasm_bind_cs_function(fully_qualified_name: MonoStringRef, args_count, arg_marshalers, res_converter, + isDisposed: false, }; let bound_fn: Function; if (args_count == 0 && !res_converter) { @@ -106,7 +107,7 @@ export function mono_wasm_bind_cs_function(fully_qualified_name: MonoStringRef, } } - (bound_fn)[bound_cs_function_symbol] = true; + (bound_fn)[bound_cs_function_symbol] = closure; _walk_exports_to_set_function(assembly, namespace, classname, methodname, signature_hash, bound_fn); endMeasure(mark, MeasuredBlock.bindCsFunction, js_fqn); @@ -124,10 +125,11 @@ export function mono_wasm_bind_cs_function(fully_qualified_name: MonoStringRef, function bind_fn_0V(closure: BindingClosure) { const method = closure.method; const fqn = closure.fqn; - (closure) = null; + if (!MonoWasmThreads) (closure) = null; return function bound_fn_0V() { const mark = startMeasure(); loaderHelpers.assert_runtime_running(); + mono_assert(!MonoWasmThreads || !closure.isDisposed, "The function was already disposed"); const sp = Module.stackSave(); try { const args = alloc_stack_frame(2); @@ -144,10 +146,11 @@ function bind_fn_1V(closure: BindingClosure) { const method = closure.method; const marshaler1 = closure.arg_marshalers[0]!; const fqn = closure.fqn; - (closure) = null; + if (!MonoWasmThreads) (closure) = null; return function bound_fn_1V(arg1: any) { const mark = startMeasure(); loaderHelpers.assert_runtime_running(); + mono_assert(!MonoWasmThreads || !closure.isDisposed, "The function was already disposed"); const sp = Module.stackSave(); try { const args = alloc_stack_frame(3); @@ -167,10 +170,11 @@ function bind_fn_1R(closure: BindingClosure) { const marshaler1 = closure.arg_marshalers[0]!; const res_converter = closure.res_converter!; const fqn = closure.fqn; - (closure) = null; + if (!MonoWasmThreads) (closure) = null; return function bound_fn_1R(arg1: any) { const mark = startMeasure(); loaderHelpers.assert_runtime_running(); + mono_assert(!MonoWasmThreads || !closure.isDisposed, "The function was already disposed"); const sp = Module.stackSave(); try { const args = alloc_stack_frame(3); @@ -194,10 +198,11 @@ function bind_fn_2R(closure: BindingClosure) { const marshaler2 = closure.arg_marshalers[1]!; const res_converter = closure.res_converter!; const fqn = closure.fqn; - (closure) = null; + if (!MonoWasmThreads) (closure) = null; return function bound_fn_2R(arg1: any, arg2: any) { const mark = startMeasure(); loaderHelpers.assert_runtime_running(); + mono_assert(!MonoWasmThreads || !closure.isDisposed, "The function was already disposed"); const sp = Module.stackSave(); try { const args = alloc_stack_frame(4); @@ -222,10 +227,11 @@ function bind_fn(closure: BindingClosure) { const res_converter = closure.res_converter; const method = closure.method; const fqn = closure.fqn; - (closure) = null; + if (!MonoWasmThreads) (closure) = null; return function bound_fn(...js_args: any[]) { const mark = startMeasure(); loaderHelpers.assert_runtime_running(); + mono_assert(!MonoWasmThreads || !closure.isDisposed, "The function was already disposed"); const sp = Module.stackSave(); try { const args = alloc_stack_frame(2 + args_count); @@ -257,6 +263,7 @@ type BindingClosure = { method: MonoMethod, arg_marshalers: (BoundMarshalerToCs)[], res_converter: BoundMarshalerToJs | undefined, + isDisposed: boolean, } export function invoke_method_and_handle_exception(method: MonoMethod, args: JSMarshalerArguments): void { diff --git a/src/mono/wasm/runtime/invoke-js.ts b/src/mono/wasm/runtime/invoke-js.ts index 5b762f4..a17ef89 100644 --- a/src/mono/wasm/runtime/invoke-js.ts +++ b/src/mono/wasm/runtime/invoke-js.ts @@ -19,7 +19,7 @@ import { endMeasure, MeasuredBlock, startMeasure } from "./profiler"; import { wrap_as_cancelable_promise } from "./cancelable-promise"; import { assert_synchronization_context } from "./pthreads/shared"; -const fn_wrapper_by_fn_handle: Function[] = [null];// 0th slot is dummy, we never free bound functions +export const fn_wrapper_by_fn_handle: Function[] = [null];// 0th slot is dummy, main thread we free them on shutdown. On web worker thread we free them when worker is detached. export function mono_wasm_bind_js_function(function_name: MonoStringRef, module_name: MonoStringRef, signature: JSFunctionSignature, function_js_handle: Int32Ptr, is_exception: Int32Ptr, result_address: MonoObjectRef): void { assert_bindings(); @@ -73,7 +73,8 @@ export function mono_wasm_bind_js_function(function_name: MonoStringRef, module_ arg_marshalers, res_converter, has_cleanup, - arg_cleanup + arg_cleanup, + isDisposed: false, }; let bound_fn: Function; if (args_count == 0 && !res_converter) { @@ -104,7 +105,7 @@ export function mono_wasm_bind_js_function(function_name: MonoStringRef, module_ } } - (bound_fn)[imported_js_function_symbol] = true; + (bound_fn)[imported_js_function_symbol] = closure; const fn_handle = fn_wrapper_by_fn_handle.length; fn_wrapper_by_fn_handle.push(bound_fn); setI32(function_js_handle, fn_handle); @@ -123,10 +124,11 @@ export function mono_wasm_bind_js_function(function_name: MonoStringRef, module_ function bind_fn_0V(closure: BindingClosure) { const fn = closure.fn; const fqn = closure.fqn; - (closure) = null; + if (!MonoWasmThreads) (closure) = null; return function bound_fn_0V(args: JSMarshalerArguments) { const mark = startMeasure(); try { + mono_assert(!MonoWasmThreads || !closure.isDisposed, "The function was already disposed"); // call user function fn(); } catch (ex) { @@ -142,10 +144,11 @@ function bind_fn_1V(closure: BindingClosure) { const fn = closure.fn; const marshaler1 = closure.arg_marshalers[0]!; const fqn = closure.fqn; - (closure) = null; + if (!MonoWasmThreads) (closure) = null; return function bound_fn_1V(args: JSMarshalerArguments) { const mark = startMeasure(); try { + mono_assert(!MonoWasmThreads || !closure.isDisposed, "The function was already disposed"); const arg1 = marshaler1(args); // call user function fn(arg1); @@ -163,10 +166,11 @@ function bind_fn_1R(closure: BindingClosure) { const marshaler1 = closure.arg_marshalers[0]!; const res_converter = closure.res_converter!; const fqn = closure.fqn; - (closure) = null; + if (!MonoWasmThreads) (closure) = null; return function bound_fn_1R(args: JSMarshalerArguments) { const mark = startMeasure(); try { + mono_assert(!MonoWasmThreads || !closure.isDisposed, "The function was already disposed"); const arg1 = marshaler1(args); // call user function const js_result = fn(arg1); @@ -186,10 +190,11 @@ function bind_fn_2R(closure: BindingClosure) { const marshaler2 = closure.arg_marshalers[1]!; const res_converter = closure.res_converter!; const fqn = closure.fqn; - (closure) = null; + if (!MonoWasmThreads) (closure) = null; return function bound_fn_2R(args: JSMarshalerArguments) { const mark = startMeasure(); try { + mono_assert(!MonoWasmThreads || !closure.isDisposed, "The function was already disposed"); const arg1 = marshaler1(args); const arg2 = marshaler2(args); // call user function @@ -212,10 +217,11 @@ function bind_fn(closure: BindingClosure) { const has_cleanup = closure.has_cleanup; const fn = closure.fn; const fqn = closure.fqn; - (closure) = null; + if (!MonoWasmThreads) (closure) = null; return function bound_fn(args: JSMarshalerArguments) { const mark = startMeasure(); try { + mono_assert(!MonoWasmThreads || !closure.isDisposed, "The function was already disposed"); const js_args = new Array(args_count); for (let index = 0; index < args_count; index++) { const marshaler = arg_marshalers[index]!; @@ -250,6 +256,7 @@ function bind_fn(closure: BindingClosure) { type BindingClosure = { fn: Function, fqn: string, + isDisposed: boolean, args_count: number, arg_marshalers: (BoundMarshalerToJs)[], res_converter: BoundMarshalerToCs | undefined, diff --git a/src/mono/wasm/runtime/loader/exit.ts b/src/mono/wasm/runtime/loader/exit.ts index 57cb33e..ae71908 100644 --- a/src/mono/wasm/runtime/loader/exit.ts +++ b/src/mono/wasm/runtime/loader/exit.ts @@ -2,7 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. import { ENVIRONMENT_IS_NODE, ENVIRONMENT_IS_WEB, INTERNAL, loaderHelpers, mono_assert, runtimeHelpers } from "./globals"; -import { mono_log_debug, consoleWebSocket, mono_log_error, mono_log_info_no_prefix } from "./logging"; +import { mono_log_debug, consoleWebSocket, mono_log_error, mono_log_info_no_prefix, mono_log_warn } from "./logging"; export function is_exited() { return loaderHelpers.exitCode !== undefined; @@ -45,7 +45,6 @@ export function mono_exit(exit_code: number, reason?: any): void { if (!is_exited()) { try { - reason.stack; if (!runtimeHelpers.runtimeReady) { mono_log_debug("abort_startup, reason: " + reason); abort_promises(reason); @@ -53,13 +52,15 @@ export function mono_exit(exit_code: number, reason?: any): void { logErrorOnExit(exit_code, reason); appendElementOnExit(exit_code); if (runtimeHelpers.jiterpreter_dump_stats) runtimeHelpers.jiterpreter_dump_stats(false); + if (exit_code === 0 && loaderHelpers.config?.interopCleanupOnExit) { + runtimeHelpers.forceDisposeProxies(true, true); + } } - catch { - // ignore any failures + catch (err) { + mono_log_warn("mono_exit failed", err); + // don't propagate any failures } - // TODO forceDisposeProxies(); here - loaderHelpers.exitCode = exit_code; } diff --git a/src/mono/wasm/runtime/loader/run.ts b/src/mono/wasm/runtime/loader/run.ts index 7d67f92..a05f2e0 100644 --- a/src/mono/wasm/runtime/loader/run.ts +++ b/src/mono/wasm/runtime/loader/run.ts @@ -125,6 +125,19 @@ export class HostBuilder implements DotnetHostBuilder { } // internal + withInteropCleanupOnExit(): DotnetHostBuilder { + try { + deep_merge_config(monoConfig, { + interopCleanupOnExit: true + }); + return this; + } catch (err) { + mono_exit(1, err); + throw err; + } + } + + // internal withAssertAfterExit(): DotnetHostBuilder { try { deep_merge_config(monoConfig, { diff --git a/src/mono/wasm/runtime/managed-exports.ts b/src/mono/wasm/runtime/managed-exports.ts index 43db6d1..49b16f7 100644 --- a/src/mono/wasm/runtime/managed-exports.ts +++ b/src/mono/wasm/runtime/managed-exports.ts @@ -10,6 +10,7 @@ import { alloc_stack_frame, get_arg, get_arg_gc_handle, set_arg_type, set_gc_han import { invoke_method_and_handle_exception } 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, marshal_string_to_js, marshal_task_to_js } from "./marshal-to-js"; +import { do_not_force_dispose } from "./gc-handles"; export function init_managed_exports(): void { const exports_fqn_asm = "System.Runtime.InteropServices.JavaScript"; @@ -61,6 +62,7 @@ export function init_managed_exports(): void { if (promise === null || promise === undefined) { promise = Promise.resolve(0); } + (promise as any)[do_not_force_dispose] = true; // prevent disposing the task in forceDisposeProxies() return await promise; } finally { Module.runtimeKeepalivePop();// after await promise ! diff --git a/src/mono/wasm/runtime/marshal-to-cs.ts b/src/mono/wasm/runtime/marshal-to-cs.ts index 2a695d4..66f1016 100644 --- a/src/mono/wasm/runtime/marshal-to-cs.ts +++ b/src/mono/wasm/runtime/marshal-to-cs.ts @@ -2,6 +2,8 @@ // The .NET Foundation licenses this file to you under the MIT license. import MonoWasmThreads from "consts:monoWasmThreads"; +import BuildConfiguration from "consts:configuration"; + import { isThenable } from "./cancelable-promise"; import cwraps from "./cwraps"; import { assert_not_disposed, cs_owned_js_handle_symbol, js_owned_gc_handle_symbol, mono_wasm_get_js_handle, setup_managed_proxy, teardown_managed_proxy } from "./gc-handles"; @@ -12,7 +14,7 @@ import { set_arg_length, get_arg, get_signature_arg1_type, get_signature_arg2_type, js_to_cs_marshalers, get_signature_res_type, bound_js_function_symbol, set_arg_u16, array_element_size, get_string_root, Span, ArraySegment, MemoryViewType, get_signature_arg3_type, set_arg_i64_big, set_arg_intptr, IDisposable, - set_arg_element_type, ManagedObject, JavaScriptMarshalerArgSize + set_arg_element_type, ManagedObject, JavaScriptMarshalerArgSize, proxy_debug_symbol } from "./marshal"; import { get_marshaler_to_js_by_type } from "./marshal-to-js"; import { _zero_region, localHeapViewF64, localHeapViewI32, localHeapViewU8 } from "./memory"; @@ -245,7 +247,7 @@ function _marshal_function_to_cs(arg: JSMarshalerArgument, value: Function, _?: mono_check(value && value instanceof Function, "Value is not a Function"); // TODO: we could try to cache value -> existing JSHandle - const marshal_function_to_cs_wrapper: any = (args: JSMarshalerArguments) => { + const wrapper: any = (args: JSMarshalerArguments) => { const exc = get_arg(args, 0); const res = get_arg(args, 1); const arg1 = get_arg(args, 2); @@ -253,6 +255,8 @@ function _marshal_function_to_cs(arg: JSMarshalerArgument, value: Function, _?: const arg3 = get_arg(args, 4); try { + mono_assert(!MonoWasmThreads || !wrapper.isDisposed, "Function is disposed and should not be invoked anymore."); + let arg1_js: any = undefined; let arg2_js: any = undefined; let arg3_js: any = undefined; @@ -275,8 +279,13 @@ function _marshal_function_to_cs(arg: JSMarshalerArgument, value: Function, _?: } }; - marshal_function_to_cs_wrapper[bound_js_function_symbol] = true; - const bound_function_handle = mono_wasm_get_js_handle(marshal_function_to_cs_wrapper)!; + wrapper[bound_js_function_symbol] = true; + wrapper.isDisposed = false; + wrapper.dispose = () => { wrapper.isDisposed = true; }; + const bound_function_handle = mono_wasm_get_js_handle(wrapper)!; + if (BuildConfiguration === "Debug") { + wrapper[proxy_debug_symbol] = `Proxy of JS Function with JSHandle ${bound_function_handle}: ${value.toString()}`; + } set_js_handle(arg, bound_function_handle); set_arg_type(arg, MarshalerType.Function);//TODO or action ? } @@ -309,6 +318,9 @@ function _marshal_task_to_cs(arg: JSMarshalerArgument, value: Promise, _?: set_arg_type(arg, MarshalerType.Task); const holder = new TaskCallbackHolder(value); setup_managed_proxy(holder, gc_handle); + if (BuildConfiguration === "Debug") { + (holder as any)[proxy_debug_symbol] = `C# Task with GCHandle ${gc_handle}`; + } if (MonoWasmThreads) addUnsettledPromise(); @@ -316,6 +328,7 @@ function _marshal_task_to_cs(arg: JSMarshalerArgument, value: Promise, _?: value.then(data => { try { loaderHelpers.assert_runtime_running(); + mono_assert(!holder.isDisposed, "This promise can't be propagated to managed code, because the Task was already freed."); if (MonoWasmThreads) settleUnsettledPromise(); runtimeHelpers.javaScriptExports.complete_task(gc_handle, null, data, res_converter || _marshal_cs_object_to_cs); @@ -327,6 +340,7 @@ function _marshal_task_to_cs(arg: JSMarshalerArgument, value: Promise, _?: }).catch(reason => { try { loaderHelpers.assert_runtime_running(); + mono_assert(!holder.isDisposed, "This promise can't be propagated to managed code, because the Task was already freed."); if (MonoWasmThreads) settleUnsettledPromise(); runtimeHelpers.javaScriptExports.complete_task(gc_handle, reason, null, undefined); @@ -353,13 +367,15 @@ export function marshal_exception_to_cs(arg: JSMarshalerArgument, value: any): v set_arg_type(arg, MarshalerType.JSException); const message = value.toString(); _marshal_string_to_cs_impl(arg, message); - const known_js_handle = value[cs_owned_js_handle_symbol]; if (known_js_handle) { set_js_handle(arg, known_js_handle); } else { const js_handle = mono_wasm_get_js_handle(value)!; + if (BuildConfiguration === "Debug" && Object.isExtensible(value)) { + value[proxy_debug_symbol] = `JS Error with JSHandle ${js_handle}`; + } set_js_handle(arg, js_handle); } } @@ -376,6 +392,9 @@ export function marshal_js_object_to_cs(arg: JSMarshalerArgument, value: any): v set_arg_type(arg, MarshalerType.JSObject); const js_handle = mono_wasm_get_js_handle(value)!; + if (BuildConfiguration === "Debug" && Object.isExtensible(value)) { + value[proxy_debug_symbol] = `JS Object with JSHandle ${js_handle}`; + } set_js_handle(arg, js_handle); } } @@ -441,6 +460,9 @@ function _marshal_cs_object_to_cs(arg: JSMarshalerArgument, value: any): void { else if (js_type == "object") { const js_handle = mono_wasm_get_js_handle(value); set_arg_type(arg, MarshalerType.JSObject); + if (BuildConfiguration === "Debug" && Object.isExtensible(value)) { + value[proxy_debug_symbol] = `JS Object with JSHandle ${js_handle}`; + } set_js_handle(arg, js_handle); } else { diff --git a/src/mono/wasm/runtime/marshal-to-js.ts b/src/mono/wasm/runtime/marshal-to-js.ts index 40f3f56..d861cd4 100644 --- a/src/mono/wasm/runtime/marshal-to-js.ts +++ b/src/mono/wasm/runtime/marshal-to-js.ts @@ -1,6 +1,9 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +import MonoWasmThreads from "consts:monoWasmThreads"; +import BuildConfiguration from "consts:configuration"; + import cwraps from "./cwraps"; import { _lookup_js_owned_object, mono_wasm_get_jsobj_from_js_handle, mono_wasm_get_js_handle, setup_managed_proxy } from "./gc-handles"; import { Module, createPromiseController, loaderHelpers, mono_assert, runtimeHelpers } from "./globals"; @@ -10,7 +13,7 @@ import { get_arg_b8, get_arg_date, get_arg_length, set_js_handle, get_arg, set_arg_type, get_signature_arg2_type, get_signature_arg1_type, cs_to_js_marshalers, get_signature_res_type, get_arg_u16, array_element_size, get_string_root, - ArraySegment, Span, MemoryViewType, get_signature_arg3_type, get_arg_i64_big, get_arg_intptr, get_arg_element_type, JavaScriptMarshalerArgSize + ArraySegment, Span, MemoryViewType, get_signature_arg3_type, get_arg_i64_big, get_arg_intptr, get_arg_element_type, JavaScriptMarshalerArgSize, proxy_debug_symbol } from "./marshal"; import { monoStringToString } from "./strings"; import { JSHandleNull, GCHandleNull, JSMarshalerArgument, JSMarshalerArguments, JSMarshalerType, MarshalerToCs, MarshalerToJs, BoundMarshalerToJs, MarshalerType } from "./types/internal"; @@ -189,9 +192,17 @@ function _marshal_delegate_to_js(arg: JSMarshalerArgument, _?: MarshalerType, re if (result === null || result === undefined) { // this will create new Function for the C# delegate result = (arg1_js: any, arg2_js: any, arg3_js: any): any => { + mono_assert(!MonoWasmThreads || !result.isDisposed, "Delegate is disposed and should not be invoked anymore."); // arg numbers are shifted by one, the real first is a gc handle of the callback return runtimeHelpers.javaScriptExports.call_delegate(gc_handle, arg1_js, arg2_js, arg3_js, res_converter, arg1_converter, arg2_converter, arg3_converter); }; + result.dispose = () => { + result.isDisposed = true; + }; + result.isDisposed = false; + if (BuildConfiguration === "Debug") { + (result as any)[proxy_debug_symbol] = `C# Delegate with GCHandle ${gc_handle}`; + } setup_managed_proxy(result, gc_handle); } @@ -224,6 +235,9 @@ export function marshal_task_to_js(arg: JSMarshalerArgument, _?: MarshalerType, } const promise = mono_wasm_get_jsobj_from_js_handle(js_handle); mono_assert(!!promise, () => `ERR28: promise not found for js_handle: ${js_handle} `); + if (BuildConfiguration === "Debug") { + (promise as any)[proxy_debug_symbol] = `JS Promise with JSHandle ${js_handle}`; + } loaderHelpers.assertIsControllablePromise(promise); const promise_control = loaderHelpers.getPromiseController(promise); @@ -261,6 +275,9 @@ export function mono_wasm_marshal_promise(args: JSMarshalerArguments): void { if (js_handle === JSHandleNull) { const { promise, promise_control } = createPromiseController(); const js_handle = mono_wasm_get_js_handle(promise)!; + if (BuildConfiguration === "Debug" && Object.isExtensible(promise)) { + (promise as any)[proxy_debug_symbol] = `JS Promise with JSHandle ${js_handle}`; + } set_js_handle(res, js_handle); if (exc_type !== MarshalerType.None) { @@ -328,6 +345,9 @@ export function marshal_exception_to_js(arg: JSMarshalerArgument): Error | null const message = marshal_string_to_js(arg); result = new ManagedError(message!); + if (BuildConfiguration === "Debug") { + (result as any)[proxy_debug_symbol] = `C# Exception with GCHandle ${gc_handle}`; + } setup_managed_proxy(result, gc_handle); } @@ -372,6 +392,9 @@ function _marshal_cs_object_to_js(arg: JSMarshalerArgument): any { // If the JS object for this gc_handle was already collected (or was never created) if (!result) { result = new ManagedObject(); + if (BuildConfiguration === "Debug") { + (result as any)[proxy_debug_symbol] = `C# Object with GCHandle ${gc_handle}`; + } setup_managed_proxy(result, gc_handle); } @@ -481,6 +504,9 @@ function _marshal_array_segment_to_js(arg: JSMarshalerArgument, element_type?: M throw new Error(`NotImplementedException ${MarshalerType[element_type]} `); } const gc_handle = get_arg_gc_handle(arg); + if (BuildConfiguration === "Debug") { + (result as any)[proxy_debug_symbol] = `C# ArraySegment with GCHandle ${gc_handle}`; + } setup_managed_proxy(result, gc_handle); return result; diff --git a/src/mono/wasm/runtime/marshal.ts b/src/mono/wasm/runtime/marshal.ts index 2d79bef..64f9fb0 100644 --- a/src/mono/wasm/runtime/marshal.ts +++ b/src/mono/wasm/runtime/marshal.ts @@ -15,6 +15,7 @@ export const js_to_cs_marshalers = new Map(); export const bound_cs_function_symbol = Symbol.for("wasm bound_cs_function"); 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"); /** * JSFunctionSignature is pointer to [ diff --git a/src/mono/wasm/runtime/pthreads/shared/index.ts b/src/mono/wasm/runtime/pthreads/shared/index.ts index dcced2d..c855775 100644 --- a/src/mono/wasm/runtime/pthreads/shared/index.ts +++ b/src/mono/wasm/runtime/pthreads/shared/index.ts @@ -155,8 +155,8 @@ export function mono_wasm_uninstall_js_worker_interop(uninstall_js_synchronizati mono_assert(runtimeHelpers.mono_wasm_bindings_is_ready, "JS interop is not installed on this worker."); mono_assert(!uninstall_js_synchronization_context || runtimeHelpers.jsSynchronizationContextInstalled, "JSSynchronizationContext is not installed on this worker."); - forceDisposeProxies(false); if (uninstall_js_synchronization_context) { + forceDisposeProxies(true, runtimeHelpers.diagnosticTracing); Module.runtimeKeepalivePop(); } diff --git a/src/mono/wasm/runtime/snapshot.ts b/src/mono/wasm/runtime/snapshot.ts index ec292db..6c206b9 100644 --- a/src/mono/wasm/runtime/snapshot.ts +++ b/src/mono/wasm/runtime/snapshot.ts @@ -159,6 +159,7 @@ async function getCacheKey(): Promise { // Now we remove assets collection from the hash. delete inputs.assets; + delete inputs.resources; // some things are calculated at runtime, so we need to add them to the hash inputs.preferredIcuAsset = loaderHelpers.preferredIcuAsset; // timezone is part of env variables, so it is already in the hash @@ -168,6 +169,7 @@ async function getCacheKey(): Promise { delete inputs.diagnosticTracing; delete inputs.appendElementOnExit; delete inputs.assertAfterExit; + delete inputs.interopCleanupOnExit; delete inputs.logExitCode; delete inputs.pthreadPoolSize; delete inputs.asyncFlushOnExit; @@ -177,6 +179,7 @@ async function getCacheKey(): Promise { delete inputs.maxParallelDownloads; delete inputs.enableDownloadRetry; delete inputs.exitAfterSnapshot; + delete inputs.extensions; inputs.GitHash = GitHash; inputs.ProductVersion = ProductVersion; diff --git a/src/mono/wasm/runtime/startup.ts b/src/mono/wasm/runtime/startup.ts index 0f2bf33..46f7ac4 100644 --- a/src/mono/wasm/runtime/startup.ts +++ b/src/mono/wasm/runtime/startup.ts @@ -32,6 +32,7 @@ import { init_legacy_exports } from "./net6-legacy/corebindings"; import { cwraps_binding_api, cwraps_mono_api } from "./net6-legacy/exports-legacy"; import { BINDING, MONO } from "./net6-legacy/globals"; import { localHeapViewU8 } from "./memory"; +import { assertNoProxies } from "./gc-handles"; // default size if MonoConfig.pthreadPoolSize is undefined const MONO_PTHREAD_POOL_SIZE = 4; @@ -320,6 +321,7 @@ async function postRunAsync(userpostRun: (() => void)[]) { } export function postRunWorker() { + assertNoProxies(); // signal next stage runtimeHelpers.runtimeReady = false; runtimeHelpers.afterPreRun = createPromiseController(); diff --git a/src/mono/wasm/runtime/types/index.ts b/src/mono/wasm/runtime/types/index.ts index 2a6aac1..6222355 100644 --- a/src/mono/wasm/runtime/types/index.ts +++ b/src/mono/wasm/runtime/types/index.ts @@ -26,6 +26,7 @@ export interface DotnetHostBuilder { run(): Promise } +// when adding new fields, please consider if it should be impacting the snapshot hash. If not, please drop it in the snapshot getCacheKey() export type MonoConfig = { /** * The subfolder containing managed assemblies and pdbs. This is relative to dotnet.js script. diff --git a/src/mono/wasm/runtime/types/internal.ts b/src/mono/wasm/runtime/types/internal.ts index acaccaf..bc91e97 100644 --- a/src/mono/wasm/runtime/types/internal.ts +++ b/src/mono/wasm/runtime/types/internal.ts @@ -67,6 +67,7 @@ export function coerceNull(ptr: T | nu return ptr as T; } +// when adding new fields, please consider if it should be impacting the snapshot hash. If not, please drop it in the snapshot getCacheKey() export type MonoConfigInternal = MonoConfig & { runtimeOptions?: string[], // array of runtime options as strings aotProfilerOptions?: AOTProfilerOptions, // dictionary-style Object. If omitted, aot profiler will not be initialized. @@ -74,6 +75,7 @@ export type MonoConfigInternal = MonoConfig & { waitForDebugger?: number, appendElementOnExit?: boolean assertAfterExit?: boolean // default true for shell/nodeJS + interopCleanupOnExit?: boolean logExitCode?: boolean forwardConsoleLogsToWS?: boolean, asyncFlushOnExit?: boolean @@ -196,6 +198,7 @@ export type RuntimeHelpers = { instantiate_asset: (asset: AssetEntry, url: string, bytes: Uint8Array) => void, instantiate_symbols_asset: (pendingAsset: AssetEntryInternal) => Promise, jiterpreter_dump_stats?: (x: boolean) => string, + forceDisposeProxies: (disposeMethods: boolean, verbose: boolean) => void, } export type AOTProfilerOptions = { diff --git a/src/mono/wasm/runtime/weak-ref.ts b/src/mono/wasm/runtime/weak-ref.ts index 52fffeb..f5c6187 100644 --- a/src/mono/wasm/runtime/weak-ref.ts +++ b/src/mono/wasm/runtime/weak-ref.ts @@ -1,7 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -const _use_weak_ref = typeof globalThis.WeakRef === "function"; +export const _use_weak_ref = typeof globalThis.WeakRef === "function"; export function create_weak_ref(js_obj: T): WeakRef { if (_use_weak_ref) { @@ -12,6 +12,9 @@ export function create_weak_ref(js_obj: T): WeakRef { return { deref: () => { return js_obj; + }, + dispose: () => { + js_obj = null!; } }; } diff --git a/src/mono/wasm/runtime/web-socket.ts b/src/mono/wasm/runtime/web-socket.ts index 28b4e15..93c5a9f 100644 --- a/src/mono/wasm/runtime/web-socket.ts +++ b/src/mono/wasm/runtime/web-socket.ts @@ -179,17 +179,17 @@ export function ws_wasm_abort(ws: WebSocketExtension): void { ws[wasm_ws_is_aborted] = true; const open_promise_control = ws[wasm_ws_pending_open_promise]; if (open_promise_control) { - open_promise_control.reject("OperationCanceledException"); + open_promise_control.reject(new Error("OperationCanceledException")); } for (const close_promise_control of ws[wasm_ws_pending_close_promises]) { - close_promise_control.reject("OperationCanceledException"); + close_promise_control.reject(new Error("OperationCanceledException")); } for (const send_promise_control of ws[wasm_ws_pending_send_promises]) { - send_promise_control.reject("OperationCanceledException"); + send_promise_control.reject(new Error("OperationCanceledException")); } ws[wasm_ws_pending_receive_promise_queue].drain(receive_promise_control => { - receive_promise_control.reject("OperationCanceledException"); + receive_promise_control.reject(new Error("OperationCanceledException")); }); // this is different from Managed implementation diff --git a/src/mono/wasm/test-main.js b/src/mono/wasm/test-main.js index 8c4eecb..2e72976 100644 --- a/src/mono/wasm/test-main.js +++ b/src/mono/wasm/test-main.js @@ -259,6 +259,7 @@ function configureRuntime(dotnet, runArgs) { .withExitOnUnhandledError() .withExitCodeLogging() .withElementOnExit() + .withInteropCleanupOnExit() .withAssertAfterExit() .withConfig({ loadAllSatelliteResources: true -- 2.7.4