Skip to content

Commit

Permalink
[browser][MT] dispatch across threads via emscripten (#97669)
Browse files Browse the repository at this point in the history
Co-authored-by: Marek Fišera <mara@neptuo.com>
  • Loading branch information
pavelsavara and maraf authored Feb 5, 2024
1 parent 64822a6 commit 68a18eb
Show file tree
Hide file tree
Showing 28 changed files with 353 additions and 247 deletions.
48 changes: 23 additions & 25 deletions src/libraries/Common/src/Interop/Browser/Interop.Runtime.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,26 @@ internal static unsafe partial class Runtime
{
[MethodImplAttribute(MethodImplOptions.InternalCall)]
internal static extern void ReleaseCSOwnedObject(nint jsHandle);
#if FEATURE_WASM_MANAGED_THREADS
[MethodImplAttribute(MethodImplOptions.InternalCall)]
internal static extern void ReleaseCSOwnedObjectPost(nint targetNativeTID, nint jsHandle);
#endif

[MethodImpl(MethodImplOptions.InternalCall)]
public static extern void InvokeJSFunction(nint functionHandle, nint data);
#if FEATURE_WASM_MANAGED_THREADS
[MethodImpl(MethodImplOptions.InternalCall)]
public static extern void InvokeJSFunctionSend(nint targetNativeTID, nint functionHandle, nint data);
#endif

[MethodImpl(MethodImplOptions.InternalCall)]
public static extern unsafe void BindCSFunction(in string fully_qualified_name, int signature_hash, void* signature, out int is_exception, out object result);
[MethodImpl(MethodImplOptions.InternalCall)]
public static extern void ResolveOrRejectPromise(nint data);
#if FEATURE_WASM_MANAGED_THREADS
[MethodImpl(MethodImplOptions.InternalCall)]
public static extern void ResolveOrRejectPromisePost(nint targetNativeTID, nint data);
#endif

#if !ENABLE_JS_INTEROP_BY_VALUE
[MethodImpl(MethodImplOptions.InternalCall)]
Expand All @@ -35,39 +49,23 @@ internal static unsafe partial class Runtime

[MethodImpl(MethodImplOptions.InternalCall)]
public static extern void InvokeJSImportSync(nint data, nint signature);

[MethodImpl(MethodImplOptions.InternalCall)]
public static extern void InvokeJSImportAsync(nint data, nint signature);
public static extern void InvokeJSImportSyncSend(nint targetNativeTID, nint data, nint signature);
[MethodImpl(MethodImplOptions.InternalCall)]
public static extern void InvokeJSImportAsyncPost(nint targetNativeTID, nint data, nint signature);
[MethodImpl(MethodImplOptions.InternalCall)]
public static extern void CancelPromise(nint taskHolderGCHandle);
[MethodImpl(MethodImplOptions.InternalCall)]
public static extern void CancelPromisePost(nint targetNativeTID, nint taskHolderGCHandle);
#else
[MethodImpl(MethodImplOptions.InternalCall)]
public static extern unsafe void BindJSImport(void* signature, out int is_exception, out object result);
[MethodImpl(MethodImplOptions.InternalCall)]
public static extern void InvokeJSImport(int importHandle, nint data);
[MethodImpl(MethodImplOptions.InternalCall)]
public static extern void CancelPromise(nint gcHandle);
#endif

#region Legacy

[MethodImplAttribute(MethodImplOptions.InternalCall)]
internal static extern void InvokeJSWithArgsRef(IntPtr jsHandle, in string method, in object?[] parms, out int exceptionalResult, out object result);
[MethodImplAttribute(MethodImplOptions.InternalCall)]
internal static extern void GetObjectPropertyRef(IntPtr jsHandle, in string propertyName, out int exceptionalResult, out object result);
[MethodImplAttribute(MethodImplOptions.InternalCall)]
internal static extern void SetObjectPropertyRef(IntPtr jsHandle, in string propertyName, in object? value, bool createIfNotExists, bool hasOwnProperty, out int exceptionalResult, out object result);
[MethodImplAttribute(MethodImplOptions.InternalCall)]
internal static extern void GetByIndexRef(IntPtr jsHandle, int index, out int exceptionalResult, out object result);
[MethodImplAttribute(MethodImplOptions.InternalCall)]
internal static extern void SetByIndexRef(IntPtr jsHandle, int index, in object? value, out int exceptionalResult, out object result);
[MethodImplAttribute(MethodImplOptions.InternalCall)]
internal static extern void GetGlobalObjectRef(in string? globalName, out int exceptionalResult, out object result);

[MethodImplAttribute(MethodImplOptions.InternalCall)]
internal static extern void TypedArrayToArrayRef(IntPtr jsHandle, out int exceptionalResult, out object result);
[MethodImplAttribute(MethodImplOptions.InternalCall)]
internal static extern void CreateCSOwnedObjectRef(in string className, in object[] parms, out int exceptionalResult, out object result);
[MethodImplAttribute(MethodImplOptions.InternalCall)]
internal static extern void TypedArrayFromRef(int arrayPtr, int begin, int end, int bytesPerElement, int type, out int exceptionalResult, out object result);

#endregion

}
}
6 changes: 0 additions & 6 deletions src/libraries/System.Console/src/System.Console.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -281,10 +281,4 @@
<Reference Include="Microsoft.Win32.Primitives" />
</ItemGroup>

<ItemGroup Condition="'$(TargetPlatformIdentifier)' == 'browser'">
<ProjectReference Include="$(LibrariesProjectRoot)System.Runtime.InteropServices\gen\Microsoft.Interop.SourceGeneration\Microsoft.Interop.SourceGeneration.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
<ProjectReference Include="$(LibrariesProjectRoot)System.Runtime.InteropServices.JavaScript\gen\JSImportGenerator\JSImportGenerator.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
<Reference Include="System.Runtime.InteropServices.JavaScript" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System.IO;
using System.Runtime.InteropServices.JavaScript;
using System.Text;
using System.Runtime.CompilerServices;
using Microsoft.Win32.SafeHandles;

namespace System
Expand Down Expand Up @@ -72,8 +72,8 @@ public override void Flush()

internal static partial class ConsolePal
{
[JSImport("globalThis.console.clear")]
public static partial void Clear();
[MethodImplAttribute(MethodImplOptions.InternalCall)]
public static extern void Clear();

private static Encoding? s_outputEncoding;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,13 +140,11 @@ public static async Task CancellationHelper(Task promise, CancellationToken canc
using (var operationRegistration = cancellationToken.Register(static s =>
{
(Task _promise, JSObject _jsController) = ((Task, JSObject))s!;
CancelablePromise.CancelPromise(_promise, static (JSObject __jsController) =>
CancelablePromise.CancelPromise(_promise);
if (!_jsController.IsDisposed)
{
if (!__jsController.IsDisposed)
{
AbortResponse(__jsController);
}
}, _jsController);
AbortResponse(_jsController);
}
}, (promise, jsController)))
{
await promise.ConfigureAwait(true);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,6 @@ namespace System.Runtime.InteropServices.JavaScript
{
public static partial class CancelablePromise
{
[JSImport("INTERNAL.mono_wasm_cancel_promise")]
private static partial void _CancelPromise(IntPtr gcHandle);

public static void CancelPromise(Task promise)
{
// this check makes sure that promiseGCHandle is still valid handle
Expand All @@ -27,56 +24,30 @@ public static void CancelPromise(Task promise)
{
return;
}
_CancelPromise(holder.GCHandle);
holder.IsCanceling = true;
Interop.Runtime.CancelPromise(holder.GCHandle);
#else
// this need to be manually dispatched via holder.ProxyContext, because we don't pass JSObject with affinity
holder.ProxyContext.SynchronizationContext.Post(static (object? h) =>

lock (holder.ProxyContext)
{
var holder = (JSHostImplementation.PromiseHolder)h!;
lock (holder.ProxyContext)
if (promise.IsCompleted || holder.IsDisposed || holder.ProxyContext._isDisposed)
{
if (holder.IsDisposed)
{
return;
}
return;
}
_CancelPromise(holder.GCHandle);
}, holder);
#endif
}

public static void CancelPromise<T>(Task promise, Action<T> callback, T state)
{
// this check makes sure that promiseGCHandle is still valid handle
if (promise.IsCompleted)
{
return;
}
JSHostImplementation.PromiseHolder? holder = promise.AsyncState as JSHostImplementation.PromiseHolder;
if (holder == null) throw new InvalidOperationException("Expected Task converted from JS Promise");
holder.IsCanceling = true;

#if !FEATURE_WASM_MANAGED_THREADS
if (holder.IsDisposed)
{
return;
}
_CancelPromise(holder.GCHandle);
callback.Invoke(state);
#else
// this need to be manually dispatched via holder.ProxyContext, because we don't pass JSObject with affinity
holder.ProxyContext.SynchronizationContext.Post(_ =>
{
lock (holder.ProxyContext)
if (holder.ProxyContext.IsCurrentThread())
{
if (holder.IsDisposed)
{
return;
}
Interop.Runtime.CancelPromise(holder.GCHandle);
}
_CancelPromise(holder.GCHandle);
callback.Invoke(state);
}, null);
else
{
// FIXME: race condition
// we know that holder.GCHandle is still valid because we hold the ProxyContext lock
// but the message may arrive to the target thread after it was resolved, making GCHandle invalid
Interop.Runtime.CancelPromisePost(holder.ProxyContext.NativeTID, holder.GCHandle);
}
}
#endif
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ namespace System.Runtime.InteropServices.JavaScript
{
// this maps to src\mono\browser\runtime\managed-exports.ts
// the public methods are protected from trimming by DynamicDependency on JSFunctionBinding.BindJSFunction
// TODO: all the calls here should be running on deputy or TP in MT, not in UI thread
internal static unsafe partial class JavaScriptExports
{
// the marshaled signature is:
Expand Down Expand Up @@ -180,6 +181,9 @@ public static void CallDelegate(JSMarshalerArgument* arguments_buffer)
{
#if FEATURE_WASM_MANAGED_THREADS
// when we arrive here, we are on the thread which owns the proxies
// if we need to dispatch the call to another thread in the future
// we may need to consider how to solve blocking of the synchronous call
// see also https://github.com/dotnet/runtime/issues/76958#issuecomment-1921418290
arg_exc.AssertCurrentThreadContext();
#endif

Expand All @@ -205,6 +209,7 @@ public static void CallDelegate(JSMarshalerArgument* arguments_buffer)
public static void CompleteTask(JSMarshalerArgument* arguments_buffer)
{
ref JSMarshalerArgument arg_exc = ref arguments_buffer[0]; // initialized by caller in alloc_stack_frame()
ref JSMarshalerArgument arg_res = ref arguments_buffer[1]; // initialized by caller in alloc_stack_frame()
ref JSMarshalerArgument arg_1 = ref arguments_buffer[2];// initialized and set by caller
// arg_2 set by caller when this is SetException call
// arg_3 set by caller when this is SetResult call
Expand All @@ -214,33 +219,49 @@ public static void CompleteTask(JSMarshalerArgument* arguments_buffer)
// when we arrive here, we are on the thread which owns the proxies
var ctx = arg_exc.AssertCurrentThreadContext();
var holder = ctx.GetPromiseHolder(arg_1.slot.GCHandle);
ToManagedCallback callback;

#if FEATURE_WASM_MANAGED_THREADS
lock (ctx)
{
// this means that CompleteTask is called before the ToManaged(out Task? value)
if (holder.Callback == null)
{
holder.CallbackReady = new ManualResetEventSlim(false);
}
}

if (holder.CallbackReady != null)
{
var threadFlag = Monitor.ThrowOnBlockingWaitOnJSInteropThread;
try
{
Monitor.ThrowOnBlockingWaitOnJSInteropThread = false;
#pragma warning disable CA1416 // Validate platform compatibility
#pragma warning disable CA1416 // Validate platform compatibility
holder.CallbackReady?.Wait();
#pragma warning restore CA1416 // Validate platform compatibility
#pragma warning restore CA1416 // Validate platform compatibility
}
finally
{
Monitor.ThrowOnBlockingWaitOnJSInteropThread = threadFlag;
}
}
#endif
var callback = holder.Callback!;

lock (ctx)
{
callback = holder.Callback!;
// if Interop.Runtime.CancelPromisePost is in flight, we can't free the GCHandle, because it's needed in JS
var isOutOfOrderCancellation = holder.IsCanceling && arg_res.slot.Type != MarshalerType.Discard;
// FIXME: when it happens we are leaking GCHandle + holder
if (!isOutOfOrderCancellation)
{
ctx.ReleasePromiseHolder(arg_1.slot.GCHandle);
}
}
#else
callback = holder.Callback!;
ctx.ReleasePromiseHolder(arg_1.slot.GCHandle);
#endif

// arg_2, arg_3 are processed by the callback
// JSProxyContext.PopOperation() is called by the callback
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,11 @@ internal static unsafe partial class JavaScriptImports
[JSImport("INTERNAL.get_dotnet_instance")]
public static partial JSObject GetDotnetInstance();
[JSImport("INTERNAL.dynamic_import")]
// TODO: the continuation should be running on deputy or TP in MT
public static partial Task<JSObject> DynamicImport(string moduleName, string moduleUrl);
#if FEATURE_WASM_MANAGED_THREADS
[JSImport("INTERNAL.thread_available")]
// TODO: the continuation should be running on deputy or TP in MT
public static partial Task ThreadAvailable();
#endif

Expand Down
Loading

0 comments on commit 68a18eb

Please sign in to comment.