Skip to content

Commit

Permalink
[browser] JSImport binding improvements (dotnet#95177)
Browse files Browse the repository at this point in the history
Co-authored-by: Marek Fišera <mara@neptuo.com>
  • Loading branch information
pavelsavara and maraf authored Nov 27, 2023
1 parent 3628544 commit f38242b
Show file tree
Hide file tree
Showing 18 changed files with 290 additions and 196 deletions.
11 changes: 6 additions & 5 deletions src/libraries/Common/src/Interop/Browser/Interop.Runtime.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,12 @@ internal static unsafe partial class Runtime
[MethodImplAttribute(MethodImplOptions.InternalCall)]
internal static extern void ReleaseCSOwnedObject(IntPtr jsHandle);
[MethodImpl(MethodImplOptions.InternalCall)]
public static extern unsafe void BindJSFunction(in string function_name, in string module_name, void* signature, out IntPtr bound_function_js_handle, out int is_exception, out object result);
public static extern unsafe void BindJSImport(void* signature, out int is_exception, out object result);
[MethodImpl(MethodImplOptions.InternalCall)]
public static extern void InvokeJSFunction(IntPtr bound_function_js_handle, void* data);
public static extern void InvokeJSFunction(int functionHandle, void* data);
[MethodImpl(MethodImplOptions.InternalCall)]
public static extern void InvokeImport(IntPtr fn_handle, void* data);
public static extern void InvokeJSImport(int importHandle, void* data);

[MethodImpl(MethodImplOptions.InternalCall)]
public static extern unsafe void BindCSFunction(in string fully_qualified_name, int signature_hash, void* signature, out int is_exception, out object result);
[MethodImpl(MethodImplOptions.InternalCall)]
Expand All @@ -30,9 +31,9 @@ internal static unsafe partial class Runtime

#if FEATURE_WASM_THREADS
[MethodImpl(MethodImplOptions.InternalCall)]
public static extern void InstallWebWorkerInterop(bool installJSSynchronizationContext);
public static extern void InstallWebWorkerInterop();
[MethodImpl(MethodImplOptions.InternalCall)]
public static extern void UninstallWebWorkerInterop(bool uninstallJSSynchronizationContext);
public static extern void UninstallWebWorkerInterop();
#endif

#region Legacy
Expand Down
1 change: 1 addition & 0 deletions src/libraries/System.Net.Http/src/System.Net.Http.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
<DefineConstants Condition="'$(TargetPlatformIdentifier)' == 'browser'">$(DefineConstants);TARGET_BROWSER</DefineConstants>
<DefineConstants Condition="'$(TargetPlatformIdentifier)' == 'wasi'">$(DefineConstants);TARGET_WASI</DefineConstants>
<DefineConstants Condition="'$(FeatureWasmThreads)' == 'true'" >$(DefineConstants);FEATURE_WASM_THREADS</DefineConstants>
<EmitCompilerGeneratedFiles Condition="'$(Configuration)' == 'Debug' and '$(TargetPlatformIdentifier)' == 'browser'">true</EmitCompilerGeneratedFiles>
</PropertyGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
<FeatureWasmThreads Condition="'$(TargetPlatformIdentifier)' == 'browser' and '$(MonoWasmBuildVariant)' == 'multithread'">true</FeatureWasmThreads>
<DefineConstants Condition="'$(TargetPlatformIdentifier)' == 'browser'">$(DefineConstants);TARGET_BROWSER</DefineConstants>
<DefineConstants Condition="'$(FeatureWasmThreads)' == 'true'" >$(DefineConstants);FEATURE_WASM_THREADS</DefineConstants>
<EmitCompilerGeneratedFiles Condition="'$(Configuration)' == 'Debug' and '$(TargetPlatformIdentifier)' == 'browser'">true</EmitCompilerGeneratedFiles>
</PropertyGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
<WasmEnableLegacyJsInterop Condition="'$(TargetPlatformIdentifier)' == 'browser' and '$(WasmEnableLegacyJsInterop)' == ''">true</WasmEnableLegacyJsInterop>
<DefineConstants Condition="'$(FeatureWasmThreads)' == 'true'" >$(DefineConstants);FEATURE_WASM_THREADS</DefineConstants>
<DefineConstants Condition="'$(WasmEnableLegacyJsInterop)' == 'false'" >$(DefineConstants);DISABLE_LEGACY_JS_INTEROP</DefineConstants>
<EmitCompilerGeneratedFiles Condition="'$(Configuration)' == 'Debug' and '$(TargetPlatformIdentifier)' == 'browser'">true</EmitCompilerGeneratedFiles>
</PropertyGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,7 @@ public static void InstallSynchronizationContext (JSMarshalerArgument* arguments
ref JSMarshalerArgument arg_exc = ref arguments_buffer[0]; // initialized by caller in alloc_stack_frame()
try
{
InstallWebWorkerInterop(true, true);
InstallWebWorkerInterop(true);
}
catch (Exception ex)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using System.Threading;

namespace System.Runtime.InteropServices.JavaScript
{
Expand All @@ -23,17 +24,29 @@ public sealed class JSFunctionBinding
internal JSFunctionBinding() { }

#region intentionally opaque internal structure

internal unsafe JSBindingHeader* Header;
internal unsafe JSBindingType* Sigs;// points to first arg, not exception, not result
internal IntPtr FnHandle;
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
{
internal const int JSMarshalerSignatureHeaderSize = 4 + 4; // without Exception and Result
internal const int JSMarshalerSignatureHeaderSize = 4 * 8; // without Exception and Result

public int Version;
public int ArgumentCount;
public int ImportHandle;
public int _Reserved;
public int FunctionNameOffset;
public int FunctionNameLength;
public int ModuleNameOffset;
public int ModuleNameLength;
public JSBindingType Exception;
public JSBindingType Result;
}
Expand Down Expand Up @@ -143,7 +156,7 @@ internal unsafe JSBindingType this[int position]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void InvokeJS(JSFunctionBinding signature, Span<JSMarshalerArgument> arguments)
{
InvokeImportImpl(signature.FnHandle, arguments);
InvokeJSImportImpl(signature, arguments);
}

/// <summary>
Expand Down Expand Up @@ -192,10 +205,10 @@ internal static unsafe void InvokeJSImpl(JSObject jsFunction, Span<JSMarshalerAr
JSObject.AssertThreadAffinity(jsFunction);
#endif

IntPtr functionJSHandle = jsFunction.JSHandle;
var functionHandle = (int)jsFunction.JSHandle;
fixed (JSMarshalerArgument* ptr = arguments)
{
Interop.Runtime.InvokeJSFunction(functionJSHandle, ptr);
Interop.Runtime.InvokeJSFunction(functionHandle, ptr);
ref JSMarshalerArgument exceptionArg = ref arguments[0];
if (exceptionArg.slot.Type != MarshalerType.None)
{
Expand All @@ -205,11 +218,11 @@ internal static unsafe void InvokeJSImpl(JSObject jsFunction, Span<JSMarshalerAr
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static unsafe void InvokeImportImpl(IntPtr fnHandle, Span<JSMarshalerArgument> arguments)
internal static unsafe void InvokeJSImportImpl(JSFunctionBinding signature, Span<JSMarshalerArgument> arguments)
{
fixed (JSMarshalerArgument* ptr = arguments)
{
Interop.Runtime.InvokeImport(fnHandle, ptr);
Interop.Runtime.InvokeJSImport(signature.ImportHandle, ptr);
ref JSMarshalerArgument exceptionArg = ref arguments[0];
if (exceptionArg.slot.Type != MarshalerType.None)
{
Expand All @@ -224,22 +237,20 @@ internal static unsafe JSFunctionBinding BindJSFunctionImpl(string functionName,
JSSynchronizationContext.AssertWebWorkerContext();
#endif

var signature = JSHostImplementation.GetMethodSignature(signatures);
var signature = JSHostImplementation.GetMethodSignature(signatures, functionName, moduleName);

Interop.Runtime.BindJSFunction(functionName, moduleName, signature.Header, out IntPtr jsFunctionHandle, out int isException, out object exceptionMessage);
Interop.Runtime.BindJSImport(signature.Header, out int isException, out object exceptionMessage);
if (isException != 0)
throw new JSException((string)exceptionMessage);

signature.FnHandle = jsFunctionHandle;

JSHostImplementation.FreeMethodSignatureBuffer(signature);

return signature;
}

internal static unsafe JSFunctionBinding BindManagedFunctionImpl(string fullyQualifiedName, int signatureHash, ReadOnlySpan<JSMarshalerType> signatures)
{
var signature = JSHostImplementation.GetMethodSignature(signatures);
var signature = JSHostImplementation.GetMethodSignature(signatures, null, null);

Interop.Runtime.BindCSFunction(fullyQualifiedName, signatureHash, signature.Header, out int isException, out object exceptionMessage);
if (isException != 0)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -237,10 +237,30 @@ public static async Task<JSObject> CancelationHelper(Task<JSObject> jsTask, Canc
}

// res type is first argument
public static unsafe JSFunctionBinding GetMethodSignature(ReadOnlySpan<JSMarshalerType> types)
public static unsafe JSFunctionBinding GetMethodSignature(ReadOnlySpan<JSMarshalerType> types, string? functionName, string? moduleName)
{
int argsCount = types.Length - 1;
int size = JSFunctionBinding.JSBindingHeader.JSMarshalerSignatureHeaderSize + ((argsCount + 2) * sizeof(JSFunctionBinding.JSBindingType));

int functionNameBytes = 0;
int functionNameOffset = 0;
if (functionName != null)
{
functionNameOffset = size;
size += 4;
functionNameBytes = functionName.Length * 2;
size += functionNameBytes;
}
int moduleNameBytes = 0;
int moduleNameOffset = 0;
if (moduleName != null)
{
moduleNameOffset = size;
size += 4;
moduleNameBytes = moduleName.Length * 2;
size += moduleNameBytes;
}

// this is never unallocated
IntPtr buffer = Marshal.AllocHGlobal(size);

Expand All @@ -254,9 +274,44 @@ public static unsafe JSFunctionBinding GetMethodSignature(ReadOnlySpan<JSMarshal
signature.ArgumentCount = argsCount;
signature.Exception = JSMarshalerType.Exception._signatureType;
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

for (int i = 0; i < argsCount; i++)
{
signature.Sigs[i] = types[i + 1]._signatureType;
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;

signature.Header[0].ImportHandle = signature.ImportHandle;
signature.Header[0].FunctionNameLength = functionNameBytes;
signature.Header[0].FunctionNameOffset = functionNameOffset;
signature.Header[0].ModuleNameLength = moduleNameBytes;
signature.Header[0].ModuleNameOffset = moduleNameOffset;
if (functionNameBytes != 0)
{
fixed (void* fn = functionName)
{
Unsafe.CopyBlock((byte*)buffer + functionNameOffset, fn, (uint)functionNameBytes);
}
}
if (moduleNameBytes != 0)
{
fixed (void* mn = moduleName)
{
Unsafe.CopyBlock((byte*)buffer + moduleNameOffset, mn, (uint)moduleNameBytes);
}

}

return signature;
Expand Down Expand Up @@ -302,30 +357,27 @@ public static void LoadSatelliteAssembly(byte[] dllBytes)
}

#if FEATURE_WASM_THREADS
public static void InstallWebWorkerInterop(bool installJSSynchronizationContext, bool isMainThread)
public static void InstallWebWorkerInterop(bool isMainThread)
{
Interop.Runtime.InstallWebWorkerInterop(installJSSynchronizationContext);
if (installJSSynchronizationContext)
Interop.Runtime.InstallWebWorkerInterop();
var currentTID = GetNativeThreadId();
var ctx = JSSynchronizationContext.CurrentJSSynchronizationContext;
if (ctx == null)
{
var currentThreadId = GetNativeThreadId();
var ctx = JSSynchronizationContext.CurrentJSSynchronizationContext;
if (ctx == null)
ctx = new JSSynchronizationContext(Thread.CurrentThread, currentTID);
ctx.previousSynchronizationContext = SynchronizationContext.Current;
JSSynchronizationContext.CurrentJSSynchronizationContext = ctx;
SynchronizationContext.SetSynchronizationContext(ctx);
if (isMainThread)
{
ctx = new JSSynchronizationContext(Thread.CurrentThread, currentThreadId);
ctx.previousSynchronizationContext = SynchronizationContext.Current;
JSSynchronizationContext.CurrentJSSynchronizationContext = ctx;
SynchronizationContext.SetSynchronizationContext(ctx);
if (isMainThread)
{
JSSynchronizationContext.MainJSSynchronizationContext = ctx;
}
JSSynchronizationContext.MainJSSynchronizationContext = ctx;
}
else if (ctx.TargetThreadId != currentThreadId)
{
Environment.FailFast($"JSSynchronizationContext.Install failed has wrong native thread id {ctx.TargetThreadId} != {currentThreadId}");
}
ctx.AwaitNewData();
}
else if (ctx.TargetTID != currentTID)
{
Environment.FailFast($"JSSynchronizationContext.Install has wrong native thread id {ctx.TargetTID} != {currentTID}");
}
ctx.AwaitNewData();
}

public static void UninstallWebWorkerInterop()
Expand Down Expand Up @@ -364,7 +416,7 @@ public static void UninstallWebWorkerInterop()
}
}

Interop.Runtime.UninstallWebWorkerInterop(uninstallJSSynchronizationContext);
Interop.Runtime.UninstallWebWorkerInterop();

if (uninstallJSSynchronizationContext)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
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;

namespace System.Runtime.InteropServices.JavaScript
{
Expand All @@ -21,7 +23,7 @@ internal sealed class JSSynchronizationContext : SynchronizationContext
{
private readonly Action _DataIsAvailable;// don't allocate Action on each call to UnsafeOnCompleted
public readonly Thread TargetThread;
public readonly IntPtr TargetThreadId;
public readonly IntPtr TargetTID;
private readonly WorkItemQueueType Queue;

internal static JSSynchronizationContext? MainJSSynchronizationContext;
Expand Down Expand Up @@ -65,17 +67,17 @@ internal static void AssertWebWorkerContext()
#endif
}

private JSSynchronizationContext(Thread targetThread, IntPtr targetThreadId, WorkItemQueueType queue)
private JSSynchronizationContext(Thread targetThread, IntPtr targetTID, WorkItemQueueType queue)
{
TargetThread = targetThread;
TargetThreadId = targetThreadId;
TargetTID = targetTID;
Queue = queue;
_DataIsAvailable = DataIsAvailable;
}

public override SynchronizationContext CreateCopy()
{
return new JSSynchronizationContext(TargetThread, TargetThreadId, Queue);
return new JSSynchronizationContext(TargetThread, TargetTID, Queue);
}

internal void AwaitNewData()
Expand All @@ -101,7 +103,7 @@ private unsafe void DataIsAvailable()
{
// 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(TargetThreadId, (void*)(delegate* unmanaged[Cdecl]<void>)&BackgroundJobHandler);
TargetThreadScheduleBackgroundJob(TargetTID, (void*)(delegate* unmanaged[Cdecl]<void>)&BackgroundJobHandler);
}

public override void Post(SendOrPostCallback d, object? state)
Expand All @@ -110,7 +112,7 @@ public override void Post(SendOrPostCallback d, object? state)

var workItem = new WorkItem(d, state, null);
if (!Queue.Writer.TryWrite(workItem))
throw new Exception("Internal error");
Environment.FailFast("JSSynchronizationContext.Post failed");
}

// This path can only run when threading is enabled
Expand All @@ -130,14 +132,14 @@ public override void Send(SendOrPostCallback d, object? state)
{
var workItem = new WorkItem(d, state, signal);
if (!Queue.Writer.TryWrite(workItem))
throw new Exception("Internal error");
Environment.FailFast("JSSynchronizationContext.Send failed");

signal.Wait();
}
}

[MethodImplAttribute(MethodImplOptions.InternalCall)]
internal static extern unsafe void TargetThreadScheduleBackgroundJob(IntPtr targetThread, void* callback);
internal static extern unsafe void TargetThreadScheduleBackgroundJob(IntPtr targetTID, void* callback);

#pragma warning disable CS3016 // Arrays as attribute arguments is not CLS-compliant
[UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })]
Expand Down
Loading

0 comments on commit f38242b

Please sign in to comment.