Skip to content

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

Merged
merged 2 commits into from
Dec 18, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public static void InvokeEntrypoint(string assemblyName, string[] args)
{
var assembly = Assembly.Load(assemblyName);
var entrypoint = FindUnderlyingEntrypoint(assembly);
var @params = entrypoint.GetParameters().Length == 1 ? new object[] { args } : new object[] { };
var @params = entrypoint.GetParameters().Length == 1 ? new object[] { args ?? Array.Empty<string>() } : new object[] { };
entrypointResult = entrypoint.Invoke(null, @params);
}
catch (Exception syncException)
Expand Down
2 changes: 1 addition & 1 deletion src/Components/Web.JS/dist/Release/blazor.server.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion src/Components/Web.JS/dist/Release/blazor.webassembly.js

Large diffs are not rendered by default.

156 changes: 27 additions & 129 deletions src/Components/Web.JS/src/Platform/Mono/MonoPlatform.ts
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
Expand All @@ -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);
Copy link
Member

@SteveSandersonMS SteveSandersonMS Dec 18, 2019

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 empty string[], e.g., the result of calling mono_string_array_new(0)?

Copy link
Contributor Author

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.

Copy link
Contributor Author

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.

},

toJavaScriptString: function toJavaScriptString(managedString: System_String) {
Expand All @@ -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');
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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 = [];

Expand Down Expand Up @@ -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 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this be typed as returning (...args: (System_Object | null)[]) => System_Object and not (...args: any[]) => any?

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 mono_bind_static_method can be declared as:

function mono_bind_static_method(fqn: string): BoundStaticMethod;

... and then any other uses of it can reference BoundStaticMethod as appropriate too.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From what I can tell, mono_bind_static_method does the JS to .NET type conversion on our behalf: https://github.com/mono/mono/blob/master/sdks/wasm/src/binding_support.js#L649-L657 and the reverse with the result. You're passing in \ reading JS types, not .NET types.

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 => {
Expand All @@ -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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks a lot better!

},
});
}
6 changes: 5 additions & 1 deletion src/Components/Web.JS/src/Platform/Mono/MonoTypes.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ declare namespace Module {
function FS_createPath(parent, path, canRead, canWrite);
function FS_createDataFile(parent, name, data, canRead, canWrite, canOwn);

function mono_call_assembly_entry_point(assemblyName: string, args: any[]): any;
function mono_bind_static_method(fqn: string): BoundStaticMethod;
}

// Emscripten declares these globals
Expand All @@ -28,3 +28,7 @@ declare namespace MONO {
var mono_wasm_runtime_is_ready: boolean;
function mono_wasm_setenv (name: string, value: string): void;
}

// mono_bind_static_method allows arbitrary JS data types to be sent over the wire. However we are
// artifically limiting it to a subset of types that we actually use.
declare type BoundStaticMethod = (...args: (string | number | null)[]) => (string | number | null);
4 changes: 0 additions & 4 deletions src/Components/Web.JS/src/Platform/Platform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,8 @@ export interface Platform {
start(loadAssemblyUrls: string[]): Promise<void>;

callEntryPoint(assemblyName: string): void;
findMethod(assemblyName: string, namespace: string, className: string, methodName: string): MethodHandle;
callMethod(method: MethodHandle, target: System_Object | null, args: (System_Object | null)[]): System_Object;

toJavaScriptString(dotNetString: System_String): string;
toDotNetString(javaScriptString: string): System_String;

toUint8Array(array: System_Array<any>): Uint8Array;

getArrayLength(array: System_Array<any>): number;
Expand Down