Skip to content

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

Description

@pavelsavara

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 GCHandle
    • Task, Func, Action
    • byte[], 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 answered
  • The 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 ? answered
  • We 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.

Metadata

Metadata

Assignees

Labels

api-approvedAPI was approved in API review, it can be implementedarch-wasmWebAssembly architecturearea-System.Runtime.InteropServices.JavaScriptblockingMarks issues that we want to fast track in order to unblock other important work

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions

, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
[API Proposal]: JavaScript interop with [JSImport] and [JSExport] attributes and Roslyn · Issue #70133 · dotnet/runtime · GitHub
Skip to content

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

Description

@pavelsavara

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 GCHandle
    • Task, Func, Action
    • byte[], 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 answered
  • The 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 ? answered
  • We 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.

Metadata

Metadata

Assignees

Labels

api-approvedAPI was approved in API review, it can be implementedarch-wasmWebAssembly architecturearea-System.Runtime.InteropServices.JavaScriptblockingMarks issues that we want to fast track in order to unblock other important work

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions

, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' [API Proposal]: JavaScript interop with [JSImport] and [JSExport] attributes and Roslyn · Issue #70133 · dotnet/runtime · GitHub
Skip to content

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

Description

@pavelsavara

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 GCHandle
    • Task, Func, Action
    • byte[], 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 answered
  • The 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 ? answered
  • We 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.

Metadata

Metadata

Assignees

Labels

api-approvedAPI was approved in API review, it can be implementedarch-wasmWebAssembly architecturearea-System.Runtime.InteropServices.JavaScriptblockingMarks issues that we want to fast track in order to unblock other important work

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions

, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' [API Proposal]: JavaScript interop with [JSImport] and [JSExport] attributes and Roslyn · Issue #70133 · dotnet/runtime · GitHub
Skip to content

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

Description

@pavelsavara

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 GCHandle
    • Task, Func, Action
    • byte[], 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 answered
  • The 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 ? answered
  • We 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.

Metadata

Metadata

Assignees

Labels

api-approvedAPI was approved in API review, it can be implementedarch-wasmWebAssembly architecturearea-System.Runtime.InteropServices.JavaScriptblockingMarks issues that we want to fast track in order to unblock other important work

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions

, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' [API Proposal]: JavaScript interop with [JSImport] and [JSExport] attributes and Roslyn · Issue #70133 · dotnet/runtime · GitHub
Skip to content

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

Description

@pavelsavara

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 GCHandle
    • Task, Func, Action
    • byte[], 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 answered
  • The 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 ? answered
  • We 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.

Metadata

Metadata

Assignees

Labels

api-approvedAPI was approved in API review, it can be implementedarch-wasmWebAssembly architecturearea-System.Runtime.InteropServices.JavaScriptblockingMarks issues that we want to fast track in order to unblock other important work

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions

, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' [API Proposal]: JavaScript interop with [JSImport] and [JSExport] attributes and Roslyn · Issue #70133 · dotnet/runtime · GitHub
Skip to content

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

Description

@pavelsavara

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 GCHandle
    • Task, Func, Action
    • byte[], 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 answered
  • The 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 ? answered
  • We 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.

Metadata

Metadata

Assignees

Labels

api-approvedAPI was approved in API review, it can be implementedarch-wasmWebAssembly architecturearea-System.Runtime.InteropServices.JavaScriptblockingMarks issues that we want to fast track in order to unblock other important work

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions

, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' [API Proposal]: JavaScript interop with [JSImport] and [JSExport] attributes and Roslyn · Issue #70133 · dotnet/runtime · GitHub
Skip to content

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

Description

@pavelsavara

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 GCHandle
    • Task, Func, Action
    • byte[], 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 answered
  • The 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 ? answered
  • We 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.

Metadata

Metadata

Assignees

Labels

api-approvedAPI was approved in API review, it can be implementedarch-wasmWebAssembly architecturearea-System.Runtime.InteropServices.JavaScriptblockingMarks issues that we want to fast track in order to unblock other important work

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions

, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); [API Proposal]: JavaScript interop with [JSImport] and [JSExport] attributes and Roslyn · Issue #70133 · dotnet/runtime · GitHub
Skip to content

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

Description

@pavelsavara

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 GCHandle
    • Task, Func, Action
    • byte[], 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 answered
  • The 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 ? answered
  • We 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.

Metadata

Metadata

Assignees

Labels

api-approvedAPI was approved in API review, it can be implementedarch-wasmWebAssembly architecturearea-System.Runtime.InteropServices.JavaScriptblockingMarks issues that we want to fast track in order to unblock other important work

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions