dotnet / runtime

.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.
https://docs.microsoft.com/dotnet/core/
MIT License
15.16k stars 4.71k forks source link

[API Proposal]: JavaScript interop with [JSImport] and [JSExport] attributes and Roslyn #70133

Closed pavelsavara closed 2 years ago

pavelsavara commented 2 years ago

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:

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

Open questions:

Risks

ghost commented 2 years ago

Tagging subscribers to this area: @dotnet/interop-contrib See info in area-owners.md if you want to be subscribed.

Issue Details
### 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 native components. Currently we don't have public C# API to do so. We propose this API together with [prototype of the implementation](https://github.com/dotnet/runtime/pull/66304). Key features are: - generate C# side of the marshaling stub in as partial method, Roslyn analyzer triggered by `JSImportAttribute` or `JSExportAttribute`. 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.BigInt` or as `JSType.Number`, configurable per parameter via `JSMarshalAsAttribute` similar to `MarshalAsAttribute` or P/Invoke - the JavaScript doesn't have natural concept of memory, instead useful marshaling needs to create JS native types. Wasm native code could not create them, and so we have to always have marshaling code also on the JS side. - generate JS side of the marshaling on runtime, do 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.Object` with mapping to well known types for some instance types and proxy via `GCHandle` for the rest. - `IJSObject` with private legacy implementation `JSObject`, which is proxy via existing `JSHandle` concept similar to `GCHandle` - `Task`, `Func`, `Action` - `byte[]`, `int[]`, `double[]` - `Span`, `Span`, `Span` and `ArraySegment`, `ArraySegment`, `ArraySegment` - Custom marshaler to and from managed and JS objects of any shape - 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.JavaScript` assembly 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](https://github.com/dotnet/runtime/pull/66304) ### API Proposal Below are types which drive the code generator ```csharp namespace System.Runtime.InteropServices.JavaScript; // these are the attributes which trigger code-gen [System.AttributeUsageAttribute(System.AttributeTargets.Method, Inherited = false, AllowMultiple = false)] [Versioning.SupportedOSPlatform("browser")] public sealed class JSImportAttribute : System.Attribute { public string FunctionName { get; } public JSImportAttribute(string functionName) => throw null; } [System.AttributeUsageAttribute(System.AttributeTargets.Method, Inherited = false, AllowMultiple = false)] [Versioning.SupportedOSPlatform("browser")] public sealed class JSExportAttribute : System.Attribute { public string FunctionName { get; } public JSExportAttribute() => throw null; public JSExportAttribute(string functionName) => 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 : System.Attribute { public JSType Type { get { throw null; } } public JSType[] TypeArguments { get; } public System.Type? CustomMarshaler { get { throw null; } } public JSMarshalAsAttribute(JSType type) => throw null; public JSMarshalAsAttribute(JSType type, JSType typeArgument1) => throw null; public JSMarshalAsAttribute(JSType type, JSType typeArgument1, JSType typeArgument2) => throw null; public JSMarshalAsAttribute(JSType type, JSType typeArgument1, JSType typeArgument2, JSType typeArgument3) => throw null; public JSMarshalAsAttribute(JSType type, JSType typeArgument1, JSType typeArgument2, JSType typeArgument3, JSType typeArgument4) => throw null; public JSMarshalAsAttribute(JSType type, System.Type customMarshaler) => throw null; } [Versioning.SupportedOSPlatform("browser")] [System.Flags] public enum JSType : int { None = 0x0, Void = 0x1, Boolean = 0x2, Number = 0x4, // max 52 integral bits BigInt = 0x8, Date = 0x10, String = 0x20, Function = 0x40, Array = 0x80, Object = 0x100, Promise = 0x200, Error = 0x400, MemoryView = 0x800, Custom = 0x1000, Any = 0x2000, } ``` Below are types which drive the code generator for custom marshler ```csharp namespace System.Runtime.InteropServices.JavaScript; // when you use [JSMarshalAs(JSType.Custom, typeof(YourMarshaler))] you mark YourMarshaler with [JSCustomMarshaller] [System.AttributeUsageAttribute(System.AttributeTargets.Struct, Inherited = false, AllowMultiple = false)] [Versioning.SupportedOSPlatform("browser")] public sealed class JSCustomMarshallerAttribute : System.Attribute { public System.Type ManagedType { get { throw null; } } public JSCustomMarshallerAttribute(System.Type managedType) => throw null; } // IJSCustomMarshaller and IJSCustomMarshaller helps the developer to stick to proper shape [Versioning.SupportedOSPlatform("browser")] public interface IJSCustomMarshaller { string JavaScriptCode { get; } } [Versioning.SupportedOSPlatform("browser")] [CLSCompliant(false)] public interface IJSCustomMarshaller : IJSCustomMarshaller { void ToManaged(in JavaScriptMarshalerArgument arg, out T value); void ToJavaScript(ref JavaScriptMarshalerArgument arg, in T value); } ``` Below are types for working with instances of JavaScript instances ```csharp namespace System.Runtime.InteropServices.JavaScript; // IJSObject is public face of the internal legacy JSObject, it represents the proxy of JavaScript object instance [Versioning.SupportedOSPlatform("browser")] public interface IJSObject : IDisposable { public bool IsDisposed { get; } } // when we marshal JS Error type [Versioning.SupportedOSPlatform("browser")] public sealed class JSException : Exception { public JSException(string msg) => throw null; } // delegates need to be marshaled as strongly typed and we need to generate code for marshaling the parameters on the actual call // below is guess on parameter types combinations which would be useful on creating JS function from string of the JS code // see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/Function // user can create same factory themselves (up to 3 generic type arguments for now), this is just convinience [Versioning.SupportedOSPlatform("browser")] public static class JSFunction { public static void New(string code, out Action function) => throw null; public static void New(string code, out Func function) => throw null; public static void New(string code, out Func function) => throw null; public static void New(string code, out Func function) => throw null; public static void New(string code, out Func function) => throw null; public static void New(string code, out Func function) => throw null; public static void New(string code, out Func function) => throw null; public static void New(string arg1Name, string code, out Action function) => throw null; public static void New(string arg1Name, string code, out Action function) => throw null; public static void New(string arg1Name, string code, out Action function) => throw null; public static void New(string arg1Name, string code, out Action function) => throw null; public static void New(string arg1Name, string code, out Action function) => throw null; public static void New(string arg1Name, string code, out Action function) => throw null; public static void New(string arg1Name, string code, out Func function) => throw null; public static void New(string arg1Name, string code, out Func function) => throw null; public static void New(string arg1Name, string code, out Func function) => throw null; public static void New(string arg1Name, string code, out Func function) => throw null; public static void New(string arg1Name, string code, out Func function) => throw null; public static void New(string arg1Name, string code, out Func function) => throw null; public static void New(string arg1Name, string code, out Func function) => throw null; public static void New(string arg1Name, string code, out Func function) => throw null; public static void New(string arg1Name, string code, out Func function) => throw null; public static void New(string arg1Name, string code, out Func function) => throw null; public static void New(string arg1Name, string code, out Func function) => throw null; public static void New(string arg1Name, string arg2Name, string code, out Action function) => throw null; public static void New(string arg1Name, string arg2Name, string code, out Action function) => throw null; public static void New(string arg1Name, string arg2Name, string code, out Action function) => throw null; public static void New(string arg1Name, string arg2Name, string code, out Action function) => throw null; public static void New(string arg1Name, string arg2Name, string code, out Func function) => throw null; public static void New(string arg1Name, string arg2Name, string code, out Func function) => throw null; public static void New(string arg1Name, string arg2Name, string code, out Func function) => throw null; public static void New(string arg1Name, string arg2Name, string code, out Func function) => throw null; } // there are many things that you can call on JavaScript object // here are few handy helpers, user will be able to create more using [JSImport] [Versioning.SupportedOSPlatform("browser")] public static class JavaScriptExtensions { public static void GetProperty(this IJSObject self, string propertyName, out bool? value) => throw null; public static void SetProperty(this IJSObject self, string propertyName, bool? value) => throw null; public static void GetProperty(this IJSObject self, string propertyName, out int? value) => throw null; public static void SetProperty(this IJSObject self, string propertyName, int? value) => throw null; public static void GetProperty(this IJSObject self, string propertyName, out long? value) => throw null; public static void SetProperty(this IJSObject self, string propertyName, long? value) => throw null; public static void GetProperty(this IJSObject self, string propertyName, out double? value) => throw null; public static void SetProperty(this IJSObject self, string propertyName, double? value) => throw null; public static void GetProperty(this IJSObject self, string propertyName, out string? value) => throw null; public static void SetProperty(this IJSObject self, string propertyName, string? value) => throw null; public static void GetProperty(this IJSObject self, string propertyName, out IJSObject? value) => throw null; public static void SetProperty(this IJSObject self, string propertyName, IJSObject? value) => throw null; } ``` Below are used by generated code ```csharp namespace System.Runtime.InteropServices.JavaScript; // to bind and call methods [Versioning.SupportedOSPlatform("browser")] [CLSCompliant(false)] public sealed class JavaScriptMarshalerSignature { public static void InvokeBoundJSFunction(JavaScriptMarshalerSignature signature, Span arguments) => throw null; public static JavaScriptMarshalerSignature BindJSFunction(string functionName, JavaScriptMarshalerType[] signatures) => throw null; public static JavaScriptMarshalerSignature BindCSFunction(string fullyQualifiedName, int signatureHash, string? exportAsName, JavaScriptMarshalerType[] signatures) => throw null; } // to create binding metadata [Versioning.SupportedOSPlatform("browser")] [StructLayout(LayoutKind.Sequential, Pack = 4, Size = 32)] public struct JavaScriptMarshalerType { public static JavaScriptMarshalerType Void { get => throw null; } public static JavaScriptMarshalerType Boolean { get => throw null; } public static JavaScriptMarshalerType Byte { get => throw null; } public static JavaScriptMarshalerType Char { get => throw null; } public static JavaScriptMarshalerType Int16 { get => throw null; } public static JavaScriptMarshalerType Int32 { get => throw null; } public static JavaScriptMarshalerType Int52 { get => throw null; } public static JavaScriptMarshalerType BigInt64 { get => throw null; } public static JavaScriptMarshalerType Double { get => throw null; } public static JavaScriptMarshalerType Single { get => throw null; } public static JavaScriptMarshalerType IntPtr { get => throw null; } public static JavaScriptMarshalerType JSObject { get => throw null; } public static JavaScriptMarshalerType Object { get => throw null; } public static JavaScriptMarshalerType String { get => throw null; } public static JavaScriptMarshalerType Exception { get => throw null; } public static JavaScriptMarshalerType DateTime { get => throw null; } public static JavaScriptMarshalerType DateTimeOffset { get => throw null; } public static JavaScriptMarshalerType Nullable(JavaScriptMarshalerType primitive) => throw null; public static JavaScriptMarshalerType Task() => throw null; public static JavaScriptMarshalerType Task(JavaScriptMarshalerType result) => throw null; public static JavaScriptMarshalerType Array(JavaScriptMarshalerType element) => throw null; public static JavaScriptMarshalerType ArraySegment(JavaScriptMarshalerType element) => throw null; public static JavaScriptMarshalerType Span(JavaScriptMarshalerType element) => throw null; public static JavaScriptMarshalerType Action() => throw null; public static JavaScriptMarshalerType Action(JavaScriptMarshalerType arg1) => throw null; public static JavaScriptMarshalerType Action(JavaScriptMarshalerType arg1, JavaScriptMarshalerType arg2) => throw null; public static JavaScriptMarshalerType Action(JavaScriptMarshalerType arg1, JavaScriptMarshalerType arg2, JavaScriptMarshalerType arg3) => throw null; public static JavaScriptMarshalerType Function(JavaScriptMarshalerType result) => throw null; public static JavaScriptMarshalerType Function(JavaScriptMarshalerType arg1, JavaScriptMarshalerType result) => throw null; public static JavaScriptMarshalerType Function(JavaScriptMarshalerType arg1, JavaScriptMarshalerType arg2, JavaScriptMarshalerType result) => throw null; public static JavaScriptMarshalerType Function(JavaScriptMarshalerType arg1, JavaScriptMarshalerType arg2, JavaScriptMarshalerType arg3, JavaScriptMarshalerType result) => throw null; public static JavaScriptMarshalerType Custom() where TMarshaler : struct => throw null; public static JavaScriptMarshalerType NativeMarshalling() where TMarshaler : struct => throw null; } // actual marshalers [Versioning.SupportedOSPlatform("browser")] [StructLayout(LayoutKind.Explicit, Pack = 16, Size = 16)] [CLSCompliant(false)] public struct JavaScriptMarshalerArgument { public delegate void ArgumentToManagedCallback(ref JavaScriptMarshalerArgument arg, out T value); public delegate void ArgumentToJavaScriptCallback(ref JavaScriptMarshalerArgument arg, in T value); public void Initialize() => throw null; public void ToManaged(out bool value) => throw null; public void ToJavaScript(in bool value) => throw null; public void ToManaged(out bool? value) => throw null; public void ToJavaScript(in bool? value) => throw null; public void ToManaged(out byte value) => throw null; public void ToJavaScript(in byte value) => throw null; public void ToManaged(out byte? value) => throw null; public void ToJavaScript(in byte? value) => throw null; public void ToManaged(out byte[]? value) => throw null; public void ToJavaScript(in byte[]? value) => throw null; public void ToManaged(out char value) => throw null; public void ToJavaScript(in char value) => throw null; public void ToManaged(out char? value) => throw null; public void ToJavaScript(in char? value) => throw null; public void ToManaged(out short value) => throw null; public void ToJavaScript(in short value) => throw null; public void ToManaged(out short? value) => throw null; public void ToJavaScript(in short? value) => throw null; public void ToManaged(out int value) => throw null; public void ToJavaScript(in int value) => throw null; public void ToManaged(out int? value) => throw null; public void ToJavaScript(in int? value) => throw null; public void ToManaged(out int[]? value) => throw null; public void ToJavaScript(in int[]? value) => throw null; public void ToManaged(out long value) => throw null; public void ToJavaScript(in long value) => throw null; public void ToManaged(out long? value) => throw null; public void ToJavaScript(in long? value) => throw null; public void ToManagedBig(out long value) => throw null; public void ToJavaScriptBig(in long value) => throw null; public void ToManagedBig(out long? value) => throw null; public void ToJavaScriptBig(in long? value) => throw null; public void ToManaged(out float value) => throw null; public void ToJavaScript(in float value) => throw null; public void ToManaged(out float? value) => throw null; public void ToJavaScript(in float? value) => throw null; public void ToManaged(out double value) => throw null; public void ToJavaScript(in double value) => throw null; public void ToManaged(out double? value) => throw null; public void ToJavaScript(in double? value) => throw null; public void ToManaged(out double[]? value) => throw null; public void ToJavaScript(in double[]? value) => throw null; public void ToManaged(out IntPtr value) => throw null; public void ToJavaScript(in IntPtr value) => throw null; public void ToManaged(out IntPtr? value) => throw null; public void ToJavaScript(in IntPtr? value) => throw null; public void ToManaged(out DateTimeOffset value) => throw null; public void ToJavaScript(in DateTimeOffset value) => throw null; public void ToManaged(out DateTimeOffset? value) => throw null; public void ToJavaScript(in DateTimeOffset? value) => throw null; public void ToManaged(out DateTime value) => throw null; public void ToJavaScript(in DateTime value) => throw null; public void ToManaged(out DateTime? value) => throw null; public void ToJavaScript(in DateTime? value) => throw null; public void ToManaged(out string? value) => throw null; public void ToJavaScript(in string? value) => throw null; public void ToManaged(out string?[]? value) => throw null; public void ToJavaScript(in string?[]? value) => throw null; public void ToManaged(out Exception? value) => throw null; public void ToJavaScript(in Exception? value) => throw null; public void ToManaged(out object? value) => throw null; public void ToJavaScript(in object? value) => throw null; public void ToManaged(out object?[]? value) => throw null; public void ToJavaScript(in object?[]? value) => throw null; public void ToManaged(out IJSObject? value) => throw null; public void ToJavaScript(in IJSObject? value) => throw null; public void ToManaged(out IJSObject?[] value) => throw null; public void ToJavaScript(in IJSObject?[] value) => throw null; public void ToManaged(out System.Threading.Tasks.Task value) => throw null; public void ToJavaScript(in System.Threading.Tasks.Task value) => throw null; public void ToManaged(out System.Threading.Tasks.Task value, ArgumentToManagedCallback marshaler) => throw null; public void ToJavaScript(in System.Threading.Tasks.Task value, ArgumentToJavaScriptCallback marshaler) => throw null; public void ToManaged(out Action? value) => throw null; public void ToJavaScript(in Action? value) => throw null; public void ToManaged(out Action? value, ArgumentToJavaScriptCallback arg1Marshaler) => throw null; public void ToJavaScript(in Action? value, ArgumentToManagedCallback arg1Marshaler) => throw null; public void ToManaged(out Action? value, ArgumentToJavaScriptCallback arg1Marshaler, ArgumentToJavaScriptCallback arg2Marshaler) => throw null; public void ToJavaScript(in Action? value, ArgumentToManagedCallback arg1Marshaler, ArgumentToManagedCallback arg2Marshaler) => throw null; public void ToManaged(out Action? value, ArgumentToJavaScriptCallback arg1Marshaler, ArgumentToJavaScriptCallback arg2Marshaler, ArgumentToJavaScriptCallback arg3Marshaler) => throw null; public void ToJavaScript(in Action? value, ArgumentToManagedCallback arg1Marshaler, ArgumentToManagedCallback arg2Marshaler, ArgumentToManagedCallback arg3Marshaler) => throw null; public void ToManaged(out Func? value, ArgumentToManagedCallback resMarshaler) => throw null; public void ToJavaScript(in Func? value, ArgumentToJavaScriptCallback resMarshaler) => throw null; public void ToManaged(out Func? value, ArgumentToJavaScriptCallback arg1Marshaler, ArgumentToManagedCallback resMarshaler) => throw null; public void ToJavaScript(in Func? value, ArgumentToManagedCallback arg1Marshaler, ArgumentToJavaScriptCallback resMarshaler) => throw null; public void ToManaged(out Func? value, ArgumentToJavaScriptCallback arg1Marshaler, ArgumentToJavaScriptCallback arg2Marshaler, ArgumentToManagedCallback resMarshaler) => throw null; public void ToJavaScript(in Func? value, ArgumentToManagedCallback arg1Marshaler, ArgumentToManagedCallback arg2Marshaler, ArgumentToJavaScriptCallback resMarshaler) => throw null; public void ToManaged(out Func? value, ArgumentToJavaScriptCallback arg1Marshaler, ArgumentToJavaScriptCallback arg2Marshaler, ArgumentToJavaScriptCallback arg3Marshaler, ArgumentToManagedCallback resMarshaler) => throw null; public void ToJavaScript(in Func? value, ArgumentToManagedCallback arg1Marshaler, ArgumentToManagedCallback arg2Marshaler, ArgumentToManagedCallback arg3Marshaler, ArgumentToJavaScriptCallback resMarshaler) => throw null; public unsafe void ToManaged(out void* value) => throw null; public unsafe void ToJavaScript(in void* value) => throw null; public void ToManaged(out Span value) => throw null; public void ToJavaScript(in Span value) => throw null; public void ToManaged(out ArraySegment value) => throw null; public void ToJavaScript(in ArraySegment value) => throw null; public void ToManaged(out Span value) => throw null; public void ToJavaScript(in Span value) => throw null; public void ToManaged(out Span value) => throw null; public void ToJavaScript(in Span value) => throw null; public void ToManaged(out ArraySegment value) => throw null; public void ToJavaScript(in ArraySegment value) => throw null; public void ToManaged(out ArraySegment value) => throw null; public void ToJavaScript(in ArraySegment value) => throw null; public void JavaScriptToNative(ref T nativeMarshaler) where T : struct => throw null; public void NativeToJavaScript(ref T nativeMarshaler) where T : struct => throw null; } ``` ### API Usage ```csharp // 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( // the generator enforces that all parameters have explicit `JSMarshalAs` annotation // this one will marshal C# string to JavaScript native string by value (with some optimizations) [JSMarshalAs(JSType.String)] string message); ``` ```csharp // code 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 = JavaScriptMarshalerSignature.BindJSFunction("console.log", new JavaScriptMarshalerType[]{ JavaScriptMarshalerType.Void, JavaScriptMarshalerType.String}); } System.Span __arguments_buffer = stackalloc JavaScriptMarshalerArgument[3]; ref JavaScriptMarshalerArgument __arg_exception = ref __arguments_buffer[0]; __arg_exception.Initialize(); ref JavaScriptMarshalerArgument __arg_return = ref __arguments_buffer[1]; __arg_return.Initialize(); ref JavaScriptMarshalerArgument __message_native__js_arg = ref __arguments_buffer[2]; __message_native__js_arg.ToJavaScript(in message); // this will also marshal exception JavaScriptMarshalerSignature.InvokeBoundJSFunction(__signature_Log_20494476, __arguments_buffer); } static volatile JavaScriptMarshalerSignature __signature_Log_20494476; ``` ```javascript // 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); } } } ``` ```csharp // More examples, from the rewrite of the runtime's implementation of Http and WebSocket wrappers on WASM. [JSImport("INTERNAL.http_wasm_get_response_header_names")] [return: JSMarshalAs(JSType.Array, JSType.String)] private static partial string[] _GetResponseHeaderNames( [JSMarshalAs(JSType.Object)] IJSObject fetchResponse); [JSImport("INTERNAL.ws_wasm_send")] [return: JSMarshalAs(JSType.Promise)] public static partial Task? WebSocketSend( [JSMarshalAs(JSType.Object)] IJSObject webSocket, [JSMarshalAs(JSType.MemoryView)] ArraySegment buffer, [JSMarshalAs(JSType.Number)] int messageType, [JSMarshalAs(JSType.Boolean)] 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 CreateFunctionDoubleDoubleDouble( [JSMarshalAs(JSType.String)] string arg1Name, [JSMarshalAs(JSType.String)] string arg2Name, [JSMarshalAs(JSType.String)] string code); // this is sample how to export managed method to be consumable by JS // the JS side wrapper would be exported into JS global namespace as JavaScriptTestHelper.AwaitTaskOfObject // all arguments are natural JS types for the caller [JSExport("JavaScriptTestHelper.AwaitTaskOfObject")] [return: JSMarshalAs(JSType.Promise, JSType.String)] public static async Task SlowFailure([JSMarshalAs(JSType.Promise, JSType.Number)] Task 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 ### Risks Open questions: - we consider that maybe we could marshal more dynamic combinations of parameters in the future. JavaScript is dynamic language after all. We made `JSType` as flags to prepare for it as it would be difficult to change in the future. - Should we have `GetProperty` and `SetProperty` as extension method rather than directly on the `IJSObject` interface ? There are many more things you could call on JS object proxy. We may also add more marker interfaces in the future, `IJSArray` comes to mind. - The `JavaScriptMarshalerArgument` has 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 made `JavaScriptMarshalerArgument.ToManaged(out Task value)` non-nullable, but in fact you can pass null Promise. Reason: forcing user to check null before calling `await` felt akward. Passing null promise is useful on synchronous returns from JS. TODO: - 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 GC's involved.
Author: pavelsavara
Assignees: pavelsavara
Labels: `api-suggestion`, `area-System.Runtime.InteropServices`, `untriaged`
Milestone: -
pavelsavara commented 2 years ago

cc @kg @lewing @jkoritzinsky @marek-safar

ghost commented 2 years ago

Tagging subscribers to 'arch-wasm': @lewing See info in area-owners.md if you want to be subscribed.

Issue Details
### 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 native components. Currently we don't have public C# API to do so. We propose this API together with [prototype of the implementation](https://github.com/dotnet/runtime/pull/66304). Key features are: - generate C# side of the marshaling stub in as partial method, Roslyn analyzer triggered by `JSImportAttribute` or `JSExportAttribute`. 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.BigInt` or as `JSType.Number`, configurable per parameter via `JSMarshalAsAttribute` similar to `MarshalAsAttribute` or P/Invoke - the JavaScript doesn't have natural concept of memory, instead useful marshaling needs to create JS native types. Wasm native code could not create them, and so we have to always have marshaling code also on the JS side. - generate JS side of the marshaling on runtime, do 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.Object` with mapping to well known types for some instance types and proxy via `GCHandle` for the rest. - `IJSObject` with private legacy implementation `JSObject`, which is proxy via existing `JSHandle` concept similar to `GCHandle` - `Task`, `Func`, `Action` - `byte[]`, `int[]`, `double[]` - `Span`, `Span`, `Span` and `ArraySegment`, `ArraySegment`, `ArraySegment` - Custom marshaler to and from managed and JS objects of any shape - 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.JavaScript` assembly 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](https://github.com/dotnet/runtime/pull/66304) ### API Proposal Below are types which drive the code generator ```csharp namespace System.Runtime.InteropServices.JavaScript; // these are the attributes which trigger code-gen [System.AttributeUsageAttribute(System.AttributeTargets.Method, Inherited = false, AllowMultiple = false)] [Versioning.SupportedOSPlatform("browser")] public sealed class JSImportAttribute : System.Attribute { public string FunctionName { get; } public JSImportAttribute(string functionName) => throw null; } [System.AttributeUsageAttribute(System.AttributeTargets.Method, Inherited = false, AllowMultiple = false)] [Versioning.SupportedOSPlatform("browser")] public sealed class JSExportAttribute : System.Attribute { public string FunctionName { get; } public JSExportAttribute() => throw null; public JSExportAttribute(string functionName) => 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 : System.Attribute { public JSType Type { get { throw null; } } public JSType[] TypeArguments { get; } public System.Type? CustomMarshaler { get { throw null; } } public JSMarshalAsAttribute(JSType type) => throw null; public JSMarshalAsAttribute(JSType type, JSType typeArgument1) => throw null; public JSMarshalAsAttribute(JSType type, JSType typeArgument1, JSType typeArgument2) => throw null; public JSMarshalAsAttribute(JSType type, JSType typeArgument1, JSType typeArgument2, JSType typeArgument3) => throw null; public JSMarshalAsAttribute(JSType type, JSType typeArgument1, JSType typeArgument2, JSType typeArgument3, JSType typeArgument4) => throw null; public JSMarshalAsAttribute(JSType type, System.Type customMarshaler) => throw null; } [Versioning.SupportedOSPlatform("browser")] [System.Flags] public enum JSType : int { None = 0x0, Void = 0x1, Boolean = 0x2, Number = 0x4, // max 52 integral bits BigInt = 0x8, Date = 0x10, String = 0x20, Function = 0x40, Array = 0x80, Object = 0x100, Promise = 0x200, Error = 0x400, MemoryView = 0x800, Custom = 0x1000, Any = 0x2000, } ``` Below are types which drive the code generator for custom marshler ```csharp namespace System.Runtime.InteropServices.JavaScript; // when you use [JSMarshalAs(JSType.Custom, typeof(YourMarshaler))] you mark YourMarshaler with [JSCustomMarshaller] [System.AttributeUsageAttribute(System.AttributeTargets.Struct, Inherited = false, AllowMultiple = false)] [Versioning.SupportedOSPlatform("browser")] public sealed class JSCustomMarshallerAttribute : System.Attribute { public System.Type ManagedType { get { throw null; } } public JSCustomMarshallerAttribute(System.Type managedType) => throw null; } // IJSCustomMarshaller and IJSCustomMarshaller helps the developer to stick to proper shape [Versioning.SupportedOSPlatform("browser")] public interface IJSCustomMarshaller { string JavaScriptCode { get; } } [Versioning.SupportedOSPlatform("browser")] [CLSCompliant(false)] public interface IJSCustomMarshaller : IJSCustomMarshaller { void ToManaged(in JavaScriptMarshalerArgument arg, out T value); void ToJavaScript(ref JavaScriptMarshalerArgument arg, in T value); } ``` Below are types for working with JavaScript instances ```csharp namespace System.Runtime.InteropServices.JavaScript; // IJSObject is public face of the internal legacy JSObject, it represents the proxy of JavaScript object instance [Versioning.SupportedOSPlatform("browser")] public interface IJSObject : IDisposable { public bool IsDisposed { get; } } // when we marshal JS Error type [Versioning.SupportedOSPlatform("browser")] public sealed class JSException : Exception { public JSException(string msg) => throw null; } // delegates need to be marshaled as strongly typed and we need to generate code for marshaling the parameters on the actual call // below is guess on parameter types combinations which would be useful on creating JS function from string of the JS code // see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/Function // user can create same factory themselves (up to 3 generic type arguments for now), this is just convinience [Versioning.SupportedOSPlatform("browser")] public static class JSFunction { public static void New(string code, out Action function) => throw null; public static void New(string code, out Func function) => throw null; public static void New(string code, out Func function) => throw null; public static void New(string code, out Func function) => throw null; public static void New(string code, out Func function) => throw null; public static void New(string code, out Func function) => throw null; public static void New(string code, out Func function) => throw null; public static void New(string arg1Name, string code, out Action function) => throw null; public static void New(string arg1Name, string code, out Action function) => throw null; public static void New(string arg1Name, string code, out Action function) => throw null; public static void New(string arg1Name, string code, out Action function) => throw null; public static void New(string arg1Name, string code, out Action function) => throw null; public static void New(string arg1Name, string code, out Action function) => throw null; public static void New(string arg1Name, string code, out Func function) => throw null; public static void New(string arg1Name, string code, out Func function) => throw null; public static void New(string arg1Name, string code, out Func function) => throw null; public static void New(string arg1Name, string code, out Func function) => throw null; public static void New(string arg1Name, string code, out Func function) => throw null; public static void New(string arg1Name, string code, out Func function) => throw null; public static void New(string arg1Name, string code, out Func function) => throw null; public static void New(string arg1Name, string code, out Func function) => throw null; public static void New(string arg1Name, string code, out Func function) => throw null; public static void New(string arg1Name, string code, out Func function) => throw null; public static void New(string arg1Name, string code, out Func function) => throw null; public static void New(string arg1Name, string arg2Name, string code, out Action function) => throw null; public static void New(string arg1Name, string arg2Name, string code, out Action function) => throw null; public static void New(string arg1Name, string arg2Name, string code, out Action function) => throw null; public static void New(string arg1Name, string arg2Name, string code, out Action function) => throw null; public static void New(string arg1Name, string arg2Name, string code, out Func function) => throw null; public static void New(string arg1Name, string arg2Name, string code, out Func function) => throw null; public static void New(string arg1Name, string arg2Name, string code, out Func function) => throw null; public static void New(string arg1Name, string arg2Name, string code, out Func function) => throw null; } // there are many things that you can call on JavaScript object // here are few handy helpers, user will be able to create more using [JSImport] [Versioning.SupportedOSPlatform("browser")] public static class JavaScriptExtensions { public static void GetProperty(this IJSObject self, string propertyName, out bool? value) => throw null; public static void SetProperty(this IJSObject self, string propertyName, bool? value) => throw null; public static void GetProperty(this IJSObject self, string propertyName, out int? value) => throw null; public static void SetProperty(this IJSObject self, string propertyName, int? value) => throw null; public static void GetProperty(this IJSObject self, string propertyName, out long? value) => throw null; public static void SetProperty(this IJSObject self, string propertyName, long? value) => throw null; public static void GetProperty(this IJSObject self, string propertyName, out double? value) => throw null; public static void SetProperty(this IJSObject self, string propertyName, double? value) => throw null; public static void GetProperty(this IJSObject self, string propertyName, out string? value) => throw null; public static void SetProperty(this IJSObject self, string propertyName, string? value) => throw null; public static void GetProperty(this IJSObject self, string propertyName, out IJSObject? value) => throw null; public static void SetProperty(this IJSObject self, string propertyName, IJSObject? value) => throw null; } ``` Below types are used by the generated code ```csharp namespace System.Runtime.InteropServices.JavaScript; // to bind and call methods [Versioning.SupportedOSPlatform("browser")] [CLSCompliant(false)] public sealed class JavaScriptMarshalerSignature { public static void InvokeBoundJSFunction(JavaScriptMarshalerSignature signature, Span arguments) => throw null; public static JavaScriptMarshalerSignature BindJSFunction(string functionName, JavaScriptMarshalerType[] signatures) => throw null; public static JavaScriptMarshalerSignature BindCSFunction(string fullyQualifiedName, int signatureHash, string? exportAsName, JavaScriptMarshalerType[] signatures) => throw null; } // to create binding metadata [Versioning.SupportedOSPlatform("browser")] [StructLayout(LayoutKind.Sequential, Pack = 4, Size = 32)] public struct JavaScriptMarshalerType { public static JavaScriptMarshalerType Void { get => throw null; } public static JavaScriptMarshalerType Boolean { get => throw null; } public static JavaScriptMarshalerType Byte { get => throw null; } public static JavaScriptMarshalerType Char { get => throw null; } public static JavaScriptMarshalerType Int16 { get => throw null; } public static JavaScriptMarshalerType Int32 { get => throw null; } public static JavaScriptMarshalerType Int52 { get => throw null; } public static JavaScriptMarshalerType BigInt64 { get => throw null; } public static JavaScriptMarshalerType Double { get => throw null; } public static JavaScriptMarshalerType Single { get => throw null; } public static JavaScriptMarshalerType IntPtr { get => throw null; } public static JavaScriptMarshalerType JSObject { get => throw null; } public static JavaScriptMarshalerType Object { get => throw null; } public static JavaScriptMarshalerType String { get => throw null; } public static JavaScriptMarshalerType Exception { get => throw null; } public static JavaScriptMarshalerType DateTime { get => throw null; } public static JavaScriptMarshalerType DateTimeOffset { get => throw null; } public static JavaScriptMarshalerType Nullable(JavaScriptMarshalerType primitive) => throw null; public static JavaScriptMarshalerType Task() => throw null; public static JavaScriptMarshalerType Task(JavaScriptMarshalerType result) => throw null; public static JavaScriptMarshalerType Array(JavaScriptMarshalerType element) => throw null; public static JavaScriptMarshalerType ArraySegment(JavaScriptMarshalerType element) => throw null; public static JavaScriptMarshalerType Span(JavaScriptMarshalerType element) => throw null; public static JavaScriptMarshalerType Action() => throw null; public static JavaScriptMarshalerType Action(JavaScriptMarshalerType arg1) => throw null; public static JavaScriptMarshalerType Action(JavaScriptMarshalerType arg1, JavaScriptMarshalerType arg2) => throw null; public static JavaScriptMarshalerType Action(JavaScriptMarshalerType arg1, JavaScriptMarshalerType arg2, JavaScriptMarshalerType arg3) => throw null; public static JavaScriptMarshalerType Function(JavaScriptMarshalerType result) => throw null; public static JavaScriptMarshalerType Function(JavaScriptMarshalerType arg1, JavaScriptMarshalerType result) => throw null; public static JavaScriptMarshalerType Function(JavaScriptMarshalerType arg1, JavaScriptMarshalerType arg2, JavaScriptMarshalerType result) => throw null; public static JavaScriptMarshalerType Function(JavaScriptMarshalerType arg1, JavaScriptMarshalerType arg2, JavaScriptMarshalerType arg3, JavaScriptMarshalerType result) => throw null; public static JavaScriptMarshalerType Custom() where TMarshaler : struct => throw null; public static JavaScriptMarshalerType NativeMarshalling() where TMarshaler : struct => throw null; } // actual marshalers [Versioning.SupportedOSPlatform("browser")] [StructLayout(LayoutKind.Explicit, Pack = 16, Size = 16)] [CLSCompliant(false)] public struct JavaScriptMarshalerArgument { public delegate void ArgumentToManagedCallback(ref JavaScriptMarshalerArgument arg, out T value); public delegate void ArgumentToJavaScriptCallback(ref JavaScriptMarshalerArgument arg, in T value); public void Initialize() => throw null; public void ToManaged(out bool value) => throw null; public void ToJavaScript(in bool value) => throw null; public void ToManaged(out bool? value) => throw null; public void ToJavaScript(in bool? value) => throw null; public void ToManaged(out byte value) => throw null; public void ToJavaScript(in byte value) => throw null; public void ToManaged(out byte? value) => throw null; public void ToJavaScript(in byte? value) => throw null; public void ToManaged(out byte[]? value) => throw null; public void ToJavaScript(in byte[]? value) => throw null; public void ToManaged(out char value) => throw null; public void ToJavaScript(in char value) => throw null; public void ToManaged(out char? value) => throw null; public void ToJavaScript(in char? value) => throw null; public void ToManaged(out short value) => throw null; public void ToJavaScript(in short value) => throw null; public void ToManaged(out short? value) => throw null; public void ToJavaScript(in short? value) => throw null; public void ToManaged(out int value) => throw null; public void ToJavaScript(in int value) => throw null; public void ToManaged(out int? value) => throw null; public void ToJavaScript(in int? value) => throw null; public void ToManaged(out int[]? value) => throw null; public void ToJavaScript(in int[]? value) => throw null; public void ToManaged(out long value) => throw null; public void ToJavaScript(in long value) => throw null; public void ToManaged(out long? value) => throw null; public void ToJavaScript(in long? value) => throw null; public void ToManagedBig(out long value) => throw null; public void ToJavaScriptBig(in long value) => throw null; public void ToManagedBig(out long? value) => throw null; public void ToJavaScriptBig(in long? value) => throw null; public void ToManaged(out float value) => throw null; public void ToJavaScript(in float value) => throw null; public void ToManaged(out float? value) => throw null; public void ToJavaScript(in float? value) => throw null; public void ToManaged(out double value) => throw null; public void ToJavaScript(in double value) => throw null; public void ToManaged(out double? value) => throw null; public void ToJavaScript(in double? value) => throw null; public void ToManaged(out double[]? value) => throw null; public void ToJavaScript(in double[]? value) => throw null; public void ToManaged(out IntPtr value) => throw null; public void ToJavaScript(in IntPtr value) => throw null; public void ToManaged(out IntPtr? value) => throw null; public void ToJavaScript(in IntPtr? value) => throw null; public void ToManaged(out DateTimeOffset value) => throw null; public void ToJavaScript(in DateTimeOffset value) => throw null; public void ToManaged(out DateTimeOffset? value) => throw null; public void ToJavaScript(in DateTimeOffset? value) => throw null; public void ToManaged(out DateTime value) => throw null; public void ToJavaScript(in DateTime value) => throw null; public void ToManaged(out DateTime? value) => throw null; public void ToJavaScript(in DateTime? value) => throw null; public void ToManaged(out string? value) => throw null; public void ToJavaScript(in string? value) => throw null; public void ToManaged(out string?[]? value) => throw null; public void ToJavaScript(in string?[]? value) => throw null; public void ToManaged(out Exception? value) => throw null; public void ToJavaScript(in Exception? value) => throw null; public void ToManaged(out object? value) => throw null; public void ToJavaScript(in object? value) => throw null; public void ToManaged(out object?[]? value) => throw null; public void ToJavaScript(in object?[]? value) => throw null; public void ToManaged(out IJSObject? value) => throw null; public void ToJavaScript(in IJSObject? value) => throw null; public void ToManaged(out IJSObject?[] value) => throw null; public void ToJavaScript(in IJSObject?[] value) => throw null; public void ToManaged(out System.Threading.Tasks.Task value) => throw null; public void ToJavaScript(in System.Threading.Tasks.Task value) => throw null; public void ToManaged(out System.Threading.Tasks.Task value, ArgumentToManagedCallback marshaler) => throw null; public void ToJavaScript(in System.Threading.Tasks.Task value, ArgumentToJavaScriptCallback marshaler) => throw null; public void ToManaged(out Action? value) => throw null; public void ToJavaScript(in Action? value) => throw null; public void ToManaged(out Action? value, ArgumentToJavaScriptCallback arg1Marshaler) => throw null; public void ToJavaScript(in Action? value, ArgumentToManagedCallback arg1Marshaler) => throw null; public void ToManaged(out Action? value, ArgumentToJavaScriptCallback arg1Marshaler, ArgumentToJavaScriptCallback arg2Marshaler) => throw null; public void ToJavaScript(in Action? value, ArgumentToManagedCallback arg1Marshaler, ArgumentToManagedCallback arg2Marshaler) => throw null; public void ToManaged(out Action? value, ArgumentToJavaScriptCallback arg1Marshaler, ArgumentToJavaScriptCallback arg2Marshaler, ArgumentToJavaScriptCallback arg3Marshaler) => throw null; public void ToJavaScript(in Action? value, ArgumentToManagedCallback arg1Marshaler, ArgumentToManagedCallback arg2Marshaler, ArgumentToManagedCallback arg3Marshaler) => throw null; public void ToManaged(out Func? value, ArgumentToManagedCallback resMarshaler) => throw null; public void ToJavaScript(in Func? value, ArgumentToJavaScriptCallback resMarshaler) => throw null; public void ToManaged(out Func? value, ArgumentToJavaScriptCallback arg1Marshaler, ArgumentToManagedCallback resMarshaler) => throw null; public void ToJavaScript(in Func? value, ArgumentToManagedCallback arg1Marshaler, ArgumentToJavaScriptCallback resMarshaler) => throw null; public void ToManaged(out Func? value, ArgumentToJavaScriptCallback arg1Marshaler, ArgumentToJavaScriptCallback arg2Marshaler, ArgumentToManagedCallback resMarshaler) => throw null; public void ToJavaScript(in Func? value, ArgumentToManagedCallback arg1Marshaler, ArgumentToManagedCallback arg2Marshaler, ArgumentToJavaScriptCallback resMarshaler) => throw null; public void ToManaged(out Func? value, ArgumentToJavaScriptCallback arg1Marshaler, ArgumentToJavaScriptCallback arg2Marshaler, ArgumentToJavaScriptCallback arg3Marshaler, ArgumentToManagedCallback resMarshaler) => throw null; public void ToJavaScript(in Func? value, ArgumentToManagedCallback arg1Marshaler, ArgumentToManagedCallback arg2Marshaler, ArgumentToManagedCallback arg3Marshaler, ArgumentToJavaScriptCallback resMarshaler) => throw null; public unsafe void ToManaged(out void* value) => throw null; public unsafe void ToJavaScript(in void* value) => throw null; public void ToManaged(out Span value) => throw null; public void ToJavaScript(in Span value) => throw null; public void ToManaged(out ArraySegment value) => throw null; public void ToJavaScript(in ArraySegment value) => throw null; public void ToManaged(out Span value) => throw null; public void ToJavaScript(in Span value) => throw null; public void ToManaged(out Span value) => throw null; public void ToJavaScript(in Span value) => throw null; public void ToManaged(out ArraySegment value) => throw null; public void ToJavaScript(in ArraySegment value) => throw null; public void ToManaged(out ArraySegment value) => throw null; public void ToJavaScript(in ArraySegment value) => throw null; public void JavaScriptToNative(ref T nativeMarshaler) where T : struct => throw null; public void NativeToJavaScript(ref T nativeMarshaler) where T : struct => throw null; } ``` ### API Usage ### Trivial example ```csharp // 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( // the generator enforces that all parameters have explicit `JSMarshalAs` annotation // this one will marshal C# string to JavaScript native string by value (with some optimizations) [JSMarshalAs(JSType.String)] string message); ``` This is code generated by Roslyn, simplified for brevity ```csharp [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 = JavaScriptMarshalerSignature.BindJSFunction("console.log", new JavaScriptMarshalerType[]{ JavaScriptMarshalerType.Void, JavaScriptMarshalerType.String}); } System.Span __arguments_buffer = stackalloc JavaScriptMarshalerArgument[3]; ref JavaScriptMarshalerArgument __arg_exception = ref __arguments_buffer[0]; __arg_exception.Initialize(); ref JavaScriptMarshalerArgument __arg_return = ref __arguments_buffer[1]; __arg_return.Initialize(); ref JavaScriptMarshalerArgument __message_native__js_arg = ref __arguments_buffer[2]; __message_native__js_arg.ToJavaScript(in message); // this will also marshal exception JavaScriptMarshalerSignature.InvokeBoundJSFunction(__signature_Log_20494476, __arguments_buffer); } static volatile JavaScriptMarshalerSignature __signature_Log_20494476; ``` This will be generated on the runtime for the JavaScript marshaling stub ```javascript 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 ```csharp // More examples, from the rewrite of the runtime's implementation of Http and WebSocket wrappers on WASM. [JSImport("INTERNAL.http_wasm_get_response_header_names")] [return: JSMarshalAs(JSType.Array, JSType.String)] private static partial string[] _GetResponseHeaderNames( [JSMarshalAs(JSType.Object)] IJSObject fetchResponse); [JSImport("INTERNAL.ws_wasm_send")] [return: JSMarshalAs(JSType.Promise)] public static partial Task? WebSocketSend( [JSMarshalAs(JSType.Object)] IJSObject webSocket, [JSMarshalAs(JSType.MemoryView)] ArraySegment buffer, [JSMarshalAs(JSType.Number)] int messageType, [JSMarshalAs(JSType.Boolean)] 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 CreateFunctionDoubleDoubleDouble( [JSMarshalAs(JSType.String)] string arg1Name, [JSMarshalAs(JSType.String)] string arg2Name, [JSMarshalAs(JSType.String)] string code); // this is sample how to export managed method to be consumable by JS // the JS side wrapper would be exported into JS global namespace as JavaScriptTestHelper.AwaitTaskOfObject // all arguments are natural JS types for the caller [JSExport("JavaScriptTestHelper.AwaitTaskOfObject")] [return: JSMarshalAs(JSType.Promise, JSType.String)] public static async Task SlowFailure([JSMarshalAs(JSType.Promise, JSType.Number)] Task 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 ### Risks Open questions: - we consider that maybe we could marshal more dynamic combinations of parameters in the future. JavaScript is dynamic language after all. We made `JSType` as flags to prepare for it as it would be difficult to change in the future. - Should we have `GetProperty` and `SetProperty` as extension method rather than directly on the `IJSObject` interface ? There are many more things you could call on JS object proxy. We may also add more marker interfaces in the future, `IJSArray` comes to mind. - The `JavaScriptMarshalerArgument` has 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 made `JavaScriptMarshalerArgument.ToManaged(out Task value)` non-nullable, but in fact you can pass null Promise. Reason: forcing user to check null before calling `await` felt akward. Passing null promise is useful on synchronous returns from JS. TODO: - 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 GC's involved.
Author: pavelsavara
Assignees: pavelsavara
Labels: `api-suggestion`, `arch-wasm`, `area-System.Runtime.InteropServices`
Milestone: 7.0.0
pavelsavara commented 2 years ago

Updated with names unified to short JS instead of JavaScript as part of various names. Our legacy JSObject sets the precedent. Thanks @maraf

jeromelaban commented 2 years ago

Very nice work!

There's a typo here:

namespace System.Runtime.InteropServices.Jav4aScript;

I'd avoid using generic delegates here:

public static void New(string code, out Func<bool> function) => throw null;

As the generic invocation and creation performance is not particularly good at this time when using AOT, at least with net6. Would using interfaces make more sense (one per type)?

jkoritzinsky commented 2 years ago

cc: @dotnet/interop-contrib

lambdageek commented 2 years ago

What if JSMarshalAs used a Type to specify the JS type. something like:

// 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 : System.Attribute
{
    public System.Type Type { get { throw null; } }
    public System.Type? CustomMarshaler { get { throw null; } }
    /// pass typeof(JSType.XYZ) for the type argument to specify the shape of the JS value 
    public JSMarshalAsAttribute(System.Type type) => throw null;
    public JSMarshalAsAttribute(System.Type type, System.Type customMarshaler) => throw null;
}
[Versioning.SupportedOSPlatform("browser")]
public static sealed class JSType
{
    public sealed interface None {}
    public sealed interface Void {}
    public sealed interface Boolean {}
    public sealed interface Number {}
    ...
    public sealed interface Function<T> {}
    public sealed interface Function<T1, T2> {}
    public sealed interface Array<T> {}
    public sealed interface Object {}
    public sealed interface Promise<T> {}
    ...
}

and then you could write [JSMarshalAs(typeof(JSType.Promise<JSType.Number>))] and [JSMarshalAs(typeof(JSType.Function<JSType.Object>))] etc

lambdageek commented 2 years ago

@pavelsavara for the JSMarshalerType I wonder if we can avoid exposing the struct as the implementation of how signatures are represented. Ultimately that's an implementation detail of the marshaler that we might want to change in the future. What if we instead make JSMarshalerType a builder that just populates some Span<byte> with the serialized signature that is passed to BindJSFunction(string functionName, Span<byte> signatures)

Something like this:

using System;

public struct JSMarshalerType {
    public static JSMarshalerType Create() { throw null; }
    public int Size {get; }

    public void WriteTo (Span<byte> dest) { }
}

public static class ExtensionMethods {
    public static ref JSMarshalerType BeginGroup(this ref JSMarshalerType t) { return ref t; }
    public static ref JSMarshalerType EndGroup(this ref JSMarshalerType t) { return ref t; }

    // primitive types
    public static ref JSMarshalerType Void (this ref JSMarshalerType t) { return ref t; }
    public static ref JSMarshalerType Boolean (this ref JSMarshalerType t) { return ref t; }
    public static ref JSMarshalerType Double (this ref JSMarshalerType t) { return ref t; }
    public static ref JSMarshalerType Int32 (this ref JSMarshalerType t) { return ref t; }

    // the "generics" must be first in a group followed by the type arguments and an EndGroup
    public static ref JSMarshalerType Nullable(this ref JSMarshalerType t) { return ref t; }
    public static ref JSMarshalerType Task(this ref JSMarshalerType t) { return ref t; }
}

So then to serialize a signature for something like Task<bool> MyFunc (Nullable<int> x, double y) the generated code would do:

    ...
    var builder = JSMarshalerType.Create ();
    builder.BeginGroup().Task().Boolean().EndGroup(); // return type is Task<bool>
    builder.BeginGroup().Nullable().Int32().EndGroup(); // first arg is. Nullable<Int32>
    builder.Double();  // second arg is double

    Span<byte> signature = stackalloc byte[builder.Size];
    builder.WriteTo(signature);
    ...
    JSFunctionSignature function = JSFunctionSignature.BindJSFunction(functionName, signature);
    ...

I tried to make JSMarshalerType a struct but maybe that's not necessary (and I guess the ref assembly would leak out some of its implementation details anyway) so maybe it can just be a class and we pay for the builder allocation.

SteveSandersonMS commented 2 years ago

Minor API style comment: for all the out param APIs like this:

public static void GetProperty(this IJSObject self, string propertyName, out long? value) => throw null;

I'm guessing the out (and not a return type) is just to avoid the use of generics (like GetProperty<long>(...)). If so, there's prior art in the JSON serializer libs to achieve this disambiguation using different method names instead of out, e.g.:

var result = jsObject.GetLongProperty("propName");

... which is a bit more flexible in terms of using this expression inside a larger expression.

SteveSandersonMS commented 2 years ago

Another area to consider is what happens for class libraries that sometimes run in browser and sometimes run in non-browser environments. It should be possible to compile without targeting browser and then at runtime do something like:

if (RuntimeInformation.IsOSPlatform("browser"))
{
    SomeJSImportedMethod(...);
}
else
{
    await jsRuntime.InvokeAsync("mymethod", ...);
}
SteveSandersonMS commented 2 years ago
    // the generator enforces that all parameters have explicit `JSMarshalAs` annotation
    // this one will marshal C# string to JavaScript native string by value (with some optimizations)
    [JSMarshalAs(JSType.String)] string message

It's probably OK because this is so low-level, but in the specific case of strings, I'd expect 99% of the time people would want the obvious mapping of .NET string to JS string, and vice-versa, so it feels like the annotation could be omitted. Same with all numeric types in the .NET-to-JS direction, as there's only one common JS-side numeric type.

I know there are edge cases, like maybe marshalling a .NET string to a JS-side Uint8Array containing UTF8 or UTF16 or something, but this would be pretty uncommon. It would be ideal if that could be specified optionally.

This is a minor detail and could be done as a future enhancement if there is demand for it.

SteveSandersonMS commented 2 years ago
        if (js_result !== undefined) throw new Error('Function console.log returned unexpected value, C# signature is void');

This is interesting. Should there be a way of declaring "please discard the return value"? It's common in JS world for functions that are normally used as if void to actually return something. Developers might not be interested in marshalling the result back into .NET in some cases.

Example: many DOM APIs, e.g., element.removeChild(otherElement) will return otherElement even though you normally discard it since you obviously already have that value. Is there any perf gain in knowing not to marshal this return value back to .NET?

SteveSandersonMS commented 2 years ago

Could you also give examples of how the memory management works?

SteveSandersonMS commented 2 years ago
// this is sample how to export managed method to be consumable by JS
// the JS side wrapper would be exported into JS global namespace as JavaScriptTestHelper.AwaitTaskOfObject
// all arguments are natural JS types for the caller
[JSExport("JavaScriptTestHelper.AwaitTaskOfObject")]

Dropping things into global namespace is a bit of a concern, as it destroys usage scenarios like "more than one .NET runtime in a single document".

Another possibility would be that these exports become available as properties on some "runtime" instance which we can return when the runtime is first started up, so the usage would be more like:

const dotNet = startRuntime(); // Can't remember what your latest API looks like for starting the runtime, but something goes here
...
await dotNet.exports.JavaScriptTestHelper.AwaitTaskOfObject(...);

What do you think?

pavelsavara commented 2 years ago

I'd expect 99% of the time people would want the obvious mapping of .NET string to JS string, and vice-versa, so it feels like the annotation could be omitted.

There is indeed default marshaling for most types and we don't have to force the user to specify it. I do enforce it based on feedback from interop group, where they told me that I should force user to be as explicit as possible to improve future compatibility. Perhaps I misunderstood ? Anyway, I'm open to relax it.

Same with all numeric types in the .NET-to-JS direction, as there's only one common JS-side numeric type.

There is actually bigint on JS side. And also Int64 doesn't always fit into Number's 52 integral bits.

SteveSandersonMS commented 2 years ago

I'm not too worried either way. If these were more commonly-used APIs I think we'd want to make the 99%-common case for both strings and numbers easier by having a default. But it's certainly fine to err on the side of being hyper-explicit for the first version of this, as we could loosen it later but not tighten it.

pavelsavara commented 2 years ago

This is interesting. Should there be a way of declaring "please discard the return value"?

I think importing 3rd party API would be rare and you should match signature on your own functions. We could easily not emit that assert and just ignore it, if we agree that's more useful for now.

Also in the future we could add JSType.Assert to be used as JSType.Void | JSType.Assert. Same idea could apply for nullability constraints or range checks. Or assert could be the default and we could relax it with JSType.Unchecked

SteveSandersonMS commented 2 years ago

I think importing 3rd party API would be rare and you should match signature on your own functions.

What about DOM APIs or other built-in browser APIs, such as the removeChild example?

Also, I think importing 3rd-party APIs will be super common. One of the key use cases for JS interop is to work with arbitrary 3rd-party JS libraries.

I was thinking the key point here is whether or not there's a perf and usability cost to marshalling something back that you don't want. Does it force the developer to explicitly dispose it? Or maybe it holds JS memory until some .NET finalizer runs?

pavelsavara commented 2 years ago

Could you also give examples of how the memory management works?

Already in Net6 we have proxies both directions. We hook into finalizers on both sides. IJSObject could be disposed manually. We do not change any of it with this proposal.

GC story for Promise/Task is more complex and we will slightly improve it. Let's take that conversation on the prototype PR if you are interested in details.

pavelsavara commented 2 years ago

I think importing 3rd party API would be rare and you should match signature on your own functions.

What about DOM APIs or other built-in browser APIs, such as the removeChild example?

OOP interop interfaces tend to create chatty interactions and therefore are expensive and slow overall. For DOM API, user would have to create thousands of proxies to achieve anything interesting. Blazor avoided that perf pitfall by passing diff DTOs on single call per page render.

External libraries could use JSImport to create OOP style APIs, where comfort is way more important than performance.

pavelsavara commented 2 years ago

[JSExport("JavaScriptTestHelper.AwaitTaskOfObject")] Dropping things into global namespace is a bit of a concern

Yes, there is also [JSExport()] without global name. It would make the function available to a future version of the bind_static_method under method's managed FQN. And perhaps we could also add EXPORTS object next to MONO on the JS API as you suggested. That object is already per runtime instance.

pavelsavara commented 2 years ago

@pavelsavara for the JSMarshalerType I wonder if we can avoid exposing the struct

Thanks for pointing that out. That was my intention indeed and I went thru few iterations already. As I'm looking at it again, I could simply change JSMarshalerType from struct to class and remove [StructLayout]. The signature buffer itself is completely internal already. And I would not have to do the builder pattern.

lambdageek commented 2 years ago

And I would not have to do the builder pattern.

The reason I prefer a builder pattern is because we could potentially allocate less - with the version of JSMashalerType in the proposal you have to allocate arrays and JSMarshalType instances and it seems like we could potentially avoid that (or use a pool of temporary objects, etc) if the exact implementation was hidden behind a builder.

It also seems more difficult to change the representation later if the marshaller constructs the signature value directly.

pavelsavara commented 2 years ago

What if JSMarshalAs used a Type to specify the JS type. something like: and then you could write [JSMarshalAs(typeof(JSType.Promise<JSType.Number>))] and [JSMarshalAs(typeof(JSType.Function<JSType.Object>))] etc

@lambdageek What you propose has benefit that it could be nested easily, I like that. Is there existing API which does this ?

On the other hand, this also would not allow for JSType.Number | JSType.Boolean nor for JSType.Number | JSType.Unchecked. We could have JSType.AnyOf<JSType.Number, JSType.Boolean> and JSType.Unchecked<JSType.Number>. Is that abusing the type system too much ? (Both are out of scope for now)

Also, would developers get confused and try to use it with any System.Type ? As in [JSMarshalAs(typeof(JSType.Function<MyType>))]

Finally things like future JSType.Unchecked are leaning from JS type to bit more to the marshaller type or marshaler configuration. I could possibly make classes out of all build-in marshalers and then use those instead of empty interfaces like JSType.Boolean. The idea would be [JSMarshalAs(typeof(JSMarshaler.Boolean))].

As it is now, I'm NOT implementing build-in marshalers in a same way as custom marshaler. TypeScript implementation lives elsewhere and code in JSMarshalerArgument has the internal stack frame data at hands. JSMarshalerArgument.ToManaged and JSMarshalerArgument.ToJS which are called from generated C# code actually have the implementation. I would have to create new marshler struct instances in generated code on stack. I also don't know that generic argument on it would be practical from runtime perspective new JSMarshaler.Promise<JSMarshaler.Boolean>, because the implementation of the promise payload need to be a generated callback, not generic method with reflection.

Overall this would make it much more flexible, but also more complex. Right now I prefer to keep it as simple as possible and stick with JSType flags enum. I'm open to change my opinion.

pavelsavara commented 2 years ago

var result = jsObject.GetLongProperty("propName"); ... which is a bit more flexible in terms of using this expression inside a larger expression.

I updated the proposal with this change.

pavelsavara commented 2 years ago

And I would not have to do the builder pattern.

The reason I prefer a builder pattern is because we could potentially allocate less It also seems more difficult to change the representation later if the marshaller constructs the signature value directly.

I made the change to make it a class, but I kept the way how it composes the signature. (builder without begin/end) The allocation should not be big deal here as this is only called once per binding. All of the JSMarshalerType instances would be very short lived and collected. The builder type sealed class JSMarshalerType is now different from the actual signature slot structure internal struct JSBindingType in the prototype.

lambdageek commented 2 years ago

What if JSMarshalAs used a Type to specify the JS type. something like: and then you could write [JSMarshalAs(typeof(JSType.Promise<JSType.Number>))] and [JSMarshalAs(typeof(JSType.Function<JSType.Object>))] etc

@lambdageek What you propose has benefit that it could be nested easily, I like that. Is there existing API which does this ?

On the other hand, this also would not allow for JSType.Number | JSType.Boolean nor for JSType.Number | JSType.Unchecked. We could have JSType.AnyOf<JSType.Number, JSType.Boolean> and JSType.Unchecked<JSType.Number>. Is that abusing the type system too much ? (Both are out of scope for now)

Also, would developers get confused and try to use it with any System.Type ? As in [JSMarshalAs(typeof(JSType.Function<MyType>))]

number | boolean and unchecked are interesting. And we do start to get into an increasingly esoteric encoding of it in the C# type system. So maybe its not worth it. (And as you say there's a temptation to pass arbitrary C# types which will not work and will just result in an error from the code generator)

Finally things like future JSType.Unchecked are leaning from JS type to bit more to the marshaller type or marshaler configuration. I could possibly make classes out of all build-in marshalers and then use those instead of empty interfaces like JSType.Boolean. The idea would be [JSMarshalAs(typeof(JSMarshaler.Boolean))].

As it is now, I'm NOT implementing build-in marshalers in a same way as custom marshaler. TypeScript implementation lives elsewhere and code in JSMarshalerArgument has the internal stack frame data at hands. JSMarshalerArgument.ToManaged and JSMarshalerArgument.ToJS which are called from generated C# code actually have the implementation. I would have to create new marshler struct instances in generated code on stack. I also don't know that generic argument on it would be practical from runtime perspective new JSMarshaler.Promise<JSMarshaler.Boolean>, because the implementation of the promise payload need to be a generated callback, not generic method with reflection.

So I have basically two concerns:

  1. we have a very limited way of encoding promises and functions at the moment, which might become constraining.
  2. enum is limited in extensibility.

One option for the future (once we exhaust the enum) is something like

   public MarshalAsAttring (string marshalSpec);

and then implement a fancy marshal spec parser in the code generator so that things like "unchecked promise<boolean|number>" can be written and then the code generator has a parser for the marshal specs that accepts some kind of grammar that we will define. Upside: it can express anything we can come up with; downside: we'll have to write a parser and document the language and write good error messages.

Overall this would make it much more flexible, but also more complex. Right now I prefer to keep it as simple as possible and stick with JSType flags enum. I'm open to change my opinion.

I don't feel strongly about this.

pavelsavara commented 2 years ago
  1. we have a very limited way of encoding promises and functions at the moment, which might become constraining.

I see limiting this is as a good thing.

public MarshalAsAttring (string marshalSpec);

This could be added in future API.

AaronRobinsonMSFT commented 2 years ago

As it is now, I'm NOT implementing build-in marshalers in a same way as custom marshaler.

I'd like to see how this would look with the proposed API. Not saying it needs to be done now, but is there a path forward. The LibraryImport source generator is mostly consistent around how marshallers work and we are iterating on it to make sure there are no special cases.

Is there existing API which does this ?

Also, would developers get confused and try to use it with any System.Type ? As in [JSMarshalAs(typeof(JSType.Function))]

The LibraryImport source generator does defer to typeof in most cases, but respects the MarshalAs as a convenience mechanism. Since the JavaScript interop pattern here is new, I think using types should be preferred instead of introducing a new enum type. At the very least it would be informative to see the two approaches and understand the cost of using types as opposed to the enumeration approach.

AaronRobinsonMSFT commented 2 years ago

public MarshalAsAttring (string marshalSpec);

This could be added in future API.

I'd avoid these string based approaches. They make validation much more complicated and force the Trimmer to do more work. Not saying this isn't the best way in this instance, but the costs here should be weight heavily against if we can do this in a more type-system focused manner. This is another instance where seeing the two approaches would be informative - especially in API review.

pavelsavara commented 2 years ago

If I moved methods from JSMarshalerArgument to individual marshlers types, it would look like this

[Versioning.SupportedOSPlatform("browser")]
public static sealed class JSMarshalers
{
    public static struct Int32ToNumber
    {
        public void ToManaged(out int value) => throw null;
        public void ToJS(in int value) => throw null;
    }

    public static struct NullableInt32ToNumber
    {
        public void ToManaged(out int? value) => throw null;
        public void ToJS(in int? value) => throw null;
    }
    ... 50 more
}

But that doesn't correspond to ideal usage [JSMarshalAs(typeof(JSMarshalers.Nullable<JSMarshalers.Int32ToNumber>))] because Nullable is not a marshaler. And even that is not nice with too long names.

If we keep the methods in the JSMarshalerArgument and only add marker types for marshalers it could be similar to what @lambdageek suggested. Something like

[Versioning.SupportedOSPlatform("browser")]
public static sealed class JSMarshalers
{
    public sealed interface None { }
    public sealed interface Void { }
    public sealed interface Boolean { }
    public sealed interface Int64ToNumber{ }
    public sealed interface Int64ToBigInt{ }
    public sealed interface Int32ToNumber { }
    public sealed interface Int16ToNumber { }
    public sealed interface ByteToNumber { }
    public sealed interface CharToString{ }
    public sealed interface String{ }
    public sealed interface Object { }
    ...
    public sealed interface Nullable<T> { }
    public sealed interface Function<T> { }
    public sealed interface Function<T1, T2> { }
    public sealed interface Action<T1, T2> { }
    public sealed interface Array<T> { }
    public sealed interface SpanToMemoryView<T> { }
    public sealed interface ArraySegmentToMemoryView<T> { }
    public sealed interface Promise<T> { }
    ... // later
    public sealed interface Unchecked<T> { } 
}

If we do JS type not marshaler type, it looks nice enough as @lambdageek showed and we could improve on it a bit if we add another marker interface IJSType

[Versioning.SupportedOSPlatform("browser")]
public interface IJSType { }
[Versioning.SupportedOSPlatform("browser")]
public static sealed class JSType
{
    public sealed interface None : IJSType { }
    public sealed interface Void : IJSType { }
    public sealed interface Boolean : IJSType { }
    public sealed interface Number : IJSType { }
    public sealed interface Object : IJSType { }

    public sealed interface Nullable<T> : IJSType where T : IJSType { }
    public sealed interface Function<T> : IJSType where T : IJSType { }
    public sealed interface Function<T1, T2> : IJSType where T1 : IJSType where T2 : IJSType { }
    public sealed interface Array<T> : IJSType where T : IJSType { }
    public sealed interface Promise<T> : IJSType where T : IJSType { }
}

I guess that's not what @AaronRobinsonMSFT want's to see, these are not marshalers but JSType on logical level. Kind of impedance mismatch.

AaronRobinsonMSFT commented 2 years ago

If we do JS type not marshaler type, it looks nice enough as @lambdageek showed and we could improve on it a bit if we add another marker interface IJSType

This seems fine to me. My bigger concern is the use of strings as a workaround for the enum value issue. If there is a possibility that the enum can be exhausted then I'd recommend the IJSType suggested.

AaronRobinsonMSFT commented 2 years ago

But that doesn't correspond to ideal usage [JSMarshalAs(typeof(JSMarshalers.Nullable))] because Nullable is not a marshaler. And even that is not nice with too long names.

public sealed interface Nullable { }

It might make sense to add this as a boolean on JSMarshalAs instead of encoding this as a type.

AaronRobinsonMSFT commented 2 years ago

public JSMarshalAsAttribute(JSType type, JSType typeArgument1) => throw null; public JSMarshalAsAttribute(JSType type, JSType typeArgument1, JSType typeArgument2) => throw null; public JSMarshalAsAttribute(JSType type, JSType typeArgument1, JSType typeArgument2, JSType typeArgument3) => throw null; public JSMarshalAsAttribute(JSType type, JSType typeArgument1, JSType typeArgument2, JSType typeArgument3, JSType typeArgument4) => throw null;

Using types directly would also permit removal of these overloads.

terrajobst commented 2 years ago

Or one could just use params -- it's not like the these attributes are going to be instantiated in practice anyways:

[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.ReturnValue, Inherited = false, AllowMultiple = false)]
[SupportedOSPlatform("browser")]
public sealed class JSMarshalAsAttribute : Attribute
{
    public JSMarshalAsAttribute(params JSType[] type);
    public JSMarshalAsAttribute(JSType type, Type customMarshaler);

    public JSType Type { get; }
    public JSType[] TypeArguments { get; }
    public Type? CustomMarshaler { get; }
}
AaronRobinsonMSFT commented 2 years ago

Or one could just use params -- it's not like the these attributes are going to be instantiated in practice anyways:

Arrays as parameters for attributes aren't CLSCompliant and can make things hard when reading metadata. See https://github.com/dotnet/runtime/issues/40461.

pavelsavara commented 2 years ago

Generating the JS code on the runtime is not CSP Compliant. Because the way how we materialize the code from string to Function instance. It is not CSP compliant same way as JS eval(). We have this problem already in Net6 and we don't improve it here.

In order to make the runtime CSP Compliant we would have to:

Alternatively we could do it only in link step of our AOT pipeline, when the developer has the wasm workload installed.

Edit: I removed JSFunction.New API from the proposal

Rest of this proposal is not making future compile-time gen of JS code more difficult than it already is.

My opinion is, that we should proceed with this for Net7 and improve on it incrementally.

pavelsavara commented 2 years ago
if (RuntimeInformation.IsOSPlatform("browser"))

I tested that you could use the new ref assembly to compile it also on other platforms. Trying to run it would produce PlatformNotSupportedException

pavelsavara commented 2 years ago

I resolved all comments which I think are resolved. Please comment again if you want to discuss it more, thank you.

pavelsavara commented 2 years ago

As the generic invocation and creation performance is not particularly good at this time when using AOT, at least with net6. Would using interfaces make more sense (one per type)?

Let's go with generic Func<> and Action<> for now. If we will have use-cases which are slow, we may optimize it.

SteveSandersonMS commented 2 years ago

Yes, there is also [JSExport()] without global name.

That's good, but the existence of JSExport with a global name is still problematic. If some .NET library uses it, then all applications consuming that library can no longer load multiple runtime instances in the same frame. A more risk averse choice would be prevention of polluting the global namespace.

I know this is largely still theoretical in Blazor's case since Blazor itself also relies on the global scope, but that's a direction we're trying to get away from.

pavelsavara commented 2 years ago

Yes, there is also [JSExport()] without global name.

That's good, but the existence of JSExport with a global name is still problematic.

OK, I'll remove it from the proposal and keep only [JSExport()].

terrajobst commented 2 years ago

Video

kg commented 2 years ago
* It seems there might be some benefits of using an established RPC envelope rather than a bespoke layout.

  * Are we sure that we're able to keep it a private implementation detail that we can change later?

Setting the benefits of established layouts aside, we need to keep in mind that if the best-case performance of this regresses too much versus the current state of things (due to overhead involved in conforming with an established envelope spec) people will resort to unsafe methods of C#<->JS interop like they do now, and we know that those can produce crashes.

I bet there's an envelope format out there that will perform well, but a lot of them are designed for different sorts of RPC (cross-process, language-agnostic, over the network, etc) and paying those costs could hurt us a lot in the long run, so we need to evaluate it carefully.

One of our consumers is Blazor and they already carefully designed their framework to reduce the amount of JS<->C# cross traffic to a minimum (due to their server hosted mode) but other consumers of the runtime may need to make lots of interop calls and we need to make sure our design does not doom us to have inadequate performance for those workloads.

pavelsavara commented 2 years ago
  • It seems there might be some benefits of using an established RPC envelope rather than a bespoke layout.
    • Are we sure that we're able to keep it a private implementation detail that we can change later?

This is about @AaronRobinsonMSFT's feedback at 59 minute of the call, about schema of the stack-frame buffer. I break it into 3 questions and my answers to them.

Is it ABI ?

Even if it's not ABI, could we still benefit from existing RPC protocol internally ?

Even if we write the flatbuffers messages by hand written code in the runtime

I can imagine that in the future, this interop could evolve to be able to marshal new types of parameters. We could always allocate chunk of memory and point to it from the proposed stack-frame slot. We do it already to marshal array of strings or array of proxies. I don't know if we want to support arbitrarily deep object graphs by reference in the future.

The user could also allocate buffer and encode any DTOs via user-space protobuf or json.

pavelsavara commented 2 years ago

Answers

pavelsavara commented 2 years ago

My TODO list I captured today, rewatching the meeting

pavelsavara commented 2 years ago

Pending open questions are

terrajobst commented 2 years ago

@pavelsavara I just wanted to say that your comments are great write-ups. Thanks for taking the time to share the action items as well as your thoughts & plans!

pavelsavara commented 2 years ago

All the feedback is now processed in the proposal as well as in the prototype. Ready for tomorrow.