Closed pavelsavara closed 2 years ago
This is @lambdageek's proposal applied to the examples. I didn't have time to implement the Roslyn parsing of it.
// 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(
[JSMarshalAs(typeof(JSType.Object))] IJSObject fetchResponse);
[JSImport("INTERNAL.http_wasm_fetch_bytes")]
[return: JSMarshalAs(typeof(JSType.Promise<JSType.Object>))]
private static partial Task<IJSObject> FetchBytes(
string uri,
string[] headerNames,
string[] headerValues,
string[] optionNames,
[JSMarshalAs(typeof(JSType.Array<JSType.Any>))] object?[] optionValues,
[JSMarshalAs(typeof(JSType.Object))] IJSObject abortControler,
IntPtr bodyPtr,
int bodyLength
);
[JSImport("INTERNAL.http_wasm_get_response_bytes")]
public static partial int GetResponseBytes(
[JSMarshalAs(typeof(JSType.Object))] IJSObject fetchResponse,
[JSMarshalAs(typeof(JSType.MemoryView))] Span<byte> buffer);
// from the rewrite of the runtime's implementation of WebSocket wrapper on WASM.
[JSImport("INTERNAL.ws_wasm_create")]
[return: JSMarshalAs(typeof(JSType.Object))]
public static partial IJSObject WebSocketCreate(
string uri,
string?[]? subProtocols,
[JSMarshalAs(typeof(JSType.Function<JSType.Number, JSType.String>))] Action<int, string> onClosed);
[JSImport("INTERNAL.ws_wasm_send")]
public static partial Task? WebSocketSend(
[JSMarshalAs(typeof(JSType.Object))] IJSObject webSocket,
[JSMarshalAs(typeof(JSType.MemoryView))] ArraySegment<byte> buffer,
int messageType,
bool endOfMessage);
// this is how to marshal strongly typed function
[JSImport("INTERNAL.create_function")]
[return: JSMarshalAs(typeof(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(
[JSMarshalAs(typeof(JSType.Promise<JSType.Number>))] 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";
}
I believe this is where we landed. Video
@pavelsavara @kg @bartonjs, did I miss anything?
JSType
System.Type
approach as opposed to enum
JSType.Array<JSType.Number>
you'd say JSType.Number[]
.JSMarshalAsAttribute
JSMarshalAs<JSType.Number>
namespace System.Runtime.InteropServices.JavaScript;
[AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = false)]
[SupportedOSPlatform("browser")]
public sealed class JSImportAttribute : Attribute
{
public string FunctionName { get; }
public JSImportAttribute(string functionName);
}
[AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = false)]
[SupportedOSPlatform("browser")]
public sealed class JSExportAttribute : Attribute
{
public JSExportAttribute();
}
[AttributeUsage(AttributeTargets.Parameter |
AttributeTargets.ReturnValue, Inherited = false, AllowMultiple = false)]
[SupportedOSPlatform("browser")]
public sealed class JSMarshalAsAttribute<T> : System.Attribute : where T: JSType
{
public JSMarshalAsAttribute();
}
[SupportedOSPlatform("browser")]
public abstract class JSType
{
internal JSType();
public sealed class None : JSType
{
internal None();
}
public sealed class Void : JSType
{
internal Void();
}
public sealed class Boolean : JSType
{
internal Boolean();
}
public sealed class Number : JSType
{
internal Number();
}
public sealed class Object : JSType
{
internal Object();
}
public sealed class Nullable<T> : JSType where T : JSType
{
internal Nullable();
}
public sealed class Function<T> : JSType where T : JSType
{
internal Function();
}
public sealed class Function<T1, T2> : JSType where T1 : JSType where T2 : JSType
{
internal Function();
}
public sealed class Array<T> : JSType where T : JSType
{
internal Array();
}
public sealed class Promise<T> : JSType where T : JSType
{
internal Promise();
}
// The other members on the enum-based design are left as exercise for the reader
}
[SupportedOSPlatform("browser")]
public abstract class JSObject : IDisposable
{
public bool IsDisposed { get; }
public bool HasProperty(string propertyName);
public string GetTypeOfProperty(string propertyName);
public bool? GetPropertyAsBoolean(string propertyName);
public int? GetPropertyAsInt32(string propertyName);
public double? GetPropertyAsDouble(string propertyName);
public string? GetPropertyAsString(string propertyName);
public JSObject? GetPropertyAsJSObject(string propertyName);
public void SetProperty(string propertyName, bool value);
public void SetProperty(string propertyName, int value);
public void SetProperty(string propertyName, double value);
public void SetProperty(string propertyName, string? value);
public void SetProperty(string propertyName, JSObject? value);
}
[SupportedOSPlatform("browser")]
public sealed class JSException : Exception
{
public JSException(string msg);
}
[SupportedOSPlatform("browser")]
public static class JSHost
{
public static JSObject GlobalThis { get; }
}
I updated it with things I remembered (removing JSHost.Import, removing the JSObject get/set using long
, and making the JSObject.SetProperty with "primitives values" not take nullable inputs.
I remember having an opinion that the GetProperty methods for primitives should either not return nullable, or add the word Nullable to their name, but don't think we settled on anything.
I would like to make JSHost.Import
work, because it's a way how I could allow C# developer to load ES6 module without manipulating .html
files or doing their own .js
files, for example in a component.
Currently I'm thinking to add Module parameter to the JSImportAttribute
like:
public partial class Foo
{
[JSImport("barMethod", "fooAlias")]
public static partial void BarMethod();
[JSImport("Goo.Foo.barMethod", "fooAlias")]
public static partial void BarFromNamespace();
}
public static void Main()
{
JSHost.Import("fooAlias", "http://my.com/foo.js")
Foo.BarMethod();
}
I like it because it allows the developer to construct the URL dynamically before first call.
I thought also about URL in the attribute. But it has a problem that it could NOT be downloaded (and bound) synchronously on the browser. [JSImport("barMethod", "http://my.com/foo.js")]
Any of it should be OK for CSP I think. Thoughts ? @SteveSandersonMS @kg
JSMarshalAsAttribute<T>
, [JSMarshalAs<>]
enforcement for all numbers except Int64
, also for JSObject
and for most Task<>
and arrays[MarshalUsing]
in
keyword for ToJS(XXX value)
methodsJSHost.Import()
and [JSImport(string functionName, string moduleName)]
JSImportAttribute.ModuleName
JSObject
public abstract class with HasProperty
, GetTypeOfProperty
, GetPropertyAsXXX
JSObject.GetPropertyAsBoolean
, GetPropertyAsInt32
, GetPropertyAsDouble
to not nullableJSObject.GetPropertyAsByteArray
and JSObject.SetProperty(, byte[] value)
JSHost.DotnetInstance
which is root of the runtime in ES6 module. We could have multiple runtimes on the JS page in the future.[JSMarshalAs<JSType.Any[]>]
doesn't work as JSType.Any[]
is not JSType
and so it doesn't fit the generic constraint JSMarshalAsAttribute<T> where T : JSType
.I learned that we could do TypeForwardedTo
for JSObject
, which allows me to make it not abstract
and still keep the old interop working. I updated the proposal with it.
JSHost
Import
should be ImportAsync
and take a CancellationToken
JSFunctionBinding
BindCSFunction
should be BindManagedFunction
namespace System.Runtime.InteropServices.JavaScript;
[AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = false)]
[SupportedOSPlatform("browser")]
public sealed class JSImportAttribute : Attribute
{
public JSImportAttribute(string functionName);
public JSImportAttribute(string functionName, string moduleName);
public string FunctionName { get; }
public string? ModuleName { get; }
}
[AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = false)]
[SupportedOSPlatform("browser")]
public sealed class JSExportAttribute : Attribute
{
public JSExportAttribute();
}
[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.ReturnValue, Inherited = false, AllowMultiple = false)]
[SupportedOSPlatform("browser")]
public sealed class JSMarshalAsAttribute<T> : Attribute where T : JSType
{
public JSMarshalAsAttribute();
}
[SupportedOSPlatform("browser")]
public abstract class JSType
{
internal JSType();
public sealed class None : JSType
{
internal None();
}
public sealed class Void : JSType
{
internal Void();
}
public sealed class Discard : JSType
{
internal Discard();
}
public sealed class Boolean : JSType
{
internal Boolean();
}
public sealed class Number : JSType
{
internal Number();
}
public sealed class BigInt : JSType
{
internal BigInt();
}
public sealed class Date : JSType
{
internal Date();
}
public sealed class String : JSType
{
internal String();
}
public sealed class Object : JSType
{
internal Object();
}
public sealed class Error : JSType
{
internal Error();
}
public sealed class MemoryView : JSType
{
internal MemoryView();
}
public sealed class Array<T> : JSType where T : JSType
{
internal Array();
}
public sealed class Promise<T> : JSType where T : JSType
{
internal Promise();
}
public sealed class Function : JSType
{
internal Function();
}
public sealed class Function<T> : JSType where T : JSType
{
internal Function();
}
public sealed class Function<T1, T2> : JSType where T1 : JSType where T2 : JSType
{
internal Function();
}
public sealed class Function<T1, T2, T3> : JSType where T1 : JSType where T2 : JSType where T3 : JSType
{
internal Function();
}
public sealed class Function<T1, T2, T3, T4> : JSType where T1 : JSType where T2 : JSType where T3 : JSType where T4 : JSType
{
internal Function();
}
public sealed class Any : JSType
{
internal Any();
}
}
[SupportedOSPlatform("browser")]
public class JSObject : IDisposable
{
internal JSObject();
public bool IsDisposed { get; }
public void Dispose();
public bool HasProperty(string propertyName);
public string GetTypeOfProperty(string propertyName);
public bool GetPropertyAsBoolean(string propertyName);
public int GetPropertyAsInt32(string propertyName);
public double GetPropertyAsDouble(string propertyName);
public string? GetPropertyAsString(string propertyName);
public JSObject? GetPropertyAsJSObject(string propertyName);
public byte[]? GetPropertyAsByteArray(string propertyName);
public void SetProperty(string propertyName, bool value);
public void SetProperty(string propertyName, int value);
public void SetProperty(string propertyName, double value);
public void SetProperty(string propertyName, string? value);
public void SetProperty(string propertyName, JSObject? value);
public void SetProperty(string propertyName, byte[]? value);
}
[SupportedOSPlatform("browser")]
public sealed class JSException : Exception
{
public JSException(string msg);
}
[SupportedOSPlatform("browser")]
public static class JSHost
{
public static JSObject GlobalThis { get; }
public static JSObject DotnetInstance { get; }
public static Task<JSObject> ImportAsync(string moduleName, string moduleUrl, CancellationToken cancellationToken = default);
}
[SupportedOSPlatform("browser")]
[CLSCompliant(false)]
[EditorBrowsable(EditorBrowsableState.Never)]
public sealed class JSFunctionBinding
{
public static void InvokeJS(JSFunctionBinding signature, Span<JSMarshalerArgument> arguments);
public static JSFunctionBinding BindJSFunction(string functionName, string moduleName, ReadOnlySpan<JSMarshalerType> signatures);
public static JSFunctionBinding BindManagedFunction(string fullyQualifiedName, int signatureHash, ReadOnlySpan<JSMarshalerType> signatures);
}
[SupportedOSPlatform("browser")]
[EditorBrowsable(EditorBrowsableState.Never)]
public sealed class JSMarshalerType
{
private JSMarshalerType();
public static JSMarshalerType Void { get; }
public static JSMarshalerType Discard { get; }
public static JSMarshalerType Boolean { get; }
public static JSMarshalerType Byte { get; }
public static JSMarshalerType Char { get; }
public static JSMarshalerType Int16 { get; }
public static JSMarshalerType Int32 { get; }
public static JSMarshalerType Int52 { get; }
public static JSMarshalerType BigInt64 { get; }
public static JSMarshalerType Double { get; }
public static JSMarshalerType Single { get; }
public static JSMarshalerType IntPtr { get; }
public static JSMarshalerType JSObject { get; }
public static JSMarshalerType Object { get; }
public static JSMarshalerType String { get; }
public static JSMarshalerType Exception { get; }
public static JSMarshalerType DateTime { get; }
public static JSMarshalerType DateTimeOffset { get; }
public static JSMarshalerType Nullable(JSMarshalerType primitive);
public static JSMarshalerType Task();
public static JSMarshalerType Task(JSMarshalerType result);
public static JSMarshalerType Array(JSMarshalerType element);
public static JSMarshalerType ArraySegment(JSMarshalerType element);
public static JSMarshalerType Span(JSMarshalerType element);
public static JSMarshalerType Action();
public static JSMarshalerType Action(JSMarshalerType arg1);
public static JSMarshalerType Action(JSMarshalerType arg1, JSMarshalerType arg2);
public static JSMarshalerType Action(JSMarshalerType arg1, JSMarshalerType arg2, JSMarshalerType arg3);
public static JSMarshalerType Function(JSMarshalerType result);
public static JSMarshalerType Function(JSMarshalerType arg1, JSMarshalerType result);
public static JSMarshalerType Function(JSMarshalerType arg1, JSMarshalerType arg2, JSMarshalerType result);
public static JSMarshalerType Function(JSMarshalerType arg1, JSMarshalerType arg2, JSMarshalerType arg3, JSMarshalerType result);
}
[SupportedOSPlatform("browser")]
[CLSCompliant(false)]
[EditorBrowsable(EditorBrowsableState.Never)]
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();
public void ToManaged(out bool value);
public void ToJS(bool value);
public void ToManaged(out bool? value);
public void ToJS(bool? value);
public void ToManaged(out byte value);
public void ToJS(byte value);
public void ToManaged(out byte? value);
public void ToJS(byte? value);
public void ToManaged(out byte[]? value);
public void ToJS(byte[]? value);
public void ToManaged(out char value);
public void ToJS(char value);
public void ToManaged(out char? value);
public void ToJS(char? value);
public void ToManaged(out short value);
public void ToJS(short value);
public void ToManaged(out short? value);
public void ToJS(short? value);
public void ToManaged(out int value);
public void ToJS(int value);
public void ToManaged(out int? value);
public void ToJS(int? value);
public void ToManaged(out int[]? value);
public void ToJS(int[]? value);
public void ToManaged(out long value);
public void ToJS(long value);
public void ToManaged(out long? value);
public void ToJS(long? value);
public void ToManagedBig(out long value);
public void ToJSBig(long value);
public void ToManagedBig(out long? value);
public void ToJSBig(long? value);
public void ToManaged(out float value);
public void ToJS(float value);
public void ToManaged(out float? value);
public void ToJS(float? value);
public void ToManaged(out double value);
public void ToJS(double value);
public void ToManaged(out double? value);
public void ToJS(double? value);
public void ToManaged(out double[]? value);
public void ToJS(double[]? value);
public void ToManaged(out IntPtr value);
public void ToJS(IntPtr value);
public void ToManaged(out IntPtr? value);
public void ToJS(IntPtr? value);
public void ToManaged(out DateTimeOffset value);
public void ToJS(DateTimeOffset value);
public void ToManaged(out DateTimeOffset? value);
public void ToJS(DateTimeOffset? value);
public void ToManaged(out DateTime value);
public void ToJS(DateTime value);
public void ToManaged(out DateTime? value);
public void ToJS(DateTime? value);
public void ToManaged(out string? value);
public void ToJS(string? value);
public void ToManaged(out string?[]? value);
public void ToJS(string?[]? value);
public void ToManaged(out Exception? value);
public void ToJS(Exception? value);
public void ToManaged(out object? value);
public void ToJS(object? value);
public void ToManaged(out object?[]? value);
public void ToJS(object?[]? value);
public void ToManaged(out JSObject? value);
public void ToJS(JSObject? value);
public void ToManaged(out JSObject?[]? value);
public void ToJS(JSObject?[]? value);
public void ToManaged(out Task? value);
public void ToJS(Task? value);
public void ToManaged<T>(out Task<T>? value, ArgumentToManagedCallback<T> marshaler);
public void ToJS<T>(Task<T>? value, ArgumentToJSCallback<T> marshaler);
public void ToManaged(out Action? value);
public void ToJS(Action? value);
public void ToManaged<T>(out Action<T>? value, ArgumentToJSCallback<T> arg1Marshaler);
public void ToJS<T>(Action<T>? value, ArgumentToManagedCallback<T> arg1Marshaler);
public void ToManaged<T1, T2>(out Action<T1, T2>? value, ArgumentToJSCallback<T1> arg1Marshaler, ArgumentToJSCallback<T2> arg2Marshaler);
public void ToJS<T1, T2>(Action<T1, T2>? value, ArgumentToManagedCallback<T1> arg1Marshaler, ArgumentToManagedCallback<T2> arg2Marshaler);
public void ToManaged<T1, T2, T3>(out Action<T1, T2, T3>? value, ArgumentToJSCallback<T1> arg1Marshaler, ArgumentToJSCallback<T2> arg2Marshaler, ArgumentToJSCallback<T3> arg3Marshaler);
public void ToJS<T1, T2, T3>(Action<T1, T2, T3>? value, ArgumentToManagedCallback<T1> arg1Marshaler, ArgumentToManagedCallback<T2> arg2Marshaler, ArgumentToManagedCallback<T3> arg3Marshaler);
public void ToManaged<TResult>(out Func<TResult>? value, ArgumentToManagedCallback<TResult> resMarshaler);
public void ToJS<TResult>(Func<TResult>? value, ArgumentToJSCallback<TResult> resMarshaler);
public void ToManaged<T, TResult>(out Func<T, TResult>? value, ArgumentToJSCallback<T> arg1Marshaler, ArgumentToManagedCallback<TResult> resMarshaler);
public void ToJS<T, TResult>(Func<T, TResult>? value, ArgumentToManagedCallback<T> arg1Marshaler, ArgumentToJSCallback<TResult> resMarshaler);
public void ToManaged<T1, T2, TResult>(out Func<T1, T2, TResult>? value, ArgumentToJSCallback<T1> arg1Marshaler, ArgumentToJSCallback<T2> arg2Marshaler, ArgumentToManagedCallback<TResult> resMarshaler);
public void ToJS<T1, T2, TResult>(Func<T1, T2, TResult>? value, ArgumentToManagedCallback<T1> arg1Marshaler, ArgumentToManagedCallback<T2> arg2Marshaler, ArgumentToJSCallback<TResult> resMarshaler);
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);
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);
public unsafe void ToManaged(out void* value);
public unsafe void ToJS(void* value);
public void ToManaged(out Span<byte> value);
public void ToJS(Span<byte> value);
public void ToManaged(out ArraySegment<byte> value);
public void ToJS(ArraySegment<byte> value);
public void ToManaged(out Span<int> value);
public void ToJS(Span<int> value);
public void ToManaged(out Span<double> value);
public void ToJS(Span<double> value);
public void ToManaged(out ArraySegment<int> value);
public void ToJS(ArraySegment<int> value);
public void ToManaged(out ArraySegment<double> value);
public void ToJS(ArraySegment<double> value);
}
Some question to the naming:
Why INTERNAL.ws_wasm_send
?
See https://developer.mozilla.org/en-US/docs/MDN/Guidelines/Code_guidelines/JavaScript, https://google.github.io/styleguide/jsguide.html, shouldn't this be something like wsWasmSend
? Every JavaScript style guide I saw is lowerCamelCase instead of snake_case.
Why
INTERNAL.ws_wasm_send
This is sample from mono internal functions. Mono follows snake_case even in JS codebase, same as in C codebase.
Mono follows snake_case even in JS codebase
This should be a sin...
I have relatively little to do with this proposal but I would be interested to know if this is only Mono related or if WASM/JS support is also planned for the CoreCLR in the long term?
I have relatively little to do with this proposal but I would be interested to know if this is only Mono related or if WASM/JS support is also planned for the CoreCLR in the long term?
Our current approach with unifying Mono and CoreCLR has been to use whichever runtime best fits a particular environment or use-case while providing a single set of libraries and APIs that can be used on either runtime. It's conceivable that as WebAssembly matures and gains features, the decision whether Mono's strengths (portability, a fast interpreter, fewer or more flexible expectations of the underlying host platform) still make sense on wasm may shift.
I see I am jumping in here fashionably late, but I am worried about how testable a Blazor component that uses [JSImpor]
and [JSExport]
directly will be in bUnit tests (tests that only run in C#). bUnit tests does not run any browser, only runs the C# parts of a Blazor component.
With the current Blazor JavaScript interop options, there is a family of IJSRuntime
interfaces that testers can mock to emulate the interaction with JavaScript. It is not clear to me how it is possible to mock the JavaScript interaction when using [JSImpor]
and [JSExport]
directly in a component under test.
cc. @SteveSandersonMS
[JSImport]
and [JSExport]
are comparable to [DllImport]
or [LibraryImport]
. They are not intended to be directly mockable on runtime level.
We are aware of that issue in Blazor. I attempted to replace the internal usage of old interop in Blazor here https://github.com/dotnet/aspnetcore/pull/41665, and run into that same mocking problem too.
We will have to wrap these new static methods into component with interface that could be mocked on higher level. It would mean that the components would not be dependent on IJSUnmarshalledRuntime
anymore. And therefore not mockable by replacing it. It's probably too late to do large changes in Net7 now and that's why we postponed it.
In my opinion the design of hiding whole platform behind single low level interface should not have been public API in the first place. There were probably historic reasons for that design, thought.
@egil I would like to hear more about your use cases. Are they integration tests ? If so, could you replace your dependencies on higher level ? Mock blazor components, instead of mocking the runtime. You are testing your application code after all, not the Blazor code, right ?
@egil I would like to hear more about your use cases. Are they integration tests ? If so, could you replace your dependencies on higher level ? Mock blazor components, instead of mocking the runtime. You are testing your application code after all, not the Blazor code, right ?
bUnit (https://bunit.dev) allows you to render a component with parameters (or a RenderFragment), inspect the component instance, the produced markup, and invoke event handlers in the component, inject services into components under test, among other things.
The services that a Blazor component depends on can thus be replaced during testing if the service allows this, like IJSRuntime
.
I.e., bUnit is about testing component logic and markup.
Its up to the tester whether they are testing a single component (unit testing) or a deep render tree (integration testing).
bUnit is different from Playwright or Selenium it runs entirely in C#, there is no browser involved.
Let me know if you need more details. bunit.dev docs section is also full of samples if you want to learn more.
I see I am jumping in here fashionably late, but I am worried about how testable a Blazor component that uses
[JSImpor]
and[JSExport]
directly will be in bUnit tests (tests that only run in C#). bUnit tests does not run any browser, only runs the C# parts of a Blazor component.With the current Blazor JavaScript interop options, there is a family of
IJSRuntime
interfaces that testers can mock to emulate the interaction with JavaScript. It is not clear to me how it is possible to mock the JavaScript interaction when using[JSImpor]
and[JSExport]
directly in a component under test.cc. @SteveSandersonMS
Mocking the underlying platform interfaces in this way is not feasible without imposing a performance tax on everyone who uses the platform interfaces, much like the P/Invoke examples that Pavel cited above. It's unfortunate that you're losing the ability to do this, but it was the wrong approach to begin with, so it is unlikely you will be able to get it back.
One option would be to do IL rewriting to insert shims, I believe there is tooling out there for this. I wouldn't advise it however because I think it would need to apply rewriting to the bcl and blazor.
If you want to run tests in C# without the wasm/js infrastructure underneath at all, I think the correct solution would be to replace the wasm source generator with one that generates a mocking compatible implementation. This would not be trivial, but since the wasm source generator is open source and available in the repo, you could perhaps modify it.
Mocking the underlying platform interfaces in this way is not feasible without imposing a performance tax on everyone who uses the platform interfaces, much like the P/Invoke examples that Pavel cited above. It's unfortunate that you're losing the ability to do this, but it was the wrong approach to begin with, so it is unlikely you will be able to get it back.
I hear what you are saying. That said, with Blazor apps this feature is going to be used quite a bit (people will use this instead of IJSRuntime
). P/Invoke is used much less by comparison, and it is less of a burden to hide P/Invoke code behind an abstraction that can be replaced at runtime.
It's going to be more annoying to Blazor devs to do this, but that would be the recommended approach, i.e., wrap your JSInvoke code into a mockable type and don't use the JSImport directly in components.
But I get this is the general purpose wasm runtime in .net and not Blazor specific, and perf is paramount.
One option would be to do IL rewriting to insert shims, I believe there is tooling out there for this. I wouldn't advise it however because I think it would need to apply rewriting to the bcl and blazor.
If you want to run tests in C# without the wasm/js infrastructure underneath at all, I think the correct solution would be to replace the wasm source generator with one that generates a mocking compatible implementation. This would not be trivial, but since the wasm source generator is open source and available in the repo, you could perhaps modify it.
Would it be possible to define a custom "os platform", [Versioning.SupportedOSPlatform("bunit")]
, and have bUnit include the needed implementation that would intercept the calls?
Blazor apps this feature is going to be used quite a bit (people will use this instead of IJSRuntime)
These new APIs should only be used as a replacement for IJSUnmarshalledRuntime
, which is not very commonly used.
We wouldn't advise people to stop using IJSRuntime
because then they will lose portability of their code. IJSRuntime
should be the preferred choice for Blazor devs in the great majority of cases because then they can seamlessly work across WebAssembly, Server, and WebView.
Blazor apps this feature is going to be used quite a bit (people will use this instead of IJSRuntime)
These new APIs should only be used as a replacement for
IJSUnmarshalledRuntime
, which is not very commonly used.We wouldn't advise people to stop using
IJSRuntime
because then they will lose portability of their code.IJSRuntime
should be the preferred choice for Blazor devs in the great majority of cases because then they can seamlessly work across WebAssembly, Server, and WebView.
Ahh ok. That makes it much less of an issue. In that case I don't mind telling bUnit users that they should wrap their JSImport code in something they can mock if they want to test code that uses it.
Thanks for addressing my concerns everyone.
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:
JSImportAttribute
orJSExportAttribute
. We re-use common code gen infrastructure from[LibraryImport]
JSType.BigInt
or asJSType.Number
, configurable per parameter viaJSMarshalAsAttribute
similar toMarshalAsAttribute
of P/InvokeString
,Boolean
,DateTime
,DateTimeOffset
,Exception
System.Object
with mapping to well known types for some instance types and proxy viaGCHandle
for the rest.JSObject
with private legacy implementationJSObject
, which is proxy via existingJSHandle
concept similar toGCHandle
Task
,Func
,Action
byte[]
,int[]
,double[]
Span<byte>
,Span<int>
,Span<double>
andArraySegment<byte>
,ArraySegment<int>
,ArraySegment<double>
[MarshalUsing(typeof(NativeMarshaler))]
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
API Proposal
Below are types which drive the code generator
Below are types for working with JavaScript instances
Below types are used by the generated code
API Usage
Trivial example
This is code generated by Roslyn, simplified for brevity
This will be generated on the runtime for the JavaScript marshaling stub
More examples
Alternative Designs
Open questions:
we consider that maybe we could marshal more dynamic combinations of parameters in the future. JavaScript is dynamic language after all. We madeJSType
as flags to prepare for it as it would be difficult to change in the future.Should we haveansweredGetProperty
andSetProperty
directly on theJSObject
TheansweredJSMarshalerArgument
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 madeansweredJSMarshalerArgument.ToManaged(out Task value)
non-nullable, but in fact you can pass null Promise. Reason: forcing user to check null before callingawait
felt akward. Passing null promise is useful on synchronous returns from JS.Risks