Skip to content

Commit 9e3f557

Browse files
pavelsavarailonatommymaraf
authored
[browser] fix 4GB JS interop (#109079)
* Update src/mono/wasm/testassets/WasmBasicTestApp/App/MemoryTest.cs Co-authored-by: Ilona Tomkowicz <32700855+ilonatommy@users.noreply.github.com> * Update src/mono/wasm/testassets/WasmBasicTestApp/App/MemoryTest.cs Co-authored-by: Ilona Tomkowicz <32700855+ilonatommy@users.noreply.github.com> * Feedback: helper function. * Feedback: `free` can we wrapped. * Update src/mono/wasm/testassets/WasmBasicTestApp/App/wwwroot/main.js Co-authored-by: Marek Fišera <mara@neptuo.com> --------- Co-authored-by: Ilona Tomkowicz <32700855+ilonatommy@users.noreply.github.com> Co-authored-by: Ilona Tomkowicz <itomkowicz@microsoft.com> Co-authored-by: Marek Fišera <mara@neptuo.com>
1 parent 797306f commit 9e3f557

19 files changed

+164
-79
lines changed
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
WASM Transition wrappers and trampolines
2+
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.
3+
4+
Interpreter to native code
5+
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.
6+
7+
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.
8+
9+
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.
10+
11+
Native code to interpreter
12+
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:
13+
14+
Restrict the set of native->interp transitions at build time. This is what we do in Mono WASM, using attributes like UnmanagedCallersOnly.
15+
JIT trampolines on demand with the appropriate signature. Each target managed function would need a dedicated trampoline, which is unfortunate.
16+
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.
17+
Native code to arbitrary managed code directly
18+
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.
19+
20+
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.
21+
22+
This means that at present converting a delegate to a function pointer does not work in WASM. As said above, this is fixable.
23+
24+
Arbitrary managed code to arbitrary native code
25+
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.
26+
27+
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.
28+
29+
Delegates
30+
A given delegate can point to various things:
31+
32+
External native code (i.e. libc)
33+
Internal native code (i.e. an icall)
34+
AOT'd managed code
35+
Interpreted managed code
36+
JITted managed code
37+
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.)
38+
39+
The ftnptr problem
40+
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.
41+
42+
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.
43+
44+
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.

src/mono/browser/runtime/debug.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4-
import { INTERNAL, Module, loaderHelpers, runtimeHelpers } from "./globals";
4+
import { INTERNAL, loaderHelpers, runtimeHelpers } from "./globals";
55
import { toBase64StringImpl } from "./base64";
66
import cwraps from "./cwraps";
77
import { VoidPtr, CharPtr } from "./types/emscripten";
88
import { mono_log_warn } from "./logging";
9-
import { forceThreadMemoryViewRefresh, localHeapViewU8 } from "./memory";
9+
import { forceThreadMemoryViewRefresh, free, localHeapViewU8, malloc } from "./memory";
1010
import { utf8ToString } from "./strings";
1111
const commands_received: any = new Map<number, CommandResponse>();
1212
commands_received.remove = function (key: number): CommandResponse {
@@ -64,9 +64,9 @@ export function mono_wasm_add_dbg_command_received (res_ok: boolean, id: number,
6464
function mono_wasm_malloc_and_set_debug_buffer (command_parameters: string) {
6565
if (command_parameters.length > _debugger_buffer_len) {
6666
if (_debugger_buffer)
67-
Module._free(_debugger_buffer);
67+
free(_debugger_buffer);
6868
_debugger_buffer_len = Math.max(command_parameters.length, _debugger_buffer_len, 256);
69-
_debugger_buffer = Module._malloc(_debugger_buffer_len);
69+
_debugger_buffer = malloc(_debugger_buffer_len);
7070
}
7171
const byteCharacters = atob(command_parameters);
7272
const heapU8 = localHeapViewU8();

src/mono/browser/runtime/interp-pgo.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33
import ProductVersion from "consts:productVersion";
44
import WasmEnableThreads from "consts:wasmEnableThreads";
55

6-
import { ENVIRONMENT_IS_WEB, Module, loaderHelpers, runtimeHelpers } from "./globals";
6+
import { ENVIRONMENT_IS_WEB, loaderHelpers, runtimeHelpers } from "./globals";
77
import { mono_log_info, mono_log_error, mono_log_warn } from "./logging";
8-
import { localHeapViewU8 } from "./memory";
8+
import { free, localHeapViewU8, malloc } from "./memory";
99
import cwraps from "./cwraps";
1010
import { MonoConfigInternal } from "./types/internal";
1111

@@ -31,7 +31,7 @@ export async function interp_pgo_save_data () {
3131
return;
3232
}
3333

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

4848
cleanupCache(tablePrefix, cacheKey); // no await
4949

50-
Module._free(pData);
50+
free(pData);
5151
} catch (exc) {
5252
mono_log_error(`Failed to save interp_pgo table: ${exc}`);
5353
}
@@ -66,14 +66,14 @@ export async function interp_pgo_load_data () {
6666
return;
6767
}
6868

69-
const pData = <any>Module._malloc(data.byteLength);
69+
const pData = <any>malloc(data.byteLength);
7070
const u8 = localHeapViewU8();
7171
u8.set(new Uint8Array(data), pData);
7272

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

76-
Module._free(pData);
76+
free(pData);
7777
}
7878

7979
async function openCache (): Promise<Cache | null> {

src/mono/browser/runtime/invoke-js.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import BuildConfiguration from "consts:configuration";
66

77
import { marshal_exception_to_cs, bind_arg_marshal_to_cs, marshal_task_to_cs } from "./marshal-to-cs";
88
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";
9-
import { forceThreadMemoryViewRefresh } from "./memory";
9+
import { fixupPointer, forceThreadMemoryViewRefresh, free } from "./memory";
1010
import { JSFunctionSignature, JSMarshalerArguments, BoundMarshalerToJs, JSFnHandle, BoundMarshalerToCs, JSHandle, MarshalerType, VoidPtrNull } from "./types/internal";
1111
import { VoidPtr } from "./types/emscripten";
1212
import { INTERNAL, Module, loaderHelpers, mono_assert, runtimeHelpers } from "./globals";
@@ -24,6 +24,7 @@ export const js_import_wrapper_by_fn_handle: Function[] = <any>[null];// 0th slo
2424
export function mono_wasm_bind_js_import_ST (signature: JSFunctionSignature): VoidPtr {
2525
if (WasmEnableThreads) return VoidPtrNull;
2626
assert_js_interop();
27+
signature = fixupPointer(signature, 0);
2728
try {
2829
bind_js_import(signature);
2930
return VoidPtrNull;
@@ -35,6 +36,8 @@ export function mono_wasm_bind_js_import_ST (signature: JSFunctionSignature): Vo
3536
export function mono_wasm_invoke_jsimport_MT (signature: JSFunctionSignature, args: JSMarshalerArguments) {
3637
if (!WasmEnableThreads) return;
3738
assert_js_interop();
39+
signature = fixupPointer(signature, 0);
40+
args = fixupPointer(args, 0);
3841

3942
const function_handle = get_signature_handle(signature);
4043

@@ -73,6 +76,7 @@ export function mono_wasm_invoke_jsimport_MT (signature: JSFunctionSignature, ar
7376
export function mono_wasm_invoke_jsimport_ST (function_handle: JSFnHandle, args: JSMarshalerArguments): void {
7477
if (WasmEnableThreads) return;
7578
loaderHelpers.assert_runtime_running();
79+
args = fixupPointer(args, 0);
7680
const bound_fn = js_import_wrapper_by_fn_handle[<any>function_handle];
7781
mono_assert(bound_fn, () => `Imported function handle expected ${function_handle}`);
7882
bound_fn(args);
@@ -334,7 +338,7 @@ function bind_fn (closure: BindingClosure) {
334338
marshal_exception_to_cs(<any>args, ex);
335339
} finally {
336340
if (receiver_should_free) {
337-
Module._free(args as any);
341+
free(args as any);
338342
}
339343
endMeasure(mark, MeasuredBlock.callCsFunction, fqn);
340344
}
@@ -364,6 +368,7 @@ export function mono_wasm_invoke_js_function_impl (bound_function_js_handle: JSH
364368
loaderHelpers.assert_runtime_running();
365369
const bound_fn = mono_wasm_get_jsobj_from_js_handle(bound_function_js_handle);
366370
mono_assert(bound_fn && typeof (bound_fn) === "function" && bound_fn[bound_js_function_symbol], () => `Bound function handle expected ${bound_function_js_handle}`);
371+
args = fixupPointer(args, 0);
367372
bound_fn(args);
368373
}
369374

src/mono/browser/runtime/jiterpreter-interp-entry.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@
33

44
import { MonoMethod, MonoType } from "./types/internal";
55
import { NativePointer } from "./types/emscripten";
6-
import { Module, mono_assert } from "./globals";
6+
import { mono_assert } from "./globals";
77
import {
8-
setI32, getU32_unaligned, _zero_region
8+
setI32, getU32_unaligned, _zero_region,
9+
malloc,
10+
free
911
} from "./memory";
1012
import { WasmOpcode } from "./jiterpreter-opcodes";
1113
import cwraps from "./cwraps";
@@ -136,7 +138,7 @@ class TrampolineInfo {
136138
this.traceName = subName;
137139
} finally {
138140
if (namePtr)
139-
Module._free(<any>namePtr);
141+
free(<any>namePtr);
140142
}
141143
}
142144

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

560562
// Initialize the parameter count in the data blob. This is used to calculate the new value of sp

src/mono/browser/runtime/jiterpreter-jit-call.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ import { MonoType, MonoMethod } from "./types/internal";
55
import { NativePointer, VoidPtr } from "./types/emscripten";
66
import { Module, mono_assert, runtimeHelpers } from "./globals";
77
import {
8-
getU8, getI32_unaligned, getU32_unaligned, setU32_unchecked, receiveWorkerHeapViews
8+
getU8, getI32_unaligned, getU32_unaligned, setU32_unchecked, receiveWorkerHeapViews,
9+
free
910
} from "./memory";
1011
import { WasmOpcode, WasmValtype } from "./jiterpreter-opcodes";
1112
import {
@@ -152,7 +153,7 @@ class TrampolineInfo {
152153
suffix = utf8ToString(pMethodName);
153154
} finally {
154155
if (pMethodName)
155-
Module._free(<any>pMethodName);
156+
free(<any>pMethodName);
156157
}
157158
}
158159

src/mono/browser/runtime/jiterpreter-support.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { WasmOpcode, WasmSimdOpcode, WasmAtomicOpcode, WasmValtype } from "./jit
88
import { MintOpcode } from "./mintops";
99
import cwraps from "./cwraps";
1010
import { mono_log_error, mono_log_info } from "./logging";
11-
import { localHeapViewU8, localHeapViewU32 } from "./memory";
11+
import { localHeapViewU8, localHeapViewU32, malloc } from "./memory";
1212
import {
1313
JiterpNumberMode, BailoutReason, JiterpreterTable,
1414
JiterpCounter, JiterpMember, OpcodeInfoType
@@ -20,7 +20,8 @@ export const maxFailures = 2,
2020
shortNameBase = 36,
2121
// NOTE: This needs to be big enough to hold the maximum module size since there's no auto-growth
2222
// support yet. If that becomes a problem, we should just make it growable
23-
blobBuilderCapacity = 24 * 1024;
23+
blobBuilderCapacity = 24 * 1024,
24+
INT32_MIN = -2147483648;
2425

2526
// uint16
2627
export declare interface MintOpcodePtr extends NativePointer {
@@ -948,7 +949,7 @@ export class BlobBuilder {
948949

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

16661667
export function copyIntoScratchBuffer (src: NativePointer, size: number): NativePointer {
16671668
if (!scratchBuffer)
1668-
scratchBuffer = Module._malloc(64);
1669+
scratchBuffer = malloc(64);
16691670
if (size > 64)
16701671
throw new Error("Scratch buffer size is 64");
16711672

@@ -2106,7 +2107,7 @@ function updateOptions () {
21062107
optionTable = <any>{};
21072108
for (const k in optionNames) {
21082109
const value = cwraps.mono_jiterp_get_option_as_int(optionNames[k]);
2109-
if (value > -2147483647)
2110+
if (value !== INT32_MIN)
21102111
(<any>optionTable)[k] = value;
21112112
else
21122113
mono_log_info(`Failed to retrieve value of option ${optionNames[k]}`);

src/mono/browser/runtime/jiterpreter.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33

44
import { MonoMethod } from "./types/internal";
55
import { NativePointer } from "./types/emscripten";
6-
import { Module, mono_assert, runtimeHelpers } from "./globals";
7-
import { getU16 } from "./memory";
6+
import { mono_assert, runtimeHelpers } from "./globals";
7+
import { free, getU16 } from "./memory";
88
import { WasmValtype, WasmOpcode, getOpcodeName } from "./jiterpreter-opcodes";
99
import { MintOpcode } from "./mintops";
1010
import cwraps from "./cwraps";
@@ -1004,7 +1004,7 @@ export function mono_interp_tier_prepare_jiterpreter (
10041004
) {
10051005
const pMethodName = cwraps.mono_wasm_method_get_full_name(method);
10061006
methodFullName = utf8ToString(pMethodName);
1007-
Module._free(<any>pMethodName);
1007+
free(<any>pMethodName);
10081008
}
10091009
const methodName = utf8ToString(cwraps.mono_wasm_method_get_name(method));
10101010
info.name = methodFullName || methodName;
@@ -1148,7 +1148,7 @@ export function jiterpreter_dump_stats (concise?: boolean): void {
11481148
const pMethodName = cwraps.mono_wasm_method_get_full_name(<any>targetMethod);
11491149
const targetMethodName = utf8ToString(pMethodName);
11501150
const hitCount = callTargetCounts[<any>targetMethod];
1151-
Module._free(<any>pMethodName);
1151+
free(<any>pMethodName);
11521152
mono_log_info(`${targetMethodName} ${hitCount}`);
11531153
}
11541154
}

src/mono/browser/runtime/managed-exports.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { marshal_int32_to_js, end_marshal_task_to_js, marshal_string_to_js, begi
1212
import { do_not_force_dispose, is_gcv_handle } from "./gc-handles";
1313
import { assert_c_interop, assert_js_interop } from "./invoke-js";
1414
import { monoThreadInfo, mono_wasm_main_thread_ptr } from "./pthreads";
15-
import { _zero_region, copyBytes } from "./memory";
15+
import { _zero_region, copyBytes, malloc } from "./memory";
1616
import { stringToUTF8Ptr } from "./strings";
1717
import { mono_log_error } from "./logging";
1818

@@ -285,7 +285,7 @@ export function invoke_async_jsexport (managedTID: PThreadPtr, method: MonoMetho
285285
} else {
286286
set_receiver_should_free(args);
287287
const bytes = JavaScriptMarshalerArgSize * size;
288-
const cpy = Module._malloc(bytes) as any;
288+
const cpy = malloc(bytes) as any;
289289
copyBytes(args as any, cpy, bytes);
290290
twraps.mono_wasm_invoke_jsexport_async_post(managedTID, method, cpy);
291291
}

0 commit comments

Comments
 (0)