diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System.Runtime.InteropServices.JavaScript.csproj b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System.Runtime.InteropServices.JavaScript.csproj index 9b20df8bd80e1..4b637cee5f174 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System.Runtime.InteropServices.JavaScript.csproj +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System.Runtime.InteropServices.JavaScript.csproj @@ -68,6 +68,7 @@ + diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSAsyncTaskScheduler.cs b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSAsyncTaskScheduler.cs new file mode 100644 index 0000000000000..423843af3301f --- /dev/null +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSAsyncTaskScheduler.cs @@ -0,0 +1,43 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace System.Runtime.InteropServices.JavaScript +{ + // executes all tasks thru queue, never inline + internal sealed class JSAsyncTaskScheduler : TaskScheduler + { + private readonly JSSynchronizationContext m_synchronizationContext; + + internal JSAsyncTaskScheduler(JSSynchronizationContext synchronizationContext) + { + m_synchronizationContext = synchronizationContext; + } + + protected override void QueueTask(Task task) + { + m_synchronizationContext.Post((_) => + { + if (!TryExecuteTask(task)) + { + Environment.FailFast("Unexpected failure in JSAsyncTaskScheduler" + Environment.CurrentManagedThreadId); + } + }, null); + } + + // this is the main difference from the SynchronizationContextTaskScheduler + protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued) + { + return false; + } + + protected override IEnumerable? GetScheduledTasks() + { + return null; + } + + public override int MaximumConcurrencyLevel => 1; + } +} diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSProxyContext.cs b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSProxyContext.cs index 372c42ff2bc99..2ef8eb48120c9 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSProxyContext.cs +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSProxyContext.cs @@ -40,6 +40,7 @@ private JSProxyContext() public int ManagedTID; // current managed thread id public bool IsMainThread; public JSSynchronizationContext SynchronizationContext; + public JSAsyncTaskScheduler? AsyncTaskScheduler; public static MainThreadingMode MainThreadingMode = MainThreadingMode.DeputyThread; public static JSThreadBlockingMode ThreadBlockingMode = JSThreadBlockingMode.NoBlockingWait; @@ -483,7 +484,7 @@ public static void ReleaseCSOwnedObject(JSObject jso, bool skipJS) { if (IsJSVHandle(jsHandle)) { - Environment.FailFast("TODO implement blocking ReleaseCSOwnedObjectSend to make sure the order of FreeJSVHandle is correct."); + Environment.FailFast($"TODO implement blocking ReleaseCSOwnedObjectSend to make sure the order of FreeJSVHandle is correct, ManagedThreadId: {Environment.CurrentManagedThreadId}. {Environment.NewLine} {Environment.StackTrace}"); } // this is async message, we need to call this as the last thing @@ -501,7 +502,7 @@ public static void ReleaseCSOwnedObject(JSObject jso, bool skipJS) } } -#endregion + #endregion #region Dispose 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 ed7fffbae25de..8d9ea3283c8df 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 @@ -56,6 +56,7 @@ public static unsafe JSSynchronizationContext InstallWebWorkerInterop(bool isMai } var proxyContext = ctx.ProxyContext; + proxyContext.AsyncTaskScheduler = new JSAsyncTaskScheduler(ctx); JSProxyContext.CurrentThreadContext = proxyContext; JSProxyContext.ExecutionContext = proxyContext; if (isMainThread) 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 e01b292cc172b..cb1e43a1a0096 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 @@ -7,6 +7,7 @@ using System.ComponentModel; using System.Threading; using static System.Runtime.InteropServices.JavaScript.JSHostImplementation; +using System.Runtime.CompilerServices; namespace System.Runtime.InteropServices.JavaScript { @@ -140,13 +141,20 @@ internal void ToJSDynamic(Task? value) { Task? task = value; + var ctx = ToJSContext; + var canMarshalTaskResultOnSameCall = CanMarshalTaskResultOnSameCall(ctx); + if (task == null) { + if (!canMarshalTaskResultOnSameCall) + { + Environment.FailFast("Marshalling null return Task to JS is not supported in MT"); + } slot.Type = MarshalerType.None; return; } - if (task.IsCompleted) + if (canMarshalTaskResultOnSameCall && task.IsCompleted) { if (task.Exception != null) { @@ -172,7 +180,6 @@ internal void ToJSDynamic(Task? value) } } - var ctx = ToJSContext; if (slot.Type != MarshalerType.TaskPreCreated) { @@ -189,7 +196,9 @@ internal void ToJSDynamic(Task? value) var taskHolder = ctx.CreateCSOwnedProxy(slot.JSHandle); #if FEATURE_WASM_MANAGED_THREADS - task.ContinueWith(Complete, taskHolder, CancellationToken.None, TaskContinuationOptions.RunContinuationsAsynchronously, TaskScheduler.FromCurrentSynchronizationContext()); + // AsyncTaskScheduler will make sure that the resolve message is always sent after this call is completed + // that is: synchronous marshaling and eventually message to the target thread, which need to arrive before the resolve message + task.ContinueWith(Complete, taskHolder, ctx.AsyncTaskScheduler!); #else task.ContinueWith(Complete, taskHolder, TaskScheduler.Current); #endif @@ -229,18 +238,18 @@ public void ToJS(Task? value) { Task? task = value; var ctx = ToJSContext; - var isCurrentThread = ctx.IsCurrentThread(); + var canMarshalTaskResultOnSameCall = CanMarshalTaskResultOnSameCall(ctx); if (task == null) { - if (!isCurrentThread) + if (!canMarshalTaskResultOnSameCall) { - Environment.FailFast("Marshalling null task to JS is not supported in MT"); + Environment.FailFast("Marshalling null return Task to JS is not supported in MT"); } slot.Type = MarshalerType.None; return; } - if (isCurrentThread && task.IsCompleted) + if (canMarshalTaskResultOnSameCall && task.IsCompleted) { if (task.Exception != null) { @@ -273,7 +282,9 @@ public void ToJS(Task? value) var taskHolder = ctx.CreateCSOwnedProxy(slot.JSHandle); #if FEATURE_WASM_MANAGED_THREADS - task.ContinueWith(Complete, taskHolder, CancellationToken.None, TaskContinuationOptions.RunContinuationsAsynchronously, TaskScheduler.FromCurrentSynchronizationContext()); + // AsyncTaskScheduler will make sure that the resolve message is always sent after this call is completed + // that is: synchronous marshaling and eventually message to the target thread, which need to arrive before the resolve message + task.ContinueWith(Complete, taskHolder, ctx.AsyncTaskScheduler!); #else task.ContinueWith(Complete, taskHolder, TaskScheduler.Current); #endif @@ -303,19 +314,19 @@ public void ToJS(Task? value, ArgumentToJSCallback marshaler) { Task? task = value; var ctx = ToJSContext; - var isCurrentThread = ctx.IsCurrentThread(); + var canMarshalTaskResultOnSameCall = CanMarshalTaskResultOnSameCall(ctx); if (task == null) { - if (!isCurrentThread) + if (!canMarshalTaskResultOnSameCall) { - Environment.FailFast("NULL not supported in MT"); + Environment.FailFast("Marshalling null return Task to JS is not supported in MT"); } slot.Type = MarshalerType.None; return; } - if (isCurrentThread && task.IsCompleted) + if (canMarshalTaskResultOnSameCall && task.IsCompleted) { if (task.Exception != null) { @@ -350,7 +361,9 @@ public void ToJS(Task? value, ArgumentToJSCallback marshaler) var taskHolder = ctx.CreateCSOwnedProxy(slot.JSHandle); #if FEATURE_WASM_MANAGED_THREADS - task.ContinueWith(Complete, new HolderAndMarshaler(taskHolder, marshaler), CancellationToken.None, TaskContinuationOptions.RunContinuationsAsynchronously, TaskScheduler.FromCurrentSynchronizationContext()); + // AsyncTaskScheduler will make sure that the resolve message is always sent after this call is completed + // that is: synchronous marshaling and eventually message to the target thread, which need to arrive before the resolve message + task.ContinueWith(Complete, new HolderAndMarshaler(taskHolder, marshaler), ctx.AsyncTaskScheduler!); #else task.ContinueWith(Complete, new HolderAndMarshaler(taskHolder, marshaler), TaskScheduler.Current); #endif @@ -370,6 +383,44 @@ static void Complete(Task task, object? thm) } } +#if !DEBUG + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#endif +#if FEATURE_WASM_MANAGED_THREADS + // We can't marshal resolved/rejected/null Task.Result directly into current argument when this is marshaling return of JSExport across threads + private bool CanMarshalTaskResultOnSameCall(JSProxyContext ctx) + { + if (slot.Type != MarshalerType.TaskPreCreated) + { + // this means that we are not in the return value of JSExport + // we are marshaling parameter of JSImport + return true; + } + + if (ctx.IsCurrentThread()) + { + // If the JS and Managed is running on the same thread we can use the args buffer, + // because the call is synchronous and the buffer will be processed. + // In that case the pre-allocated Promise would be discarded as necessary + // and the result will be marshaled by `try_marshal_sync_task_to_js` + return true; + } + + // Otherwise this is JSExport return value and we can't use the args buffer, because the args buffer arrived in async message and nobody is reading after this. + // In such case the JS side already pre-created the Promise and we have to use it, to resolve it in separate call via `mono_wasm_resolve_or_reject_promise_post` + // there is JSVHandle in this arg + return false; + } +#else +#pragma warning disable CA1822 // Mark members as static + private bool CanMarshalTaskResultOnSameCall(JSProxyContext _) + { + // in ST build this is always synchronous and we can marshal the result directly + return true; + } +#pragma warning restore CA1822 // Mark members as static +#endif + private sealed record HolderAndMarshaler(JSObject TaskHolder, ArgumentToJSCallback Marshaler); private static void RejectPromise(JSObject holder, Exception ex)