Skip to content

[browser][MT] dispatch across threads via emscripten #97669

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Feb 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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