Skip to content

Commit 68a18eb

Browse files
pavelsavaramaraf
andauthored
[browser][MT] dispatch across threads via emscripten (#97669)
Co-authored-by: Marek Fišera <[email protected]>
1 parent 64822a6 commit 68a18eb

File tree

28 files changed

+353
-247
lines changed

28 files changed

+353
-247
lines changed

src/libraries/Common/src/Interop/Browser/Interop.Runtime.cs

Lines changed: 23 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,26 @@ internal static unsafe partial class Runtime
1313
{
1414
[MethodImplAttribute(MethodImplOptions.InternalCall)]
1515
internal static extern void ReleaseCSOwnedObject(nint jsHandle);
16+
#if FEATURE_WASM_MANAGED_THREADS
17+
[MethodImplAttribute(MethodImplOptions.InternalCall)]
18+
internal static extern void ReleaseCSOwnedObjectPost(nint targetNativeTID, nint jsHandle);
19+
#endif
20+
1621
[MethodImpl(MethodImplOptions.InternalCall)]
1722
public static extern void InvokeJSFunction(nint functionHandle, nint data);
23+
#if FEATURE_WASM_MANAGED_THREADS
24+
[MethodImpl(MethodImplOptions.InternalCall)]
25+
public static extern void InvokeJSFunctionSend(nint targetNativeTID, nint functionHandle, nint data);
26+
#endif
27+
1828
[MethodImpl(MethodImplOptions.InternalCall)]
1929
public static extern unsafe void BindCSFunction(in string fully_qualified_name, int signature_hash, void* signature, out int is_exception, out object result);
2030
[MethodImpl(MethodImplOptions.InternalCall)]
2131
public static extern void ResolveOrRejectPromise(nint data);
32+
#if FEATURE_WASM_MANAGED_THREADS
33+
[MethodImpl(MethodImplOptions.InternalCall)]
34+
public static extern void ResolveOrRejectPromisePost(nint targetNativeTID, nint data);
35+
#endif
2236

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

3650
[MethodImpl(MethodImplOptions.InternalCall)]
3751
public static extern void InvokeJSImportSync(nint data, nint signature);
38-
3952
[MethodImpl(MethodImplOptions.InternalCall)]
40-
public static extern void InvokeJSImportAsync(nint data, nint signature);
53+
public static extern void InvokeJSImportSyncSend(nint targetNativeTID, nint data, nint signature);
54+
[MethodImpl(MethodImplOptions.InternalCall)]
55+
public static extern void InvokeJSImportAsyncPost(nint targetNativeTID, nint data, nint signature);
56+
[MethodImpl(MethodImplOptions.InternalCall)]
57+
public static extern void CancelPromise(nint taskHolderGCHandle);
58+
[MethodImpl(MethodImplOptions.InternalCall)]
59+
public static extern void CancelPromisePost(nint targetNativeTID, nint taskHolderGCHandle);
4160
#else
4261
[MethodImpl(MethodImplOptions.InternalCall)]
4362
public static extern unsafe void BindJSImport(void* signature, out int is_exception, out object result);
4463
[MethodImpl(MethodImplOptions.InternalCall)]
4564
public static extern void InvokeJSImport(int importHandle, nint data);
65+
[MethodImpl(MethodImplOptions.InternalCall)]
66+
public static extern void CancelPromise(nint gcHandle);
4667
#endif
4768

48-
#region Legacy
49-
50-
[MethodImplAttribute(MethodImplOptions.InternalCall)]
51-
internal static extern void InvokeJSWithArgsRef(IntPtr jsHandle, in string method, in object?[] parms, out int exceptionalResult, out object result);
52-
[MethodImplAttribute(MethodImplOptions.InternalCall)]
53-
internal static extern void GetObjectPropertyRef(IntPtr jsHandle, in string propertyName, out int exceptionalResult, out object result);
54-
[MethodImplAttribute(MethodImplOptions.InternalCall)]
55-
internal static extern void SetObjectPropertyRef(IntPtr jsHandle, in string propertyName, in object? value, bool createIfNotExists, bool hasOwnProperty, out int exceptionalResult, out object result);
56-
[MethodImplAttribute(MethodImplOptions.InternalCall)]
57-
internal static extern void GetByIndexRef(IntPtr jsHandle, int index, out int exceptionalResult, out object result);
58-
[MethodImplAttribute(MethodImplOptions.InternalCall)]
59-
internal static extern void SetByIndexRef(IntPtr jsHandle, int index, in object? value, out int exceptionalResult, out object result);
60-
[MethodImplAttribute(MethodImplOptions.InternalCall)]
61-
internal static extern void GetGlobalObjectRef(in string? globalName, out int exceptionalResult, out object result);
62-
63-
[MethodImplAttribute(MethodImplOptions.InternalCall)]
64-
internal static extern void TypedArrayToArrayRef(IntPtr jsHandle, out int exceptionalResult, out object result);
65-
[MethodImplAttribute(MethodImplOptions.InternalCall)]
66-
internal static extern void CreateCSOwnedObjectRef(in string className, in object[] parms, out int exceptionalResult, out object result);
67-
[MethodImplAttribute(MethodImplOptions.InternalCall)]
68-
internal static extern void TypedArrayFromRef(int arrayPtr, int begin, int end, int bytesPerElement, int type, out int exceptionalResult, out object result);
69-
70-
#endregion
7169

7270
}
7371
}

src/libraries/System.Console/src/System.Console.csproj

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -281,10 +281,4 @@
281281
<Reference Include="Microsoft.Win32.Primitives" />
282282
</ItemGroup>
283283

284-
<ItemGroup Condition="'$(TargetPlatformIdentifier)' == 'browser'">
285-
<ProjectReference Include="$(LibrariesProjectRoot)System.Runtime.InteropServices\gen\Microsoft.Interop.SourceGeneration\Microsoft.Interop.SourceGeneration.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
286-
<ProjectReference Include="$(LibrariesProjectRoot)System.Runtime.InteropServices.JavaScript\gen\JSImportGenerator\JSImportGenerator.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
287-
<Reference Include="System.Runtime.InteropServices.JavaScript" />
288-
</ItemGroup>
289-
290284
</Project>

src/libraries/System.Console/src/System/ConsolePal.WebAssembly.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System.IO;
5-
using System.Runtime.InteropServices.JavaScript;
65
using System.Text;
6+
using System.Runtime.CompilerServices;
77
using Microsoft.Win32.SafeHandles;
88

99
namespace System
@@ -72,8 +72,8 @@ public override void Flush()
7272

7373
internal static partial class ConsolePal
7474
{
75-
[JSImport("globalThis.console.clear")]
76-
public static partial void Clear();
75+
[MethodImplAttribute(MethodImplOptions.InternalCall)]
76+
public static extern void Clear();
7777

7878
private static Encoding? s_outputEncoding;
7979

src/libraries/System.Net.Http/src/System/Net/Http/BrowserHttpHandler/BrowserHttpInterop.cs

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -140,13 +140,11 @@ public static async Task CancellationHelper(Task promise, CancellationToken canc
140140
using (var operationRegistration = cancellationToken.Register(static s =>
141141
{
142142
(Task _promise, JSObject _jsController) = ((Task, JSObject))s!;
143-
CancelablePromise.CancelPromise(_promise, static (JSObject __jsController) =>
143+
CancelablePromise.CancelPromise(_promise);
144+
if (!_jsController.IsDisposed)
144145
{
145-
if (!__jsController.IsDisposed)
146-
{
147-
AbortResponse(__jsController);
148-
}
149-
}, _jsController);
146+
AbortResponse(_jsController);
147+
}
150148
}, (promise, jsController)))
151149
{
152150
await promise.ConfigureAwait(true);

src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/CancelablePromise.cs

Lines changed: 17 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,6 @@ namespace System.Runtime.InteropServices.JavaScript
99
{
1010
public static partial class CancelablePromise
1111
{
12-
[JSImport("INTERNAL.mono_wasm_cancel_promise")]
13-
private static partial void _CancelPromise(IntPtr gcHandle);
14-
1512
public static void CancelPromise(Task promise)
1613
{
1714
// this check makes sure that promiseGCHandle is still valid handle
@@ -27,56 +24,30 @@ public static void CancelPromise(Task promise)
2724
{
2825
return;
2926
}
30-
_CancelPromise(holder.GCHandle);
27+
holder.IsCanceling = true;
28+
Interop.Runtime.CancelPromise(holder.GCHandle);
3129
#else
32-
// this need to be manually dispatched via holder.ProxyContext, because we don't pass JSObject with affinity
33-
holder.ProxyContext.SynchronizationContext.Post(static (object? h) =>
30+
31+
lock (holder.ProxyContext)
3432
{
35-
var holder = (JSHostImplementation.PromiseHolder)h!;
36-
lock (holder.ProxyContext)
33+
if (promise.IsCompleted || holder.IsDisposed || holder.ProxyContext._isDisposed)
3734
{
38-
if (holder.IsDisposed)
39-
{
40-
return;
41-
}
35+
return;
4236
}
43-
_CancelPromise(holder.GCHandle);
44-
}, holder);
45-
#endif
46-
}
47-
48-
public static void CancelPromise<T>(Task promise, Action<T> callback, T state)
49-
{
50-
// this check makes sure that promiseGCHandle is still valid handle
51-
if (promise.IsCompleted)
52-
{
53-
return;
54-
}
55-
JSHostImplementation.PromiseHolder? holder = promise.AsyncState as JSHostImplementation.PromiseHolder;
56-
if (holder == null) throw new InvalidOperationException("Expected Task converted from JS Promise");
37+
holder.IsCanceling = true;
5738

58-
#if !FEATURE_WASM_MANAGED_THREADS
59-
if (holder.IsDisposed)
60-
{
61-
return;
62-
}
63-
_CancelPromise(holder.GCHandle);
64-
callback.Invoke(state);
65-
#else
66-
// this need to be manually dispatched via holder.ProxyContext, because we don't pass JSObject with affinity
67-
holder.ProxyContext.SynchronizationContext.Post(_ =>
68-
{
69-
lock (holder.ProxyContext)
39+
if (holder.ProxyContext.IsCurrentThread())
7040
{
71-
if (holder.IsDisposed)
72-
{
73-
return;
74-
}
41+
Interop.Runtime.CancelPromise(holder.GCHandle);
7542
}
76-
77-
_CancelPromise(holder.GCHandle);
78-
callback.Invoke(state);
79-
}, null);
43+
else
44+
{
45+
// FIXME: race condition
46+
// we know that holder.GCHandle is still valid because we hold the ProxyContext lock
47+
// but the message may arrive to the target thread after it was resolved, making GCHandle invalid
48+
Interop.Runtime.CancelPromisePost(holder.ProxyContext.NativeTID, holder.GCHandle);
49+
}
50+
}
8051
#endif
8152
}
8253
}

src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Interop/JavaScriptExports.cs

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ namespace System.Runtime.InteropServices.JavaScript
1414
{
1515
// this maps to src\mono\browser\runtime\managed-exports.ts
1616
// the public methods are protected from trimming by DynamicDependency on JSFunctionBinding.BindJSFunction
17+
// TODO: all the calls here should be running on deputy or TP in MT, not in UI thread
1718
internal static unsafe partial class JavaScriptExports
1819
{
1920
// the marshaled signature is:
@@ -180,6 +181,9 @@ public static void CallDelegate(JSMarshalerArgument* arguments_buffer)
180181
{
181182
#if FEATURE_WASM_MANAGED_THREADS
182183
// when we arrive here, we are on the thread which owns the proxies
184+
// if we need to dispatch the call to another thread in the future
185+
// we may need to consider how to solve blocking of the synchronous call
186+
// see also https://github.com/dotnet/runtime/issues/76958#issuecomment-1921418290
183187
arg_exc.AssertCurrentThreadContext();
184188
#endif
185189

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

218224
#if FEATURE_WASM_MANAGED_THREADS
219225
lock (ctx)
220226
{
227+
// this means that CompleteTask is called before the ToManaged(out Task? value)
221228
if (holder.Callback == null)
222229
{
223230
holder.CallbackReady = new ManualResetEventSlim(false);
224231
}
225232
}
233+
226234
if (holder.CallbackReady != null)
227235
{
228236
var threadFlag = Monitor.ThrowOnBlockingWaitOnJSInteropThread;
229237
try
230238
{
231239
Monitor.ThrowOnBlockingWaitOnJSInteropThread = false;
232-
#pragma warning disable CA1416 // Validate platform compatibility
240+
#pragma warning disable CA1416 // Validate platform compatibility
233241
holder.CallbackReady?.Wait();
234-
#pragma warning restore CA1416 // Validate platform compatibility
242+
#pragma warning restore CA1416 // Validate platform compatibility
235243
}
236244
finally
237245
{
238246
Monitor.ThrowOnBlockingWaitOnJSInteropThread = threadFlag;
239247
}
240248
}
241-
#endif
242-
var callback = holder.Callback!;
249+
250+
lock (ctx)
251+
{
252+
callback = holder.Callback!;
253+
// if Interop.Runtime.CancelPromisePost is in flight, we can't free the GCHandle, because it's needed in JS
254+
var isOutOfOrderCancellation = holder.IsCanceling && arg_res.slot.Type != MarshalerType.Discard;
255+
// FIXME: when it happens we are leaking GCHandle + holder
256+
if (!isOutOfOrderCancellation)
257+
{
258+
ctx.ReleasePromiseHolder(arg_1.slot.GCHandle);
259+
}
260+
}
261+
#else
262+
callback = holder.Callback!;
243263
ctx.ReleasePromiseHolder(arg_1.slot.GCHandle);
264+
#endif
244265

245266
// arg_2, arg_3 are processed by the callback
246267
// JSProxyContext.PopOperation() is called by the callback

src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Interop/JavaScriptImports.Generated.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,11 @@ internal static unsafe partial class JavaScriptImports
4343
[JSImport("INTERNAL.get_dotnet_instance")]
4444
public static partial JSObject GetDotnetInstance();
4545
[JSImport("INTERNAL.dynamic_import")]
46+
// TODO: the continuation should be running on deputy or TP in MT
4647
public static partial Task<JSObject> DynamicImport(string moduleName, string moduleUrl);
4748
#if FEATURE_WASM_MANAGED_THREADS
4849
[JSImport("INTERNAL.thread_available")]
50+
// TODO: the continuation should be running on deputy or TP in MT
4951
public static partial Task ThreadAvailable();
5052
#endif
5153

0 commit comments

Comments
 (0)