-
Notifications
You must be signed in to change notification settings - Fork 5.2k
Labels
api-approvedAPI was approved in API review, it can be implementedAPI was approved in API review, it can be implementedarch-wasmWebAssembly architectureWebAssembly architecturearea-System.Runtime.InteropServices.JavaScriptblockingMarks issues that we want to fast track in order to unblock other important workMarks issues that we want to fast track in order to unblock other important work
Milestone
Description
Background and motivation
When .NET is running on WebAssembly, for example as part of Blazor, developers may want to interact with the browser's JavaScript engine and JS code. Currently we don't have public C# API to do so.
We propose this API together with prototype of the implementation.
Key features are:
- generate C# side of the marshaling stub in as partial method, Roslyn analyzer triggered by
JSImportAttributeorJSExportAttribute. We re-use common code gen infrastructure from[LibraryImport] - allow different marshalers for the same managed type, for example Int64 could be marshaled as
JSType.BigIntor asJSType.Number, configurable per parameter viaJSMarshalAsAttributesimilar toMarshalAsAttributeof P/Invoke - there is no way to create a JS instance solely by manipulations WASM memory. The marshaling depends on a library of JS helper routines to create JS values and manipulate JS object properties. That's why we need to generate JS code too.
- generate JS side of the marshaling on runtime, to decrease download size. Provide necessary metadata during method binding.
- marshaled types are:
- subset of primitive numeric types and their nullable alternative
String,Boolean,DateTime,DateTimeOffset,Exception- dynamic marshaling of
System.Objectwith mapping to well known types for some instance types and proxy viaGCHandlefor the rest. JSObjectwith private legacy implementationJSObject, which is proxy via existingJSHandleconcept similar toGCHandleTask,Func,Actionbyte[],int[],double[]Span<byte>,Span<int>,Span<double>andArraySegment<byte>,ArraySegment<int>,ArraySegment<double>- Custom P/Invoke marshaler with
[MarshalUsing(typeof(NativeMarshaler))]
- we have 2 garbage collectors to worry about
- we do have existing private interop in
System.Private.Runtime.InteropServices.JavaScriptassembly and also semi-private JavaScript embedding API. These are used by Blazor and other partners and this proposal could help to phase it out gradually.
There more implementation details described on the prototype PR
API Proposal
Below are types which drive the code generator
namespace System.Runtime.InteropServices.JavaScript;
[System.AttributeUsageAttribute(System.AttributeTargets.Method, Inherited = false, AllowMultiple = false)]
[Versioning.SupportedOSPlatform("browser")]
public sealed class JSImportAttribute : System.Attribute
{
public string FunctionName { get; }
public string ModuleName { get; }
public JSImportAttribute(string functionName) => throw null;
public JSImportAttribute(string functionName, string moduleName) => throw null;
}
[System.AttributeUsageAttribute(System.AttributeTargets.Method, Inherited = false, AllowMultiple = false)]
[Versioning.SupportedOSPlatform("browser")]
public sealed class JSExportAttribute : System.Attribute
{
public JSExportAttribute() => throw null;
}
// this is used to annotate the marshaled parameters
[System.AttributeUsageAttribute(System.AttributeTargets.Parameter | System.AttributeTargets.ReturnValue, Inherited = false, AllowMultiple = false)]
[Versioning.SupportedOSPlatform("browser")]
public sealed class JSMarshalAsAttribute<T> : System.Attribute where T : JSType
{
public JSMarshalAsAttribute() => throw null;
}
[Versioning.SupportedOSPlatform("browser")]
public abstract class JSType
{
internal JSType() => throw null;
public sealed class None : JSType
{
internal None() => throw null;
}
public sealed class Void : JSType
{
internal Void() => throw null;
}
public sealed class Discard : JSType
{
internal Discard() => throw null;
}
public sealed class Boolean : JSType
{
internal Boolean() => throw null;
}
public sealed class Number : JSType
{
internal Number() => throw null;
}
public sealed class BigInt : JSType
{
internal BigInt() => throw null;
}
public sealed class Date : JSType
{
internal Date() => throw null;
}
public sealed class String : JSType
{
internal String() => throw null;
}
public sealed class Object : JSType
{
internal Object() => throw null;
}
public sealed class Error : JSType
{
internal Error() => throw null;
}
public sealed class MemoryView : JSType
{
internal MemoryView() => throw null;
}
public sealed class Array<T> : JSType where T : JSType
{
internal Array() => throw null;
}
public sealed class Promise<T> : JSType where T : JSType
{
internal Promise() => throw null;
}
public sealed class Function : JSType
{
internal Function() => throw null;
}
public sealed class Function<T> : JSType where T : JSType
{
internal Function() => throw null;
}
public sealed class Function<T1, T2> : JSType where T1 : JSType where T2 : JSType
{
internal Function() => throw null;
}
public sealed class Function<T1, T2, T3> : JSType where T1 : JSType where T2 : JSType where T3 : JSType
{
internal Function() => throw null;
}
public sealed class Function<T1, T2, T3, T4> : JSType where T1 : JSType where T2 : JSType where T3 : JSType where T4 : JSType
{
internal Function() => throw null;
}
public sealed class Any : JSType
{
internal Any() => throw null;
}
}Below are types for working with JavaScript instances
namespace System.Runtime.InteropServices.JavaScript;
[Versioning.SupportedOSPlatform("browser")]
public class JSObject : System.IDisposable
{
internal JSObject() => throw null;
public bool IsDisposed { get => throw null; }
public void Dispose() => throw null;
public bool HasProperty(string propertyName) => throw null;
public string GetTypeOfProperty(string propertyName) => throw null;
public bool GetPropertyAsBoolean(string propertyName) => throw null;
public int GetPropertyAsInt32(string propertyName) => throw null;
public double GetPropertyAsDouble(string propertyName) => throw null;
public string? GetPropertyAsString(string propertyName) => throw null;
public JSObject? GetPropertyAsJSObject(string propertyName) => throw null;
public byte[]? GetPropertyAsByteArray(string propertyName) => throw null;
public void SetProperty(string propertyName, bool value) => throw null;
public void SetProperty(string propertyName, int value) => throw null;
public void SetProperty(string propertyName, double value) => throw null;
public void SetProperty(string propertyName, string? value) => throw null;
public void SetProperty(string propertyName, JSObject? value) => throw null;
public void SetProperty(string propertyName, byte[]? value) => throw null;
}
// when we marshal JS Error type
[Versioning.SupportedOSPlatform("browser")]
public sealed class JSException : System.Exception
{
public JSException(string msg) => throw null;
}
[Versioning.SupportedOSPlatform("browser")]
public static class JSHost
{
public static JSObject GlobalThis { get => throw null; }
public static JSObject DotnetInstance { get => throw null; }
public static System.Threading.Tasks.Task<JSObject> Import(string moduleName, string moduleUrl) => throw null;
}Below types are used by the generated code
[Versioning.SupportedOSPlatform("browser")]
[CLSCompliant(false)]
[System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)]
// to bind and call methods
public sealed class JSFunctionBinding
{
public static void InvokeJS(JSFunctionBinding signature, Span<JSMarshalerArgument> arguments) => throw null;
public static JSFunctionBinding BindJSFunction(string functionName, string moduleName, System.ReadOnlySpan<JSMarshalerType> signatures) => throw null;
public static JSFunctionBinding BindCSFunction(string fullyQualifiedName, int signatureHash, System.ReadOnlySpan<JSMarshalerType> signatures) => throw null;
}
[Versioning.SupportedOSPlatform("browser")]
[System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)]
// to create binding metadata
public sealed class JSMarshalerType
{
private JSMarshalerType() => throw null;
public static JSMarshalerType Void { get => throw null; }
public static JSMarshalerType Discard { get => throw null; }
public static JSMarshalerType Boolean { get => throw null; }
public static JSMarshalerType Byte { get => throw null; }
public static JSMarshalerType Char { get => throw null; }
public static JSMarshalerType Int16 { get => throw null; }
public static JSMarshalerType Int32 { get => throw null; }
public static JSMarshalerType Int52 { get => throw null; }
public static JSMarshalerType BigInt64 { get => throw null; }
public static JSMarshalerType Double { get => throw null; }
public static JSMarshalerType Single { get => throw null; }
public static JSMarshalerType IntPtr { get => throw null; }
public static JSMarshalerType JSObject { get => throw null; }
public static JSMarshalerType Object { get => throw null; }
public static JSMarshalerType String { get => throw null; }
public static JSMarshalerType Exception { get => throw null; }
public static JSMarshalerType DateTime { get => throw null; }
public static JSMarshalerType DateTimeOffset { get => throw null; }
public static JSMarshalerType Nullable(JSMarshalerType primitive) => throw null;
public static JSMarshalerType Task() => throw null;
public static JSMarshalerType Task(JSMarshalerType result) => throw null;
public static JSMarshalerType Array(JSMarshalerType element) => throw null;
public static JSMarshalerType ArraySegment(JSMarshalerType element) => throw null;
public static JSMarshalerType Span(JSMarshalerType element) => throw null;
public static JSMarshalerType Action() => throw null;
public static JSMarshalerType Action(JSMarshalerType arg1) => throw null;
public static JSMarshalerType Action(JSMarshalerType arg1, JSMarshalerType arg2) => throw null;
public static JSMarshalerType Action(JSMarshalerType arg1, JSMarshalerType arg2, JSMarshalerType arg3) => throw null;
public static JSMarshalerType Function(JSMarshalerType result) => throw null;
public static JSMarshalerType Function(JSMarshalerType arg1, JSMarshalerType result) => throw null;
public static JSMarshalerType Function(JSMarshalerType arg1, JSMarshalerType arg2, JSMarshalerType result) => throw null;
public static JSMarshalerType Function(JSMarshalerType arg1, JSMarshalerType arg2, JSMarshalerType arg3, JSMarshalerType result) => throw null;
}
[Versioning.SupportedOSPlatform("browser")]
[CLSCompliant(false)]
[System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)]
// actual marshalers
public struct JSMarshalerArgument
{
public delegate void ArgumentToManagedCallback<T>(ref JSMarshalerArgument arg, out T value);
public delegate void ArgumentToJSCallback<T>(ref JSMarshalerArgument arg, T value);
public void Initialize() => throw null;
public void ToManaged(out bool value) => throw null;
public void ToJS(bool value) => throw null;
public void ToManaged(out bool? value) => throw null;
public void ToJS(bool? value) => throw null;
public void ToManaged(out byte value) => throw null;
public void ToJS(byte value) => throw null;
public void ToManaged(out byte? value) => throw null;
public void ToJS(byte? value) => throw null;
public void ToManaged(out byte[]? value) => throw null;
public void ToJS(byte[]? value) => throw null;
public void ToManaged(out char value) => throw null;
public void ToJS(char value) => throw null;
public void ToManaged(out char? value) => throw null;
public void ToJS(char? value) => throw null;
public void ToManaged(out short value) => throw null;
public void ToJS(short value) => throw null;
public void ToManaged(out short? value) => throw null;
public void ToJS(short? value) => throw null;
public void ToManaged(out int value) => throw null;
public void ToJS(int value) => throw null;
public void ToManaged(out int? value) => throw null;
public void ToJS(int? value) => throw null;
public void ToManaged(out int[]? value) => throw null;
public void ToJS(int[]? value) => throw null;
public void ToManaged(out long value) => throw null;
public void ToJS(long value) => throw null;
public void ToManaged(out long? value) => throw null;
public void ToJS(long? value) => throw null;
public void ToManagedBig(out long value) => throw null;
public void ToJSBig(long value) => throw null;
public void ToManagedBig(out long? value) => throw null;
public void ToJSBig(long? value) => throw null;
public void ToManaged(out float value) => throw null;
public void ToJS(float value) => throw null;
public void ToManaged(out float? value) => throw null;
public void ToJS(float? value) => throw null;
public void ToManaged(out double value) => throw null;
public void ToJS(double value) => throw null;
public void ToManaged(out double? value) => throw null;
public void ToJS(double? value) => throw null;
public void ToManaged(out double[]? value) => throw null;
public void ToJS(double[]? value) => throw null;
public void ToManaged(out IntPtr value) => throw null;
public void ToJS(IntPtr value) => throw null;
public void ToManaged(out IntPtr? value) => throw null;
public void ToJS(IntPtr? value) => throw null;
public void ToManaged(out DateTimeOffset value) => throw null;
public void ToJS(DateTimeOffset value) => throw null;
public void ToManaged(out DateTimeOffset? value) => throw null;
public void ToJS(DateTimeOffset? value) => throw null;
public void ToManaged(out DateTime value) => throw null;
public void ToJS(DateTime value) => throw null;
public void ToManaged(out DateTime? value) => throw null;
public void ToJS(DateTime? value) => throw null;
public void ToManaged(out string? value) => throw null;
public void ToJS(string? value) => throw null;
public void ToManaged(out string?[]? value) => throw null;
public void ToJS(string?[]? value) => throw null;
public void ToManaged(out Exception? value) => throw null;
public void ToJS(Exception? value) => throw null;
public void ToManaged(out object? value) => throw null;
public void ToJS(object? value) => throw null;
public void ToManaged(out object?[]? value) => throw null;
public void ToJS(object?[]? value) => throw null;
public void ToManaged(out JSObject? value) => throw null;
public void ToJS(JSObject? value) => throw null;
public void ToManaged(out JSObject?[]? value) => throw null;
public void ToJS(JSObject?[]? value) => throw null;
public void ToManaged(out System.Threading.Tasks.Task? value) => throw null;
public void ToJS(System.Threading.Tasks.Task? value) => throw null;
public void ToManaged<T>(out System.Threading.Tasks.Task<T>? value, ArgumentToManagedCallback<T> marshaler) => throw null;
public void ToJS<T>(System.Threading.Tasks.Task<T>? value, ArgumentToJSCallback<T> marshaler) => throw null;
public void ToManaged(out Action? value) => throw null;
public void ToJS(Action? value) => throw null;
public void ToManaged<T>(out Action<T>? value, ArgumentToJSCallback<T> arg1Marshaler) => throw null;
public void ToJS<T>(Action<T>? value, ArgumentToManagedCallback<T> arg1Marshaler) => throw null;
public void ToManaged<T1, T2>(out Action<T1, T2>? value, ArgumentToJSCallback<T1> arg1Marshaler, ArgumentToJSCallback<T2> arg2Marshaler) => throw null;
public void ToJS<T1, T2>(Action<T1, T2>? value, ArgumentToManagedCallback<T1> arg1Marshaler, ArgumentToManagedCallback<T2> arg2Marshaler) => throw null;
public void ToManaged<T1, T2, T3>(out Action<T1, T2, T3>? value, ArgumentToJSCallback<T1> arg1Marshaler, ArgumentToJSCallback<T2> arg2Marshaler, ArgumentToJSCallback<T3> arg3Marshaler) => throw null;
public void ToJS<T1, T2, T3>(Action<T1, T2, T3>? value, ArgumentToManagedCallback<T1> arg1Marshaler, ArgumentToManagedCallback<T2> arg2Marshaler, ArgumentToManagedCallback<T3> arg3Marshaler) => throw null;
public void ToManaged<TResult>(out Func<TResult>? value, ArgumentToManagedCallback<TResult> resMarshaler) => throw null;
public void ToJS<TResult>(Func<TResult>? value, ArgumentToJSCallback<TResult> resMarshaler) => throw null;
public void ToManaged<T, TResult>(out Func<T, TResult>? value, ArgumentToJSCallback<T> arg1Marshaler, ArgumentToManagedCallback<TResult> resMarshaler) => throw null;
public void ToJS<T, TResult>(Func<T, TResult>? value, ArgumentToManagedCallback<T> arg1Marshaler, ArgumentToJSCallback<TResult> resMarshaler) => throw null;
public void ToManaged<T1, T2, TResult>(out Func<T1, T2, TResult>? value, ArgumentToJSCallback<T1> arg1Marshaler, ArgumentToJSCallback<T2> arg2Marshaler, ArgumentToManagedCallback<TResult> resMarshaler) => throw null;
public void ToJS<T1, T2, TResult>(Func<T1, T2, TResult>? value, ArgumentToManagedCallback<T1> arg1Marshaler, ArgumentToManagedCallback<T2> arg2Marshaler, ArgumentToJSCallback<TResult> resMarshaler) => throw null;
public void ToManaged<T1, T2, T3, TResult>(out Func<T1, T2, T3, TResult>? value, ArgumentToJSCallback<T1> arg1Marshaler, ArgumentToJSCallback<T2> arg2Marshaler, ArgumentToJSCallback<T3> arg3Marshaler, ArgumentToManagedCallback<TResult> resMarshaler) => throw null;
public void ToJS<T1, T2, T3, TResult>(Func<T1, T2, T3, TResult>? value, ArgumentToManagedCallback<T1> arg1Marshaler, ArgumentToManagedCallback<T2> arg2Marshaler, ArgumentToManagedCallback<T3> arg3Marshaler, ArgumentToJSCallback<TResult> resMarshaler) => throw null;
public unsafe void ToManaged(out void* value) => throw null;
public unsafe void ToJS(void* value) => throw null;
public void ToManaged(out Span<byte> value) => throw null;
public void ToJS(Span<byte> value) => throw null;
public void ToManaged(out ArraySegment<byte> value) => throw null;
public void ToJS(ArraySegment<byte> value) => throw null;
public void ToManaged(out Span<int> value) => throw null;
public void ToJS(Span<int> value) => throw null;
public void ToManaged(out Span<double> value) => throw null;
public void ToJS(Span<double> value) => throw null;
public void ToManaged(out ArraySegment<int> value) => throw null;
public void ToJS(ArraySegment<int> value) => throw null;
public void ToManaged(out ArraySegment<double> value) => throw null;
public void ToJS(ArraySegment<double> value) => throw null;
}API Usage
Trivial example
// here we bind to well known console.log on the blobal JS namespace
[JSImport("console.log")]
// there is no return value marshaling, but exception would be marshaled
internal static partial void Log(
// this one will marshal C# string to JavaScript native string by value (with some optimizations)
string message);This is code generated by Roslyn, simplified for brevity
[System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Interop.JavaScript.JSImportGenerator", "42.42.42.42")]
public static partial void Log(string message)
{
if (__signature_Log_20494476 == null)
{
__signature_Log_20494476 = JSFunctionBinding.BindJSFunction("console.log", null,
new JSMarshalerType[]{
JSMarshalerType.Discard,
JSMarshalerType.String});
}
System.Span<JSMarshalerArgument> __arguments_buffer = stackalloc JSMarshalerArgument[3];
ref JSMarshalerArgument __arg_exception = ref __arguments_buffer[0];
__arg_exception.Initialize();
ref JSMarshalerArgument __arg_return = ref __arguments_buffer[1];
__arg_return.Initialize();
ref JSMarshalerArgument __message_native__js_arg = ref __arguments_buffer[2];
__message_native__js_arg.ToJS(in message);
// this will also marshal exception
JSFunctionBinding.InvokeJS(__signature_Log_20494476, __arguments_buffer);
}
static volatile JSFunctionBinding __signature_Log_20494476;This will be generated on the runtime for the JavaScript marshaling stub
function factory(closure) {
//# sourceURL=https://mono-wasm.invalid/_bound_js_console_log
const { signature, fn, marshal_exception_to_cs, converter2 } = closure;
return function _bound_js_console_log(args) {
try {
const arg0 = converter2(args + 32, signature + 72); // String
// fn is reference to console.log here
const js_result = fn(arg0);
if (js_result !== undefined) throw new Error('Function console.log returned unexpected value, C# signature is void');
} catch (ex) {
marshal_exception_to_cs(args, ex);
}
}
}More examples
// from the rewrite of the runtime's implementation of Http wrapper on WASM.
[JSImport("INTERNAL.http_wasm_get_response_header_names")]
private static partial string[] _GetResponseHeaderNames(
JSObject fetchResponse);
[JSImport("INTERNAL.http_wasm_fetch_bytes")]
private static partial Task<JSObject> FetchBytes(
string uri,
string[] headerNames,
string[] headerValues,
string[] optionNames,
[JSMarshalAs<JSType.Array<JSType.Any>] object?[] optionValues,
JSObject abortControler,
IntPtr bodyPtr,
int bodyLength
);
[JSImport("INTERNAL.http_wasm_get_response_bytes")]
public static partial int GetResponseBytes(
JSObject fetchResponse,
[JSMarshalAs<JSType.MemoryView>] Span<byte> buffer);
// from the rewrite of the runtime's implementation of WebSocket wrapper on WASM.
[JSImport("INTERNAL.ws_wasm_create")]
public static partial JSObject WebSocketCreate(
string uri,
string?[]? subProtocols,
[JSMarshalAs<JSType.Function<JSType.Number, JSType.String>>] Action<int, string> onClosed);
[JSImport("INTERNAL.ws_wasm_send")]
public static partial Task? WebSocketSend(
JSObject webSocket,
[JSMarshalAs<JSType.MemoryView>] ArraySegment<byte> buffer,
int messageType,
bool endOfMessage);
// this is how to marshal strongly typed function
[JSImport("INTERNAL.create_function")]
[return: JSMarshalAs<JSType.Function<JSType.Number, JSType.Number, JSType.Number>]
public static partial Func<double, double, double> CreateFunctionDoubleDoubleDouble(
string arg1Name,
string arg2Name,
string code);
// this is sample how to export managed method to be consumable by JS
// the JS side wrapper would be exported into EXPORTS JS API object
// all arguments are natural JS types for the caller
[JSExport]
public static async Task<string> SlowFailure(Task<int> promisedNumber)
{
var delayMs = await promisedNumber;
// this would be marshled as JS promise rejection
if (promisedNumber<0) throw new ArgumentException("delayMs");
await Task.Delay(delayMs);
return "Slow hello";
}Alternative Designs
- We have existing private interop. It has few design flaws, the worst of them is that it gives to JS code naked pointers to managed objects. They could move on GC making it fragile.
- We could do full dynamic marshaling on runtime, but it would need lot of reflection and it's not trimming friendly
Open questions:
we consider that maybe we could marshal more dynamic combinations of parameters in the future. JavaScript is dynamic language after all. We madeJSTypeas flags to prepare for it as it would be difficult to change in the future.Should we haveansweredGetPropertyandSetPropertydirectly on theJSObjectTheansweredJSMarshalerArgumenthas marshalers on it. For primitive types we do both nullable and non-nullable alternative. In JS world everything is nullable. Shall we enforce nullability constraint on runtime ?We madeansweredJSMarshalerArgument.ToManaged(out Task value)non-nullable, but in fact you can pass null Promise. Reason: forcing user to check null before callingawaitfelt akward. Passing null promise is useful on synchronous returns from JS.
Risks
- this proposal is not improving CSP compliance, we may want to evolve the solution in the future to generate JS files during compile time.
- the quality of the generator in the prototype is low. It doesn't handle all negative scenarios and the diagnostic messages are just sketch.
- We validated the design from perf perspective with the team, but we have to measure it yet.
- Same for memory leaks, there are 2 GCs involved.
noseratiolambdageek, maraf, yamachu, christianrondeau and veikkoeeva
Metadata
Metadata
Assignees
Labels
api-approvedAPI was approved in API review, it can be implementedAPI was approved in API review, it can be implementedarch-wasmWebAssembly architectureWebAssembly architecturearea-System.Runtime.InteropServices.JavaScriptblockingMarks issues that we want to fast track in order to unblock other important workMarks issues that we want to fast track in order to unblock other important work