[browser] introduce JSProxyContext (#95959)

This commit is contained in:
Pavel Savara 2023-12-20 08:57:32 +01:00 committed by GitHub
parent 5cf6892d8d
commit 8bd23f0792
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
54 changed files with 1372 additions and 706 deletions

4
.gitignore vendored
View File

@ -187,10 +187,6 @@ node_modules/
*.metaproj
*.metaproj.tmp
bin.localpkg/
src/mono/wasm/runtime/dotnet.d.ts.sha256
src/mono/wasm/runtime/dotnet-legacy.d.ts.sha256
src/mono/sample/wasm/browser-nextjs/public/
# RIA/Silverlight projects
Generated_Code/

View File

@ -34,7 +34,7 @@ internal static partial class Interop
#if FEATURE_WASM_THREADS
[MethodImpl(MethodImplOptions.InternalCall)]
public static extern void InstallWebWorkerInterop();
public static extern void InstallWebWorkerInterop(IntPtr proxyContextGCHandle);
[MethodImpl(MethodImplOptions.InternalCall)]
public static extern void UninstallWebWorkerInterop();
#endif

View File

@ -16,6 +16,8 @@
<TargetPlatformIdentifier>$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)'))</TargetPlatformIdentifier>
<DefineConstants Condition="'$(TargetPlatformIdentifier)' == 'windows'">$(DefineConstants);TargetsWindows</DefineConstants>
<DefineConstants Condition="'$(TargetPlatformIdentifier)' == 'browser'">$(DefineConstants);TARGETS_BROWSER</DefineConstants>
<!-- Active issue https://github.com/dotnet/runtime/issues/96173 -->
<_XUnitBackgroundExec>false</_XUnitBackgroundExec>
</PropertyGroup>
<PropertyGroup Condition="'$(TargetOS)' == 'browser'">

View File

@ -17,6 +17,9 @@
<!-- This WASM test is problematic and slow right now. This sets the xharness timeout but there is also override in sendtohelix-browser.targets -->
<WasmXHarnessTestsTimeout>01:15:00</WasmXHarnessTestsTimeout>
<!-- Active issue https://github.com/dotnet/runtime/issues/96173 -->
<_XUnitBackgroundExec>false</_XUnitBackgroundExec>
</PropertyGroup>
<ItemGroup>

View File

@ -21,6 +21,7 @@ namespace Microsoft.Interop.JavaScript
{
}
// TODO order parameters in such way that affinity capturing parameters are emitted first
public override IEnumerable<StatementSyntax> Generate(TypePositionInfo info, StubCodeContext context)
{
string argName = context.GetAdditionalIdentifier(info, "js_arg");

View File

@ -46,6 +46,7 @@
<Compile Include="System\Runtime\InteropServices\JavaScript\JSImportAttribute.cs" />
<Compile Include="System\Runtime\InteropServices\JavaScript\CancelablePromise.cs" />
<Compile Include="System\Runtime\InteropServices\JavaScript\SynchronizationContextExtensions.cs" />
<Compile Include="System\Runtime\InteropServices\JavaScript\JSProxyContext.cs" />
<Compile Include="System\Runtime\InteropServices\JavaScript\MarshalerType.cs" />
<Compile Include="System\Runtime\InteropServices\JavaScript\Marshaling\JSMarshalerArgument.BigInt64.cs" />

View File

@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.
using System.Threading;
using System.Threading.Tasks;
namespace System.Runtime.InteropServices.JavaScript
@ -21,13 +22,24 @@ namespace System.Runtime.InteropServices.JavaScript
JSHostImplementation.PromiseHolder? holder = promise.AsyncState as JSHostImplementation.PromiseHolder;
if (holder == null) throw new InvalidOperationException("Expected Task converted from JS Promise");
#if FEATURE_WASM_THREADS
holder.SynchronizationContext!.Send(static (JSHostImplementation.PromiseHolder holder) =>
#if !FEATURE_WASM_THREADS
if (holder.IsDisposed)
{
#endif
return;
}
_CancelPromise(holder.GCHandle);
#if FEATURE_WASM_THREADS
#else
holder.ProxyContext.SynchronizationContext.Post(static (object? h) =>
{
var holder = (JSHostImplementation.PromiseHolder)h!;
lock (holder.ProxyContext)
{
if (holder.IsDisposed)
{
return;
}
}
_CancelPromise(holder.GCHandle);
}, holder);
#endif
}
@ -42,15 +54,27 @@ namespace System.Runtime.InteropServices.JavaScript
JSHostImplementation.PromiseHolder? holder = promise.AsyncState as JSHostImplementation.PromiseHolder;
if (holder == null) throw new InvalidOperationException("Expected Task converted from JS Promise");
#if FEATURE_WASM_THREADS
holder.SynchronizationContext!.Send((JSHostImplementation.PromiseHolder holder) =>
#if !FEATURE_WASM_THREADS
if (holder.IsDisposed)
{
#endif
return;
}
_CancelPromise(holder.GCHandle);
callback.Invoke(state);
#else
holder.ProxyContext.SynchronizationContext.Post(_ =>
{
lock (holder.ProxyContext)
{
if (holder.IsDisposed)
{
return;
}
}
_CancelPromise(holder.GCHandle);
callback.Invoke(state);
#if FEATURE_WASM_THREADS
}, holder);
}, null);
#endif
}
}

View File

@ -26,6 +26,11 @@ namespace System.Runtime.InteropServices.JavaScript
ref JSMarshalerArgument arg_2 = ref arguments_buffer[3]; // initialized and set by caller
try
{
#if FEATURE_WASM_THREADS
// when we arrive here, we are on the thread which owns the proxies
arg_exc.AssertCurrentThreadContext();
#endif
arg_1.ToManaged(out IntPtr entrypointPtr);
if (entrypointPtr == IntPtr.Zero)
{
@ -103,6 +108,10 @@ namespace System.Runtime.InteropServices.JavaScript
ref JSMarshalerArgument arg_2 = ref arguments_buffer[3];
try
{
#if FEATURE_WASM_THREADS
// when we arrive here, we are on the thread which owns the proxies
arg_exc.AssertCurrentThreadContext();
#endif
arg_1.ToManaged(out byte[]? dllBytes);
arg_2.ToManaged(out byte[]? pdbBytes);
@ -121,6 +130,10 @@ namespace System.Runtime.InteropServices.JavaScript
ref JSMarshalerArgument arg_1 = ref arguments_buffer[2];
try
{
#if FEATURE_WASM_THREADS
// when we arrive here, we are on the thread which owns the proxies
arg_exc.AssertCurrentThreadContext();
#endif
arg_1.ToManaged(out byte[]? dllBytes);
if (dllBytes != null)
@ -140,32 +153,12 @@ namespace System.Runtime.InteropServices.JavaScript
{
ref JSMarshalerArgument arg_exc = ref arguments_buffer[0]; // initialized by caller in alloc_stack_frame()
ref JSMarshalerArgument arg_1 = ref arguments_buffer[2]; // initialized and set by caller
try
{
var gcHandle = arg_1.slot.GCHandle;
if (IsGCVHandle(gcHandle))
{
if (ThreadJsOwnedHolders.Remove(gcHandle, out PromiseHolder? holder))
{
holder.GCHandle = IntPtr.Zero;
holder.Callback!(null);
}
}
else
{
GCHandle handle = (GCHandle)gcHandle;
var target = handle.Target!;
if (target is PromiseHolder holder)
{
holder.GCHandle = IntPtr.Zero;
holder.Callback!(null);
}
else
{
ThreadJsOwnedObjects.Remove(target);
}
handle.Free();
}
// when we arrive here, we are on the thread which owns the proxies
var ctx = arg_exc.AssertCurrentThreadContext();
ctx.ReleaseJSOwnedObjectByGCHandle(arg_1.slot.GCHandle);
}
catch (Exception ex)
{
@ -185,6 +178,11 @@ namespace System.Runtime.InteropServices.JavaScript
// arg_4 set by JS caller when there are arguments
try
{
#if FEATURE_WASM_THREADS
// when we arrive here, we are on the thread which owns the proxies
arg_exc.AssertCurrentThreadContext();
#endif
GCHandle callback_gc_handle = (GCHandle)arg_1.slot.GCHandle;
if (callback_gc_handle.Target is ToManagedCallback callback)
{
@ -210,34 +208,34 @@ namespace System.Runtime.InteropServices.JavaScript
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
try
{
var holderGCHandle = arg_1.slot.GCHandle;
if (IsGCVHandle(holderGCHandle))
// 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);
#if FEATURE_WASM_THREADS
lock (ctx)
{
if (ThreadJsOwnedHolders.Remove(holderGCHandle, out PromiseHolder? holder))
if (holder.Callback == null)
{
holder.GCHandle = IntPtr.Zero;
// arg_2, arg_3 are processed by the callback
holder.Callback!(arguments_buffer);
holder.CallbackReady = new ManualResetEventSlim(false);
}
}
else
if (holder.CallbackReady != null)
{
GCHandle handle = (GCHandle)holderGCHandle;
var target = handle.Target!;
if (target is PromiseHolder holder)
{
holder.GCHandle = IntPtr.Zero;
// arg_2, arg_3 are processed by the callback
holder.Callback!(arguments_buffer);
}
else
{
ThreadJsOwnedObjects.Remove(target);
}
handle.Free();
#pragma warning disable CA1416 // Validate platform compatibility
holder.CallbackReady?.Wait();
#pragma warning restore CA1416 // Validate platform compatibility
}
#endif
var callback = holder.Callback!;
ctx.ReleasePromiseHolder(arg_1.slot.GCHandle);
// arg_2, arg_3 are processed by the callback
// JSProxyContext.PopOperation() is called by the callback
callback!(arguments_buffer);
}
catch (Exception ex)
{
@ -254,6 +252,9 @@ namespace System.Runtime.InteropServices.JavaScript
ref JSMarshalerArgument arg_1 = ref arguments_buffer[2];// initialized and set by caller
try
{
// when we arrive here, we are on the thread which owns the proxies
arg_exc.AssertCurrentThreadContext();
GCHandle exception_gc_handle = (GCHandle)arg_1.slot.GCHandle;
if (exception_gc_handle.Target is Exception exception)
{
@ -275,17 +276,10 @@ namespace System.Runtime.InteropServices.JavaScript
// this is here temporarily, until JSWebWorker becomes public API
[DynamicDependency(DynamicallyAccessedMemberTypes.NonPublicMethods, "System.Runtime.InteropServices.JavaScript.JSWebWorker", "System.Runtime.InteropServices.JavaScript")]
// the marshaled signature is:
// void InstallSynchronizationContext()
public static void InstallSynchronizationContext (JSMarshalerArgument* arguments_buffer) {
ref JSMarshalerArgument arg_exc = ref arguments_buffer[0]; // initialized by caller in alloc_stack_frame()
try
{
InstallWebWorkerInterop(true);
}
catch (Exception ex)
{
arg_exc.ToJS(ex);
}
// void InstallMainSynchronizationContext()
public static void InstallMainSynchronizationContext()
{
InstallWebWorkerInterop(true);
}
#endif

View File

@ -7,19 +7,6 @@ namespace System.Runtime.InteropServices.JavaScript
{
internal static unsafe partial class JavaScriptImports
{
public static void ResolveOrRejectPromise(Span<JSMarshalerArgument> arguments)
{
fixed (JSMarshalerArgument* ptr = arguments)
{
Interop.Runtime.ResolveOrRejectPromise(ptr);
ref JSMarshalerArgument exceptionArg = ref arguments[0];
if (exceptionArg.slot.Type != MarshalerType.None)
{
JSHostImplementation.ThrowException(ref exceptionArg);
}
}
}
#if !DISABLE_LEGACY_JS_INTEROP
#region legacy

View File

@ -32,17 +32,10 @@ namespace System.Runtime.InteropServices.JavaScript
public static void GetCSOwnedObjectByJSHandleRef(nint jsHandle, int shouldAddInflight, out JSObject? result)
{
if (JSHostImplementation.ThreadCsOwnedObjects.TryGetValue(jsHandle, out WeakReference<JSObject>? reference))
{
reference.TryGetTarget(out JSObject? jsObject);
if (shouldAddInflight != 0)
{
jsObject?.AddInFlight();
}
result = jsObject;
return;
}
result = null;
#if FEATURE_WASM_THREADS
LegacyHostImplementation.ThrowIfLegacyWorkerThread();
#endif
result = JSProxyContext.MainThreadContext.GetCSOwnedObjectByJSHandle(jsHandle, shouldAddInflight);
}
public static IntPtr GetCSOwnedObjectJSHandleRef(in JSObject jsObject, int shouldAddInflight)
@ -71,32 +64,7 @@ namespace System.Runtime.InteropServices.JavaScript
#if FEATURE_WASM_THREADS
LegacyHostImplementation.ThrowIfLegacyWorkerThread();
#endif
JSObject? res = null;
if (!JSHostImplementation.ThreadCsOwnedObjects.TryGetValue(jsHandle, out WeakReference<JSObject>? reference) ||
!reference.TryGetTarget(out res) ||
res.IsDisposed)
{
#pragma warning disable CS0612 // Type or member is obsolete
res = mappedType switch
{
LegacyHostImplementation.MappedType.JSObject => new JSObject(jsHandle),
LegacyHostImplementation.MappedType.Array => new Array(jsHandle),
LegacyHostImplementation.MappedType.ArrayBuffer => new ArrayBuffer(jsHandle),
LegacyHostImplementation.MappedType.DataView => new DataView(jsHandle),
LegacyHostImplementation.MappedType.Function => new Function(jsHandle),
LegacyHostImplementation.MappedType.Uint8Array => new Uint8Array(jsHandle),
_ => throw new ArgumentOutOfRangeException(nameof(mappedType))
};
#pragma warning restore CS0612 // Type or member is obsolete
JSHostImplementation.ThreadCsOwnedObjects[jsHandle] = new WeakReference<JSObject>(res, trackResurrection: true);
}
if (shouldAddInflight != 0)
{
res.AddInFlight();
}
jsObject = res;
jsObject = JSProxyContext.MainThreadContext.CreateCSOwnedProxy(jsHandle, mappedType, shouldAddInflight);
}
public static void GetJSOwnedObjectByGCHandleRef(int gcHandle, out object result)
@ -107,7 +75,7 @@ namespace System.Runtime.InteropServices.JavaScript
public static IntPtr GetJSOwnedObjectGCHandleRef(in object obj)
{
return JSHostImplementation.GetJSOwnedObjectGCHandle(obj, GCHandleType.Normal);
return JSProxyContext.MainThreadContext.GetJSOwnedObjectGCHandle(obj, GCHandleType.Normal);
}
public static IntPtr CreateTaskSource()

View File

@ -47,10 +47,10 @@ namespace System.Runtime.InteropServices.JavaScript
}
#if FEATURE_WASM_THREADS
var currentTID = JSSynchronizationContext.CurrentJSSynchronizationContext?.TargetTID;
if (jsException.OwnerTID != currentTID)
if (!jsException.ProxyContext.IsCurrentThread())
{
return bs;
// if we are on another thread, it would be too expensive and risky to obtain lazy stack trace.
return bs + Environment.NewLine + "... omitted JavaScript stack trace from another thread.";
}
#endif
string? jsStackTrace = jsException.GetPropertyAsString("stack");

View File

@ -30,9 +30,6 @@ namespace System.Runtime.InteropServices.JavaScript
internal static volatile uint nextImportHandle = 1;
internal int ImportHandle;
internal bool IsAsync;
#if FEATURE_WASM_THREADS
internal bool IsThreadCaptured;
#endif
[StructLayout(LayoutKind.Sequential, Pack = 4)]
internal struct JSBindingHeader
@ -177,7 +174,7 @@ namespace System.Runtime.InteropServices.JavaScript
if (RuntimeInformation.OSArchitecture != Architecture.Wasm)
throw new PlatformNotSupportedException();
return BindJSFunctionImpl(functionName, moduleName, signatures);
return BindJSImportImpl(functionName, moduleName, signatures);
}
/// <summary>
@ -220,15 +217,19 @@ namespace System.Runtime.InteropServices.JavaScript
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static unsafe void InvokeJSImportImpl(JSFunctionBinding signature, Span<JSMarshalerArgument> arguments)
{
#if FEATURE_WASM_THREADS
var targetContext = JSProxyContext.SealJSImportCapturing();
JSProxyContext.AssertIsInteropThread();
arguments[0].slot.ContextHandle = targetContext.ContextHandle;
arguments[1].slot.ContextHandle = targetContext.ContextHandle;
#else
var targetContext = JSProxyContext.MainThreadContext;
#endif
if (signature.IsAsync)
{
// pre-allocate the result handle and Task
#if FEATURE_WASM_THREADS
JSSynchronizationContext.AssertWebWorkerContext();
var holder = new JSHostImplementation.PromiseHolder(JSSynchronizationContext.CurrentJSSynchronizationContext!);
#else
var holder = new JSHostImplementation.PromiseHolder();
#endif
var holder = new JSHostImplementation.PromiseHolder(targetContext);
arguments[1].slot.Type = MarshalerType.TaskPreCreated;
arguments[1].slot.GCHandle = holder.GCHandle;
}
@ -253,10 +254,10 @@ namespace System.Runtime.InteropServices.JavaScript
}
}
internal static unsafe JSFunctionBinding BindJSFunctionImpl(string functionName, string moduleName, ReadOnlySpan<JSMarshalerType> signatures)
internal static unsafe JSFunctionBinding BindJSImportImpl(string functionName, string moduleName, ReadOnlySpan<JSMarshalerType> signatures)
{
#if FEATURE_WASM_THREADS
JSSynchronizationContext.AssertWebWorkerContext();
JSProxyContext.AssertIsInteropThread();
#endif
var signature = JSHostImplementation.GetMethodSignature(signatures, functionName, moduleName);
@ -284,5 +285,19 @@ namespace System.Runtime.InteropServices.JavaScript
return signature;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static unsafe void ResolveOrRejectPromise(Span<JSMarshalerArgument> arguments)
{
fixed (JSMarshalerArgument* ptr = arguments)
{
Interop.Runtime.ResolveOrRejectPromise(ptr);
ref JSMarshalerArgument exceptionArg = ref arguments[0];
if (exceptionArg.slot.Type != MarshalerType.None)
{
JSHostImplementation.ThrowException(ref exceptionArg);
}
}
}
}
}

View File

@ -22,7 +22,7 @@ namespace System.Runtime.InteropServices.JavaScript
get
{
#if FEATURE_WASM_THREADS
JSSynchronizationContext.AssertWebWorkerContext();
JSProxyContext.AssertIsInteropThread();
#endif
return JavaScriptImports.GetGlobalThis();
}
@ -36,7 +36,7 @@ namespace System.Runtime.InteropServices.JavaScript
get
{
#if FEATURE_WASM_THREADS
JSSynchronizationContext.AssertWebWorkerContext();
JSProxyContext.AssertIsInteropThread();
#endif
return JavaScriptImports.GetDotnetInstance();
}
@ -54,7 +54,7 @@ namespace System.Runtime.InteropServices.JavaScript
public static Task<JSObject> ImportAsync(string moduleName, string moduleUrl, CancellationToken cancellationToken = default)
{
#if FEATURE_WASM_THREADS
JSSynchronizationContext.AssertWebWorkerContext();
JSProxyContext.AssertIsInteropThread();
#endif
return JSHostImplementation.ImportAsync(moduleName, moduleUrl, cancellationToken);
}
@ -65,7 +65,7 @@ namespace System.Runtime.InteropServices.JavaScript
get
{
#if FEATURE_WASM_THREADS
return JSSynchronizationContext.CurrentJSSynchronizationContext ?? JSSynchronizationContext.MainJSSynchronizationContext!;
return (JSProxyContext.ExecutionContext ?? JSProxyContext.MainThreadContext).SynchronizationContext;
#else
return null!;
#endif

View File

@ -12,34 +12,24 @@ namespace System.Runtime.InteropServices.JavaScript
public sealed class PromiseHolder
{
public nint GCHandle; // could be also virtual GCVHandle
public readonly nint GCHandle; // could be also virtual GCVHandle
public ToManagedCallback? Callback;
public JSProxyContext ProxyContext;
public bool IsDisposed;
#if FEATURE_WASM_THREADS
// the JavaScript object could only exist on the single web worker and can't migrate to other workers
internal JSSynchronizationContext SynchronizationContext;
public ManualResetEventSlim? CallbackReady;
#endif
#if FEATURE_WASM_THREADS
// TODO possibly unify signature with non-MT and pass null
public PromiseHolder(JSSynchronizationContext targetContext)
public PromiseHolder(JSProxyContext targetContext)
{
GCHandle = (IntPtr)InteropServices.GCHandle.Alloc(this, GCHandleType.Normal);
SynchronizationContext = targetContext;
ProxyContext = targetContext;
}
#else
public PromiseHolder()
{
GCHandle = (IntPtr)InteropServices.GCHandle.Alloc(this, GCHandleType.Normal);
}
#endif
public PromiseHolder(nint gcvHandle)
public PromiseHolder(JSProxyContext targetContext, nint gcvHandle)
{
GCHandle = gcvHandle;
#if FEATURE_WASM_THREADS
JSSynchronizationContext.AssertWebWorkerContext();
SynchronizationContext = JSSynchronizationContext.CurrentJSSynchronizationContext!;
#endif
ProxyContext = targetContext;
}
}

View File

@ -1,7 +1,6 @@
// 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.Diagnostics.CodeAnalysis;
using System.IO;
using System.Reflection;
@ -16,115 +15,6 @@ namespace System.Runtime.InteropServices.JavaScript
{
private const string TaskGetResultName = "get_Result";
private static MethodInfo? s_taskGetResultMethodInfo;
// we use this to maintain identity of JSHandle for a JSObject proxy
#if FEATURE_WASM_THREADS
[ThreadStatic]
#endif
private static Dictionary<nint, WeakReference<JSObject>>? s_csOwnedObjects;
public static Dictionary<nint, WeakReference<JSObject>> ThreadCsOwnedObjects
{
get
{
s_csOwnedObjects ??= new();
return s_csOwnedObjects;
}
}
// we use this to maintain identity of GCHandle for a managed object
#if FEATURE_WASM_THREADS
[ThreadStatic]
#endif
private static Dictionary<object, nint>? s_jsOwnedObjects;
public static Dictionary<object, nint> ThreadJsOwnedObjects
{
get
{
s_jsOwnedObjects ??= new Dictionary<object, nint>(ReferenceEqualityComparer.Instance);
return s_jsOwnedObjects;
}
}
// this is similar to GCHandle, but the GCVHandle is allocated on JS side and this keeps the C# proxy alive
#if FEATURE_WASM_THREADS
[ThreadStatic]
#endif
private static Dictionary<nint, PromiseHolder>? s_jsOwnedHolders;
public static Dictionary<nint, PromiseHolder> ThreadJsOwnedHolders
{
get
{
s_jsOwnedHolders ??= new Dictionary<nint, PromiseHolder>();
return s_jsOwnedHolders;
}
}
// JSVHandle is like JSHandle, but it's not tracked and allocated by the JS side
// It's used when we need to create JSHandle-like identity ahead of time, before calling JS.
// they have negative values, so that they don't collide with JSHandles.
#if FEATURE_WASM_THREADS
[ThreadStatic]
#endif
public static nint NextJSVHandle;
#if FEATURE_WASM_THREADS
[ThreadStatic]
#endif
private static List<nint>? s_JSVHandleFreeList;
public static List<nint> JSVHandleFreeList
{
get
{
s_JSVHandleFreeList ??= new();
return s_JSVHandleFreeList;
}
}
public static nint AllocJSVHandle()
{
#if FEATURE_WASM_THREADS
// TODO, when Task is passed to JSImport as parameter, it could be sent from another thread (in the future)
// and so we need to use JSVHandleFreeList of the target thread
JSSynchronizationContext.AssertWebWorkerContext();
#endif
if (JSVHandleFreeList.Count > 0)
{
var jsvHandle = JSVHandleFreeList[JSVHandleFreeList.Count];
JSVHandleFreeList.RemoveAt(JSVHandleFreeList.Count - 1);
return jsvHandle;
}
if (NextJSVHandle == IntPtr.Zero)
{
NextJSVHandle = -2;
}
return NextJSVHandle--;
}
public static void FreeJSVHandle(nint jsvHandle)
{
JSVHandleFreeList.Add(jsvHandle);
}
public static bool IsGCVHandle(nint gcHandle)
{
return gcHandle < -1;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ReleaseCSOwnedObject(nint jsHandle)
{
if (jsHandle != IntPtr.Zero)
{
#if FEATURE_WASM_THREADS
JSSynchronizationContext.AssertWebWorkerContext();
#endif
ThreadCsOwnedObjects.Remove(jsHandle);
Interop.Runtime.ReleaseCSOwnedObject(jsHandle);
}
}
public static bool GetTaskResultDynamic(Task task, out object? value)
{
@ -143,30 +33,6 @@ namespace System.Runtime.InteropServices.JavaScript
throw new InvalidOperationException();
}
// A JSOwnedObject is a managed object with its lifetime controlled by javascript.
// The managed side maintains a strong reference to the object, while the JS side
// maintains a weak reference and notifies the managed side if the JS wrapper object
// has been reclaimed by the JS GC. At that point, the managed side will release its
// strong references, allowing the managed object to be collected.
// This ensures that things like delegates and promises will never 'go away' while JS
// is expecting to be able to invoke or await them.
public static IntPtr GetJSOwnedObjectGCHandle(object obj, GCHandleType handleType = GCHandleType.Normal)
{
if (obj == null)
{
return IntPtr.Zero;
}
IntPtr gcHandle;
if (ThreadJsOwnedObjects.TryGetValue(obj, out gcHandle))
{
return gcHandle;
}
IntPtr result = (IntPtr)GCHandle.Alloc(obj, handleType);
ThreadJsOwnedObjects[obj] = result;
return result;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static RuntimeMethodHandle GetMethodHandleFromIntPtr(IntPtr ptr)
@ -282,7 +148,6 @@ namespace System.Runtime.InteropServices.JavaScript
signature.Result = types[0]._signatureType;
#if FEATURE_WASM_THREADS
signature.ImportHandle = (int)Interlocked.Increment(ref JSFunctionBinding.nextImportHandle);
signature.IsThreadCaptured = false;
#else
signature.ImportHandle = (int)JSFunctionBinding.nextImportHandle++;
#endif
@ -290,12 +155,6 @@ namespace System.Runtime.InteropServices.JavaScript
for (int i = 0; i < argsCount; i++)
{
var type = signature.Sigs[i] = types[i + 1]._signatureType;
#if FEATURE_WASM_THREADS
if (i > 0 && (type.Type == MarshalerType.JSObject || type.Type == MarshalerType.JSException))
{
signature.IsThreadCaptured = true;
}
#endif
}
signature.IsAsync = types[0]._signatureType.Type == MarshalerType.Task;
@ -330,24 +189,7 @@ namespace System.Runtime.InteropServices.JavaScript
signature.Sigs = null;
}
public static JSObject CreateCSOwnedProxy(nint jsHandle)
{
#if FEATURE_WASM_THREADS
JSSynchronizationContext.AssertWebWorkerContext();
#endif
JSObject? res;
if (!ThreadCsOwnedObjects.TryGetValue(jsHandle, out WeakReference<JSObject>? reference) ||
!reference.TryGetTarget(out res) ||
res.IsDisposed)
{
res = new JSObject(jsHandle);
ThreadCsOwnedObjects[jsHandle] = new WeakReference<JSObject>(res, trackResurrection: true);
}
return res;
}
[Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "It's always part of the single compilation (and trimming) unit.")]
[UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "It's always part of the single compilation (and trimming) unit.")]
public static void LoadLazyAssembly(byte[] dllBytes, byte[]? pdbBytes)
{
if (pdbBytes == null)
@ -356,7 +198,7 @@ namespace System.Runtime.InteropServices.JavaScript
AssemblyLoadContext.Default.LoadFromStream(new MemoryStream(dllBytes), new MemoryStream(pdbBytes));
}
[Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "It's always part of the single compilation (and trimming) unit.")]
[UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "It's always part of the single compilation (and trimming) unit.")]
public static void LoadSatelliteAssembly(byte[] dllBytes)
{
AssemblyLoadContext.Default.LoadFromStream(new MemoryStream(dllBytes));
@ -365,92 +207,33 @@ namespace System.Runtime.InteropServices.JavaScript
#if FEATURE_WASM_THREADS
public static void InstallWebWorkerInterop(bool isMainThread)
{
Interop.Runtime.InstallWebWorkerInterop();
var currentTID = GetNativeThreadId();
var ctx = JSSynchronizationContext.CurrentJSSynchronizationContext;
if (ctx == null)
var ctx = new JSSynchronizationContext(isMainThread);
ctx.previousSynchronizationContext = SynchronizationContext.Current;
SynchronizationContext.SetSynchronizationContext(ctx);
var proxyContext = ctx.ProxyContext;
JSProxyContext.CurrentThreadContext = proxyContext;
JSProxyContext.ExecutionContext = proxyContext;
if (isMainThread)
{
ctx = new JSSynchronizationContext(Thread.CurrentThread, currentTID);
ctx.previousSynchronizationContext = SynchronizationContext.Current;
JSSynchronizationContext.CurrentJSSynchronizationContext = ctx;
SynchronizationContext.SetSynchronizationContext(ctx);
if (isMainThread)
{
JSSynchronizationContext.MainJSSynchronizationContext = ctx;
}
}
else if (ctx.TargetTID != currentTID)
{
Environment.FailFast($"JSSynchronizationContext.Install has wrong native thread id {ctx.TargetTID} != {currentTID}");
JSProxyContext.MainThreadContext = proxyContext;
}
ctx.AwaitNewData();
Interop.Runtime.InstallWebWorkerInterop(proxyContext.ContextHandle);
}
public static void UninstallWebWorkerInterop()
{
var ctx = JSSynchronizationContext.CurrentJSSynchronizationContext;
var uninstallJSSynchronizationContext = ctx != null;
if (uninstallJSSynchronizationContext)
var ctx = JSProxyContext.CurrentThreadContext;
if (ctx == null) throw new InvalidOperationException();
var syncContext = ctx.SynchronizationContext;
if (SynchronizationContext.Current == syncContext)
{
try
{
foreach (var jsObjectWeak in ThreadCsOwnedObjects.Values)
{
if (jsObjectWeak.TryGetTarget(out var jso))
{
jso.Dispose();
}
}
SynchronizationContext.SetSynchronizationContext(ctx!.previousSynchronizationContext);
JSSynchronizationContext.CurrentJSSynchronizationContext = null;
ctx.isDisposed = true;
}
catch (Exception ex)
{
Environment.FailFast($"Unexpected error in UninstallWebWorkerInterop, ManagedThreadId: {Thread.CurrentThread.ManagedThreadId}. " + ex);
}
SynchronizationContext.SetSynchronizationContext(syncContext.previousSynchronizationContext);
}
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}");
}
}
Interop.Runtime.UninstallWebWorkerInterop();
if (uninstallJSSynchronizationContext)
{
try
{
foreach (var gch in ThreadJsOwnedObjects.Values)
{
GCHandle gcHandle = (GCHandle)gch;
gcHandle.Free();
}
foreach (var holder in ThreadJsOwnedHolders.Values)
{
unsafe
{
holder.Callback!.Invoke(null);
}
}
}
catch (Exception ex)
{
Environment.FailFast($"Unexpected error in UninstallWebWorkerInterop, ManagedThreadId: {Thread.CurrentThread.ManagedThreadId}. " + ex);
}
}
ThreadCsOwnedObjects.Clear();
ThreadJsOwnedObjects.Clear();
JSVHandleFreeList.Clear();
NextJSVHandle = IntPtr.Zero;
ctx.Dispose();
}
[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "external_eventloop")]
@ -460,15 +243,6 @@ namespace System.Runtime.InteropServices.JavaScript
{
GetThreadExternalEventloop(thread) = true;
}
[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "thread_id")]
private static extern ref long GetThreadNativeThreadId(Thread @this);
public static IntPtr GetNativeThreadId()
{
return (int)GetThreadNativeThreadId(Thread.CurrentThread);
}
#endif
}

View File

@ -3,7 +3,9 @@
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices.JavaScript;
using System.Runtime.Versioning;
using System.Threading;
namespace System.Runtime.InteropServices.JavaScript
{
@ -18,7 +20,7 @@ namespace System.Runtime.InteropServices.JavaScript
{
internal JSMarshalerArgumentImpl slot;
[StructLayout(LayoutKind.Explicit, Pack = 16, Size = 16)]
[StructLayout(LayoutKind.Explicit, Pack = 32, Size = 32)]
internal struct JSMarshalerArgumentImpl
{
[FieldOffset(0)]
@ -55,6 +57,9 @@ namespace System.Runtime.InteropServices.JavaScript
internal MarshalerType Type;
[FieldOffset(13)]
internal MarshalerType ElementType;
[FieldOffset(16)]
internal IntPtr ContextHandle;
}
/// <summary>
@ -64,6 +69,98 @@ namespace System.Runtime.InteropServices.JavaScript
public unsafe void Initialize()
{
slot.Type = MarshalerType.None;
#if FEATURE_WASM_THREADS
// we know that this is at the start of some JSImport call, but we don't know yet what would be the target thread
// also this is called multiple times
JSProxyContext.JSImportWithUnknownContext();
slot.ContextHandle = IntPtr.Zero;
#endif
}
#if FEATURE_WASM_THREADS
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal unsafe void InitializeWithContext(JSProxyContext knownProxyContext)
{
slot.Type = MarshalerType.None;
slot.ContextHandle = knownProxyContext.ContextHandle;
}
#endif
// this is always called from ToManaged() marshaler
#pragma warning disable CA1822 // Mark members as static
internal JSProxyContext ToManagedContext
#pragma warning restore CA1822 // Mark members as static
{
get
{
#if !FEATURE_WASM_THREADS
return JSProxyContext.MainThreadContext;
#else
// ContextHandle always has to be set
// during JSImport, this is marshaling result/exception and it would be set by:
// - InvokeJSImport implementation
// - ActionJS.InvokeJS
// - ResolveVoidPromise/ResolvePromise/RejectPromise
// during JSExport, this is marshaling parameters and it would be set by:
// - alloc_stack_frame
// - set_js_handle/set_gc_handle
var proxyContextGCHandle = (GCHandle)slot.ContextHandle;
if (proxyContextGCHandle == default)
{
Environment.FailFast($"ContextHandle not set, ManagedThreadId: {Environment.CurrentManagedThreadId}. {Environment.NewLine} {Environment.StackTrace}");
}
var argumentContext = (JSProxyContext)proxyContextGCHandle.Target!;
return argumentContext;
#endif
}
}
// this is always called from ToJS() marshaler
#pragma warning disable CA1822 // Mark members as static
internal JSProxyContext ToJSContext
#pragma warning restore CA1822 // Mark members as static
{
get
{
#if !FEATURE_WASM_THREADS
return JSProxyContext.MainThreadContext;
#else
if (JSProxyContext.CapturingState == JSProxyContext.JSImportOperationState.JSImportParams)
{
// we are called from ToJS, during JSImport
// we need to check for captured or default context
return JSProxyContext.CurrentOperationContext;
}
// ContextHandle must be set be set by JS side of JSExport, and we are marshaling result of JSExport
var proxyContextGCHandle = slot.ContextHandle;
if (proxyContextGCHandle == IntPtr.Zero)
{
Environment.FailFast($"ContextHandle not set, ManagedThreadId: {Environment.CurrentManagedThreadId}. {Environment.NewLine} {Environment.StackTrace}");
}
var argumentContext = (JSProxyContext)((GCHandle)proxyContextGCHandle).Target!;
return argumentContext;
#endif
}
}
// make sure that we are on a thread with JS interop and that it matches the target of the argument
#pragma warning disable CA1822 // Mark members as static
internal JSProxyContext AssertCurrentThreadContext()
#pragma warning restore CA1822 // Mark members as static
{
#if !FEATURE_WASM_THREADS
return JSProxyContext.MainThreadContext;
#else
var currentThreadContext = JSProxyContext.CurrentThreadContext;
if (currentThreadContext == null)
{
Environment.FailFast($"Must be called on same thread with JS interop, ManagedThreadId: {Environment.CurrentManagedThreadId}. {Environment.NewLine} {Environment.StackTrace}");
}
if (slot.ContextHandle != currentThreadContext.ContextHandle)
{
Environment.FailFast($"Must be called on same thread which created the stack frame, ManagedThreadId: {Environment.CurrentManagedThreadId}. {Environment.NewLine} {Environment.StackTrace}");
}
return currentThreadContext;
#endif
}
}
}

View File

@ -10,53 +10,37 @@ namespace System.Runtime.InteropServices.JavaScript
public partial class JSObject
{
internal nint JSHandle;
#if FEATURE_WASM_THREADS
private readonly object _thisLock = new object();
private SynchronizationContext? m_SynchronizationContext;
#endif
internal JSProxyContext ProxyContext;
public SynchronizationContext SynchronizationContext
{
get
{
#if FEATURE_WASM_THREADS
return m_SynchronizationContext!;
return ProxyContext.SynchronizationContext;
#else
throw new PlatformNotSupportedException();
#endif
}
}
#if FEATURE_WASM_THREADS
// the JavaScript object could only exist on the single web worker and can't migrate to other workers
internal nint OwnerTID;
#endif
#if !DISABLE_LEGACY_JS_INTEROP
internal GCHandle? InFlight;
internal int InFlightCounter;
#endif
private bool _isDisposed;
internal bool _isDisposed;
internal JSObject(IntPtr jsHandle)
internal JSObject(IntPtr jsHandle, JSProxyContext ctx)
{
ProxyContext = ctx;
JSHandle = jsHandle;
#if FEATURE_WASM_THREADS
var ctx = JSSynchronizationContext.CurrentJSSynchronizationContext;
if (ctx == null)
{
Environment.FailFast("Missing CurrentJSSynchronizationContext");
}
m_SynchronizationContext = ctx;
OwnerTID = ctx!.TargetTID;
#endif
}
#if !DISABLE_LEGACY_JS_INTEROP
internal void AddInFlight()
{
ObjectDisposedException.ThrowIf(IsDisposed, this);
lock (this)
lock (ProxyContext)
{
InFlightCounter++;
if (InFlightCounter == 1)
@ -72,7 +56,7 @@ namespace System.Runtime.InteropServices.JavaScript
// we only want JSObject to be disposed (from GC finalizer) once there is no in-flight reference and also no natural C# reference
internal void ReleaseInFlight()
{
lock (this)
lock (ProxyContext)
{
Debug.Assert(InFlightCounter != 0, "InFlightCounter != 0");
@ -94,19 +78,17 @@ namespace System.Runtime.InteropServices.JavaScript
{
return;
}
JSSynchronizationContext.AssertWebWorkerContext();
var currentTID = JSSynchronizationContext.CurrentJSSynchronizationContext!.TargetTID;
if (value is JSObject jsObject)
{
if (jsObject.OwnerTID != currentTID)
if (!jsObject.ProxyContext.IsCurrentThread())
{
throw new InvalidOperationException("The JavaScript object can be used only on the thread where it was created.");
}
}
else if (value is JSException jsException)
{
if (jsException.jsException != null && jsException.jsException.OwnerTID != currentTID)
if (jsException.jsException != null && !jsException.jsException.ProxyContext.IsCurrentThread())
{
throw new InvalidOperationException("The JavaScript object can be used only on the thread where it was created.");
}
@ -123,43 +105,24 @@ namespace System.Runtime.InteropServices.JavaScript
/// <inheritdoc />
public override string ToString() => $"(js-obj js '{JSHandle}')";
internal void DisposeLocal()
{
JSHostImplementation.ThreadCsOwnedObjects.Remove(JSHandle);
_isDisposed = true;
JSHandle = IntPtr.Zero;
}
private void DisposeThis()
internal void DisposeImpl(bool skipJsCleanup = false)
{
if (!_isDisposed)
{
#if FEATURE_WASM_THREADS
if (SynchronizationContext == SynchronizationContext.Current)
if (ProxyContext.IsCurrentThread())
{
lock (_thisLock)
{
JSHostImplementation.ReleaseCSOwnedObject(JSHandle);
_isDisposed = true;
JSHandle = IntPtr.Zero;
m_SynchronizationContext = null;
} //lock
JSProxyContext.ReleaseCSOwnedObject(this, skipJsCleanup);
return;
}
SynchronizationContext.Post(static (object? s) =>
ProxyContext.SynchronizationContext.Post(static (object? s) =>
{
var self = (JSObject)s!;
lock (self._thisLock)
{
JSHostImplementation.ReleaseCSOwnedObject(self.JSHandle);
self._isDisposed = true;
self.JSHandle = IntPtr.Zero;
self.m_SynchronizationContext = null;
} //lock
}, this);
var x = ((JSObject self, bool skipJS))s!;
JSProxyContext.ReleaseCSOwnedObject(x.self, x.skipJS);
}, (this, skipJsCleanup));
#else
JSHostImplementation.ReleaseCSOwnedObject(JSHandle);
JSProxyContext.ReleaseCSOwnedObject(this, skipJsCleanup);
_isDisposed = true;
JSHandle = IntPtr.Zero;
#endif
@ -168,7 +131,7 @@ namespace System.Runtime.InteropServices.JavaScript
~JSObject()
{
DisposeThis();
DisposeImpl();
}
/// <summary>
@ -176,8 +139,7 @@ namespace System.Runtime.InteropServices.JavaScript
/// </summary>
public void Dispose()
{
DisposeThis();
GC.SuppressFinalize(this);
DisposeImpl();
}
}
}

View File

@ -0,0 +1,596 @@
// 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.Runtime.CompilerServices;
using System.Threading;
using static System.Runtime.InteropServices.JavaScript.JSHostImplementation;
namespace System.Runtime.InteropServices.JavaScript
{
internal sealed class JSProxyContext : IDisposable
{
private bool _isDisposed;
// we use this to maintain identity of JSHandle for a JSObject proxy
private readonly Dictionary<nint, WeakReference<JSObject>> ThreadCsOwnedObjects = new();
// we use this to maintain identity of GCHandle for a managed object
private readonly Dictionary<object, nint> ThreadJsOwnedObjects = new(ReferenceEqualityComparer.Instance);
// this is similar to GCHandle, but the GCVHandle is allocated on JS side and this keeps the C# proxy alive
private readonly Dictionary<nint, PromiseHolder> ThreadJsOwnedHolders = new();
// JSVHandle is like JSHandle, but it's not tracked and allocated by the JS side
// It's used when we need to create JSHandle-like identity ahead of time, before calling JS.
// they have negative values, so that they don't collide with JSHandles.
private nint NextJSVHandle = -2;
private readonly List<nint> JSVHandleFreeList = new();
#if !FEATURE_WASM_THREADS
private JSProxyContext()
{
}
#else
public nint ContextHandle;
public nint NativeTID;
public int ManagedTID;
public bool IsMainThread;
public JSSynchronizationContext SynchronizationContext;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool IsCurrentThread()
{
return ManagedTID == Thread.CurrentThread.ManagedThreadId;
}
[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "thread_id")]
private static extern ref long GetThreadNativeThreadId(Thread @this);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static IntPtr GetNativeThreadId()
{
return (int)GetThreadNativeThreadId(Thread.CurrentThread);
}
public JSProxyContext(bool isMainThread, JSSynchronizationContext synchronizationContext)
{
SynchronizationContext = synchronizationContext;
NativeTID = GetNativeThreadId();
ManagedTID = Thread.CurrentThread.ManagedThreadId;
IsMainThread = isMainThread;
ContextHandle = (nint)GCHandle.Alloc(this, GCHandleType.Normal);
}
#endif
#region Current operation context
#if !FEATURE_WASM_THREADS
public static readonly JSProxyContext MainThreadContext = new();
public static JSProxyContext CurrentThreadContext => MainThreadContext;
public static JSProxyContext CurrentOperationContext => MainThreadContext;
public static JSProxyContext PushOperationWithCurrentThreadContext()
{
// in single threaded build we don't have to keep stack of operations and the context/thread is always the same
return MainThreadContext;
}
#else
// Context of the main thread
private static JSProxyContext? _MainThreadContext;
public static JSProxyContext MainThreadContext
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => _MainThreadContext!;
set => _MainThreadContext = value;
}
public enum JSImportOperationState
{
None,
JSImportParams,
}
[ThreadStatic]
private static JSProxyContext? _CapturedOperationContext;
[ThreadStatic]
private static JSImportOperationState _CapturingState;
public static JSImportOperationState CapturingState => _CapturingState;
// there will be call to JS from JSImport generated code, but we don't know which target thread yet
public static void JSImportWithUnknownContext()
{
// it would be ideal to assert here, that we arrived here with JSImportOperationState.None
// but any exception during JSImportOperationState.JSImportParams phase could make this state un-balanced
// typically this would be exception which is validating the marshaled value
// manually re-setting _CapturingState on each throw site would be possible, but fragile
// luckily, we always reset it here before any new JSImport call
// so the code which could interact with _CapturedOperationContext value will receive fresh values
_CapturingState = JSImportOperationState.JSImportParams;
_CapturedOperationContext = null;
}
// there will be no capture during following call to JS
public static void JSImportNoCapture()
{
_CapturingState = JSImportOperationState.None;
_CapturedOperationContext = null;
}
// we are at the end of marshaling of the JSImport parameters
public static JSProxyContext SealJSImportCapturing()
{
if (_CapturingState != JSImportOperationState.JSImportParams)
{
Environment.FailFast($"Method only allowed during JSImport capturing phase, ManagedThreadId: {Environment.CurrentManagedThreadId}. {Environment.NewLine} {Environment.StackTrace}");
}
_CapturingState = JSImportOperationState.None;
var capturedOperationContext = _CapturedOperationContext;
_CapturedOperationContext = null;
if (capturedOperationContext != null)
{
return capturedOperationContext;
}
// it could happen that we are in operation, in which we didn't capture target thread/context
var executionContext = ExecutionContext;
if (executionContext != null)
{
// we could will call JS on the current thread (or child task), if it has the JS interop installed
return executionContext;
}
// otherwise we will call JS on the main thread, which always has JS interop
return MainThreadContext;
}
// this is called only during marshaling (in) parameters of JSImport, which have existing ProxyContext (thread affinity)
// together with CurrentOperationContext is will validate that all parameters of the call have same context/affinity
public static void CaptureContextFromParameter(JSProxyContext parameterContext)
{
if (_CapturingState != JSImportOperationState.JSImportParams)
{
Environment.FailFast($"Method only allowed during JSImport capturing phase, ManagedThreadId: {Environment.CurrentManagedThreadId}. {Environment.NewLine} {Environment.StackTrace}");
}
var capturedContext = _CapturedOperationContext;
if (capturedContext == null)
{
_CapturedOperationContext = capturedContext;
}
else if (parameterContext != capturedContext)
{
_CapturedOperationContext = null;
_CapturingState = JSImportOperationState.None;
throw new InvalidOperationException("All JSObject proxies need to have same thread affinity. See https://aka.ms/dotnet-JS-interop-threads");
}
}
// Context flowing from parent thread into child tasks.
// Could be null on threads which don't have JS interop, like managed thread pool threads. Unless they inherit it from the current Task
// TODO flow it also with ExecutionContext to child threads ?
private static readonly AsyncLocal<JSProxyContext?> _currentThreadContext = new AsyncLocal<JSProxyContext?>();
public static JSProxyContext? ExecutionContext
{
get => _currentThreadContext.Value;
set => _currentThreadContext.Value = value;
}
[ThreadStatic]
public static JSProxyContext? CurrentThreadContext;
// This is context to dispatch into. In order of preference
// - captured context by arguments of current/pending JSImport call
// - current thread context, for calls from JSWebWorker threads with the interop installed
// - main thread, for calls from any other thread, like managed thread pool or `new Thread`
public static JSProxyContext CurrentOperationContext
{
get
{
if (_CapturingState != JSImportOperationState.JSImportParams)
{
Environment.FailFast($"Method only allowed during JSImport capturing phase, ManagedThreadId: {Environment.CurrentManagedThreadId}. {Environment.NewLine} {Environment.StackTrace}");
}
var capturedOperationContext = _CapturedOperationContext;
if (capturedOperationContext != null)
{
return capturedOperationContext;
}
// it could happen that we are in operation, in which we didn't capture target thread/context
var executionContext = ExecutionContext;
if (executionContext != null)
{
// capture this fallback for validation of all other parameters
_CapturedOperationContext = executionContext;
// we could will call JS on the current thread (or child task), if it has the JS interop installed
return executionContext;
}
// otherwise we will call JS on the main thread, which always has JS interop
var mainThreadContext = MainThreadContext;
// capture this fallback for validation of all other parameters
// such validation could fail if Task is marshaled earlier than JSObject and uses different target context
_CapturedOperationContext = mainThreadContext;
return mainThreadContext;
}
}
#endif
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static JSProxyContext AssertIsInteropThread()
{
#if FEATURE_WASM_THREADS
var ctx = CurrentThreadContext;
if (ctx == null)
{
throw new InvalidOperationException($"Please use dedicated worker for working with JavaScript interop, ManagedThreadId:{Thread.CurrentThread.ManagedThreadId}. See https://aka.ms/dotnet-JS-interop-threads");
}
if (ctx._isDisposed)
{
ObjectDisposedException.ThrowIf(ctx._isDisposed, ctx);
}
return ctx;
#else
return MainThreadContext;
#endif
}
#endregion
#region Handles
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsJSVHandle(nint jsHandle)
{
return jsHandle < -1;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsGCVHandle(nint gcHandle)
{
return gcHandle < -1;
}
public nint AllocJSVHandle()
{
lock (this)
{
if (JSVHandleFreeList.Count > 0)
{
var jsvHandle = JSVHandleFreeList[JSVHandleFreeList.Count - 1];
JSVHandleFreeList.RemoveAt(JSVHandleFreeList.Count - 1);
return jsvHandle;
}
if (NextJSVHandle == IntPtr.Zero)
{
NextJSVHandle = -2;
}
return NextJSVHandle--;
}
}
public void FreeJSVHandle(nint jsvHandle)
{
lock (this)
{
JSVHandleFreeList.Add(jsvHandle);
}
}
// A JSOwnedObject is a managed object with its lifetime controlled by javascript.
// The managed side maintains a strong reference to the object, while the JS side
// maintains a weak reference and notifies the managed side if the JS wrapper object
// has been reclaimed by the JS GC. At that point, the managed side will release its
// strong references, allowing the managed object to be collected.
// This ensures that things like delegates and promises will never 'go away' while JS
// is expecting to be able to invoke or await them.
public IntPtr GetJSOwnedObjectGCHandle(object obj, GCHandleType handleType = GCHandleType.Normal)
{
if (obj == null)
{
return IntPtr.Zero;
}
lock (this)
{
if (ThreadJsOwnedObjects.TryGetValue(obj, out IntPtr gcHandle))
{
return gcHandle;
}
IntPtr result = (IntPtr)GCHandle.Alloc(obj, handleType);
ThreadJsOwnedObjects[obj] = result;
return result;
}
}
public PromiseHolder CreatePromiseHolder()
{
lock (this)
{
return new PromiseHolder(this);
}
}
public PromiseHolder GetPromiseHolder(nint gcHandle)
{
lock (this)
{
PromiseHolder? holder;
if (IsGCVHandle(gcHandle))
{
if (!ThreadJsOwnedHolders.TryGetValue(gcHandle, out holder))
{
holder = new PromiseHolder(this, gcHandle);
ThreadJsOwnedHolders.Add(gcHandle, holder);
}
}
else
{
holder = (PromiseHolder)((GCHandle)gcHandle).Target!;
}
return holder;
}
}
public unsafe void ReleasePromiseHolder(nint holderGCHandle)
{
lock (this)
{
PromiseHolder? holder;
if (IsGCVHandle(holderGCHandle))
{
if (!ThreadJsOwnedHolders.Remove(holderGCHandle, out holder))
{
throw new InvalidOperationException("ReleasePromiseHolder expected PromiseHolder " + holderGCHandle);
}
}
else
{
GCHandle handle = (GCHandle)holderGCHandle;
var target = handle.Target!;
if (target is PromiseHolder holder2)
{
holder = holder2;
}
else
{
throw new InvalidOperationException("ReleasePromiseHolder expected PromiseHolder" + holderGCHandle);
}
handle.Free();
}
holder.IsDisposed = true;
}
}
public unsafe void ReleaseJSOwnedObjectByGCHandle(nint gcHandle)
{
ToManagedCallback? holderCallback = null;
lock (this)
{
PromiseHolder? holder = null;
if (IsGCVHandle(gcHandle))
{
if (!ThreadJsOwnedHolders.Remove(gcHandle, out holder))
{
throw new InvalidOperationException("ReleaseJSOwnedObjectByGCHandle expected in ThreadJsOwnedHolders");
}
}
else
{
GCHandle handle = (GCHandle)gcHandle;
var target = handle.Target!;
if (target is PromiseHolder holder2)
{
holder = holder2;
}
else
{
if (!ThreadJsOwnedObjects.Remove(target))
{
throw new InvalidOperationException("ReleaseJSOwnedObjectByGCHandle expected in ThreadJsOwnedObjects");
}
}
handle.Free();
}
if (holder != null)
{
holderCallback = holder.Callback;
holder.IsDisposed = true;
}
}
holderCallback?.Invoke(null);
}
public JSObject CreateCSOwnedProxy(nint jsHandle)
{
lock (this)
{
JSObject? res;
if (!ThreadCsOwnedObjects.TryGetValue(jsHandle, out WeakReference<JSObject>? reference) ||
!reference.TryGetTarget(out res) ||
res.IsDisposed)
{
res = new JSObject(jsHandle, this);
ThreadCsOwnedObjects[jsHandle] = new WeakReference<JSObject>(res, trackResurrection: true);
}
return res;
}
}
public static void ReleaseCSOwnedObject(JSObject proxy, bool skipJS)
{
if (proxy.IsDisposed)
{
return;
}
var ctx = proxy.ProxyContext;
#if FEATURE_WASM_THREADS
if (!ctx.IsCurrentThread())
{
throw new InvalidOperationException($"ReleaseCSOwnedObject has to run on the thread with same affinity as the proxy. ManagedThreadId: {Environment.CurrentManagedThreadId} JSHandle: {proxy.JSHandle}");
}
#endif
lock (ctx)
{
if (proxy.IsDisposed)
{
return;
}
proxy._isDisposed = true;
GC.SuppressFinalize(proxy);
var jsHandle = proxy.JSHandle;
if (!ctx.ThreadCsOwnedObjects.Remove(jsHandle))
{
Environment.FailFast($"ReleaseCSOwnedObject expected to find registration for JSHandle: {jsHandle}, ManagedThreadId: {Environment.CurrentManagedThreadId}. {Environment.NewLine} {Environment.StackTrace}");
};
if (!skipJS)
{
Interop.Runtime.ReleaseCSOwnedObject(jsHandle);
}
if (IsJSVHandle(jsHandle))
{
ctx.FreeJSVHandle(jsHandle);
}
}
}
#endregion
#region Legacy
// legacy
public void RegisterCSOwnedObject(JSObject proxy)
{
lock (this)
{
ThreadCsOwnedObjects[(int)proxy.JSHandle] = new WeakReference<JSObject>(proxy, trackResurrection: true);
}
}
// legacy
public JSObject? GetCSOwnedObjectByJSHandle(nint jsHandle, int shouldAddInflight)
{
lock (this)
{
if (ThreadCsOwnedObjects.TryGetValue(jsHandle, out WeakReference<JSObject>? reference))
{
reference.TryGetTarget(out JSObject? jsObject);
if (shouldAddInflight != 0)
{
jsObject?.AddInFlight();
}
return jsObject;
}
}
return null;
}
// legacy
public JSObject CreateCSOwnedProxy(nint jsHandle, LegacyHostImplementation.MappedType mappedType, int shouldAddInflight)
{
lock (this)
{
JSObject? res = null;
if (!ThreadCsOwnedObjects.TryGetValue(jsHandle, out WeakReference<JSObject>? reference) ||
!reference.TryGetTarget(out res) ||
res.IsDisposed)
{
#pragma warning disable CS0612 // Type or member is obsolete
res = mappedType switch
{
LegacyHostImplementation.MappedType.JSObject => new JSObject(jsHandle, JSProxyContext.MainThreadContext),
LegacyHostImplementation.MappedType.Array => new Array(jsHandle),
LegacyHostImplementation.MappedType.ArrayBuffer => new ArrayBuffer(jsHandle),
LegacyHostImplementation.MappedType.DataView => new DataView(jsHandle),
LegacyHostImplementation.MappedType.Function => new Function(jsHandle),
LegacyHostImplementation.MappedType.Uint8Array => new Uint8Array(jsHandle),
_ => throw new ArgumentOutOfRangeException(nameof(mappedType))
};
#pragma warning restore CS0612 // Type or member is obsolete
ThreadCsOwnedObjects[jsHandle] = new WeakReference<JSObject>(res, trackResurrection: true);
}
if (shouldAddInflight != 0)
{
res.AddInFlight();
}
return res;
}
}
#endregion
#region Dispose
private void Dispose(bool disposing)
{
lock (this)
{
if (!_isDisposed)
{
#if FEATURE_WASM_THREADS
if (!IsCurrentThread())
{
Environment.FailFast($"JSProxyContext must be disposed on the thread which owns it, ManagedThreadId: {Environment.CurrentManagedThreadId}. {Environment.NewLine} {Environment.StackTrace}");
}
((GCHandle)ContextHandle).Free();
#endif
List<WeakReference<JSObject>> copy = new(ThreadCsOwnedObjects.Values);
foreach (var jsObjectWeak in copy)
{
if (jsObjectWeak.TryGetTarget(out var jso))
{
jso.Dispose();
}
}
#if FEATURE_WASM_THREADS
Interop.Runtime.UninstallWebWorkerInterop();
#endif
foreach (var gch in ThreadJsOwnedObjects.Values)
{
GCHandle gcHandle = (GCHandle)gch;
gcHandle.Free();
}
foreach (var holder in ThreadJsOwnedHolders.Values)
{
unsafe
{
holder.Callback!.Invoke(null);
}
}
ThreadCsOwnedObjects.Clear();
ThreadJsOwnedObjects.Clear();
JSVHandleFreeList.Clear();
NextJSVHandle = IntPtr.Zero;
if (disposing)
{
#if FEATURE_WASM_THREADS
SynchronizationContext.Dispose();
#endif
}
_isDisposed = true;
}
}
}
~JSProxyContext()
{
Dispose(disposing: false);
}
public void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
#endregion
}
}

View File

@ -6,9 +6,8 @@
using System.Threading;
using System.Threading.Channels;
using System.Runtime.CompilerServices;
using WorkItemQueueType = System.Threading.Channels.Channel<System.Runtime.InteropServices.JavaScript.JSSynchronizationContext.WorkItem>;
using static System.Runtime.InteropServices.JavaScript.JSHostImplementation;
using System.Collections.Generic;
using WorkItemQueueType = System.Threading.Channels.Channel<System.Runtime.InteropServices.JavaScript.JSSynchronizationContext.WorkItem>;
namespace System.Runtime.InteropServices.JavaScript
{
@ -21,17 +20,12 @@ namespace System.Runtime.InteropServices.JavaScript
/// </summary>
internal sealed class JSSynchronizationContext : SynchronizationContext
{
internal readonly JSProxyContext ProxyContext;
private readonly Action _DataIsAvailable;// don't allocate Action on each call to UnsafeOnCompleted
public readonly Thread TargetThread;
public readonly IntPtr TargetTID;
private readonly WorkItemQueueType Queue;
internal static JSSynchronizationContext? MainJSSynchronizationContext;
[ThreadStatic]
internal static JSSynchronizationContext? CurrentJSSynchronizationContext;
internal SynchronizationContext? previousSynchronizationContext;
internal bool isDisposed;
internal bool _isDisposed;
internal readonly struct WorkItem
{
@ -47,42 +41,33 @@ namespace System.Runtime.InteropServices.JavaScript
}
}
internal JSSynchronizationContext(Thread targetThread, IntPtr targetThreadId)
: this(
targetThread, targetThreadId,
Channel.CreateUnbounded<WorkItem>(
new UnboundedChannelOptions { SingleWriter = false, SingleReader = true, AllowSynchronousContinuations = true }
)
)
public JSSynchronizationContext(bool isMainThread)
{
}
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 targetTID, WorkItemQueueType queue)
{
TargetThread = targetThread;
TargetTID = targetTID;
Queue = queue;
ProxyContext = new JSProxyContext(isMainThread, this);
Queue = Channel.CreateUnbounded<WorkItem>(new UnboundedChannelOptions { SingleWriter = false, SingleReader = true, AllowSynchronousContinuations = true });
_DataIsAvailable = DataIsAvailable;
}
internal JSSynchronizationContext(JSProxyContext proxyContext, WorkItemQueueType queue, Action dataIsAvailable)
{
ProxyContext = proxyContext;
Queue = queue;
_DataIsAvailable = dataIsAvailable;
}
public override SynchronizationContext CreateCopy()
{
return new JSSynchronizationContext(TargetThread, TargetTID, Queue);
return new JSSynchronizationContext(ProxyContext, Queue, _DataIsAvailable);
}
internal void AwaitNewData()
{
ObjectDisposedException.ThrowIf(isDisposed, this);
if (_isDisposed)
{
// FIXME: there could be abandoned work, but here we have no way how to propagate the failure
// ObjectDisposedException.ThrowIf(_isDisposed, this);
return;
}
var vt = Queue.Reader.WaitToReadAsync();
if (vt.IsCompleted)
@ -103,16 +88,16 @@ namespace System.Runtime.InteropServices.JavaScript
{
// While we COULD pump here, we don't want to. We want the pump to happen on the next event loop turn.
// Otherwise we could get a chain where a pump generates a new work item and that makes us pump again, forever.
TargetThreadScheduleBackgroundJob(TargetTID, (void*)(delegate* unmanaged[Cdecl]<void>)&BackgroundJobHandler);
TargetThreadScheduleBackgroundJob(ProxyContext.NativeTID, (void*)(delegate* unmanaged[Cdecl]<void>)&BackgroundJobHandler);
}
public override void Post(SendOrPostCallback d, object? state)
{
ObjectDisposedException.ThrowIf(isDisposed, this);
ObjectDisposedException.ThrowIf(_isDisposed, this);
var workItem = new WorkItem(d, state, null);
if (!Queue.Writer.TryWrite(workItem))
Environment.FailFast("JSSynchronizationContext.Post failed");
Environment.FailFast($"JSSynchronizationContext.Post failed, ManagedThreadId: {Environment.CurrentManagedThreadId}. {Environment.NewLine} {Environment.StackTrace}");
}
// This path can only run when threading is enabled
@ -120,9 +105,9 @@ namespace System.Runtime.InteropServices.JavaScript
public override void Send(SendOrPostCallback d, object? state)
{
ObjectDisposedException.ThrowIf(isDisposed, this);
ObjectDisposedException.ThrowIf(_isDisposed, this);
if (Thread.CurrentThread == TargetThread)
if (ProxyContext.IsCurrentThread())
{
d(state);
return;
@ -132,7 +117,7 @@ namespace System.Runtime.InteropServices.JavaScript
{
var workItem = new WorkItem(d, state, signal);
if (!Queue.Writer.TryWrite(workItem))
Environment.FailFast("JSSynchronizationContext.Send failed");
Environment.FailFast($"JSSynchronizationContext.Send failed, ManagedThreadId: {Environment.CurrentManagedThreadId}. {Environment.NewLine} {Environment.StackTrace}");
signal.Wait();
}
@ -147,14 +132,16 @@ namespace System.Runtime.InteropServices.JavaScript
// this callback will arrive on the target thread, called from mono_background_exec
private static void BackgroundJobHandler()
{
CurrentJSSynchronizationContext!.Pump();
var ctx = JSProxyContext.AssertIsInteropThread();
ctx.SynchronizationContext.Pump();
}
private void Pump()
{
if (isDisposed)
if (_isDisposed)
{
// FIXME: there could be abandoned work, but here we have no way how to propagate the failure
// ObjectDisposedException.ThrowIf(_isDisposed, this);
return;
}
try
@ -181,9 +168,28 @@ namespace System.Runtime.InteropServices.JavaScript
finally
{
// If an item throws, we want to ensure that the next pump gets scheduled appropriately regardless.
if(!isDisposed) AwaitNewData();
if (!_isDisposed) AwaitNewData();
}
}
private void Dispose(bool disposing)
{
if (!_isDisposed)
{
if (disposing)
{
Queue.Writer.Complete();
}
previousSynchronizationContext = null;
_isDisposed = true;
}
}
internal void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
}
}

View File

@ -32,15 +32,19 @@ namespace System.Runtime.InteropServices.JavaScript
public static async Task<T> RunAsync<T>(Func<Task<T>> body, CancellationToken cancellationToken)
{
// TODO remove main thread condition later
if (Thread.CurrentThread.ManagedThreadId == 1) await JavaScriptImports.ThreadAvailable().ConfigureAwait(false);
if (JSProxyContext.MainThreadContext.IsCurrentThread())
{
await JavaScriptImports.ThreadAvailable().ConfigureAwait(false);
}
return await RunAsyncImpl(body, cancellationToken).ConfigureAwait(false);
}
public static async Task RunAsync(Func<Task> body, CancellationToken cancellationToken)
{
// TODO remove main thread condition later
if (Thread.CurrentThread.ManagedThreadId == 1) await JavaScriptImports.ThreadAvailable().ConfigureAwait(false);
if (JSProxyContext.MainThreadContext.IsCurrentThread())
{
await JavaScriptImports.ThreadAvailable().ConfigureAwait(false);
}
await RunAsyncImpl(body, cancellationToken).ConfigureAwait(false);
}

View File

@ -16,19 +16,19 @@ namespace System.Runtime.InteropServices.JavaScript
/// </summary>
/// <param name="_params">Parameters.</param>
public Array(params object[] _params)
: base(JavaScriptImports.CreateCSOwnedObject(nameof(Array), _params))
: base(JavaScriptImports.CreateCSOwnedObject(nameof(Array), _params), JSProxyContext.MainThreadContext)
{
#if FEATURE_WASM_THREADS
LegacyHostImplementation.ThrowIfLegacyWorkerThread();
#endif
LegacyHostImplementation.RegisterCSOwnedObject(this);
JSProxyContext.MainThreadContext.RegisterCSOwnedObject(this);
}
/// <summary>
/// Initializes a new instance of the Array/> class.
/// </summary>
/// <param name="jsHandle">Js handle.</param>
internal Array(IntPtr jsHandle) : base(jsHandle)
internal Array(IntPtr jsHandle) : base(jsHandle, JSProxyContext.MainThreadContext)
{ }
/// <summary>

View File

@ -11,19 +11,19 @@ namespace System.Runtime.InteropServices.JavaScript
/// </summary>
/// <param name="length">Length.</param>
public ArrayBuffer(int length)
: base(JavaScriptImports.CreateCSOwnedObject(nameof(ArrayBuffer), new object[] { length }))
: base(JavaScriptImports.CreateCSOwnedObject(nameof(ArrayBuffer), new object[] { length }), JSProxyContext.MainThreadContext)
{
#if FEATURE_WASM_THREADS
LegacyHostImplementation.ThrowIfLegacyWorkerThread();
#endif
LegacyHostImplementation.RegisterCSOwnedObject(this);
JSProxyContext.MainThreadContext.RegisterCSOwnedObject(this);
}
/// <summary>
/// Initializes a new instance of the JavaScript Core ArrayBuffer class.
/// </summary>
/// <param name="jsHandle">Js handle.</param>
internal ArrayBuffer(IntPtr jsHandle) : base(jsHandle)
internal ArrayBuffer(IntPtr jsHandle) : base(jsHandle, JSProxyContext.MainThreadContext)
{ }
/// <summary>

View File

@ -15,12 +15,12 @@ namespace System.Runtime.InteropServices.JavaScript
/// </summary>
/// <param name="buffer">ArrayBuffer to use as the storage backing the new DataView object.</param>
public DataView(ArrayBuffer buffer)
: base(JavaScriptImports.CreateCSOwnedObject(nameof(DataView), new object[] { buffer }))
: base(JavaScriptImports.CreateCSOwnedObject(nameof(DataView), new object[] { buffer }), JSProxyContext.MainThreadContext)
{
#if FEATURE_WASM_THREADS
LegacyHostImplementation.ThrowIfLegacyWorkerThread();
#endif
LegacyHostImplementation.RegisterCSOwnedObject(this);
JSProxyContext.MainThreadContext.RegisterCSOwnedObject(this);
}
/// <summary>
@ -29,12 +29,12 @@ namespace System.Runtime.InteropServices.JavaScript
/// <param name="buffer">ArrayBuffer to use as the storage backing the new DataView object.</param>
/// <param name="byteOffset">The offset, in bytes, to the first byte in the above buffer for the new view to reference. If unspecified, the buffer view starts with the first byte.</param>
public DataView(ArrayBuffer buffer, int byteOffset)
: base(JavaScriptImports.CreateCSOwnedObject(nameof(DataView), new object[] { buffer, byteOffset }))
: base(JavaScriptImports.CreateCSOwnedObject(nameof(DataView), new object[] { buffer, byteOffset }), JSProxyContext.MainThreadContext)
{
#if FEATURE_WASM_THREADS
LegacyHostImplementation.ThrowIfLegacyWorkerThread();
#endif
LegacyHostImplementation.RegisterCSOwnedObject(this);
JSProxyContext.MainThreadContext.RegisterCSOwnedObject(this);
}
/// <summary>
@ -44,19 +44,19 @@ namespace System.Runtime.InteropServices.JavaScript
/// <param name="byteOffset">The offset, in bytes, to the first byte in the above buffer for the new view to reference. If unspecified, the buffer view starts with the first byte.</param>
/// <param name="byteLength">The number of elements in the byte array. If unspecified, the view's length will match the buffer's length.</param>
public DataView(ArrayBuffer buffer, int byteOffset, int byteLength)
: base(JavaScriptImports.CreateCSOwnedObject(nameof(DataView), new object[] { buffer, byteOffset, byteLength }))
: base(JavaScriptImports.CreateCSOwnedObject(nameof(DataView), new object[] { buffer, byteOffset, byteLength }), JSProxyContext.MainThreadContext)
{
#if FEATURE_WASM_THREADS
LegacyHostImplementation.ThrowIfLegacyWorkerThread();
#endif
LegacyHostImplementation.RegisterCSOwnedObject(this);
JSProxyContext.MainThreadContext.RegisterCSOwnedObject(this);
}
/// <summary>
/// Initializes a new instance of the DataView class.
/// </summary>
/// <param name="jsHandle">Js handle.</param>
internal DataView(IntPtr jsHandle) : base(jsHandle)
internal DataView(IntPtr jsHandle) : base(jsHandle, JSProxyContext.MainThreadContext)
{ }
/// <summary>

View File

@ -16,15 +16,15 @@ namespace System.Runtime.InteropServices.JavaScript
public class Function : JSObject
{
public Function(params object[] args)
: base(JavaScriptImports.CreateCSOwnedObject(nameof(Function), args))
: base(JavaScriptImports.CreateCSOwnedObject(nameof(Function), args), JSProxyContext.MainThreadContext)
{
#if FEATURE_WASM_THREADS
LegacyHostImplementation.ThrowIfLegacyWorkerThread();
#endif
LegacyHostImplementation.RegisterCSOwnedObject(this);
JSProxyContext.MainThreadContext.RegisterCSOwnedObject(this);
}
internal Function(IntPtr jsHandle) : base(jsHandle)
internal Function(IntPtr jsHandle) : base(jsHandle, JSProxyContext.MainThreadContext)
{ }
/// <summary>

View File

@ -18,12 +18,6 @@ namespace System.Runtime.InteropServices.JavaScript
jsObj?.ReleaseInFlight();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void RegisterCSOwnedObject(JSObject proxy)
{
JSHostImplementation.ThreadCsOwnedObjects[(int)proxy.JSHandle] = new WeakReference<JSObject>(proxy, trackResurrection: true);
}
public static MarshalType GetMarshalTypeFromType(Type type)
{
if (type is null)

View File

@ -9,24 +9,24 @@ namespace System.Runtime.InteropServices.JavaScript
public sealed class Uint8Array : JSObject
{
public Uint8Array(int length)
: base(JavaScriptImports.CreateCSOwnedObject(nameof(Uint8Array), new object[] { length }))
: base(JavaScriptImports.CreateCSOwnedObject(nameof(Uint8Array), new object[] { length }), JSProxyContext.MainThreadContext)
{
#if FEATURE_WASM_THREADS
LegacyHostImplementation.ThrowIfLegacyWorkerThread();
#endif
LegacyHostImplementation.RegisterCSOwnedObject(this);
JSProxyContext.MainThreadContext.RegisterCSOwnedObject(this);
}
public Uint8Array(ArrayBuffer buffer)
: base(JavaScriptImports.CreateCSOwnedObject(nameof(Uint8Array), new object[] { buffer }))
: base(JavaScriptImports.CreateCSOwnedObject(nameof(Uint8Array), new object[] { buffer }), JSProxyContext.MainThreadContext)
{
#if FEATURE_WASM_THREADS
LegacyHostImplementation.ThrowIfLegacyWorkerThread();
#endif
LegacyHostImplementation.RegisterCSOwnedObject(this);
JSProxyContext.MainThreadContext.RegisterCSOwnedObject(this);
}
internal Uint8Array(IntPtr jsHandle) : base(jsHandle)
internal Uint8Array(IntPtr jsHandle) : base(jsHandle, JSProxyContext.MainThreadContext)
{ }
public int Length

View File

@ -133,7 +133,8 @@ namespace System.Runtime.InteropServices.JavaScript
return;
}
slot.Type = MarshalerType.ArraySegment;
slot.GCHandle = JSHostImplementation.GetJSOwnedObjectGCHandle(value.Array, GCHandleType.Pinned);
var ctx = ToJSContext;
slot.GCHandle = ctx.GetJSOwnedObjectGCHandle(value.Array, GCHandleType.Pinned);
var refPtr = (IntPtr)Unsafe.AsPointer(ref MemoryMarshal.GetArrayDataReference(value.Array));
slot.IntPtrValue = refPtr + value.Offset;
slot.Length = value.Count;

View File

@ -135,7 +135,8 @@ namespace System.Runtime.InteropServices.JavaScript
return;
}
slot.Type = MarshalerType.ArraySegment;
slot.GCHandle = JSHostImplementation.GetJSOwnedObjectGCHandle(value.Array, GCHandleType.Pinned);
var ctx = ToJSContext;
slot.GCHandle = ctx.GetJSOwnedObjectGCHandle(value.Array, GCHandleType.Pinned);
var refPtr = (IntPtr)Unsafe.AsPointer(ref MemoryMarshal.GetArrayDataReference(value.Array));
slot.IntPtrValue = refPtr + (value.Offset * sizeof(double));
slot.Length = value.Count;

View File

@ -33,7 +33,8 @@ namespace System.Runtime.InteropServices.JavaScript
if (slot.JSHandle != IntPtr.Zero)
{
// this is JSException round-trip
jsException = JSHostImplementation.CreateCSOwnedProxy(slot.JSHandle);
var ctx = ToManagedContext;
jsException = ctx.CreateCSOwnedProxy(slot.JSHandle);
}
string? message;
@ -65,11 +66,21 @@ namespace System.Runtime.InteropServices.JavaScript
var jse = cpy as JSException;
if (jse != null && jse.jsException != null)
{
ObjectDisposedException.ThrowIf(jse.jsException.IsDisposed, value);
#if FEATURE_WASM_THREADS
JSObject.AssertThreadAffinity(value);
var ctx = jse.jsException.ProxyContext;
if (JSProxyContext.CapturingState == JSProxyContext.JSImportOperationState.JSImportParams)
{
JSProxyContext.CaptureContextFromParameter(ctx);
slot.ContextHandle = ctx.ContextHandle;
}
else if (slot.ContextHandle != ctx.ContextHandle)
{
Environment.FailFast($"ContextHandle mismatch, ManagedThreadId: {Environment.CurrentManagedThreadId}. {Environment.NewLine} {Environment.StackTrace}");
}
#endif
// this is JSException roundtrip
ObjectDisposedException.ThrowIf(jse.jsException.IsDisposed, value);
slot.Type = MarshalerType.JSException;
slot.JSHandle = jse.jsException.JSHandle;
}
@ -77,7 +88,9 @@ namespace System.Runtime.InteropServices.JavaScript
{
ToJS(cpy.Message);
slot.Type = MarshalerType.Exception;
slot.GCHandle = JSHostImplementation.GetJSOwnedObjectGCHandle(cpy);
var ctx = ToJSContext;
slot.GCHandle = ctx.GetJSOwnedObjectGCHandle(cpy);
}
}
}

View File

@ -9,9 +9,9 @@ namespace System.Runtime.InteropServices.JavaScript
{
private JSObject JSObject;
public ActionJS(IntPtr jsHandle)
public ActionJS(JSObject holder)
{
JSObject = JSHostImplementation.CreateCSOwnedProxy(jsHandle);
JSObject = holder;
}
public void InvokeJS()
@ -26,8 +26,14 @@ namespace System.Runtime.InteropServices.JavaScript
Span<JSMarshalerArgument> arguments = stackalloc JSMarshalerArgument[4];
ref JSMarshalerArgument args_exception = ref arguments[0];
ref JSMarshalerArgument args_return = ref arguments[1];
#if FEATURE_WASM_THREADS
args_exception.InitializeWithContext(JSObject.ProxyContext);
args_return.InitializeWithContext(JSObject.ProxyContext);
JSProxyContext.JSImportNoCapture();
#else
args_exception.Initialize();
args_return.Initialize();
#endif
JSFunctionBinding.InvokeJSFunction(JSObject, arguments);
}
@ -39,9 +45,9 @@ namespace System.Runtime.InteropServices.JavaScript
private ArgumentToJSCallback<T> Arg1Marshaler;
private JSObject JSObject;
public ActionJS(IntPtr jsHandle, ArgumentToJSCallback<T> arg1Marshaler)
public ActionJS(JSObject holder, ArgumentToJSCallback<T> arg1Marshaler)
{
JSObject = JSHostImplementation.CreateCSOwnedProxy(jsHandle);
JSObject = holder;
Arg1Marshaler = arg1Marshaler;
}
@ -56,10 +62,18 @@ namespace System.Runtime.InteropServices.JavaScript
ref JSMarshalerArgument args_return = ref arguments[1];
ref JSMarshalerArgument args_arg1 = ref arguments[2];
#if FEATURE_WASM_THREADS
args_exception.InitializeWithContext(JSObject.ProxyContext);
args_return.InitializeWithContext(JSObject.ProxyContext);
args_arg1.InitializeWithContext(JSObject.ProxyContext);
JSProxyContext.JSImportNoCapture();
#else
args_exception.Initialize();
args_return.Initialize();
#endif
Arg1Marshaler(ref args_arg1, arg1);
JSFunctionBinding.InvokeJSFunction(JSObject, arguments);
}
}
@ -70,9 +84,9 @@ namespace System.Runtime.InteropServices.JavaScript
private ArgumentToJSCallback<T2> Arg2Marshaler;
private JSObject JSObject;
public ActionJS(IntPtr jsHandle, ArgumentToJSCallback<T1> arg1Marshaler, ArgumentToJSCallback<T2> arg2Marshaler)
public ActionJS(JSObject holder, ArgumentToJSCallback<T1> arg1Marshaler, ArgumentToJSCallback<T2> arg2Marshaler)
{
JSObject = JSHostImplementation.CreateCSOwnedProxy(jsHandle);
JSObject = holder;
Arg1Marshaler = arg1Marshaler;
Arg2Marshaler = arg2Marshaler;
}
@ -89,11 +103,20 @@ namespace System.Runtime.InteropServices.JavaScript
ref JSMarshalerArgument args_arg1 = ref arguments[2];
ref JSMarshalerArgument args_arg2 = ref arguments[3];
#if FEATURE_WASM_THREADS
args_exception.InitializeWithContext(JSObject.ProxyContext);
args_return.InitializeWithContext(JSObject.ProxyContext);
args_arg1.InitializeWithContext(JSObject.ProxyContext);
args_arg2.InitializeWithContext(JSObject.ProxyContext);
JSProxyContext.JSImportNoCapture();
#else
args_exception.Initialize();
args_return.Initialize();
#endif
Arg1Marshaler(ref args_arg1, arg1);
Arg2Marshaler(ref args_arg2, arg2);
JSFunctionBinding.InvokeJSFunction(JSObject, arguments);
}
}
@ -105,9 +128,9 @@ namespace System.Runtime.InteropServices.JavaScript
private ArgumentToJSCallback<T3> Arg3Marshaler;
private JSObject JSObject;
public ActionJS(IntPtr jsHandle, ArgumentToJSCallback<T1> arg1Marshaler, ArgumentToJSCallback<T2> arg2Marshaler, ArgumentToJSCallback<T3> arg3Marshaler)
public ActionJS(JSObject holder, ArgumentToJSCallback<T1> arg1Marshaler, ArgumentToJSCallback<T2> arg2Marshaler, ArgumentToJSCallback<T3> arg3Marshaler)
{
JSObject = JSHostImplementation.CreateCSOwnedProxy(jsHandle);
JSObject = holder;
Arg1Marshaler = arg1Marshaler;
Arg2Marshaler = arg2Marshaler;
Arg3Marshaler = arg3Marshaler;
@ -126,12 +149,22 @@ namespace System.Runtime.InteropServices.JavaScript
ref JSMarshalerArgument args_arg2 = ref arguments[3];
ref JSMarshalerArgument args_arg3 = ref arguments[4];
#if FEATURE_WASM_THREADS
args_exception.InitializeWithContext(JSObject.ProxyContext);
args_return.InitializeWithContext(JSObject.ProxyContext);
args_arg1.InitializeWithContext(JSObject.ProxyContext);
args_arg2.InitializeWithContext(JSObject.ProxyContext);
args_arg3.InitializeWithContext(JSObject.ProxyContext);
JSProxyContext.JSImportNoCapture();
#else
args_exception.Initialize();
args_return.Initialize();
#endif
Arg1Marshaler(ref args_arg1, arg1);
Arg2Marshaler(ref args_arg2, arg2);
Arg3Marshaler(ref args_arg3, arg3);
JSFunctionBinding.InvokeJSFunction(JSObject, arguments);
}
}
@ -149,7 +182,9 @@ namespace System.Runtime.InteropServices.JavaScript
return;
}
value = new ActionJS(slot.JSHandle).InvokeJS;
var ctx = ToManagedContext;
var holder = ctx.CreateCSOwnedProxy(slot.JSHandle);
value = new ActionJS(holder).InvokeJS;
}
/// <summary>
@ -167,7 +202,9 @@ namespace System.Runtime.InteropServices.JavaScript
return;
}
value = new ActionJS<T>(slot.JSHandle, arg1Marshaler).InvokeJS;
var ctx = ToManagedContext;
var holder = ctx.CreateCSOwnedProxy(slot.JSHandle);
value = new ActionJS<T>(holder, arg1Marshaler).InvokeJS;
}
/// <summary>
@ -187,7 +224,9 @@ namespace System.Runtime.InteropServices.JavaScript
return;
}
value = new ActionJS<T1, T2>(slot.JSHandle, arg1Marshaler, arg2Marshaler).InvokeJS;
var ctx = ToManagedContext;
var holder = ctx.CreateCSOwnedProxy(slot.JSHandle);
value = new ActionJS<T1, T2>(holder, arg1Marshaler, arg2Marshaler).InvokeJS;
}
/// <summary>
@ -209,7 +248,9 @@ namespace System.Runtime.InteropServices.JavaScript
return;
}
value = new ActionJS<T1, T2, T3>(slot.JSHandle, arg1Marshaler, arg2Marshaler, arg3Marshaler).InvokeJS;
var ctx = ToManagedContext;
var holder = ctx.CreateCSOwnedProxy(slot.JSHandle);
value = new ActionJS<T1, T2, T3>(holder, arg1Marshaler, arg2Marshaler, arg3Marshaler).InvokeJS;
}
private sealed class FuncJS<TResult>
@ -217,9 +258,9 @@ namespace System.Runtime.InteropServices.JavaScript
private JSObject JSObject;
private ArgumentToManagedCallback<TResult> ResMarshaler;
public FuncJS(IntPtr jsHandle, ArgumentToManagedCallback<TResult> resMarshaler)
public FuncJS(JSObject holder, ArgumentToManagedCallback<TResult> resMarshaler)
{
JSObject = JSHostImplementation.CreateCSOwnedProxy(jsHandle);
JSObject = holder;
ResMarshaler = resMarshaler;
}
@ -235,12 +276,19 @@ namespace System.Runtime.InteropServices.JavaScript
Span<JSMarshalerArgument> arguments = stackalloc JSMarshalerArgument[4];
ref JSMarshalerArgument args_exception = ref arguments[0];
ref JSMarshalerArgument args_return = ref arguments[1];
#if FEATURE_WASM_THREADS
args_exception.InitializeWithContext(JSObject.ProxyContext);
args_return.InitializeWithContext(JSObject.ProxyContext);
JSProxyContext.JSImportNoCapture();
#else
args_exception.Initialize();
args_return.Initialize();
#endif
JSFunctionBinding.InvokeJSFunction(JSObject, arguments);
ResMarshaler(ref args_return, out TResult res);
return res;
}
@ -252,9 +300,9 @@ namespace System.Runtime.InteropServices.JavaScript
private ArgumentToManagedCallback<TResult> ResMarshaler;
private JSObject JSObject;
public FuncJS(IntPtr jsHandle, ArgumentToJSCallback<T> arg1Marshaler, ArgumentToManagedCallback<TResult> resMarshaler)
public FuncJS(JSObject holder, ArgumentToJSCallback<T> arg1Marshaler, ArgumentToManagedCallback<TResult> resMarshaler)
{
JSObject = JSHostImplementation.CreateCSOwnedProxy(jsHandle);
JSObject = holder;
Arg1Marshaler = arg1Marshaler;
ResMarshaler = resMarshaler;
}
@ -270,8 +318,15 @@ namespace System.Runtime.InteropServices.JavaScript
ref JSMarshalerArgument args_return = ref arguments[1];
ref JSMarshalerArgument args_arg1 = ref arguments[2];
#if FEATURE_WASM_THREADS
args_exception.InitializeWithContext(JSObject.ProxyContext);
args_return.InitializeWithContext(JSObject.ProxyContext);
args_arg1.InitializeWithContext(JSObject.ProxyContext);
JSProxyContext.JSImportNoCapture();
#else
args_exception.Initialize();
args_return.Initialize();
#endif
Arg1Marshaler(ref args_arg1, arg1);
JSFunctionBinding.InvokeJSFunction(JSObject, arguments);
@ -288,9 +343,9 @@ namespace System.Runtime.InteropServices.JavaScript
private ArgumentToManagedCallback<TResult> ResMarshaler;
private JSObject JSObject;
public FuncJS(IntPtr jsHandle, ArgumentToJSCallback<T1> arg1Marshaler, ArgumentToJSCallback<T2> arg2Marshaler, ArgumentToManagedCallback<TResult> resMarshaler)
public FuncJS(JSObject holder, ArgumentToJSCallback<T1> arg1Marshaler, ArgumentToJSCallback<T2> arg2Marshaler, ArgumentToManagedCallback<TResult> resMarshaler)
{
JSObject = JSHostImplementation.CreateCSOwnedProxy(jsHandle);
JSObject = holder;
Arg1Marshaler = arg1Marshaler;
Arg2Marshaler = arg2Marshaler;
ResMarshaler = resMarshaler;
@ -308,8 +363,16 @@ namespace System.Runtime.InteropServices.JavaScript
ref JSMarshalerArgument args_arg1 = ref arguments[2];
ref JSMarshalerArgument args_arg2 = ref arguments[3];
#if FEATURE_WASM_THREADS
args_exception.InitializeWithContext(JSObject.ProxyContext);
args_return.InitializeWithContext(JSObject.ProxyContext);
args_arg1.InitializeWithContext(JSObject.ProxyContext);
args_arg2.InitializeWithContext(JSObject.ProxyContext);
JSProxyContext.JSImportNoCapture();
#else
args_exception.Initialize();
args_return.Initialize();
#endif
Arg1Marshaler(ref args_arg1, arg1);
Arg2Marshaler(ref args_arg2, arg2);
@ -328,9 +391,9 @@ namespace System.Runtime.InteropServices.JavaScript
private ArgumentToManagedCallback<TResult> ResMarshaler;
private JSObject JSObject;
public FuncJS(IntPtr jsHandle, ArgumentToJSCallback<T1> arg1Marshaler, ArgumentToJSCallback<T2> arg2Marshaler, ArgumentToJSCallback<T3> arg3Marshaler, ArgumentToManagedCallback<TResult> resMarshaler)
public FuncJS(JSObject holder, ArgumentToJSCallback<T1> arg1Marshaler, ArgumentToJSCallback<T2> arg2Marshaler, ArgumentToJSCallback<T3> arg3Marshaler, ArgumentToManagedCallback<TResult> resMarshaler)
{
JSObject = JSHostImplementation.CreateCSOwnedProxy(jsHandle);
JSObject = holder;
Arg1Marshaler = arg1Marshaler;
Arg2Marshaler = arg2Marshaler;
Arg3Marshaler = arg3Marshaler;
@ -350,15 +413,24 @@ namespace System.Runtime.InteropServices.JavaScript
ref JSMarshalerArgument args_arg2 = ref arguments[3];
ref JSMarshalerArgument args_arg3 = ref arguments[4];
#if FEATURE_WASM_THREADS
args_exception.InitializeWithContext(JSObject.ProxyContext);
args_return.InitializeWithContext(JSObject.ProxyContext);
args_arg1.InitializeWithContext(JSObject.ProxyContext);
args_arg2.InitializeWithContext(JSObject.ProxyContext);
args_arg3.InitializeWithContext(JSObject.ProxyContext);
JSProxyContext.JSImportNoCapture();
#else
args_exception.Initialize();
args_return.Initialize();
#endif
Arg1Marshaler(ref args_arg1, arg1);
Arg2Marshaler(ref args_arg2, arg2);
Arg3Marshaler(ref args_arg3, arg3);
JSFunctionBinding.InvokeJSFunction(JSObject, arguments);
ResMarshaler(ref args_return, out TResult res);
return res;
}
}
@ -378,7 +450,9 @@ namespace System.Runtime.InteropServices.JavaScript
return;
}
value = new FuncJS<TResult>(slot.JSHandle, resMarshaler).InvokeJS;
var ctx = ToManagedContext;
var holder = ctx.CreateCSOwnedProxy(slot.JSHandle);
value = new FuncJS<TResult>(holder, resMarshaler).InvokeJS;
}
/// <summary>
@ -398,7 +472,9 @@ namespace System.Runtime.InteropServices.JavaScript
return;
}
value = new FuncJS<T, TResult>(slot.JSHandle, arg1Marshaler, resMarshaler).InvokeJS;
var ctx = ToManagedContext;
var holder = ctx.CreateCSOwnedProxy(slot.JSHandle);
value = new FuncJS<T, TResult>(holder, arg1Marshaler, resMarshaler).InvokeJS;
}
@ -421,7 +497,9 @@ namespace System.Runtime.InteropServices.JavaScript
return;
}
value = new FuncJS<T1, T2, TResult>(slot.JSHandle, arg1Marshaler, arg2Marshaler, resMarshaler).InvokeJS;
var ctx = ToManagedContext;
var holder = ctx.CreateCSOwnedProxy(slot.JSHandle);
value = new FuncJS<T1, T2, TResult>(holder, arg1Marshaler, arg2Marshaler, resMarshaler).InvokeJS;
}
/// <summary>
@ -444,8 +522,9 @@ namespace System.Runtime.InteropServices.JavaScript
value = null;
return;
}
value = new FuncJS<T1, T2, T3, TResult>(slot.JSHandle, arg1Marshaler, arg2Marshaler, arg3Marshaler, resMarshaler).InvokeJS;
var ctx = ToManagedContext;
var holder = ctx.CreateCSOwnedProxy(slot.JSHandle);
value = new FuncJS<T1, T2, T3, TResult>(holder, arg1Marshaler, arg2Marshaler, arg3Marshaler, resMarshaler).InvokeJS;
}
/// <summary>
@ -463,7 +542,8 @@ namespace System.Runtime.InteropServices.JavaScript
// eventual exception is handled by C# caller
};
slot.Type = MarshalerType.Function;
slot.GCHandle = JSHostImplementation.GetJSOwnedObjectGCHandle(cb);
var ctx = ToJSContext;
slot.GCHandle = ctx.GetJSOwnedObjectGCHandle(cb);
}
/// <summary>
@ -484,7 +564,8 @@ namespace System.Runtime.InteropServices.JavaScript
// eventual exception is handled by C# caller
};
slot.Type = MarshalerType.Action;
slot.GCHandle = JSHostImplementation.GetJSOwnedObjectGCHandle(cb);
var ctx = ToJSContext;
slot.GCHandle = ctx.GetJSOwnedObjectGCHandle(cb);
}
/// <summary>
@ -509,7 +590,8 @@ namespace System.Runtime.InteropServices.JavaScript
// eventual exception is handled by C# caller
};
slot.Type = MarshalerType.Action;
slot.GCHandle = JSHostImplementation.GetJSOwnedObjectGCHandle(cb);
var ctx = ToJSContext;
slot.GCHandle = ctx.GetJSOwnedObjectGCHandle(cb);
}
/// <summary>
@ -538,7 +620,8 @@ namespace System.Runtime.InteropServices.JavaScript
// eventual exception is handled by C# caller
};
slot.Type = MarshalerType.Action;
slot.GCHandle = JSHostImplementation.GetJSOwnedObjectGCHandle(cb);
var ctx = ToJSContext;
slot.GCHandle = ctx.GetJSOwnedObjectGCHandle(cb);
}
/// <summary>
@ -559,7 +642,8 @@ namespace System.Runtime.InteropServices.JavaScript
// eventual exception is handled by C# caller
};
slot.Type = MarshalerType.Function;
slot.GCHandle = JSHostImplementation.GetJSOwnedObjectGCHandle(cb);
var ctx = ToJSContext;
slot.GCHandle = ctx.GetJSOwnedObjectGCHandle(cb);
}
/// <summary>
@ -584,7 +668,8 @@ namespace System.Runtime.InteropServices.JavaScript
// eventual exception is handled by C# caller
};
slot.Type = MarshalerType.Function;
slot.GCHandle = JSHostImplementation.GetJSOwnedObjectGCHandle(cb);
var ctx = ToJSContext;
slot.GCHandle = ctx.GetJSOwnedObjectGCHandle(cb);
}
/// <summary>
@ -613,7 +698,8 @@ namespace System.Runtime.InteropServices.JavaScript
// eventual exception is handled by C# caller
};
slot.Type = MarshalerType.Function;
slot.GCHandle = JSHostImplementation.GetJSOwnedObjectGCHandle(cb);
var ctx = ToJSContext;
slot.GCHandle = ctx.GetJSOwnedObjectGCHandle(cb);
}
/// <summary>
@ -646,7 +732,8 @@ namespace System.Runtime.InteropServices.JavaScript
// eventual exception is handled by C# caller
};
slot.Type = MarshalerType.Function;
slot.GCHandle = JSHostImplementation.GetJSOwnedObjectGCHandle(cb);
var ctx = ToJSContext;
slot.GCHandle = ctx.GetJSOwnedObjectGCHandle(cb);
}
}
}

View File

@ -132,8 +132,9 @@ namespace System.Runtime.InteropServices.JavaScript
slot.Type = MarshalerType.None;
return;
}
var ctx = ToJSContext;
slot.Type = MarshalerType.ArraySegment;
slot.GCHandle = JSHostImplementation.GetJSOwnedObjectGCHandle(value.Array, GCHandleType.Pinned);
slot.GCHandle = ctx.GetJSOwnedObjectGCHandle(value.Array, GCHandleType.Pinned);
var refPtr = (IntPtr)Unsafe.AsPointer(ref MemoryMarshal.GetArrayDataReference(value.Array));
slot.IntPtrValue = refPtr + (value.Offset * sizeof(int));
slot.Length = value.Count;

View File

@ -20,7 +20,8 @@ namespace System.Runtime.InteropServices.JavaScript
value = null;
return;
}
value = JSHostImplementation.CreateCSOwnedProxy(slot.JSHandle);
var ctx = ToManagedContext;
value = ctx.CreateCSOwnedProxy(slot.JSHandle);
}
/// <summary>
@ -34,13 +35,25 @@ namespace System.Runtime.InteropServices.JavaScript
if (value == null)
{
slot.Type = MarshalerType.None;
// Note: when null JSObject is passed as argument, it can't be used to capture the target thread in JSProxyContext.CapturedInstance
// in case there is no other argument to capture it from, the call will be dispatched according to JSProxyContext.Default
}
else
{
ObjectDisposedException.ThrowIf(value.IsDisposed, value);
#if FEATURE_WASM_THREADS
JSObject.AssertThreadAffinity(value);
var ctx = value.ProxyContext;
if (JSProxyContext.CapturingState == JSProxyContext.JSImportOperationState.JSImportParams)
{
JSProxyContext.CaptureContextFromParameter(ctx);
slot.ContextHandle = ctx.ContextHandle;
}
else if (slot.ContextHandle != ctx.ContextHandle)
{
Environment.FailFast($"ContextHandle mismatch, ManagedThreadId: {Environment.CurrentManagedThreadId}. {Environment.NewLine} {Environment.StackTrace}");
}
#endif
ObjectDisposedException.ThrowIf(value.IsDisposed, value);
slot.Type = MarshalerType.JSObject;
slot.JSHandle = value.JSHandle;
}

View File

@ -317,7 +317,8 @@ namespace System.Runtime.InteropServices.JavaScript
else
{
slot.Type = MarshalerType.Object;
slot.GCHandle = JSHostImplementation.GetJSOwnedObjectGCHandle(value);
var ctx = ToJSContext;
slot.GCHandle = ctx.GetJSOwnedObjectGCHandle(value);
}
}

View File

@ -4,6 +4,8 @@
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Threading.Tasks;
using System.ComponentModel;
using System.Threading;
using static System.Runtime.InteropServices.JavaScript.JSHostImplementation;
namespace System.Runtime.InteropServices.JavaScript
@ -17,7 +19,7 @@ namespace System.Runtime.InteropServices.JavaScript
/// <typeparam name="T">Type of the marshaled value.</typeparam>
/// <param name="arg">The low-level argument representation.</param>
/// <param name="value">The value to be marshaled.</param>
[System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)]
[EditorBrowsableAttribute(EditorBrowsableState.Never)]
public delegate void ArgumentToManagedCallback<T>(ref JSMarshalerArgument arg, out T value);
/// <summary>
@ -27,7 +29,7 @@ namespace System.Runtime.InteropServices.JavaScript
/// <typeparam name="T">Type of the marshaled value.</typeparam>
/// <param name="arg">The low-level argument representation.</param>
/// <param name="value">The value to be marshaled.</param>
[System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)]
[EditorBrowsableAttribute(EditorBrowsableState.Never)]
public delegate void ArgumentToJSCallback<T>(ref JSMarshalerArgument arg, T value);
/// <summary>
@ -43,30 +45,38 @@ namespace System.Runtime.InteropServices.JavaScript
value = null;
return;
}
PromiseHolder holder = GetPromiseHolder(slot.GCHandle);
TaskCompletionSource tcs = new TaskCompletionSource(holder);
ToManagedCallback callback = (JSMarshalerArgument* arguments_buffer) =>
var ctx = ToManagedContext;
lock (ctx)
{
if (arguments_buffer == null)
PromiseHolder holder = ctx.GetPromiseHolder(slot.GCHandle);
TaskCompletionSource tcs = new TaskCompletionSource(holder);
ToManagedCallback callback = (JSMarshalerArgument* arguments_buffer) =>
{
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)
{
arg_2.ToManaged(out Exception? fail);
tcs.SetException(fail!);
}
else
{
tcs.SetResult();
}
// eventual exception is handled by caller
};
holder.Callback = callback;
value = tcs.Task;
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)
{
arg_2.ToManaged(out Exception? fail);
tcs.SetException(fail!);
}
else
{
tcs.SetResult();
}
// eventual exception is handled by caller
};
holder.Callback = callback;
value = tcs.Task;
#if FEATURE_WASM_THREADS
// if the other thread created it, signal that it's ready
holder.CallbackReady?.Set();
#endif
}
}
/// <summary>
@ -74,8 +84,8 @@ namespace System.Runtime.InteropServices.JavaScript
/// It's used by JSImport code generator and should not be used by developers in source code.
/// </summary>
/// <param name="value">The value to be marshaled.</param>
/// <param name="marshaler">The generated callback which marshals the result value of the <see cref="System.Threading.Tasks.Task"/>.</param>
/// <typeparam name="T">Type of marshaled result of the <see cref="System.Threading.Tasks.Task"/>.</typeparam>
/// <param name="marshaler">The generated callback which marshals the result value of the <see cref="Task"/>.</param>
/// <typeparam name="T">Type of marshaled result of the <see cref="Task"/>.</typeparam>
public unsafe void ToManaged<T>(out Task<T>? value, ArgumentToManagedCallback<T> marshaler)
{
// there is no nice way in JS how to check that JS promise is already resolved, to send MarshalerType.TaskRejected, MarshalerType.TaskResolved
@ -84,52 +94,43 @@ namespace System.Runtime.InteropServices.JavaScript
value = null;
return;
}
PromiseHolder holder = GetPromiseHolder(slot.GCHandle);
TaskCompletionSource<T> tcs = new TaskCompletionSource<T>(holder);
ToManagedCallback callback = (JSMarshalerArgument* arguments_buffer) =>
var ctx = ToManagedContext;
lock (ctx)
{
if (arguments_buffer == null)
var holder = ctx.GetPromiseHolder(slot.GCHandle);
TaskCompletionSource<T> tcs = new TaskCompletionSource<T>(holder);
ToManagedCallback callback = (JSMarshalerArgument* arguments_buffer) =>
{
tcs.TrySetException(new TaskCanceledException("WebWorker which is origin of the Promise is being terminated."));
return;
}
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)
{
arg_2.ToManaged(out Exception? fail);
if (fail == null) throw new InvalidOperationException(SR.FailedToMarshalException);
tcs.SetException(fail);
}
else
{
marshaler(ref arg_3, out T result);
tcs.SetResult(result);
}
// eventual exception is handled by caller
};
holder.Callback = callback;
value = tcs.Task;
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)
{
arg_2.ToManaged(out Exception? fail);
if (fail == null) throw new InvalidOperationException(SR.FailedToMarshalException);
tcs.SetException(fail);
}
else
{
marshaler(ref arg_3, out T result);
tcs.SetResult(result);
}
// eventual exception is handled by caller
};
holder.Callback = callback;
value = tcs.Task;
#if FEATURE_WASM_THREADS
// if the other thread created it, signal that it's ready
holder.CallbackReady?.Set();
#endif
}
}
// TODO unregister and collect pending PromiseHolder also when no C# is awaiting ?
private static PromiseHolder GetPromiseHolder(nint gcHandle)
{
PromiseHolder holder;
if (IsGCVHandle(gcHandle))
{
// this path should only happen when the Promise is passed as argument of JSExport
holder = new PromiseHolder(gcHandle);
// TODO for MT this must hit the ThreadJsOwnedHolders in the correct thread
ThreadJsOwnedHolders.Add(gcHandle, holder);
}
else
{
holder = (PromiseHolder)((GCHandle)gcHandle).Target!;
}
return holder;
}
internal void ToJSDynamic(Task? value)
{
@ -167,10 +168,12 @@ namespace System.Runtime.InteropServices.JavaScript
}
}
var ctx = ToJSContext;
if (slot.Type != MarshalerType.TaskPreCreated)
{
// this path should only happen when the Task is passed as argument of JSImport
slot.JSHandle = AllocJSVHandle();
slot.JSHandle = ctx.AllocJSVHandle();
slot.Type = MarshalerType.Task;
}
else
@ -179,7 +182,7 @@ namespace System.Runtime.InteropServices.JavaScript
// promise and handle is pre-allocated in slot.JSHandle
}
var taskHolder = new JSObject(slot.JSHandle);
var taskHolder = ctx.CreateCSOwnedProxy(slot.JSHandle);
#if FEATURE_WASM_THREADS
task.ContinueWith(Complete, taskHolder, TaskScheduler.FromCurrentSynchronizationContext());
@ -245,10 +248,12 @@ namespace System.Runtime.InteropServices.JavaScript
}
}
var ctx = ToJSContext;
if (slot.Type != MarshalerType.TaskPreCreated)
{
// this path should only happen when the Task is passed as argument of JSImport
slot.JSHandle = AllocJSVHandle();
slot.JSHandle = ctx.AllocJSVHandle();
slot.Type = MarshalerType.Task;
}
else
@ -257,7 +262,7 @@ namespace System.Runtime.InteropServices.JavaScript
// promise and handle is pre-allocated in slot.JSHandle
}
var taskHolder = new JSObject(slot.JSHandle);
var taskHolder = ctx.CreateCSOwnedProxy(slot.JSHandle);
#if FEATURE_WASM_THREADS
task.ContinueWith(Complete, taskHolder, TaskScheduler.FromCurrentSynchronizationContext());
@ -316,10 +321,11 @@ namespace System.Runtime.InteropServices.JavaScript
}
}
var ctx = ToJSContext;
if (slot.Type != MarshalerType.TaskPreCreated)
{
// this path should only happen when the Task is passed as argument of JSImport
slot.JSHandle = AllocJSVHandle();
slot.JSHandle = ctx.AllocJSVHandle();
slot.Type = MarshalerType.Task;
}
else
@ -328,7 +334,7 @@ namespace System.Runtime.InteropServices.JavaScript
// promise and handle is pre-allocated in slot.JSHandle
}
var taskHolder = new JSObject(slot.JSHandle);
var taskHolder = ctx.CreateCSOwnedProxy(slot.JSHandle);
#if FEATURE_WASM_THREADS
task.ContinueWith(Complete, new HolderAndMarshaler<T>(taskHolder, marshaler), TaskScheduler.FromCurrentSynchronizationContext());
@ -357,14 +363,26 @@ namespace System.Runtime.InteropServices.JavaScript
{
holder.AssertNotDisposed();
#if FEATURE_WASM_THREADS
JSObject.AssertThreadAffinity(holder);
#endif
Span<JSMarshalerArgument> args = stackalloc JSMarshalerArgument[4];
ref JSMarshalerArgument exc = ref args[0];
ref JSMarshalerArgument res = ref args[1];
ref JSMarshalerArgument arg_handle = ref args[2];
ref JSMarshalerArgument arg_value = ref args[3];
#if FEATURE_WASM_THREADS
exc.InitializeWithContext(holder.ProxyContext);
res.InitializeWithContext(holder.ProxyContext);
arg_value.InitializeWithContext(holder.ProxyContext);
arg_handle.InitializeWithContext(holder.ProxyContext);
JSProxyContext.JSImportNoCapture();
#else
exc.Initialize();
res.Initialize();
#endif
// should update existing promise
arg_handle.slot.Type = MarshalerType.TaskRejected;
@ -373,14 +391,24 @@ namespace System.Runtime.InteropServices.JavaScript
// should fail it with exception
arg_value.ToJS(ex);
JavaScriptImports.ResolveOrRejectPromise(args);
// we can free the JSHandle here and the holder.resolve_or_reject will do the rest
holder.DisposeImpl(skipJsCleanup: true);
holder.DisposeLocal();
#if !FEATURE_WASM_THREADS
// order of operations with DisposeImpl matters
JSFunctionBinding.ResolveOrRejectPromise(args);
#else
// order of operations with DisposeImpl matters
JSFunctionBinding.ResolveOrRejectPromise(args);
#endif
}
private static void ResolveVoidPromise(JSObject holder)
{
holder.AssertNotDisposed();
#if FEATURE_WASM_THREADS
JSObject.AssertThreadAffinity(holder);
#endif
Span<JSMarshalerArgument> args = stackalloc JSMarshalerArgument[4];
ref JSMarshalerArgument exc = ref args[0];
@ -388,8 +416,16 @@ namespace System.Runtime.InteropServices.JavaScript
ref JSMarshalerArgument arg_handle = ref args[2];
ref JSMarshalerArgument arg_value = ref args[3];
#if FEATURE_WASM_THREADS
exc.InitializeWithContext(holder.ProxyContext);
res.InitializeWithContext(holder.ProxyContext);
arg_value.InitializeWithContext(holder.ProxyContext);
arg_handle.InitializeWithContext(holder.ProxyContext);
JSProxyContext.JSImportNoCapture();
#else
exc.Initialize();
res.Initialize();
#endif
// should update existing promise
arg_handle.slot.Type = MarshalerType.TaskResolved;
@ -397,14 +433,24 @@ namespace System.Runtime.InteropServices.JavaScript
arg_value.slot.Type = MarshalerType.Void;
JavaScriptImports.ResolveOrRejectPromise(args);
// we can free the JSHandle here and the holder.resolve_or_reject will do the rest
holder.DisposeImpl(skipJsCleanup: true);
holder.DisposeLocal();
#if !FEATURE_WASM_THREADS
// order of operations with DisposeImpl matters
JSFunctionBinding.ResolveOrRejectPromise(args);
#else
// order of operations with DisposeImpl matters
JSFunctionBinding.ResolveOrRejectPromise(args);
#endif
}
private static void ResolvePromise<T>(JSObject holder, T value, ArgumentToJSCallback<T> marshaler)
{
holder.AssertNotDisposed();
#if FEATURE_WASM_THREADS
JSObject.AssertThreadAffinity(holder);
#endif
Span<JSMarshalerArgument> args = stackalloc JSMarshalerArgument[4];
ref JSMarshalerArgument exc = ref args[0];
@ -412,8 +458,16 @@ namespace System.Runtime.InteropServices.JavaScript
ref JSMarshalerArgument arg_handle = ref args[2];
ref JSMarshalerArgument arg_value = ref args[3];
#if FEATURE_WASM_THREADS
exc.InitializeWithContext(holder.ProxyContext);
res.InitializeWithContext(holder.ProxyContext);
arg_value.InitializeWithContext(holder.ProxyContext);
arg_handle.InitializeWithContext(holder.ProxyContext);
JSProxyContext.JSImportNoCapture();
#else
exc.Initialize();
res.Initialize();
#endif
// should update existing promise
arg_handle.slot.Type = MarshalerType.TaskResolved;
@ -422,9 +476,16 @@ namespace System.Runtime.InteropServices.JavaScript
// and resolve it with value
marshaler(ref arg_value, value);
JavaScriptImports.ResolveOrRejectPromise(args);
// we can free the JSHandle here and the holder.resolve_or_reject will do the rest
holder.DisposeImpl(skipJsCleanup: true);
holder.DisposeLocal();
#if !FEATURE_WASM_THREADS
// order of operations with DisposeImpl matters
JSFunctionBinding.ResolveOrRejectPromise(args);
#else
// order of operations with DisposeImpl matters
JSFunctionBinding.ResolveOrRejectPromise(args);
#endif
}
}
}

View File

@ -23,5 +23,6 @@
<None Include="$(CompilerGeneratedFilesOutputPath)\..\browser-wasm\generated\Microsoft.Interop.JavaScript.JSImportGenerator\Microsoft.Interop.JavaScript.JsExportGenerator\JSExports.g.cs" />
<WasmExtraFilesToDeploy Include="System\Runtime\InteropServices\JavaScript\JavaScriptTestHelper.mjs" />
<WasmExtraFilesToDeploy Include="System\Runtime\InteropServices\JavaScript\SecondRuntimeTest.js" />
<ProjectReference Include="$(LibrariesProjectRoot)System.Runtime.InteropServices.JavaScript\src\System.Runtime.InteropServices.JavaScript.csproj" SkipUseReferenceAssembly="true"/>
</ItemGroup>
</Project>

View File

@ -7,6 +7,7 @@ using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using System.Threading;
using Xunit;
using System.Diagnostics.CodeAnalysis;
#pragma warning disable xUnit1026 // Theory methods should use all of their parameters
namespace System.Runtime.InteropServices.JavaScript.Tests
@ -16,7 +17,7 @@ namespace System.Runtime.InteropServices.JavaScript.Tests
[Fact]
public unsafe void StructSize()
{
Assert.Equal(16, sizeof(JSMarshalerArgument));
Assert.Equal(32, sizeof(JSMarshalerArgument));
}
[Fact]
@ -378,7 +379,7 @@ namespace System.Runtime.InteropServices.JavaScript.Tests
[Theory]
[MemberData(nameof(MarshalObjectArrayCasesThrow))]
public unsafe void JsImportObjectArrayThrows(object[]? expected)
public void JsImportObjectArrayThrows(object[]? expected)
{
Assert.Throws<NotSupportedException>(() => JavaScriptTestHelper.echo1_ObjectArray(expected));
}
@ -1959,15 +1960,15 @@ namespace System.Runtime.InteropServices.JavaScript.Tests
#endregion
private void JsExportTest<T>(T value
private void JsExportTest<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] T>(T value
, Func<T, string, T> invoke, string echoName, string jsType, string? jsClass = null)
{
T res;
res = invoke(value, echoName);
Assert.Equal(value, res);
Assert.Equal<T>(value, res);
}
private void JsImportTest<T>(T value
private void JsImportTest<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] T>(T value
, Action<T> store1
, Func<T> retrieve1
, Func<T, T> echo1

View File

@ -999,19 +999,20 @@ namespace System.Runtime.InteropServices.JavaScript.Tests
{
if (_module == null)
{
// Log("JavaScriptTestHelper.mjs importing");
_module = await JSHost.ImportAsync("JavaScriptTestHelper", "../JavaScriptTestHelper.mjs");
await Setup();
// Log("JavaScriptTestHelper.mjs imported");
_module = await JSHost.ImportAsync("JavaScriptTestHelper", "../JavaScriptTestHelper.mjs"); ;
await Setup(); ;
}
var p = echopromise_String("aaa");
await p;
// this gives browser chance to serve UI thread event loop before every test
await Task.Yield();
}
public static Task DisposeAsync()
{
_module.Dispose();
_module?.Dispose();
_module = null;
return Task.CompletedTask;
}

View File

@ -367,7 +367,15 @@ export function backback(arg1, arg2, arg3) {
// console.log('backback A')
return (brg1, brg2) => {
// console.log('backback B')
return arg1(brg1 + arg2, brg2 + arg3);
try {
var res = arg1(brg1 + arg2, brg2 + arg3);
// console.log('backback C')
return res
}
catch (e) {
// console.log('backback E ' + e)
throw e;
}
}
}

View File

@ -1,3 +1,6 @@
!Makefile
.stamp-wasm-install-and-select*
emsdk
runtime/dotnet.d.ts.sha256
runtime/dotnet-legacy.d.ts.sha256

View File

@ -42,7 +42,7 @@ extern void* mono_wasm_invoke_js_blazor (MonoString **exceptionMessage, void *ca
#endif /* DISABLE_LEGACY_JS_INTEROP */
#ifndef DISABLE_THREADS
extern void mono_wasm_install_js_worker_interop ();
extern void mono_wasm_install_js_worker_interop (int context_gc_handle);
extern void mono_wasm_uninstall_js_worker_interop ();
#endif /* DISABLE_THREADS */

View File

@ -88,6 +88,7 @@ const fn_signatures: SigLine[] = [
[true, "mono_wasm_profiler_init_browser", "void", ["number"]],
[false, "mono_wasm_exec_regression", "number", ["number", "string"]],
[false, "mono_wasm_invoke_method_bound", "number", ["number", "number", "number"]],
[false, "mono_wasm_invoke_method_raw", "number", ["number", "number"]],
[true, "mono_wasm_write_managed_pointer_unsafe", "void", ["number", "number"]],
[true, "mono_wasm_copy_managed_pointer", "void", ["number", "number"]],
[true, "mono_wasm_i52_to_f64", "number", ["number", "number"]],
@ -229,6 +230,7 @@ export interface t_Cwraps {
mono_wasm_set_main_args(argc: number, argv: VoidPtr): void;
mono_wasm_exec_regression(verbose_level: number, image: string): number;
mono_wasm_invoke_method_bound(method: MonoMethod, args: JSMarshalerArguments, fail: MonoStringRef): number;
mono_wasm_invoke_method_raw(method: MonoMethod, fail: MonoStringRef): number;
mono_wasm_write_managed_pointer_unsafe(destination: VoidPtr | MonoObjectRef, pointer: ManagedPointer): void;
mono_wasm_copy_managed_pointer(destination: VoidPtr | MonoObjectRef, source: VoidPtr | MonoObjectRef): void;
mono_wasm_i52_to_f64(source: VoidPtr, error: Int32Ptr): number;

View File

@ -322,6 +322,27 @@ mono_wasm_invoke_method_bound (MonoMethod *method, void* args /*JSMarshalerArgum
return is_err;
}
EMSCRIPTEN_KEEPALIVE int
mono_wasm_invoke_method_raw (MonoMethod *method, MonoString **out_exc)
{
PVOLATILE(MonoObject) temp_exc = NULL;
int is_err = 0;
MONO_ENTER_GC_UNSAFE;
mono_runtime_invoke (method, NULL, NULL, (MonoObject **)&temp_exc);
if (temp_exc && out_exc) {
PVOLATILE(MonoObject) exc2 = NULL;
store_volatile((MonoObject**)out_exc, (MonoObject*)mono_object_to_string ((MonoObject*)temp_exc, (MonoObject **)&exc2));
if (exc2)
store_volatile((MonoObject**)out_exc, (MonoObject*)mono_string_new (root_domain, "Exception Double Fault"));
is_err = 1;
}
MONO_EXIT_GC_UNSAFE;
return is_err;
}
EMSCRIPTEN_KEEPALIVE MonoMethod*
mono_wasm_assembly_get_entry_point (MonoAssembly *assembly, int auto_insert_breakpoint)
{

View File

@ -98,6 +98,7 @@ export function register_with_jsv_handle(js_obj: any, jsv_handle: JSHandle) {
}
}
// note: in MT, this is called from locked JSProxyContext. Don't call anything that would need locking.
export function mono_wasm_release_cs_owned_object(js_handle: JSHandle): void {
let obj: any;
if (is_js_handle(js_handle)) {
@ -108,6 +109,7 @@ export function mono_wasm_release_cs_owned_object(js_handle: JSHandle): void {
else if (is_jsv_handle(js_handle)) {
obj = _cs_owned_objects_by_jsv_handle[0 - <any>js_handle];
_cs_owned_objects_by_jsv_handle[0 - <any>js_handle] = undefined;
// see free list in JSProxyContext.FreeJSVHandle
}
mono_assert(obj !== undefined && obj !== null, "ObjectDisposedException");
if (typeof obj[cs_owned_js_handle_symbol] !== "undefined") {

View File

@ -4,6 +4,7 @@
import { wrap_as_cancelable_promise } from "./cancelable-promise";
import { ENVIRONMENT_IS_NODE, Module, loaderHelpers, mono_assert } from "./globals";
import { MemoryViewType, Span } from "./marshal";
import { assert_synchronization_context } from "./pthreads/shared";
import type { VoidPtr } from "./types/emscripten";
import { ControllablePromise } from "./types/internal";
@ -112,6 +113,7 @@ export function http_wasm_fetch_bytes(url: string, header_names: string[], heade
export function http_wasm_fetch(url: string, header_names: string[], header_values: string[], option_names: string[], option_values: any[], abort_controller: AbortController, body: Uint8Array | ReadableStream | null): ControllablePromise<ResponseExtension> {
verifyEnvironment();
assert_synchronization_context();
mono_assert(url && typeof url === "string", "expected url string");
mono_assert(header_names && header_values && Array.isArray(header_names) && Array.isArray(header_values) && header_names.length === header_values.length, "expected headerNames and headerValues arrays");
mono_assert(option_names && option_values && Array.isArray(option_names) && Array.isArray(option_values) && option_names.length === option_values.length, "expected headerNames and headerValues arrays");

View File

@ -9,7 +9,7 @@ import { bind_arg_marshal_to_cs } from "./marshal-to-cs";
import { marshal_exception_to_js, bind_arg_marshal_to_js, end_marshal_task_to_js } from "./marshal-to-js";
import {
get_arg, get_sig, get_signature_argument_count, is_args_exception,
bound_cs_function_symbol, get_signature_version, alloc_stack_frame, get_signature_type,
bound_cs_function_symbol, get_signature_version, alloc_stack_frame, get_signature_type, set_args_context,
} from "./marshal";
import { mono_wasm_new_external_root, mono_wasm_new_root } from "./roots";
import { monoStringToString } from "./strings";
@ -356,8 +356,9 @@ export function invoke_method_and_handle_exception(method: MonoMethod, args: JSM
assert_bindings();
const fail_root = mono_wasm_new_root<MonoString>();
try {
set_args_context(args);
const fail = cwraps.mono_wasm_invoke_method_bound(method, args, fail_root.address);
if (fail) throw new Error("ERR24: Unexpected error: " + monoStringToString(fail_root));
if (fail) runtimeHelpers.abort("ERR24: Unexpected error: " + monoStringToString(fail_root));
if (is_args_exception(args)) {
const exc = get_arg(args, 0);
throw marshal_exception_to_js(exc);
@ -368,6 +369,18 @@ export function invoke_method_and_handle_exception(method: MonoMethod, args: JSM
}
}
export function invoke_method_raw(method: MonoMethod): void {
assert_bindings();
const fail_root = mono_wasm_new_root<MonoString>();
try {
const fail = cwraps.mono_wasm_invoke_method_raw(method, fail_root.address);
if (fail) runtimeHelpers.abort("ERR24: Unexpected error: " + monoStringToString(fail_root));
}
finally {
fail_root.release();
}
}
export const exportsByAssembly: Map<string, any> = new Map();
function _walk_exports_to_set_function(assembly: string, namespace: string, classname: string, methodname: string, signature_hash: number, fn: Function): void {
const parts = `${namespace}.${classname}`.replace(/\//g, ".").split(".");

View File

@ -7,7 +7,7 @@ import { GCHandle, MarshalerToCs, MarshalerToJs, MarshalerType, MonoMethod } fro
import cwraps from "./cwraps";
import { runtimeHelpers, Module, loaderHelpers, mono_assert } from "./globals";
import { alloc_stack_frame, get_arg, set_arg_type, set_gc_handle } from "./marshal";
import { invoke_method_and_handle_exception } from "./invoke-cs";
import { invoke_method_and_handle_exception, invoke_method_raw } 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, end_marshal_task_to_js, marshal_string_to_js, begin_marshal_task_to_js } from "./marshal-to-js";
import { do_not_force_dispose } from "./gc-handles";
@ -24,8 +24,8 @@ export function init_managed_exports(): void {
if (!runtimeHelpers.runtime_interop_exports_class)
throw "Can't find " + runtimeHelpers.runtime_interop_namespace + "." + runtimeHelpers.runtime_interop_exports_classname + " class";
const install_sync_context = MonoWasmThreads ? get_method("InstallSynchronizationContext") : undefined;
mono_assert(!MonoWasmThreads || install_sync_context, "Can't find InstallSynchronizationContext method");
const install_main_synchronization_context = MonoWasmThreads ? get_method("InstallMainSynchronizationContext") : undefined;
mono_assert(!MonoWasmThreads || install_main_synchronization_context, "Can't find InstallMainSynchronizationContext method");
const call_entry_point = get_method("CallEntrypoint");
mono_assert(call_entry_point, "Can't find CallEntrypoint method");
const release_js_owned_object_by_gc_handle_method = get_method("ReleaseJSOwnedObjectByGCHandle");
@ -188,17 +188,8 @@ export function init_managed_exports(): void {
Module.stackRestore(sp);
}
};
if (MonoWasmThreads && install_sync_context) {
runtimeHelpers.javaScriptExports.install_synchronization_context = () => {
const sp = Module.stackSave();
try {
const args = alloc_stack_frame(2);
invoke_method_and_handle_exception(install_sync_context, args);
} finally {
Module.stackRestore(sp);
}
};
if (MonoWasmThreads && install_main_synchronization_context) {
runtimeHelpers.javaScriptExports.install_main_synchronization_context = () => invoke_method_raw(install_main_synchronization_context);
}
}

View File

@ -337,10 +337,14 @@ function _marshal_task_to_cs(arg: JSMarshalerArgument, value: Promise<any>, _?:
}
try {
mono_assert(!holder.isDisposed, "This promise can't be propagated to managed code, because the Task was already freed.");
if (MonoWasmThreads)
if (MonoWasmThreads) {
settleUnsettledPromise();
}
// we can unregister the GC handle on JS side
teardown_managed_proxy(holder, gc_handle, true);
// order of operations with teardown_managed_proxy matters
// so that managed user code running in the continuation could allocate the same GCHandle number and the local registry would be already ok with that
runtimeHelpers.javaScriptExports.complete_task(gc_handle, null, data, res_converter || _marshal_cs_object_to_cs);
teardown_managed_proxy(holder, gc_handle, true); // this holds holder alive for finalizer, until the promise is freed, (holding promise instead would not work)
}
catch (ex) {
runtimeHelpers.abort(ex);
@ -352,10 +356,13 @@ function _marshal_task_to_cs(arg: JSMarshalerArgument, value: Promise<any>, _?:
}
try {
mono_assert(!holder.isDisposed, "This promise can't be propagated to managed code, because the Task was already freed.");
if (MonoWasmThreads)
if (MonoWasmThreads) {
settleUnsettledPromise();
}
// we can unregister the GC handle on JS side
teardown_managed_proxy(holder, gc_handle, true);
// order of operations with teardown_managed_proxy matters
runtimeHelpers.javaScriptExports.complete_task(gc_handle, reason, null, undefined);
teardown_managed_proxy(holder, gc_handle, true); // this holds holder alive for finalizer, until the promise is freed
}
catch (ex) {
runtimeHelpers.abort(ex);

View File

@ -265,7 +265,7 @@ export function end_marshal_task_to_js(args: JSMarshalerArguments, res_converter
}
// otherwise drop the eagerPromise's handle
const js_handle = get_arg_js_handle(res);
const js_handle = mono_wasm_get_js_handle(eagerPromise);
mono_wasm_release_cs_owned_object(js_handle);
// get the synchronous result

View File

@ -18,7 +18,7 @@ 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");
export const JavaScriptMarshalerArgSize = 16;
export const JavaScriptMarshalerArgSize = 32;
export const JSMarshalerTypeSize = 32;
export const JSMarshalerSignatureHeaderSize = 4 * 8; // without Exception and Result
@ -26,6 +26,7 @@ export function alloc_stack_frame(size: number): JSMarshalerArguments {
const bytes = JavaScriptMarshalerArgSize * size;
const args = Module.stackAlloc(bytes) as any;
_zero_region(args, bytes);
set_args_context(args);
return args;
}
@ -40,6 +41,15 @@ export function is_args_exception(args: JSMarshalerArguments): boolean {
return exceptionType !== MarshalerType.None;
}
export function set_args_context(args: JSMarshalerArguments): void {
if (!MonoWasmThreads) return;
mono_assert(args, "Null args");
const exc = get_arg(args, 0);
const res = get_arg(args, 1);
set_arg_proxy_context(exc);
set_arg_proxy_context(res);
}
export function get_sig(signature: JSFunctionSignature, index: number): JSMarshalerType {
mono_assert(signature, "Null signatures");
return <any>signature + (index * JSMarshalerTypeSize) + JSMarshalerSignatureHeaderSize;
@ -252,9 +262,16 @@ export function get_arg_js_handle(arg: JSMarshalerArgument): JSHandle {
return <any>getI32(<any>arg + 4);
}
export function set_arg_proxy_context(arg: JSMarshalerArgument): void {
if (!MonoWasmThreads) return;
mono_assert(arg, "Null arg");
setI32(<any>arg + 16, <any>runtimeHelpers.proxy_context_gc_handle);
}
export function set_js_handle(arg: JSMarshalerArgument, jsHandle: JSHandle): void {
mono_assert(arg, "Null arg");
setI32(<any>arg + 4, <any>jsHandle);
set_arg_proxy_context(arg);
}
export function get_arg_gc_handle(arg: JSMarshalerArgument): GCHandle {
@ -265,6 +282,7 @@ export function get_arg_gc_handle(arg: JSMarshalerArgument): GCHandle {
export function set_gc_handle(arg: JSMarshalerArgument, gcHandle: GCHandle): void {
mono_assert(arg, "Null arg");
setI32(<any>arg + 4, <any>gcHandle);
set_arg_proxy_context(arg);
}
export function get_string_root(arg: JSMarshalerArgument): WasmRoot<MonoString> {
@ -331,7 +349,7 @@ export class ManagedError extends Error implements IDisposable {
if (this.managed_stack) {
return this.managed_stack;
}
if (loaderHelpers.is_runtime_running() && (!MonoWasmThreads || runtimeHelpers.jsSynchronizationContextInstalled)) {
if (loaderHelpers.is_runtime_running() && (!MonoWasmThreads || runtimeHelpers.proxy_context_gc_handle)) {
const gc_handle = (<any>this)[js_owned_gc_handle_symbol];
if (gc_handle !== GCHandleNull) {
const managed_stack = runtimeHelpers.javaScriptExports.get_managed_stack_trace(gc_handle);

View File

@ -11,6 +11,7 @@ import { mono_log_debug } from "../../logging";
import { bindings_init } from "../../startup";
import { forceDisposeProxies } from "../../gc-handles";
import { pthread_self } from "../worker";
import { GCHandle, GCHandleNull } from "../../types/internal";
export interface PThreadInfo {
readonly pthreadId: pthreadPtr;
@ -166,11 +167,11 @@ export function isMonoWorkerMessagePreload(message: MonoWorkerMessage): message
return false;
}
export function mono_wasm_install_js_worker_interop(): void {
export function mono_wasm_install_js_worker_interop(context_gc_handle: GCHandle): void {
if (!MonoWasmThreads) return;
bindings_init();
if (!runtimeHelpers.jsSynchronizationContextInstalled) {
runtimeHelpers.jsSynchronizationContextInstalled = true;
if (!runtimeHelpers.proxy_context_gc_handle) {
runtimeHelpers.proxy_context_gc_handle = context_gc_handle;
mono_log_debug("Installed JSSynchronizationContext");
}
Module.runtimeKeepalivePush();
@ -184,19 +185,19 @@ export function mono_wasm_install_js_worker_interop(): void {
export function mono_wasm_uninstall_js_worker_interop(): void {
if (!MonoWasmThreads) return;
mono_assert(runtimeHelpers.mono_wasm_bindings_is_ready, "JS interop is not installed on this worker.");
mono_assert(runtimeHelpers.jsSynchronizationContextInstalled, "JSSynchronizationContext is not installed on this worker.");
mono_assert(runtimeHelpers.proxy_context_gc_handle, "JSSynchronizationContext is not installed on this worker.");
forceDisposeProxies(true, runtimeHelpers.diagnosticTracing);
Module.runtimeKeepalivePop();
runtimeHelpers.jsSynchronizationContextInstalled = false;
runtimeHelpers.proxy_context_gc_handle = GCHandleNull;
runtimeHelpers.mono_wasm_bindings_is_ready = false;
set_thread_info(pthread_self ? pthread_self.pthreadId : 0, true, false, false);
}
export function assert_synchronization_context(): void {
if (MonoWasmThreads) {
mono_assert(runtimeHelpers.jsSynchronizationContextInstalled, "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");
mono_assert(runtimeHelpers.proxy_context_gc_handle, "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");
}
}

View File

@ -289,8 +289,7 @@ async function onRuntimeInitializedAsync(userOnRuntimeInitialized: () => void) {
}
if (MonoWasmThreads) {
runtimeHelpers.javaScriptExports.install_synchronization_context();
runtimeHelpers.jsSynchronizationContextInstalled = true;
runtimeHelpers.javaScriptExports.install_main_synchronization_context();
}
if (!runtimeHelpers.mono_wasm_runtime_is_ready) mono_wasm_runtime_ready();

View File

@ -194,7 +194,7 @@ export type RuntimeHelpers = {
getMemory(): WebAssembly.Memory,
getWasmIndirectFunctionTable(): WebAssembly.Table,
runtimeReady: boolean,
jsSynchronizationContextInstalled: boolean,
proxy_context_gc_handle: GCHandle,
cspPolicy: boolean,
allAssetsInMemory: PromiseAndController<void>,
@ -348,8 +348,8 @@ export interface JavaScriptExports {
// the marshaled signature is: Task<int>? CallEntrypoint(MonoMethod* entrypointPtr, string[] args)
call_entry_point(entry_point: MonoMethod, args?: string[]): Promise<number>;
// the marshaled signature is: void InstallSynchronizationContext()
install_synchronization_context(): void;
// the marshaled signature is: void InstallMainSynchronizationContext()
install_main_synchronization_context(): void;
// the marshaled signature is: string GetManagedStackTrace(GCHandle exception)
get_managed_stack_trace(exception_gc_handle: GCHandle): string | null

View File

@ -13,6 +13,7 @@ import { mono_log_warn } from "./logging";
import { viewOrCopy, utf8ToStringRelaxed, stringToUTF8 } from "./strings";
import { IDisposable } from "./marshal";
import { wrap_as_cancelable } from "./cancelable-promise";
import { assert_synchronization_context } from "./pthreads/shared";
const wasm_ws_pending_send_buffer = Symbol.for("wasm ws_pending_send_buffer");
const wasm_ws_pending_send_buffer_offset = Symbol.for("wasm ws_pending_send_buffer_offset");
@ -44,6 +45,7 @@ function verifyEnvironment() {
export function ws_wasm_create(uri: string, sub_protocols: string[] | null, receive_status_ptr: VoidPtr, onClosed: (code: number, reason: string) => void): WebSocketExtension {
verifyEnvironment();
assert_synchronization_context();
mono_assert(uri && typeof uri === "string", () => `ERR12: Invalid uri ${typeof uri}`);
mono_assert(typeof onClosed === "function", () => `ERR12: Invalid onClosed ${typeof onClosed}`);

View File

@ -28,3 +28,5 @@ yarn-error.log*
.env.development.local
.env.test.local
.env.production.local
public