[browser][mt] Release all proxies of C# and JS objects (#88052)
authorPavel Savara <pavel.savara@gmail.com>
Mon, 24 Jul 2023 20:03:13 +0000 (22:03 +0200)
committerGitHub <noreply@github.com>
Mon, 24 Jul 2023 20:03:13 +0000 (22:03 +0200)
33 files changed:
src/libraries/System.Net.WebSockets.Client/src/System/Net/WebSockets/BrowserWebSockets/BrowserWebSocket.cs
src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Interop/JavaScriptExports.cs
src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSFunctionBinding.cs
src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSHost.cs
src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSHostImplementation.cs
src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSObject.cs
src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSSynchronizationContext.cs
src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Marshaling/JSMarshalerArgument.Task.cs
src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/SynchronizationContextExtensions.cs
src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/WebWorker.cs
src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/JSImportExportTest.cs
src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/JavaScriptTestHelper.cs
src/mono/sample/wasm/browser-threads-minimal/main.js
src/mono/wasm/runtime/cancelable-promise.ts
src/mono/wasm/runtime/exports-internal.ts
src/mono/wasm/runtime/exports.ts
src/mono/wasm/runtime/gc-handles.ts
src/mono/wasm/runtime/invoke-cs.ts
src/mono/wasm/runtime/invoke-js.ts
src/mono/wasm/runtime/loader/exit.ts
src/mono/wasm/runtime/loader/run.ts
src/mono/wasm/runtime/managed-exports.ts
src/mono/wasm/runtime/marshal-to-cs.ts
src/mono/wasm/runtime/marshal-to-js.ts
src/mono/wasm/runtime/marshal.ts
src/mono/wasm/runtime/pthreads/shared/index.ts
src/mono/wasm/runtime/snapshot.ts
src/mono/wasm/runtime/startup.ts
src/mono/wasm/runtime/types/index.ts
src/mono/wasm/runtime/types/internal.ts
src/mono/wasm/runtime/weak-ref.ts
src/mono/wasm/runtime/web-socket.ts
src/mono/wasm/test-main.js

index ded2f5c..121fa85 100644 (file)
@@ -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);
index e89142b..20c0279 100644 (file)
@@ -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)
             {
index bec4aff..d5b0036 100644 (file)
@@ -172,6 +172,7 @@ namespace System.Runtime.InteropServices.JavaScript
         [MethodImpl(MethodImplOptions.AggressiveInlining)]
         internal static unsafe void InvokeJSImpl(JSObject jsFunction, Span<JSMarshalerArgument> 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<JSMarshalerType> 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);
index 219a3b9..16e3324 100644 (file)
@@ -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<JSObject> ImportAsync(string moduleName, string moduleUrl, CancellationToken cancellationToken = default)
         {
+#if FEATURE_WASM_THREADS
+            JSSynchronizationContext.AssertWebWorkerContext();
+#endif
             return JSHostImplementation.ImportAsync(moduleName, moduleUrl, cancellationToken);
         }
 
index 499c593..94707e6 100644 (file)
@@ -32,13 +32,28 @@ namespace System.Runtime.InteropServices.JavaScript
         }
 
         // we use this to maintain identity of GCHandle for a managed object
-        public static Dictionary<object, IntPtr> s_gcHandleFromJSOwnedObject = new Dictionary<object, IntPtr>(ReferenceEqualityComparer.Instance);
+#if FEATURE_WASM_THREADS
+        [ThreadStatic]
+#endif
+        private static Dictionary<object, IntPtr>? s_jsOwnedObjects;
+
+        public static Dictionary<object, IntPtr> ThreadJsOwnedObjects
+        {
+            get
+            {
+                s_jsOwnedObjects ??= new Dictionary<object, IntPtr>(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<JSObject>? 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);
         }
 
index d53934a..57dc9ab 100644 (file)
@@ -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);
         }
 
index 1a807e5..5aabaac 100644 (file)
@@ -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;
index 99844db..e5fa56a 100644 (file)
@@ -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<T> tcs = new TaskCompletionSource<T>(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)
index b91fe71..bebe252 100644 (file)
@@ -7,7 +7,7 @@ using System.Threading.Tasks;
 namespace System.Runtime.InteropServices.JavaScript
 {
     /// <summary>
-    /// This is draft for possible public API of SynchronizationContext
+    /// Extensions of SynchronizationContext which propagate errors and return values
     /// </summary>
     public static class SynchronizationContextExtension
     {
index 98a9037..8184ca9 100644 (file)
@@ -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<T>(SynchronizationContext ctx, TaskCompletionSource<T> tcs, Exception ex)
+        private static void SendWhenException<T>(SynchronizationContext ctx, TaskCompletionSource<T> 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<T>(SynchronizationContext ctx, TaskCompletionSource<T> tcs, Task<T> done)
+        private static void SendWhenDone<T>(SynchronizationContext ctx, TaskCompletionSource<T> tcs, Task<T> done)
         {
             try
             {
-                ctx.Post((_) =>
+                ctx.Send((_) =>
                 {
                     PropagateCompletion(tcs, done);
                 }, null);
index a341f3c..b55bc19 100644 (file)
@@ -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<JSException>(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<JSException>(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<object[]> 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)
index fea5cc4..f61ab28 100644 (file)
@@ -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;
+        }
     }
 }
 
index d35c744..7263133 100644 (file)
@@ -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();
index 3b59aef..2fbb648 100644 (file)
@@ -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"));
 }
 
index 7854b3b..bfd9d53 100644 (file)
@@ -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,
index e3d1bdc..6ecd3b6 100644 (file)
@@ -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();
index c95e0a9..857b737 100644 (file)
@@ -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<any>;
 
 // 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[<any>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 = (<any>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
index a7a8bd1..b38393a 100644 (file)
@@ -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,
             }
         }
 
-        (<any>bound_fn)[bound_cs_function_symbol] = true;
+        (<any>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;
-    (<any>closure) = null;
+    if (!MonoWasmThreads) (<any>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;
-    (<any>closure) = null;
+    if (!MonoWasmThreads) (<any>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;
-    (<any>closure) = null;
+    if (!MonoWasmThreads) (<any>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;
-    (<any>closure) = null;
+    if (!MonoWasmThreads) (<any>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;
-    (<any>closure) = null;
+    if (!MonoWasmThreads) (<any>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 {
index 5b762f4..a17ef89 100644 (file)
@@ -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[] = <any>[null];// 0th slot is dummy, we never free bound functions
+export const fn_wrapper_by_fn_handle: Function[] = <any>[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_
             }
         }
 
-        (<any>bound_fn)[imported_js_function_symbol] = true;
+        (<any>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, <any>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;
-    (<any>closure) = null;
+    if (!MonoWasmThreads) (<any>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;
-    (<any>closure) = null;
+    if (!MonoWasmThreads) (<any>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;
-    (<any>closure) = null;
+    if (!MonoWasmThreads) (<any>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;
-    (<any>closure) = null;
+    if (!MonoWasmThreads) (<any>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;
-    (<any>closure) = null;
+    if (!MonoWasmThreads) (<any>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,
index 57cb33e..ae71908 100644 (file)
@@ -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;
     }
 
index 7d67f92..a05f2e0 100644 (file)
@@ -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, {
index 43db6d1..49b16f7 100644 (file)
@@ -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 !
index 2a695d4..66f1016 100644 (file)
@@ -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<any>, _?:
     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<any>, _?:
     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<any>, _?:
     }).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 {
index 40f3f56..d861cd4 100644 (file)
@@ -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<any>(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;
index 2d79bef..64f9fb0 100644 (file)
@@ -15,6 +15,7 @@ export const js_to_cs_marshalers = new Map<MarshalerType, MarshalerToCs>();
 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 [
index dcced2d..c855775 100644 (file)
@@ -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();
     }
 
index ec292db..6c206b9 100644 (file)
@@ -159,6 +159,7 @@ async function getCacheKey(): Promise<string | null> {
 
     // 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<string | null> {
     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<string | null> {
     delete inputs.maxParallelDownloads;
     delete inputs.enableDownloadRetry;
     delete inputs.exitAfterSnapshot;
+    delete inputs.extensions;
 
     inputs.GitHash = GitHash;
     inputs.ProductVersion = ProductVersion;
index 0f2bf33..46f7ac4 100644 (file)
@@ -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<void>();
index 2a6aac1..6222355 100644 (file)
@@ -26,6 +26,7 @@ export interface DotnetHostBuilder {
     run(): Promise<number>
 }
 
+// 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.
index acaccaf..bc91e97 100644 (file)
@@ -67,6 +67,7 @@ export function coerceNull<T extends ManagedPointer | NativePointer>(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<void>,
     jiterpreter_dump_stats?: (x: boolean) => string,
+    forceDisposeProxies: (disposeMethods: boolean, verbose: boolean) => void,
 }
 
 export type AOTProfilerOptions = {
index 52fffeb..f5c6187 100644 (file)
@@ -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<T extends object>(js_obj: T): WeakRef<T> {
     if (_use_weak_ref) {
@@ -12,6 +12,9 @@ export function create_weak_ref<T extends object>(js_obj: T): WeakRef<T> {
         return <any>{
             deref: () => {
                 return js_obj;
+            },
+            dispose: () => {
+                js_obj = null!;
             }
         };
     }
index 28b4e15..93c5a9f 100644 (file)
@@ -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
index 8c4eecb..2e72976 100644 (file)
@@ -259,6 +259,7 @@ function configureRuntime(dotnet, runArgs) {
         .withExitOnUnhandledError()
         .withExitCodeLogging()
         .withElementOnExit()
+        .withInteropCleanupOnExit()
         .withAssertAfterExit()
         .withConfig({
             loadAllSatelliteResources: true