-
Notifications
You must be signed in to change notification settings - Fork 10.4k
Use mono_bind_static_method for invoking JS methods #17942
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,19 +1,9 @@ | ||
import { MethodHandle, System_Object, System_String, System_Array, Pointer, Platform } from '../Platform'; | ||
import { System_Object, System_String, System_Array, Pointer, Platform } from '../Platform'; | ||
import { getFileNameFromUrl } from '../Url'; | ||
import { attachDebuggerHotkey, hasDebuggingEnabled } from './MonoDebugger'; | ||
import { showErrorNotification } from '../../BootErrors'; | ||
|
||
const assemblyHandleCache: { [assemblyName: string]: number } = {}; | ||
const typeHandleCache: { [fullyQualifiedTypeName: string]: number } = {}; | ||
const methodHandleCache: { [fullyQualifiedMethodName: string]: MethodHandle } = {}; | ||
|
||
let assembly_load: (assemblyName: string) => number; | ||
let find_class: (assemblyHandle: number, namespace: string, className: string) => number; | ||
let find_method: (typeHandle: number, methodName: string, unknownArg: number) => MethodHandle; | ||
let invoke_method: (method: MethodHandle, target: System_Object, argsArrayPtr: number, exceptionFlagIntPtr: number) => System_Object; | ||
let mono_string_array_new: (length: number) => System_Array<System_String>; | ||
let mono_string_get_utf8: (managedString: System_String) => Mono.Utf8Ptr; | ||
let mono_string: (jsString: string) => System_String; | ||
const appBinDirName = 'appBinDir'; | ||
const uint64HighOrderShift = Math.pow(2, 32); | ||
const maxSafeNumberHighPart = Math.pow(2, 21) - 1; // The high-order int32 from Number.MAX_SAFE_INTEGER | ||
|
@@ -38,49 +28,16 @@ export const monoPlatform: Platform = { | |
}); | ||
}, | ||
|
||
findMethod: findMethod, | ||
|
||
callEntryPoint: function callEntryPoint(assemblyName: string) { | ||
// Instead of using Module.mono_call_assembly_entry_point, we have our own logic for invoking | ||
// the entrypoint which adds support for async main. | ||
// Currently we disregard the return value from the entrypoint, whether it's sync or async. | ||
// In the future, we might want Blazor.start to return a Promise<Promise<value>>, where the | ||
// outer promise reflects the startup process, and the inner one reflects the possibly-async | ||
// .NET entrypoint method. | ||
const invokeEntrypoint = findMethod('Microsoft.AspNetCore.Blazor', 'Microsoft.AspNetCore.Blazor.Hosting', 'EntrypointInvoker', 'InvokeEntrypoint'); | ||
this.callMethod(invokeEntrypoint, null, [ | ||
this.toDotNetString(assemblyName), | ||
mono_string_array_new(0) // In the future, we may have a way of supplying arg strings. For now, we always supply an empty string[]. | ||
]); | ||
}, | ||
|
||
callMethod: function callMethod(method: MethodHandle, target: System_Object, args: System_Object[]): System_Object { | ||
if (args.length > 4) { | ||
// Hopefully this restriction can be eased soon, but for now make it clear what's going on | ||
throw new Error(`Currently, MonoPlatform supports passing a maximum of 4 arguments from JS to .NET. You tried to pass ${args.length}.`); | ||
} | ||
|
||
const stack = Module.stackSave(); | ||
|
||
try { | ||
const argsBuffer = Module.stackAlloc(args.length); | ||
const exceptionFlagManagedInt = Module.stackAlloc(4); | ||
for (let i = 0; i < args.length; ++i) { | ||
Module.setValue(argsBuffer + i * 4, args[i], 'i32'); | ||
} | ||
Module.setValue(exceptionFlagManagedInt, 0, 'i32'); | ||
|
||
const res = invoke_method(method, target, argsBuffer, exceptionFlagManagedInt); | ||
|
||
if (Module.getValue(exceptionFlagManagedInt, 'i32') !== 0) { | ||
// If the exception flag is set, the returned value is exception.ToString() | ||
throw new Error(monoPlatform.toJavaScriptString(<System_String>res)); | ||
} | ||
|
||
return res; | ||
} finally { | ||
Module.stackRestore(stack); | ||
} | ||
const invokeEntrypoint = bindStaticMethod('Microsoft.AspNetCore.Blazor', 'Microsoft.AspNetCore.Blazor.Hosting.EntrypointInvoker', 'InvokeEntrypoint'); | ||
// Note we're passing in null because passing arrays is problematic until https://github.com/mono/mono/issues/18245 is resolved. | ||
invokeEntrypoint(assemblyName, null); | ||
}, | ||
|
||
toJavaScriptString: function toJavaScriptString(managedString: System_String) { | ||
|
@@ -94,10 +51,6 @@ export const monoPlatform: Platform = { | |
return res; | ||
}, | ||
|
||
toDotNetString: function toDotNetString(jsString: string): System_String { | ||
return mono_string(jsString); | ||
}, | ||
|
||
toUint8Array: function toUint8Array(array: System_Array<any>): Uint8Array { | ||
const dataPtr = getArrayDataPointer(array); | ||
const length = Module.getValue(dataPtr, 'i32'); | ||
|
@@ -158,44 +111,6 @@ export const monoPlatform: Platform = { | |
}, | ||
}; | ||
|
||
function findAssembly(assemblyName: string): number { | ||
let assemblyHandle = assemblyHandleCache[assemblyName]; | ||
if (!assemblyHandle) { | ||
assemblyHandle = assembly_load(assemblyName); | ||
if (!assemblyHandle) { | ||
throw new Error(`Could not find assembly "${assemblyName}"`); | ||
} | ||
assemblyHandleCache[assemblyName] = assemblyHandle; | ||
} | ||
return assemblyHandle; | ||
} | ||
|
||
function findType(assemblyName: string, namespace: string, className: string): number { | ||
const fullyQualifiedTypeName = `[${assemblyName}]${namespace}.${className}`; | ||
let typeHandle = typeHandleCache[fullyQualifiedTypeName]; | ||
if (!typeHandle) { | ||
typeHandle = find_class(findAssembly(assemblyName), namespace, className); | ||
if (!typeHandle) { | ||
throw new Error(`Could not find type "${className}" in namespace "${namespace}" in assembly "${assemblyName}"`); | ||
} | ||
typeHandleCache[fullyQualifiedTypeName] = typeHandle; | ||
} | ||
return typeHandle; | ||
} | ||
|
||
function findMethod(assemblyName: string, namespace: string, className: string, methodName: string): MethodHandle { | ||
const fullyQualifiedMethodName = `[${assemblyName}]${namespace}.${className}::${methodName}`; | ||
let methodHandle = methodHandleCache[fullyQualifiedMethodName]; | ||
if (!methodHandle) { | ||
methodHandle = find_method(findType(assemblyName, namespace, className), methodName, -1); | ||
if (!methodHandle) { | ||
throw new Error(`Could not find method "${methodName}" on type "${namespace}.${className}"`); | ||
} | ||
methodHandleCache[fullyQualifiedMethodName] = methodHandle; | ||
} | ||
return methodHandle; | ||
} | ||
|
||
function addScriptTagsToDocument() { | ||
const browserSupportsNativeWebAssembly = typeof WebAssembly !== 'undefined' && WebAssembly.validate; | ||
if (!browserSupportsNativeWebAssembly) { | ||
|
@@ -254,26 +169,8 @@ function createEmscriptenModuleInstance(loadAssemblyUrls: string[], onReady: () | |
'number', | ||
'number', | ||
]); | ||
assembly_load = Module.cwrap('mono_wasm_assembly_load', 'number', ['string']); | ||
find_class = Module.cwrap('mono_wasm_assembly_find_class', 'number', [ | ||
'number', | ||
'string', | ||
'string', | ||
]); | ||
find_method = Module.cwrap('mono_wasm_assembly_find_method', 'number', [ | ||
'number', | ||
'string', | ||
'number', | ||
]); | ||
invoke_method = Module.cwrap('mono_wasm_invoke_method', 'number', [ | ||
'number', | ||
'number', | ||
'number', | ||
]); | ||
|
||
mono_string_get_utf8 = Module.cwrap('mono_wasm_string_get_utf8', 'number', ['number']); | ||
mono_string = Module.cwrap('mono_wasm_string_from_js', 'number', ['string']); | ||
mono_string_array_new = Module.cwrap('mono_wasm_string_array_new', 'number', ['number']); | ||
|
||
MONO.loaded_files = []; | ||
|
||
|
@@ -346,10 +243,16 @@ function getArrayDataPointer<T>(array: System_Array<T>): number { | |
return <number><any>array + 12; // First byte from here is length, then following bytes are entries | ||
} | ||
|
||
function bindStaticMethod(assembly: string, typeName: string, method: string) : (...args: any[]) => any { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Shouldn't this be typed as returning I suspect it would be good to declare a type referencing a bound method. For example, in MonoType.d.ts, add something like: type BoundStaticMethod = (...args: (System_Object | number | null)[]) => (System_Object | number | null); ... and then
... and then any other uses of it can reference There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. From what I can tell, That said, we could limit it to the subset of types we expect to pass around which is strings or numbers. |
||
// Fully qualified name looks like this: "[debugger-test] Math:IntAdd" | ||
const fqn = `[${assembly}] ${typeName}:${method}`; | ||
return Module.mono_bind_static_method(fqn); | ||
} | ||
|
||
function attachInteropInvoker(): void { | ||
const dotNetDispatcherInvokeMethodHandle = findMethod('Mono.WebAssembly.Interop', 'Mono.WebAssembly.Interop', 'MonoWebAssemblyJSRuntime', 'InvokeDotNet'); | ||
const dotNetDispatcherBeginInvokeMethodHandle = findMethod('Mono.WebAssembly.Interop', 'Mono.WebAssembly.Interop', 'MonoWebAssemblyJSRuntime', 'BeginInvokeDotNet'); | ||
const dotNetDispatcherEndInvokeJSMethodHandle = findMethod('Mono.WebAssembly.Interop', 'Mono.WebAssembly.Interop', 'MonoWebAssemblyJSRuntime', 'EndInvokeJS'); | ||
const dotNetDispatcherInvokeMethodHandle = bindStaticMethod('Mono.WebAssembly.Interop', 'Mono.WebAssembly.Interop.MonoWebAssemblyJSRuntime', 'InvokeDotNet'); | ||
const dotNetDispatcherBeginInvokeMethodHandle = bindStaticMethod('Mono.WebAssembly.Interop', 'Mono.WebAssembly.Interop.MonoWebAssemblyJSRuntime', 'BeginInvokeDotNet'); | ||
const dotNetDispatcherEndInvokeJSMethodHandle = bindStaticMethod('Mono.WebAssembly.Interop', 'Mono.WebAssembly.Interop.MonoWebAssemblyJSRuntime', 'EndInvokeJS'); | ||
|
||
DotNet.attachDispatcher({ | ||
beginInvokeDotNetFromJS: (callId: number, assemblyName: string | null, methodIdentifier: string, dotNetObjectId: any | null, argsJson: string): void => { | ||
|
@@ -362,30 +265,25 @@ function attachInteropInvoker(): void { | |
? dotNetObjectId.toString() | ||
: assemblyName; | ||
|
||
monoPlatform.callMethod(dotNetDispatcherBeginInvokeMethodHandle, null, [ | ||
callId ? monoPlatform.toDotNetString(callId.toString()) : null, | ||
monoPlatform.toDotNetString(assemblyNameOrDotNetObjectId), | ||
monoPlatform.toDotNetString(methodIdentifier), | ||
monoPlatform.toDotNetString(argsJson), | ||
]); | ||
dotNetDispatcherBeginInvokeMethodHandle( | ||
callId ? callId.toString() : null, | ||
assemblyNameOrDotNetObjectId, | ||
methodIdentifier, | ||
argsJson, | ||
); | ||
}, | ||
endInvokeJSFromDotNet: (asyncHandle, succeeded, serializedArgs): void => { | ||
monoPlatform.callMethod( | ||
dotNetDispatcherEndInvokeJSMethodHandle, | ||
null, | ||
[monoPlatform.toDotNetString(serializedArgs)] | ||
dotNetDispatcherEndInvokeJSMethodHandle( | ||
serializedArgs | ||
); | ||
}, | ||
invokeDotNetFromJS: (assemblyName, methodIdentifier, dotNetObjectId, argsJson) => { | ||
const resultJsonStringPtr = monoPlatform.callMethod(dotNetDispatcherInvokeMethodHandle, null, [ | ||
assemblyName ? monoPlatform.toDotNetString(assemblyName) : null, | ||
monoPlatform.toDotNetString(methodIdentifier), | ||
dotNetObjectId ? monoPlatform.toDotNetString(dotNetObjectId.toString()) : null, | ||
monoPlatform.toDotNetString(argsJson), | ||
]) as System_String; | ||
return resultJsonStringPtr | ||
? monoPlatform.toJavaScriptString(resultJsonStringPtr) | ||
: null; | ||
return dotNetDispatcherInvokeMethodHandle( | ||
assemblyName ? assemblyName : null, | ||
methodIdentifier, | ||
dotNetObjectId ? dotNetObjectId.toString() : null, | ||
argsJson, | ||
) as string; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This looks a lot better! |
||
}, | ||
}); | ||
} |
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it really right to pass
null
here as the second param? Shouldn't we be passing an emptystring[]
, e.g., the result of callingmono_string_array_new(0)
?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
AFAIK,
mono_bind_static_method
converts JS values to the .NET ones. So we'd have to pass in an empty JS array. Let me play around with this.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Running in to this issue: mono/mono#18245 if I try passing in an array. I'm passing in a null now and null-coalescing it in
InvokeEntrypoint
. I think we can work out the WASM folks to figure out a nice way to pass arbitrary types here.