Skip to content

[browser] fix 4GB JS interop #109079

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 10 commits into from
Oct 31, 2024
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
@@ -0,0 +1,44 @@
WASM Transition wrappers and trampolines
When running managed code in the browser there are multiple scenarios that call for JITcode or pre-compiled wrappers/trampolines. I'll attempt to enumerate them here and describe how we could address each scenario. The following does not apply to WASI since it has no facilities for JIT, but the same motivations apply due to similar platform constraints.

Interpreter to native code
Unlike every other target, it's not possible to call a target function of an unknown signature in WASM. The call instruction encodes an exact signature (number of parameters, parameter types, and return type). The call instruction is also variadic, so it expects a set number of parameters on the stack.

If you know there are a limited set of signatures you need to call, you can pre-generate a bunch of individual wrappers at compile time, or have a switch statement that dispatches to a call with the appropriate signature. Mono WASM currently uses a combination of both approaches to address these scenarios. The jiterpreter has support for optimizing a subset of these wrappers on-the-fly.

With a JIT facility, you can generate a small WASM helper function on-the-fly that can load parameter values from the stack/heap and then pass them to a target function pointer with the appropriate signature. The jiterpreter already has support for doing this, which could be generalized or (ideally) reimplemented in a simpler form to support all scenarios for these interpreter->native transitions.

Native code to interpreter
Similarly, if a native function expects a function pointer with a given signature, we can't hand it a generic dispatch helper (the signature will not match) or a trampoline (we don't know in advance every possible signature that the user might want to make a function pointer for). There are multiple solutions:

Restrict the set of native->interp transitions at build time. This is what we do in Mono WASM, using attributes like UnmanagedCallersOnly.
JIT trampolines on demand with the appropriate signature. Each target managed function would need a dedicated trampoline, which is unfortunate.
Change the call signature on the native side to accept a userdata parameter which contains the managed function to call. In this case, we could reuse a generic trampoline for all call targets, and only need one trampoline per signature. This is currently how native-to-interp transition wrappers work in Mono WASM, and the jiterpreter has support for generating optimized trampolines with matching signatures on-the-fly.
Native code to arbitrary managed code directly
In Mono Wasm AOT we currently try to compile every managed method into a native wasm function. The calling convention for these methods does not match C, so any transition from native code directly into managed code requires a calling convention wrapper for each signature. These are generated at compile time, and it is feasible to know all the possible signatures since we know every signature in the managed binary, and the wasm type system's expressiveness is so limited that a significant % of managed signatures all map to the same wasm signature.

These transition wrappers are somewhat expensive as-is and have similar constraints at present (you need to annotate the method(s) you expect to call so we can generate dedicated wrappers, because we don't have a way to generate dedicated trampolines.) The jiterpreter could address this if necessary, but currently doesn't.

This means that at present converting a delegate to a function pointer does not work in WASM. As said above, this is fixable.

Arbitrary managed code to arbitrary native code
This can be done seamlessly at AOT compile time as long as we know the target signature - we perform a wasm indirect call, specifying the signature and loading the right arguments onto the stack.

Delegate invocations are more complex and typically bounce through a helper, with arguments temporarily stored on the stack or in the heap and flowing through a calling convention helper like mentioned above. More on this below.

Delegates
A given delegate can point to various things:

External native code (i.e. libc)
Internal native code (i.e. an icall)
AOT'd managed code
Interpreted managed code
JITted managed code
At present in Mono WASM we solve this by making all delegate invocations go through a helper which knows how to dispatch to the right kind of handler for each scenario, and we store the arguments on the stack/in the heap. At present for WASM we don't have the 'JITted managed code' scenario and some of the others may not work as expected, due to the ftnptr problem (explained below.)

The ftnptr problem
On other targets, it's possible to create a unique function pointer value for any call target, managed or native, by jitting a little trampoline on demand. On WASM it is presently not straightforward to do this (we could do it with the jiterpreter), so we don't. With the interpreter in the picture it gets more complex.

There are two types of function pointer; one is a 'real' native function pointer to i.e. a libc function, the other is a 'fake' function pointer which points to an interpreted method (which somewhat obviously has no dedicated trampoline or callable function pointer). As a result, any time a ftnptr is used, we need to know what 'kind' of pointer it is and invoke it appropriately.

If a ftnptr 'leaks' from the managed world into the native world, or vice versa, we have to be careful to do something appropriate to convert one type to the other, or detect this unsafe operation and abort. At present we have some known deficiencies in this area.
8 changes: 4 additions & 4 deletions src/mono/browser/runtime/debug.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

import { INTERNAL, Module, loaderHelpers, runtimeHelpers } from "./globals";
import { INTERNAL, loaderHelpers, runtimeHelpers } from "./globals";
import { toBase64StringImpl } from "./base64";
import cwraps from "./cwraps";
import { VoidPtr, CharPtr } from "./types/emscripten";
import { mono_log_warn } from "./logging";
import { forceThreadMemoryViewRefresh, localHeapViewU8 } from "./memory";
import { forceThreadMemoryViewRefresh, free, localHeapViewU8, malloc } from "./memory";
import { utf8ToString } from "./strings";
const commands_received: any = new Map<number, CommandResponse>();
commands_received.remove = function (key: number): CommandResponse {
Expand Down Expand Up @@ -64,9 +64,9 @@ export function mono_wasm_add_dbg_command_received (res_ok: boolean, id: number,
function mono_wasm_malloc_and_set_debug_buffer (command_parameters: string) {
if (command_parameters.length > _debugger_buffer_len) {
if (_debugger_buffer)
Module._free(_debugger_buffer);
free(_debugger_buffer);
_debugger_buffer_len = Math.max(command_parameters.length, _debugger_buffer_len, 256);
_debugger_buffer = Module._malloc(_debugger_buffer_len);
_debugger_buffer = malloc(_debugger_buffer_len);
}
const byteCharacters = atob(command_parameters);
const heapU8 = localHeapViewU8();
Expand Down
12 changes: 6 additions & 6 deletions src/mono/browser/runtime/interp-pgo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
import ProductVersion from "consts:productVersion";
import WasmEnableThreads from "consts:wasmEnableThreads";

import { ENVIRONMENT_IS_WEB, Module, loaderHelpers, runtimeHelpers } from "./globals";
import { ENVIRONMENT_IS_WEB, loaderHelpers, runtimeHelpers } from "./globals";
import { mono_log_info, mono_log_error, mono_log_warn } from "./logging";
import { localHeapViewU8 } from "./memory";
import { free, localHeapViewU8, malloc } from "./memory";
import cwraps from "./cwraps";
import { MonoConfigInternal } from "./types/internal";

Expand All @@ -31,7 +31,7 @@ export async function interp_pgo_save_data () {
return;
}

const pData = <any>Module._malloc(expectedSize);
const pData = <any>malloc(expectedSize);
const saved = cwraps.mono_interp_pgo_save_table(pData, expectedSize) === 0;
if (!saved) {
mono_log_error("Failed to save interp_pgo table (Unknown error)");
Expand All @@ -47,7 +47,7 @@ export async function interp_pgo_save_data () {

cleanupCache(tablePrefix, cacheKey); // no await

Module._free(pData);
free(pData);
} catch (exc) {
mono_log_error(`Failed to save interp_pgo table: ${exc}`);
}
Expand All @@ -66,14 +66,14 @@ export async function interp_pgo_load_data () {
return;
}

const pData = <any>Module._malloc(data.byteLength);
const pData = <any>malloc(data.byteLength);
const u8 = localHeapViewU8();
u8.set(new Uint8Array(data), pData);

if (cwraps.mono_interp_pgo_load_table(pData, data.byteLength))
mono_log_error("Failed to load interp_pgo table (Unknown error)");

Module._free(pData);
free(pData);
}

async function openCache (): Promise<Cache | null> {
Expand Down
9 changes: 7 additions & 2 deletions src/mono/browser/runtime/invoke-js.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import BuildConfiguration from "consts:configuration";

import { marshal_exception_to_cs, bind_arg_marshal_to_cs, marshal_task_to_cs } from "./marshal-to-cs";
import { get_signature_argument_count, bound_js_function_symbol, get_sig, get_signature_version, get_signature_type, imported_js_function_symbol, get_signature_handle, get_signature_function_name, get_signature_module_name, is_receiver_should_free, get_caller_native_tid, get_sync_done_semaphore_ptr, get_arg } from "./marshal";
import { forceThreadMemoryViewRefresh } from "./memory";
import { fixupPointer, forceThreadMemoryViewRefresh, free } from "./memory";
import { JSFunctionSignature, JSMarshalerArguments, BoundMarshalerToJs, JSFnHandle, BoundMarshalerToCs, JSHandle, MarshalerType, VoidPtrNull } from "./types/internal";
import { VoidPtr } from "./types/emscripten";
import { INTERNAL, Module, loaderHelpers, mono_assert, runtimeHelpers } from "./globals";
Expand All @@ -24,6 +24,7 @@ export const js_import_wrapper_by_fn_handle: Function[] = <any>[null];// 0th slo
export function mono_wasm_bind_js_import_ST (signature: JSFunctionSignature): VoidPtr {
if (WasmEnableThreads) return VoidPtrNull;
assert_js_interop();
signature = fixupPointer(signature, 0);
try {
bind_js_import(signature);
return VoidPtrNull;
Expand All @@ -35,6 +36,8 @@ export function mono_wasm_bind_js_import_ST (signature: JSFunctionSignature): Vo
export function mono_wasm_invoke_jsimport_MT (signature: JSFunctionSignature, args: JSMarshalerArguments) {
if (!WasmEnableThreads) return;
assert_js_interop();
signature = fixupPointer(signature, 0);
args = fixupPointer(args, 0);

const function_handle = get_signature_handle(signature);

Expand Down Expand Up @@ -73,6 +76,7 @@ export function mono_wasm_invoke_jsimport_MT (signature: JSFunctionSignature, ar
export function mono_wasm_invoke_jsimport_ST (function_handle: JSFnHandle, args: JSMarshalerArguments): void {
if (WasmEnableThreads) return;
loaderHelpers.assert_runtime_running();
args = fixupPointer(args, 0);
const bound_fn = js_import_wrapper_by_fn_handle[<any>function_handle];
mono_assert(bound_fn, () => `Imported function handle expected ${function_handle}`);
bound_fn(args);
Expand Down Expand Up @@ -334,7 +338,7 @@ function bind_fn (closure: BindingClosure) {
marshal_exception_to_cs(<any>args, ex);
} finally {
if (receiver_should_free) {
Module._free(args as any);
free(args as any);
}
endMeasure(mark, MeasuredBlock.callCsFunction, fqn);
}
Expand Down Expand Up @@ -364,6 +368,7 @@ export function mono_wasm_invoke_js_function_impl (bound_function_js_handle: JSH
loaderHelpers.assert_runtime_running();
const bound_fn = mono_wasm_get_jsobj_from_js_handle(bound_function_js_handle);
mono_assert(bound_fn && typeof (bound_fn) === "function" && bound_fn[bound_js_function_symbol], () => `Bound function handle expected ${bound_function_js_handle}`);
args = fixupPointer(args, 0);
bound_fn(args);
}

Expand Down
10 changes: 6 additions & 4 deletions src/mono/browser/runtime/jiterpreter-interp-entry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@

import { MonoMethod, MonoType } from "./types/internal";
import { NativePointer } from "./types/emscripten";
import { Module, mono_assert } from "./globals";
import { mono_assert } from "./globals";
import {
setI32, getU32_unaligned, _zero_region
setI32, getU32_unaligned, _zero_region,
malloc,
free
} from "./memory";
import { WasmOpcode } from "./jiterpreter-opcodes";
import cwraps from "./cwraps";
Expand Down Expand Up @@ -136,7 +138,7 @@ class TrampolineInfo {
this.traceName = subName;
} finally {
if (namePtr)
Module._free(<any>namePtr);
free(<any>namePtr);
}
}

Expand Down Expand Up @@ -554,7 +556,7 @@ function generate_wasm_body (
// FIXME: Pre-allocate these buffers and their constant slots at the start before we
// generate function bodies, so that even if we run out of constant slots for MonoType we
// will always have put the buffers in a constant slot. This will be necessary for thread safety
const scratchBuffer = <any>Module._malloc(sizeOfJiterpEntryData);
const scratchBuffer = <any>malloc(sizeOfJiterpEntryData);
_zero_region(scratchBuffer, sizeOfJiterpEntryData);

// Initialize the parameter count in the data blob. This is used to calculate the new value of sp
Expand Down
5 changes: 3 additions & 2 deletions src/mono/browser/runtime/jiterpreter-jit-call.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import { MonoType, MonoMethod } from "./types/internal";
import { NativePointer, VoidPtr } from "./types/emscripten";
import { Module, mono_assert, runtimeHelpers } from "./globals";
import {
getU8, getI32_unaligned, getU32_unaligned, setU32_unchecked, receiveWorkerHeapViews
getU8, getI32_unaligned, getU32_unaligned, setU32_unchecked, receiveWorkerHeapViews,
free
} from "./memory";
import { WasmOpcode, WasmValtype } from "./jiterpreter-opcodes";
import {
Expand Down Expand Up @@ -152,7 +153,7 @@ class TrampolineInfo {
suffix = utf8ToString(pMethodName);
} finally {
if (pMethodName)
Module._free(<any>pMethodName);
free(<any>pMethodName);
}
}

Expand Down
11 changes: 6 additions & 5 deletions src/mono/browser/runtime/jiterpreter-support.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { WasmOpcode, WasmSimdOpcode, WasmAtomicOpcode, WasmValtype } from "./jit
import { MintOpcode } from "./mintops";
import cwraps from "./cwraps";
import { mono_log_error, mono_log_info } from "./logging";
import { localHeapViewU8, localHeapViewU32 } from "./memory";
import { localHeapViewU8, localHeapViewU32, malloc } from "./memory";
import {
JiterpNumberMode, BailoutReason, JiterpreterTable,
JiterpCounter, JiterpMember, OpcodeInfoType
Expand All @@ -20,7 +20,8 @@ export const maxFailures = 2,
shortNameBase = 36,
// NOTE: This needs to be big enough to hold the maximum module size since there's no auto-growth
// support yet. If that becomes a problem, we should just make it growable
blobBuilderCapacity = 24 * 1024;
blobBuilderCapacity = 24 * 1024,
INT32_MIN = -2147483648;

// uint16
export declare interface MintOpcodePtr extends NativePointer {
Expand Down Expand Up @@ -948,7 +949,7 @@ export class BlobBuilder {

constructor () {
this.capacity = blobBuilderCapacity;
this.buffer = <any>Module._malloc(this.capacity);
this.buffer = <any>malloc(this.capacity);
mono_assert(this.buffer, () => `Failed to allocate ${blobBuilderCapacity}b buffer for BlobBuilder`);
localHeapViewU8().fill(0, this.buffer, this.buffer + this.capacity);
this.size = 0;
Expand Down Expand Up @@ -1665,7 +1666,7 @@ export function append_exit (builder: WasmBuilder, ip: MintOpcodePtr, opcodeCoun

export function copyIntoScratchBuffer (src: NativePointer, size: number): NativePointer {
if (!scratchBuffer)
scratchBuffer = Module._malloc(64);
scratchBuffer = malloc(64);
if (size > 64)
throw new Error("Scratch buffer size is 64");

Expand Down Expand Up @@ -2106,7 +2107,7 @@ function updateOptions () {
optionTable = <any>{};
for (const k in optionNames) {
const value = cwraps.mono_jiterp_get_option_as_int(optionNames[k]);
if (value > -2147483647)
if (value !== INT32_MIN)
(<any>optionTable)[k] = value;
else
mono_log_info(`Failed to retrieve value of option ${optionNames[k]}`);
Expand Down
8 changes: 4 additions & 4 deletions src/mono/browser/runtime/jiterpreter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@

import { MonoMethod } from "./types/internal";
import { NativePointer } from "./types/emscripten";
import { Module, mono_assert, runtimeHelpers } from "./globals";
import { getU16 } from "./memory";
import { mono_assert, runtimeHelpers } from "./globals";
import { free, getU16 } from "./memory";
import { WasmValtype, WasmOpcode, getOpcodeName } from "./jiterpreter-opcodes";
import { MintOpcode } from "./mintops";
import cwraps from "./cwraps";
Expand Down Expand Up @@ -1004,7 +1004,7 @@ export function mono_interp_tier_prepare_jiterpreter (
) {
const pMethodName = cwraps.mono_wasm_method_get_full_name(method);
methodFullName = utf8ToString(pMethodName);
Module._free(<any>pMethodName);
free(<any>pMethodName);
}
const methodName = utf8ToString(cwraps.mono_wasm_method_get_name(method));
info.name = methodFullName || methodName;
Expand Down Expand Up @@ -1148,7 +1148,7 @@ export function jiterpreter_dump_stats (concise?: boolean): void {
const pMethodName = cwraps.mono_wasm_method_get_full_name(<any>targetMethod);
const targetMethodName = utf8ToString(pMethodName);
const hitCount = callTargetCounts[<any>targetMethod];
Module._free(<any>pMethodName);
free(<any>pMethodName);
mono_log_info(`${targetMethodName} ${hitCount}`);
}
}
Expand Down
4 changes: 2 additions & 2 deletions src/mono/browser/runtime/managed-exports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { marshal_int32_to_js, end_marshal_task_to_js, marshal_string_to_js, begi
import { do_not_force_dispose, is_gcv_handle } from "./gc-handles";
import { assert_c_interop, assert_js_interop } from "./invoke-js";
import { monoThreadInfo, mono_wasm_main_thread_ptr } from "./pthreads";
import { _zero_region, copyBytes } from "./memory";
import { _zero_region, copyBytes, malloc } from "./memory";
import { stringToUTF8Ptr } from "./strings";
import { mono_log_error } from "./logging";

Expand Down Expand Up @@ -285,7 +285,7 @@ export function invoke_async_jsexport (managedTID: PThreadPtr, method: MonoMetho
} else {
set_receiver_should_free(args);
const bytes = JavaScriptMarshalerArgSize * size;
const cpy = Module._malloc(bytes) as any;
const cpy = malloc(bytes) as any;
copyBytes(args as any, cpy, bytes);
twraps.mono_wasm_invoke_jsexport_async_post(managedTID, method, cpy);
}
Expand Down
Loading
Loading