Closed pavelsavara closed 2 years ago
Tagging subscribers to this area: @dotnet/interop-contrib See info in area-owners.md if you want to be subscribed.
Author: | pavelsavara |
---|---|
Assignees: | pavelsavara |
Labels: | `api-suggestion`, `area-System.Runtime.InteropServices`, `untriaged` |
Milestone: | - |
cc @kg @lewing @jkoritzinsky @marek-safar
Tagging subscribers to 'arch-wasm': @lewing See info in area-owners.md if you want to be subscribed.
Author: | pavelsavara |
---|---|
Assignees: | pavelsavara |
Labels: | `api-suggestion`, `arch-wasm`, `area-System.Runtime.InteropServices` |
Milestone: | 7.0.0 |
Updated with names unified to short JS
instead of JavaScript
as part of various names. Our legacy JSObject
sets the precedent. Thanks @maraf
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)?
cc: @dotnet/interop-contrib
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
@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.
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.
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", ...);
}
// 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.
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?
Could you also give examples of how the memory management works?
// 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?
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.
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.
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
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?
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.
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.
[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 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.
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.
What if
JSMarshalAs
used aType
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.
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.
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.
What if
JSMarshalAs
used aType
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 forJSType.Number | JSType.Unchecked
. We could haveJSType.AnyOf<JSType.Number, JSType.Boolean>
andJSType.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 likeJSType.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
andJSMarshalerArgument.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 perspectivenew 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:
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.
- 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.
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.
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.
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.
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.
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.
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.
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; }
}
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.
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:
*.js
files.JSFunction.New
API when not used. Non-trivial JS compiler tool chain changes..js
file and to be registered differently.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.
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
I resolved all comments which I think are resolved. Please comment again if you want to discuss it more, thank you.
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.
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.
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()]
.
string
.void
even if the JS side returns a value
[return: JSMarshalAs(JSType.Discard)]
might be a good way of doing that* 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.
- 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
MonoString*
MonoString*
as number on flatbuffer schema, what is the benefit of the added abstraction layer ?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.
Answers
JSType.NeverThrows
and eliminate try-catch
from the generated JS code.
JSExport
and generated C# wrapper.LibraryImport
attributes
normal pre existing js function
which the user would like to call from C#static partial method
in C# and annotates it generated C# code
which user doesn't ever need to seegenerated JS code
which user has no visibility tostring
or js number
or function
or Promise
dispose()
on it. You have to pass them back to managed, to use them.Span<byte>
memory view. It has API similar to Uint8Array
but it is not Uint8Array
. You can get Uint8Array
copy of bytes easily or you can do set on it.System.Object
on C# side and we will try our bestMy TODO list I captured today, rewatching the meeting
[StructLayout]
from ref assembly JSMarshalerArgument
to make it more opaque/privatevoid
contract on runtime, use JSType.Discard
as default
JSType.Void
to not generate the checkundefined
Discard
JSType.Unchecked
Task.CompleteTask
instead of null, doesn't work for Task<int>
and so it's better to keep it consistent.null
-> C# byte
-> 0
null
-> C# byte?
-> Nullable<byte>(null)
null
-> C# string
-> null
null
-> C# string?
-> null
null
-> C# Task
-> null
null
-> C# Task?
-> null
null
-> C# Task<int>
-> null
null
-> C# Task<int>?
-> null
JSType.AssertNotNull
ToManaged(out XXX? value)
Span
on the with methods returning Task
ManagedObject
, ManagedError
, ArraySegment
proxies and Span
view,
Span
after the callJSMarshalAs
annotation for types which are very obvious
string
, double
, exception
, bool
, void
in both directions. long
to js Number
in to JS directionNullable<>
and Task<>
of themstring[]
in both directionsbyte[]
, int[]
, double[]
in to JS directionSystem.Object
signature
NotImplementedException
rather than choose weak defaultPending open questions are
JSType
as enum flags or as System.Type
@lambdageek's proposalGetProperty
as extension methods, is that OK ?[MarshalUsing]
out of scope, because there is still incomming community feedback in progress ?@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!
All the feedback is now processed in the proposal as well as in the prototype. Ready for tomorrow.
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