Background and motivation
When .NET is running on WebAssembly, for example as part of Blazor, developers may want to interact with the browser's JavaScript engine and JS code. Currently we don't have public C# API to do so.
We propose this API together with prototype of the implementation.
Key features are:
- generate C# side of the marshaling stub in as partial method, Roslyn analyzer triggered by
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 of P/Invoke - there is no way to create a JS instance solely by manipulations WASM memory. The marshaling depends on a library of JS helper routines to create JS values and manipulate JS object properties. That's why we need to generate JS code too.
- generate JS side of the marshaling on runtime, to decrease download size. Provide necessary metadata during method binding.
- marshaled types are:
- subset of primitive numeric types and their nullable alternative
String, Boolean, DateTime, DateTimeOffset, Exception- dynamic marshaling of
System.Object with mapping to well known types for some instance types and proxy via GCHandle for the rest. JSObject with private legacy implementation JSObject, which is proxy via existing JSHandle concept similar to GCHandleTask, Func, Actionbyte[], int[], double[]Span<byte>, Span<int>, Span<double> and ArraySegment<byte>, ArraySegment<int>, ArraySegment<double>- Custom P/Invoke marshaler with
[MarshalUsing(typeof(NativeMarshaler))]
- we have 2 garbage collectors to worry about
- we do have existing private interop in
System.Private.Runtime.InteropServices.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
namespaceSystem.Runtime.InteropServices.JavaScript;[System.AttributeUsageAttribute(System.AttributeTargets.Method,Inherited=false,AllowMultiple=false)][Versioning.SupportedOSPlatform("browser")]publicsealedclassJSImportAttribute:System.Attribute{publicstringFunctionName{get;}publicstringModuleName{get;}publicJSImportAttribute(stringfunctionName)=>thrownull;publicJSImportAttribute(stringfunctionName,stringmoduleName)=>thrownull;}[System.AttributeUsageAttribute(System.AttributeTargets.Method,Inherited=false,AllowMultiple=false)][Versioning.SupportedOSPlatform("browser")]publicsealedclassJSExportAttribute:System.Attribute{publicJSExportAttribute()=>thrownull;}// this is used to annotate the marshaled parameters[System.AttributeUsageAttribute(System.AttributeTargets.Parameter|System.AttributeTargets.ReturnValue,Inherited=false,AllowMultiple=false)][Versioning.SupportedOSPlatform("browser")]publicsealedclassJSMarshalAsAttribute<T>:System.AttributewhereT:JSType{publicJSMarshalAsAttribute()=>thrownull;}[Versioning.SupportedOSPlatform("browser")]publicabstractclassJSType{internalJSType()=>thrownull;publicsealedclassNone:JSType{internalNone()=>thrownull;}publicsealedclassVoid:JSType{internalVoid()=>thrownull;}publicsealedclassDiscard:JSType{internalDiscard()=>thrownull;}publicsealedclassBoolean:JSType{internalBoolean()=>thrownull;}publicsealedclassNumber:JSType{internalNumber()=>thrownull;}publicsealedclassBigInt:JSType{internalBigInt()=>thrownull;}publicsealedclassDate:JSType{internalDate()=>thrownull;}publicsealedclassString:JSType{internalString()=>thrownull;}publicsealedclassObject:JSType{internalObject()=>thrownull;}publicsealedclassError:JSType{internalError()=>thrownull;}publicsealedclassMemoryView:JSType{internalMemoryView()=>thrownull;}publicsealedclassArray<T>:JSTypewhereT:JSType{internalArray()=>thrownull;}publicsealedclassPromise<T>:JSTypewhereT:JSType{internalPromise()=>thrownull;}publicsealedclassFunction:JSType{internalFunction()=>thrownull;}publicsealedclassFunction<T>:JSTypewhereT:JSType{internalFunction()=>thrownull;}publicsealedclassFunction<T1,T2>:JSTypewhereT1:JSTypewhereT2:JSType{internalFunction()=>thrownull;}publicsealedclassFunction<T1,T2,T3>:JSTypewhereT1:JSTypewhereT2:JSTypewhereT3:JSType{internalFunction()=>thrownull;}publicsealedclassFunction<T1,T2,T3,T4>:JSTypewhereT1:JSTypewhereT2:JSTypewhereT3:JSTypewhereT4:JSType{internalFunction()=>thrownull;}publicsealedclassAny:JSType{internalAny()=>thrownull;}}Below are types for working with JavaScript instances
namespaceSystem.Runtime.InteropServices.JavaScript;[Versioning.SupportedOSPlatform("browser")]publicclassJSObject:System.IDisposable{internalJSObject()=>thrownull;publicboolIsDisposed{get=>thrownull;}publicvoidDispose()=>thrownull;publicboolHasProperty(stringpropertyName)=>thrownull;publicstringGetTypeOfProperty(stringpropertyName)=>thrownull;publicboolGetPropertyAsBoolean(stringpropertyName)=>thrownull;publicintGetPropertyAsInt32(stringpropertyName)=>thrownull;publicdoubleGetPropertyAsDouble(stringpropertyName)=>thrownull;publicstring?GetPropertyAsString(stringpropertyName)=>thrownull;publicJSObject?GetPropertyAsJSObject(stringpropertyName)=>thrownull;publicbyte[]?GetPropertyAsByteArray(stringpropertyName)=>thrownull;publicvoidSetProperty(stringpropertyName,boolvalue)=>thrownull;publicvoidSetProperty(stringpropertyName,intvalue)=>thrownull;publicvoidSetProperty(stringpropertyName,doublevalue)=>thrownull;publicvoidSetProperty(stringpropertyName,string?value)=>thrownull;publicvoidSetProperty(stringpropertyName,JSObject?value)=>thrownull;publicvoidSetProperty(stringpropertyName,byte[]?value)=>thrownull;}// when we marshal JS Error type[Versioning.SupportedOSPlatform("browser")]publicsealedclassJSException:System.Exception{publicJSException(stringmsg)=>thrownull;}[Versioning.SupportedOSPlatform("browser")]publicstaticclassJSHost{publicstaticJSObjectGlobalThis{get=>thrownull;}publicstaticJSObjectDotnetInstance{get=>thrownull;}publicstaticSystem.Threading.Tasks.Task<JSObject>Import(stringmoduleName,stringmoduleUrl)=>thrownull;}Below types are used by the generated code
[Versioning.SupportedOSPlatform("browser")][CLSCompliant(false)][System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)]// to bind and call methodspublicsealedclassJSFunctionBinding{publicstaticvoidInvokeJS(JSFunctionBindingsignature,Span<JSMarshalerArgument>arguments)=>thrownull;publicstaticJSFunctionBindingBindJSFunction(stringfunctionName,stringmoduleName,System.ReadOnlySpan<JSMarshalerType>signatures)=>thrownull;publicstaticJSFunctionBindingBindCSFunction(stringfullyQualifiedName,intsignatureHash,System.ReadOnlySpan<JSMarshalerType>signatures)=>thrownull;}[Versioning.SupportedOSPlatform("browser")][System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)]// to create binding metadatapublicsealedclassJSMarshalerType{privateJSMarshalerType()=>thrownull;publicstaticJSMarshalerTypeVoid{get=>thrownull;}publicstaticJSMarshalerTypeDiscard{get=>thrownull;}publicstaticJSMarshalerTypeBoolean{get=>thrownull;}publicstaticJSMarshalerTypeByte{get=>thrownull;}publicstaticJSMarshalerTypeChar{get=>thrownull;}publicstaticJSMarshalerTypeInt16{get=>thrownull;}publicstaticJSMarshalerTypeInt32{get=>thrownull;}publicstaticJSMarshalerTypeInt52{get=>thrownull;}publicstaticJSMarshalerTypeBigInt64{get=>thrownull;}publicstaticJSMarshalerTypeDouble{get=>thrownull;}publicstaticJSMarshalerTypeSingle{get=>thrownull;}publicstaticJSMarshalerTypeIntPtr{get=>thrownull;}publicstaticJSMarshalerTypeJSObject{get=>thrownull;}publicstaticJSMarshalerTypeObject{get=>thrownull;}publicstaticJSMarshalerTypeString{get=>thrownull;}publicstaticJSMarshalerTypeException{get=>thrownull;}publicstaticJSMarshalerTypeDateTime{get=>thrownull;}publicstaticJSMarshalerTypeDateTimeOffset{get=>thrownull;}publicstaticJSMarshalerTypeNullable(JSMarshalerTypeprimitive)=>thrownull;publicstaticJSMarshalerTypeTask()=>thrownull;publicstaticJSMarshalerTypeTask(JSMarshalerTyperesult)=>thrownull;publicstaticJSMarshalerTypeArray(JSMarshalerTypeelement)=>thrownull;publicstaticJSMarshalerTypeArraySegment(JSMarshalerTypeelement)=>thrownull;publicstaticJSMarshalerTypeSpan(JSMarshalerTypeelement)=>thrownull;publicstaticJSMarshalerTypeAction()=>thrownull;publicstaticJSMarshalerTypeAction(JSMarshalerTypearg1)=>thrownull;publicstaticJSMarshalerTypeAction(JSMarshalerTypearg1,JSMarshalerTypearg2)=>thrownull;publicstaticJSMarshalerTypeAction(JSMarshalerTypearg1,JSMarshalerTypearg2,JSMarshalerTypearg3)=>thrownull;publicstaticJSMarshalerTypeFunction(JSMarshalerTyperesult)=>thrownull;publicstaticJSMarshalerTypeFunction(JSMarshalerTypearg1,JSMarshalerTyperesult)=>thrownull;publicstaticJSMarshalerTypeFunction(JSMarshalerTypearg1,JSMarshalerTypearg2,JSMarshalerTyperesult)=>thrownull;publicstaticJSMarshalerTypeFunction(JSMarshalerTypearg1,JSMarshalerTypearg2,JSMarshalerTypearg3,JSMarshalerTyperesult)=>thrownull;}[Versioning.SupportedOSPlatform("browser")][CLSCompliant(false)][System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)]// actual marshalerspublicstructJSMarshalerArgument{publicdelegatevoidArgumentToManagedCallback<T>(refJSMarshalerArgumentarg,outTvalue);publicdelegatevoidArgumentToJSCallback<T>(refJSMarshalerArgumentarg,Tvalue);publicvoidInitialize()=>thrownull;publicvoidToManaged(outboolvalue)=>thrownull;publicvoidToJS(boolvalue)=>thrownull;publicvoidToManaged(outbool?value)=>thrownull;publicvoidToJS(bool?value)=>thrownull;publicvoidToManaged(outbytevalue)=>thrownull;publicvoidToJS(bytevalue)=>thrownull;publicvoidToManaged(outbyte?value)=>thrownull;publicvoidToJS(byte?value)=>thrownull;publicvoidToManaged(outbyte[]?value)=>thrownull;publicvoidToJS(byte[]?value)=>thrownull;publicvoidToManaged(outcharvalue)=>thrownull;publicvoidToJS(charvalue)=>thrownull;publicvoidToManaged(outchar?value)=>thrownull;publicvoidToJS(char?value)=>thrownull;publicvoidToManaged(outshortvalue)=>thrownull;publicvoidToJS(shortvalue)=>thrownull;publicvoidToManaged(outshort?value)=>thrownull;publicvoidToJS(short?value)=>thrownull;publicvoidToManaged(outintvalue)=>thrownull;publicvoidToJS(intvalue)=>thrownull;publicvoidToManaged(outint?value)=>thrownull;publicvoidToJS(int?value)=>thrownull;publicvoidToManaged(outint[]?value)=>thrownull;publicvoidToJS(int[]?value)=>thrownull;publicvoidToManaged(outlongvalue)=>thrownull;publicvoidToJS(longvalue)=>thrownull;publicvoidToManaged(outlong?value)=>thrownull;publicvoidToJS(long?value)=>thrownull;publicvoidToManagedBig(outlongvalue)=>thrownull;publicvoidToJSBig(longvalue)=>thrownull;publicvoidToManagedBig(outlong?value)=>thrownull;publicvoidToJSBig(long?value)=>thrownull;publicvoidToManaged(outfloatvalue)=>thrownull;publicvoidToJS(floatvalue)=>thrownull;publicvoidToManaged(outfloat?value)=>thrownull;publicvoidToJS(float?value)=>thrownull;publicvoidToManaged(outdoublevalue)=>thrownull;publicvoidToJS(doublevalue)=>thrownull;publicvoidToManaged(outdouble?value)=>thrownull;publicvoidToJS(double?value)=>thrownull;publicvoidToManaged(outdouble[]?value)=>thrownull;publicvoidToJS(double[]?value)=>thrownull;publicvoidToManaged(outIntPtrvalue)=>thrownull;publicvoidToJS(IntPtrvalue)=>thrownull;publicvoidToManaged(outIntPtr?value)=>thrownull;publicvoidToJS(IntPtr?value)=>thrownull;publicvoidToManaged(outDateTimeOffsetvalue)=>thrownull;publicvoidToJS(DateTimeOffsetvalue)=>thrownull;publicvoidToManaged(outDateTimeOffset?value)=>thrownull;publicvoidToJS(DateTimeOffset?value)=>thrownull;publicvoidToManaged(outDateTimevalue)=>thrownull;publicvoidToJS(DateTimevalue)=>thrownull;publicvoidToManaged(outDateTime?value)=>thrownull;publicvoidToJS(DateTime?value)=>thrownull;publicvoidToManaged(outstring?value)=>thrownull;publicvoidToJS(string?value)=>thrownull;publicvoidToManaged(outstring?[]?value)=>thrownull;publicvoidToJS(string?[]?value)=>thrownull;publicvoidToManaged(outException?value)=>thrownull;publicvoidToJS(Exception?value)=>thrownull;publicvoidToManaged(outobject?value)=>thrownull;publicvoidToJS(object?value)=>thrownull;publicvoidToManaged(outobject?[]?value)=>thrownull;publicvoidToJS(object?[]?value)=>thrownull;publicvoidToManaged(outJSObject?value)=>thrownull;publicvoidToJS(JSObject?value)=>thrownull;publicvoidToManaged(outJSObject?[]?value)=>thrownull;publicvoidToJS(JSObject?[]?value)=>thrownull;publicvoidToManaged(outSystem.Threading.Tasks.Task?value)=>thrownull;publicvoidToJS(System.Threading.Tasks.Task?value)=>thrownull;publicvoidToManaged<T>(outSystem.Threading.Tasks.Task<T>?value,ArgumentToManagedCallback<T>marshaler)=>thrownull;publicvoidToJS<T>(System.Threading.Tasks.Task<T>?value,ArgumentToJSCallback<T>marshaler)=>thrownull;publicvoidToManaged(outAction?value)=>thrownull;publicvoidToJS(Action?value)=>thrownull;publicvoidToManaged<T>(outAction<T>?value,ArgumentToJSCallback<T>arg1Marshaler)=>thrownull;publicvoidToJS<T>(Action<T>?value,ArgumentToManagedCallback<T>arg1Marshaler)=>thrownull;publicvoidToManaged<T1,T2>(outAction<T1,T2>?value,ArgumentToJSCallback<T1>arg1Marshaler,ArgumentToJSCallback<T2>arg2Marshaler)=>thrownull;publicvoidToJS<T1,T2>(Action<T1,T2>?value,ArgumentToManagedCallback<T1>arg1Marshaler,ArgumentToManagedCallback<T2>arg2Marshaler)=>thrownull;publicvoidToManaged<T1,T2,T3>(outAction<T1,T2,T3>?value,ArgumentToJSCallback<T1>arg1Marshaler,ArgumentToJSCallback<T2>arg2Marshaler,ArgumentToJSCallback<T3>arg3Marshaler)=>thrownull;publicvoidToJS<T1,T2,T3>(Action<T1,T2,T3>?value,ArgumentToManagedCallback<T1>arg1Marshaler,ArgumentToManagedCallback<T2>arg2Marshaler,ArgumentToManagedCallback<T3>arg3Marshaler)=>thrownull;publicvoidToManaged<TResult>(outFunc<TResult>?value,ArgumentToManagedCallback<TResult>resMarshaler)=>thrownull;publicvoidToJS<TResult>(Func<TResult>?value,ArgumentToJSCallback<TResult>resMarshaler)=>thrownull;publicvoidToManaged<T,TResult>(outFunc<T,TResult>?value,ArgumentToJSCallback<T>arg1Marshaler,ArgumentToManagedCallback<TResult>resMarshaler)=>thrownull;publicvoidToJS<T,TResult>(Func<T,TResult>?value,ArgumentToManagedCallback<T>arg1Marshaler,ArgumentToJSCallback<TResult>resMarshaler)=>thrownull;publicvoidToManaged<T1,T2,TResult>(outFunc<T1,T2,TResult>?value,ArgumentToJSCallback<T1>arg1Marshaler,ArgumentToJSCallback<T2>arg2Marshaler,ArgumentToManagedCallback<TResult>resMarshaler)=>thrownull;publicvoidToJS<T1,T2,TResult>(Func<T1,T2,TResult>?value,ArgumentToManagedCallback<T1>arg1Marshaler,ArgumentToManagedCallback<T2>arg2Marshaler,ArgumentToJSCallback<TResult>resMarshaler)=>thrownull;publicvoidToManaged<T1,T2,T3,TResult>(outFunc<T1,T2,T3,TResult>?value,ArgumentToJSCallback<T1>arg1Marshaler,ArgumentToJSCallback<T2>arg2Marshaler,ArgumentToJSCallback<T3>arg3Marshaler,ArgumentToManagedCallback<TResult>resMarshaler)=>thrownull;publicvoidToJS<T1,T2,T3,TResult>(Func<T1,T2,T3,TResult>?value,ArgumentToManagedCallback<T1>arg1Marshaler,ArgumentToManagedCallback<T2>arg2Marshaler,ArgumentToManagedCallback<T3>arg3Marshaler,ArgumentToJSCallback<TResult>resMarshaler)=>thrownull;publicunsafevoidToManaged(outvoid*value)=>thrownull;publicunsafevoidToJS(void*value)=>thrownull;publicvoidToManaged(outSpan<byte>value)=>thrownull;publicvoidToJS(Span<byte>value)=>thrownull;publicvoidToManaged(outArraySegment<byte>value)=>thrownull;publicvoidToJS(ArraySegment<byte>value)=>thrownull;publicvoidToManaged(outSpan<int>value)=>thrownull;publicvoidToJS(Span<int>value)=>thrownull;publicvoidToManaged(outSpan<double>value)=>thrownull;publicvoidToJS(Span<double>value)=>thrownull;publicvoidToManaged(outArraySegment<int>value)=>thrownull;publicvoidToJS(ArraySegment<int>value)=>thrownull;publicvoidToManaged(outArraySegment<double>value)=>thrownull;publicvoidToJS(ArraySegment<double>value)=>thrownull;}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 marshaledinternalstaticpartialvoidLog(// this one will marshal C# string to JavaScript native string by value (with some optimizations)stringmessage);This is code generated by Roslyn, simplified for brevity
[System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Interop.JavaScript.JSImportGenerator","42.42.42.42")]publicstaticpartialvoidLog(stringmessage){if(__signature_Log_20494476==null){__signature_Log_20494476=JSFunctionBinding.BindJSFunction("console.log",null,newJSMarshalerType[]{JSMarshalerType.Discard,JSMarshalerType.String});}System.Span<JSMarshalerArgument>__arguments_buffer=stackallocJSMarshalerArgument[3];refJSMarshalerArgument__arg_exception=ref__arguments_buffer[0];__arg_exception.Initialize();refJSMarshalerArgument__arg_return=ref__arguments_buffer[1];__arg_return.Initialize();refJSMarshalerArgument__message_native__js_arg=ref__arguments_buffer[2];__message_native__js_arg.ToJS(inmessage);// this will also marshal exceptionJSFunctionBinding.InvokeJS(__signature_Log_20494476,__arguments_buffer);}staticvolatileJSFunctionBinding__signature_Log_20494476;This will be generated on the runtime for the JavaScript marshaling stub
functionfactory(closure){//# sourceURL=https://mono-wasm.invalid/_bound_js_console_logconst{ signature, fn, marshal_exception_to_cs, converter2 }=closure;returnfunction_bound_js_console_log(args){try{constarg0=converter2(args+32,signature+72);// String// fn is reference to console.log hereconstjs_result=fn(arg0);if(js_result!==undefined)thrownewError('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")]privatestaticpartialstring[]_GetResponseHeaderNames(JSObjectfetchResponse);[JSImport("INTERNAL.http_wasm_fetch_bytes")]privatestaticpartialTask<JSObject>FetchBytes(stringuri,string[]headerNames,string[]headerValues,string[]optionNames,[JSMarshalAs<JSType.Array<JSType.Any>]object?[]optionValues,JSObjectabortControler,IntPtrbodyPtr,intbodyLength);[JSImport("INTERNAL.http_wasm_get_response_bytes")]publicstaticpartialintGetResponseBytes(JSObjectfetchResponse,[JSMarshalAs<JSType.MemoryView>]Span<byte>buffer);// from the rewrite of the runtime's implementation of WebSocket wrapper on WASM.[JSImport("INTERNAL.ws_wasm_create")]publicstaticpartialJSObjectWebSocketCreate(stringuri,string?[]?subProtocols,[JSMarshalAs<JSType.Function<JSType.Number,JSType.String>>]Action<int,string>onClosed);[JSImport("INTERNAL.ws_wasm_send")]publicstaticpartialTask?WebSocketSend(JSObjectwebSocket,[JSMarshalAs<JSType.MemoryView>]ArraySegment<byte>buffer,intmessageType,boolendOfMessage);// this is how to marshal strongly typed function[JSImport("INTERNAL.create_function")][return:JSMarshalAs<JSType.Function<JSType.Number,JSType.Number,JSType.Number>]publicstaticpartialFunc<double,double,double>CreateFunctionDoubleDoubleDouble(stringarg1Name,stringarg2Name,stringcode);// 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]publicstaticasyncTask<string>SlowFailure(Task<int>promisedNumber){vardelayMs=awaitpromisedNumber;// this would be marshled as JS promise rejectionif(promisedNumber<0)thrownewArgumentException("delayMs");awaitTask.Delay(delayMs);return"Slow hello";}Alternative Designs
- We have existing private interop. It has few design flaws, the worst of them is that it gives to JS code naked pointers to managed objects. They could move on GC making it fragile.
- We could do full dynamic marshaling on runtime, but it would need lot of reflection and it's not trimming friendly
Open questions:
we consider that maybe we could marshal more dynamic combinations of parameters in the future. JavaScript is dynamic language after all. We made JSType as flags to prepare for it as it would be difficult to change in the future.Should we have GetProperty and SetProperty directly on the JSObject answeredThe JSMarshalerArgument 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 ? answeredWe made JSMarshalerArgument.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. answered
Risks
- this proposal is not improving CSP compliance, we may want to evolve the solution in the future to generate JS files during compile time.
- the quality of the generator in the prototype is low. It doesn't handle all negative scenarios and the diagnostic messages are just sketch.
- We validated the design from perf perspective with the team, but we have to measure it yet.
- Same for memory leaks, there are 2 GCs involved.
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:
JSImportAttributeorJSExportAttribute. We re-use common code gen infrastructure from[LibraryImport]JSType.BigIntor asJSType.Number, configurable per parameter viaJSMarshalAsAttributesimilar toMarshalAsAttributeof P/InvokeString,Boolean,DateTime,DateTimeOffset,ExceptionSystem.Objectwith mapping to well known types for some instance types and proxy viaGCHandlefor the rest.JSObjectwith private legacy implementationJSObject, which is proxy via existingJSHandleconcept similar toGCHandleTask,Func,Actionbyte[],int[],double[]Span<byte>,Span<int>,Span<double>andArraySegment<byte>,ArraySegment<int>,ArraySegment<double>[MarshalUsing(typeof(NativeMarshaler))]System.Private.Runtime.InteropServices.JavaScriptassembly and also semi-private JavaScript embedding API. These are used by Blazor and other partners and this proposal could help to phase it out gradually.There more implementation details described on the prototype PR
API Proposal
Below are types which drive the code generator
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 madeJSTypeas flags to prepare for it as it would be difficult to change in the future.Should we haveansweredGetPropertyandSetPropertydirectly on theJSObjectTheansweredJSMarshalerArgumenthas marshalers on it. For primitive types we do both nullable and non-nullable alternative. In JS world everything is nullable. Shall we enforce nullability constraint on runtime ?We madeansweredJSMarshalerArgument.ToManaged(out Task value)non-nullable, but in fact you can pass null Promise. Reason: forcing user to check null before callingawaitfelt akward. Passing null promise is useful on synchronous returns from JS.Risks