diff --git a/src/mono/browser/runtime/WASM Transition wrappers and trampolines.txt b/src/mono/browser/runtime/WASM Transition wrappers and trampolines.txt new file mode 100644 index 00000000000000..b7968ba8ac164c --- /dev/null +++ b/src/mono/browser/runtime/WASM Transition wrappers and trampolines.txt @@ -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. diff --git a/src/mono/browser/runtime/debug.ts b/src/mono/browser/runtime/debug.ts index 99963d964f00cc..522c3c41158b28 100644 --- a/src/mono/browser/runtime/debug.ts +++ b/src/mono/browser/runtime/debug.ts @@ -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(); commands_received.remove = function (key: number): CommandResponse { @@ -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(); diff --git a/src/mono/browser/runtime/interp-pgo.ts b/src/mono/browser/runtime/interp-pgo.ts index 90557b5f9e810f..3701deafd53d81 100644 --- a/src/mono/browser/runtime/interp-pgo.ts +++ b/src/mono/browser/runtime/interp-pgo.ts @@ -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"; @@ -31,7 +31,7 @@ export async function interp_pgo_save_data () { return; } - const pData = Module._malloc(expectedSize); + const pData = 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)"); @@ -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}`); } @@ -66,14 +66,14 @@ export async function interp_pgo_load_data () { return; } - const pData = Module._malloc(data.byteLength); + const pData = 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 { diff --git a/src/mono/browser/runtime/invoke-js.ts b/src/mono/browser/runtime/invoke-js.ts index 191bd8a0fc2934..0440a29c27e81b 100644 --- a/src/mono/browser/runtime/invoke-js.ts +++ b/src/mono/browser/runtime/invoke-js.ts @@ -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"; @@ -24,6 +24,7 @@ export const js_import_wrapper_by_fn_handle: Function[] = [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; @@ -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); @@ -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[function_handle]; mono_assert(bound_fn, () => `Imported function handle expected ${function_handle}`); bound_fn(args); @@ -334,7 +338,7 @@ function bind_fn (closure: BindingClosure) { marshal_exception_to_cs(args, ex); } finally { if (receiver_should_free) { - Module._free(args as any); + free(args as any); } endMeasure(mark, MeasuredBlock.callCsFunction, fqn); } @@ -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); } diff --git a/src/mono/browser/runtime/jiterpreter-interp-entry.ts b/src/mono/browser/runtime/jiterpreter-interp-entry.ts index 84be352d9125ae..f152a70da25706 100644 --- a/src/mono/browser/runtime/jiterpreter-interp-entry.ts +++ b/src/mono/browser/runtime/jiterpreter-interp-entry.ts @@ -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"; @@ -136,7 +138,7 @@ class TrampolineInfo { this.traceName = subName; } finally { if (namePtr) - Module._free(namePtr); + free(namePtr); } } @@ -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 = Module._malloc(sizeOfJiterpEntryData); + const scratchBuffer = malloc(sizeOfJiterpEntryData); _zero_region(scratchBuffer, sizeOfJiterpEntryData); // Initialize the parameter count in the data blob. This is used to calculate the new value of sp diff --git a/src/mono/browser/runtime/jiterpreter-jit-call.ts b/src/mono/browser/runtime/jiterpreter-jit-call.ts index 48e9797cb29e8f..102e402f284e4c 100644 --- a/src/mono/browser/runtime/jiterpreter-jit-call.ts +++ b/src/mono/browser/runtime/jiterpreter-jit-call.ts @@ -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 { @@ -152,7 +153,7 @@ class TrampolineInfo { suffix = utf8ToString(pMethodName); } finally { if (pMethodName) - Module._free(pMethodName); + free(pMethodName); } } diff --git a/src/mono/browser/runtime/jiterpreter-support.ts b/src/mono/browser/runtime/jiterpreter-support.ts index 7f9c3586bcb907..e914669dbb444e 100644 --- a/src/mono/browser/runtime/jiterpreter-support.ts +++ b/src/mono/browser/runtime/jiterpreter-support.ts @@ -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 @@ -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 { @@ -948,7 +949,7 @@ export class BlobBuilder { constructor () { this.capacity = blobBuilderCapacity; - this.buffer = Module._malloc(this.capacity); + this.buffer = 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; @@ -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"); @@ -2106,7 +2107,7 @@ function updateOptions () { optionTable = {}; for (const k in optionNames) { const value = cwraps.mono_jiterp_get_option_as_int(optionNames[k]); - if (value > -2147483647) + if (value !== INT32_MIN) (optionTable)[k] = value; else mono_log_info(`Failed to retrieve value of option ${optionNames[k]}`); diff --git a/src/mono/browser/runtime/jiterpreter.ts b/src/mono/browser/runtime/jiterpreter.ts index 996e7ed34f9d15..abaa7d63cd8e56 100644 --- a/src/mono/browser/runtime/jiterpreter.ts +++ b/src/mono/browser/runtime/jiterpreter.ts @@ -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"; @@ -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(pMethodName); + free(pMethodName); } const methodName = utf8ToString(cwraps.mono_wasm_method_get_name(method)); info.name = methodFullName || methodName; @@ -1148,7 +1148,7 @@ export function jiterpreter_dump_stats (concise?: boolean): void { const pMethodName = cwraps.mono_wasm_method_get_full_name(targetMethod); const targetMethodName = utf8ToString(pMethodName); const hitCount = callTargetCounts[targetMethod]; - Module._free(pMethodName); + free(pMethodName); mono_log_info(`${targetMethodName} ${hitCount}`); } } diff --git a/src/mono/browser/runtime/managed-exports.ts b/src/mono/browser/runtime/managed-exports.ts index 79bbcbf2af964c..472c47708c4739 100644 --- a/src/mono/browser/runtime/managed-exports.ts +++ b/src/mono/browser/runtime/managed-exports.ts @@ -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"; @@ -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); } diff --git a/src/mono/browser/runtime/marshal-to-cs.ts b/src/mono/browser/runtime/marshal-to-cs.ts index 58db72d21dd076..326905214f9778 100644 --- a/src/mono/browser/runtime/marshal-to-cs.ts +++ b/src/mono/browser/runtime/marshal-to-cs.ts @@ -8,7 +8,7 @@ import WasmEnableJsInteropByValue from "consts:wasmEnableJsInteropByValue"; import { PromiseHolder, isThenable } from "./cancelable-promise"; import cwraps from "./cwraps"; import { alloc_gcv_handle, assert_not_disposed, cs_owned_js_handle_symbol, js_owned_gc_handle_symbol, mono_wasm_get_js_handle, setup_managed_proxy } from "./gc-handles"; -import { Module, mono_assert, runtimeHelpers } from "./globals"; +import { mono_assert, runtimeHelpers } from "./globals"; import { ManagedError, set_gc_handle, set_js_handle, set_arg_type, set_arg_i32, set_arg_f64, set_arg_i52, set_arg_f32, set_arg_i16, set_arg_u8, set_arg_bool, set_arg_date, @@ -18,7 +18,7 @@ import { set_arg_element_type, ManagedObject, JavaScriptMarshalerArgSize, proxy_debug_symbol, get_arg_gc_handle, get_arg_type, set_arg_proxy_context, get_arg_intptr } from "./marshal"; import { get_marshaler_to_js_by_type } from "./marshal-to-js"; -import { _zero_region, localHeapViewF64, localHeapViewI32, localHeapViewU8 } from "./memory"; +import { _zero_region, fixupPointer, localHeapViewF64, localHeapViewI32, localHeapViewU8, malloc } from "./memory"; import { stringToMonoStringRoot, stringToUTF16 } from "./strings"; import { JSMarshalerArgument, JSMarshalerArguments, JSMarshalerType, MarshalerToCs, MarshalerToJs, BoundMarshalerToCs, MarshalerType } from "./types/internal"; import { TypedArray } from "./types/emscripten"; @@ -218,7 +218,7 @@ export function marshal_string_to_cs (arg: JSMarshalerArgument, value: string) { function _marshal_string_to_cs_impl (arg: JSMarshalerArgument, value: string) { if (WasmEnableJsInteropByValue) { const bufferLen = value.length * 2; - const buffer = Module._malloc(bufferLen);// together with Marshal.FreeHGlobal + const buffer = malloc(bufferLen);// together with Marshal.FreeHGlobal stringToUTF16(buffer as any, buffer as any + bufferLen, value); set_arg_intptr(arg, buffer); set_arg_length(arg, value.length); @@ -458,7 +458,7 @@ export function marshal_array_to_cs_impl (arg: JSMarshalerArgument, value: Array mono_assert(element_size != -1, () => `Element type ${element_type} not supported`); const length = value.length; const buffer_length = element_size * length; - const buffer_ptr = Module._malloc(buffer_length); + const buffer_ptr = malloc(buffer_length) as any; if (element_type == MarshalerType.String) { mono_check(Array.isArray(value), "Value is not an Array"); _zero_region(buffer_ptr, buffer_length); @@ -494,11 +494,13 @@ export function marshal_array_to_cs_impl (arg: JSMarshalerArgument, value: Array targetView.set(value); } else if (element_type == MarshalerType.Int32) { mono_check(Array.isArray(value) || value instanceof Int32Array, "Value is not an Array or Int32Array"); - const targetView = localHeapViewI32().subarray(buffer_ptr >> 2, (buffer_ptr >> 2) + length); + const bufferOffset = fixupPointer(buffer_ptr, 2); + const targetView = localHeapViewI32().subarray(bufferOffset, bufferOffset + length); targetView.set(value); } else if (element_type == MarshalerType.Double) { mono_check(Array.isArray(value) || value instanceof Float64Array, "Value is not an Array or Float64Array"); - const targetView = localHeapViewF64().subarray(buffer_ptr >> 3, (buffer_ptr >> 3) + length); + const bufferOffset = fixupPointer(buffer_ptr, 3); + const targetView = localHeapViewF64().subarray(bufferOffset, bufferOffset + length); targetView.set(value); } else { throw new Error("not implemented"); diff --git a/src/mono/browser/runtime/marshal-to-js.ts b/src/mono/browser/runtime/marshal-to-js.ts index 38354b39d0fe75..29d9f56ff6fb3a 100644 --- a/src/mono/browser/runtime/marshal-to-js.ts +++ b/src/mono/browser/runtime/marshal-to-js.ts @@ -7,7 +7,7 @@ import WasmEnableJsInteropByValue from "consts:wasmEnableJsInteropByValue"; import cwraps from "./cwraps"; import { _lookup_js_owned_object, mono_wasm_get_js_handle, mono_wasm_get_jsobj_from_js_handle, mono_wasm_release_cs_owned_object, register_with_jsv_handle, setup_managed_proxy, teardown_managed_proxy } from "./gc-handles"; -import { Module, loaderHelpers, mono_assert } from "./globals"; +import { loaderHelpers, mono_assert } from "./globals"; import { ManagedObject, ManagedError, get_arg_gc_handle, get_arg_js_handle, get_arg_type, get_arg_i32, get_arg_f64, get_arg_i52, get_arg_i16, get_arg_u8, get_arg_f32, @@ -20,7 +20,7 @@ import { monoStringToString, utf16ToString } from "./strings"; import { GCHandleNull, JSMarshalerArgument, JSMarshalerArguments, JSMarshalerType, MarshalerToCs, MarshalerToJs, BoundMarshalerToJs, MarshalerType, JSHandle } from "./types/internal"; import { TypedArray } from "./types/emscripten"; import { get_marshaler_to_cs_by_type, jsinteropDoc, marshal_exception_to_cs } from "./marshal-to-cs"; -import { localHeapViewF64, localHeapViewI32, localHeapViewU8 } from "./memory"; +import { fixupPointer, free, localHeapViewF64, localHeapViewI32, localHeapViewU8 } from "./memory"; import { call_delegate } from "./managed-exports"; import { mono_log_debug } from "./logging"; import { invoke_later_when_on_ui_thread_async } from "./invoke-js"; @@ -345,6 +345,7 @@ export function mono_wasm_resolve_or_reject_promise_impl (args: JSMarshalerArgum mono_log_debug("This promise resolution/rejection can't be propagated to managed code, mono runtime already exited."); return; } + args = fixupPointer(args, 0); const exc = get_arg(args, 0); const receiver_should_free = WasmEnableThreads && is_receiver_should_free(args); try { @@ -363,7 +364,7 @@ export function mono_wasm_resolve_or_reject_promise_impl (args: JSMarshalerArgum holder.resolve_or_reject(type, js_handle, arg_value); if (receiver_should_free) { // this works together with AllocHGlobal in JSFunctionBinding.ResolveOrRejectPromise - Module._free(args as any); + free(args as any); } else { set_arg_type(res, MarshalerType.Void); set_arg_type(exc, MarshalerType.None); @@ -386,7 +387,7 @@ export function marshal_string_to_js (arg: JSMarshalerArgument): string | null { const buffer = get_arg_intptr(arg); const len = get_arg_length(arg) * 2; const value = utf16ToString(buffer, buffer + len); - Module._free(buffer as any); + free(buffer as any); return value; } else { mono_assert(!WasmEnableThreads, "Marshaling strings by reference is not supported in multithreaded mode"); @@ -524,18 +525,21 @@ function _marshal_array_to_js_impl (arg: JSMarshalerArgument, element_type: Mars result[index] = _marshal_js_object_to_js(element_arg); } } else if (element_type == MarshalerType.Byte) { - const sourceView = localHeapViewU8().subarray(buffer_ptr, buffer_ptr + length); + const bufferOffset = fixupPointer(buffer_ptr, 0); + const sourceView = localHeapViewU8().subarray(bufferOffset, bufferOffset + length); result = sourceView.slice();//copy } else if (element_type == MarshalerType.Int32) { - const sourceView = localHeapViewI32().subarray(buffer_ptr >> 2, (buffer_ptr >> 2) + length); + const bufferOffset = fixupPointer(buffer_ptr, 2); + const sourceView = localHeapViewI32().subarray(bufferOffset, bufferOffset + length); result = sourceView.slice();//copy } else if (element_type == MarshalerType.Double) { - const sourceView = localHeapViewF64().subarray(buffer_ptr >> 3, (buffer_ptr >> 3) + length); + const bufferOffset = fixupPointer(buffer_ptr, 3); + const sourceView = localHeapViewF64().subarray(bufferOffset, bufferOffset + length); result = sourceView.slice();//copy } else { throw new Error(`NotImplementedException ${element_type}. ${jsinteropDoc}`); } - Module._free(buffer_ptr); + free(buffer_ptr); return result; } diff --git a/src/mono/browser/runtime/marshal.ts b/src/mono/browser/runtime/marshal.ts index b26a94926f88e7..5d537670c4d9db 100644 --- a/src/mono/browser/runtime/marshal.ts +++ b/src/mono/browser/runtime/marshal.ts @@ -232,7 +232,7 @@ export function get_arg_i32 (arg: JSMarshalerArgument): number { export function get_arg_intptr (arg: JSMarshalerArgument): number { mono_assert(arg, "Null arg"); - return getI32(arg); + return getU32(arg); } export function get_arg_i52 (arg: JSMarshalerArgument): number { @@ -291,7 +291,7 @@ export function set_arg_i32 (arg: JSMarshalerArgument, value: number): void { export function set_arg_intptr (arg: JSMarshalerArgument, value: VoidPtr): void { mono_assert(arg, "Null arg"); - setI32(arg, value); + setU32(arg, value); } export function set_arg_i52 (arg: JSMarshalerArgument, value: number): void { diff --git a/src/mono/browser/runtime/memory.ts b/src/mono/browser/runtime/memory.ts index 1834bcf3b88891..0f9f751854de17 100644 --- a/src/mono/browser/runtime/memory.ts +++ b/src/mono/browser/runtime/memory.ts @@ -17,7 +17,7 @@ let alloca_base: VoidPtr, alloca_offset: VoidPtr, alloca_limit: VoidPtr; function _ensure_allocated (): void { if (alloca_base) return; - alloca_base = Module._malloc(alloca_buffer_size); + alloca_base = malloc(alloca_buffer_size); alloca_offset = alloca_base; alloca_limit = (alloca_base + alloca_buffer_size); } @@ -37,6 +37,15 @@ export function temp_malloc (size: number): VoidPtr { return result; } +// returns always uint32 (not negative Number) +export function malloc (size: number): VoidPtr { + return (Module._malloc(size) as any >>> 0) as any; +} + +export function free (ptr: VoidPtr) { + Module._free(ptr); +} + export function _create_temp_frame (): void { _ensure_allocated(); alloca_stack.push(alloca_offset); @@ -326,7 +335,7 @@ export function withStackAlloc (bytesWanted: number, f: (pt // and it is copied to that location. returns the address of the allocation. export function mono_wasm_load_bytes_into_heap (bytes: Uint8Array): VoidPtr { // pad sizes by 16 bytes for simd - const memoryOffset = Module._malloc(bytes.length + 16); + const memoryOffset = malloc(bytes.length + 16); if (memoryOffset <= 0) { mono_log_error(`malloc failed to allocate ${(bytes.length + 16)} bytes.`); throw new Error("Out of memory"); @@ -501,3 +510,7 @@ export function forceThreadMemoryViewRefresh () { runtimeHelpers.updateMemoryViews(); } } + +export function fixupPointer (signature: any, shiftAmount: number): any { + return ((signature as any) >>> shiftAmount) as any; +} diff --git a/src/mono/browser/runtime/roots.ts b/src/mono/browser/runtime/roots.ts index fdd4b2a5302f2a..5b81331c29ff25 100644 --- a/src/mono/browser/runtime/roots.ts +++ b/src/mono/browser/runtime/roots.ts @@ -4,10 +4,10 @@ import WasmEnableThreads from "consts:wasmEnableThreads"; import cwraps from "./cwraps"; -import { Module, mono_assert, runtimeHelpers } from "./globals"; +import { mono_assert, runtimeHelpers } from "./globals"; import { VoidPtr, ManagedPointer, NativePointer } from "./types/emscripten"; import { MonoObjectRef, MonoObjectRefNull, MonoObject, is_nullish, WasmRoot, WasmRootBuffer } from "./types/internal"; -import { _zero_region, localHeapViewU32 } from "./memory"; +import { _zero_region, free, localHeapViewU32, malloc } from "./memory"; import { gc_locked } from "./gc-lock"; const maxScratchRoots = 8192; @@ -31,7 +31,7 @@ export function mono_wasm_new_root_buffer (capacity: number, name?: string): Was capacity = capacity | 0; const capacityBytes = capacity * 4; - const offset = Module._malloc(capacityBytes); + const offset = malloc(capacityBytes); if ((offset % 4) !== 0) throw new Error("Malloc returned an unaligned offset"); @@ -238,7 +238,7 @@ export class WasmRootBufferImpl implements WasmRootBuffer { mono_assert(!WasmEnableThreads || !gc_locked, "GC must not be locked when disposing a GC root"); cwraps.mono_wasm_deregister_root(this.__offset); _zero_region(this.__offset, this.__count * 4); - Module._free(this.__offset); + free(this.__offset); } this.__handle = (this.__offset) = this.__count = this.__offset32 = 0; diff --git a/src/mono/browser/runtime/startup.ts b/src/mono/browser/runtime/startup.ts index 6bc0b69c118826..8b37d90e281810 100644 --- a/src/mono/browser/runtime/startup.ts +++ b/src/mono/browser/runtime/startup.ts @@ -16,7 +16,7 @@ import { init_polyfills_async } from "./polyfills"; import { strings_init, utf8ToString } from "./strings"; import { init_managed_exports } from "./managed-exports"; import { cwraps_internal } from "./exports-internal"; -import { CharPtr, InstantiateWasmCallBack, InstantiateWasmSuccessCallback } from "./types/emscripten"; +import { CharPtr, EmscriptenModule, InstantiateWasmCallBack, InstantiateWasmSuccessCallback } from "./types/emscripten"; import { wait_for_all_assets } from "./assets"; import { replace_linker_placeholders } from "./exports-binding"; import { endMeasure, MeasuredBlock, startMeasure } from "./profiler"; @@ -28,7 +28,7 @@ import { populateEmscriptenPool, mono_wasm_init_threads } from "./pthreads"; import { currentWorkerThreadEvents, dotnetPthreadCreated, initWorkerThreadEvents, monoThreadInfo } from "./pthreads"; import { mono_wasm_pthread_ptr, update_thread_info } from "./pthreads"; import { jiterpreter_allocate_tables } from "./jiterpreter-support"; -import { localHeapViewU8 } from "./memory"; +import { localHeapViewU8, malloc } from "./memory"; import { assertNoProxies } from "./gc-handles"; import { runtimeList } from "./exports"; import { nativeAbort, nativeExit } from "./run"; @@ -70,11 +70,11 @@ export function configureEmscriptenStartup (module: DotnetModuleInternal): void // these all could be overridden on DotnetModuleConfig, we are chaing them to async below, as opposed to emscripten // when user set configSrc or config, we are running our default startup sequence. const userInstantiateWasm: undefined | ((imports: WebAssembly.Imports, successCallback: InstantiateWasmSuccessCallback) => any) = module.instantiateWasm; - const userPreInit: (() => void)[] = !module.preInit ? [] : typeof module.preInit === "function" ? [module.preInit] : module.preInit; - const userPreRun: (() => void)[] = !module.preRun ? [] : typeof module.preRun === "function" ? [module.preRun] : module.preRun as any; - const userpostRun: (() => void)[] = !module.postRun ? [] : typeof module.postRun === "function" ? [module.postRun] : module.postRun as any; + const userPreInit: ((module:EmscriptenModule) => void)[] = !module.preInit ? [] : typeof module.preInit === "function" ? [module.preInit] : module.preInit; + const userPreRun: ((module:EmscriptenModule) => void)[] = !module.preRun ? [] : typeof module.preRun === "function" ? [module.preRun] : module.preRun as any; + const userpostRun: ((module:EmscriptenModule) => void)[] = !module.postRun ? [] : typeof module.postRun === "function" ? [module.postRun] : module.postRun as any; // eslint-disable-next-line @typescript-eslint/no-empty-function - const userOnRuntimeInitialized: () => void = module.onRuntimeInitialized ? module.onRuntimeInitialized : () => { }; + const userOnRuntimeInitialized: (module:EmscriptenModule) => void = module.onRuntimeInitialized ? module.onRuntimeInitialized : () => { }; // execution order == [0] == // - default or user Module.instantiateWasm (will start downloading dotnet.native.wasm) @@ -144,7 +144,7 @@ async function instantiateWasmWorker ( Module.wasmModule = null; } -function preInit (userPreInit: (() => void)[]) { +function preInit (userPreInit: ((module:EmscriptenModule) => void)[]) { Module.addRunDependency("mono_pre_init"); const mark = startMeasure(); try { @@ -152,7 +152,7 @@ function preInit (userPreInit: (() => void)[]) { mono_log_debug("preInit"); runtimeHelpers.beforePreInit.promise_control.resolve(); // all user Module.preInit callbacks - userPreInit.forEach(fn => fn()); + userPreInit.forEach(fn => fn(Module)); } catch (err) { mono_log_error("user preInint() failed", err); loaderHelpers.mono_exit(1, err); @@ -219,7 +219,7 @@ export function preRunWorker () { } } -async function preRunAsync (userPreRun: (() => void)[]) { +async function preRunAsync (userPreRun: ((module:EmscriptenModule) => void)[]) { Module.addRunDependency("mono_pre_run_async"); // wait for previous stages try { @@ -228,7 +228,7 @@ async function preRunAsync (userPreRun: (() => void)[]) { mono_log_debug("preRunAsync"); const mark = startMeasure(); // all user Module.preRun callbacks - userPreRun.map(fn => fn()); + userPreRun.map(fn => fn(Module)); endMeasure(mark, MeasuredBlock.preRun); } catch (err) { mono_log_error("preRunAsync() failed", err); @@ -240,7 +240,7 @@ async function preRunAsync (userPreRun: (() => void)[]) { Module.removeRunDependency("mono_pre_run_async"); } -async function onRuntimeInitializedAsync (userOnRuntimeInitialized: () => void) { +async function onRuntimeInitializedAsync (userOnRuntimeInitialized: (module:EmscriptenModule) => void) { try { // wait for previous stage await runtimeHelpers.afterPreRun.promise; @@ -343,7 +343,7 @@ async function onRuntimeInitializedAsync (userOnRuntimeInitialized: () => void) // call user code try { - userOnRuntimeInitialized(); + userOnRuntimeInitialized(Module); } catch (err: any) { mono_log_error("user callback onRuntimeInitialized() failed", err); throw err; @@ -361,7 +361,7 @@ async function onRuntimeInitializedAsync (userOnRuntimeInitialized: () => void) runtimeHelpers.afterOnRuntimeInitialized.promise_control.resolve(); } -async function postRunAsync (userpostRun: (() => void)[]) { +async function postRunAsync (userpostRun: ((module:EmscriptenModule) => void)[]) { // wait for previous stage try { await runtimeHelpers.afterOnRuntimeInitialized.promise; @@ -373,7 +373,7 @@ async function postRunAsync (userpostRun: (() => void)[]) { Module["FS_createPath"]("/", "usr/share", true, true); // all user Module.postRun callbacks - userpostRun.map(fn => fn()); + userpostRun.map(fn => fn(Module)); endMeasure(mark, MeasuredBlock.postRun); } catch (err) { mono_log_error("postRunAsync() failed", err); @@ -471,7 +471,7 @@ export function mono_wasm_set_runtime_options (options: string[]): void { if (!Array.isArray(options)) throw new Error("Expected runtimeOptions to be an array of strings"); - const argv = Module._malloc(options.length * 4); + const argv = malloc(options.length * 4); let aindex = 0; for (let i = 0; i < options.length; ++i) { const option = options[i]; @@ -631,7 +631,7 @@ export function bindings_init (): void { init_managed_exports(); initialize_marshalers_to_js(); initialize_marshalers_to_cs(); - runtimeHelpers._i52_error_scratch_buffer = Module._malloc(4); + runtimeHelpers._i52_error_scratch_buffer = malloc(4); endMeasure(mark, MeasuredBlock.bindingsInit); } catch (err) { mono_log_error("Error in bindings_init", err); @@ -665,7 +665,7 @@ export function mono_wasm_asm_loaded (assembly_name: CharPtr, assembly_ptr: numb export function mono_wasm_set_main_args (name: string, allRuntimeArguments: string[]): void { const main_argc = allRuntimeArguments.length + 1; - const main_argv = Module._malloc(main_argc * 4); + const main_argv = malloc(main_argc * 4); let aindex = 0; Module.setValue(main_argv + (aindex * 4), cwraps.mono_wasm_strdup(name), "i32"); aindex += 1; diff --git a/src/mono/browser/runtime/strings.ts b/src/mono/browser/runtime/strings.ts index 0eda60ba850a00..2b52f82b0320b8 100644 --- a/src/mono/browser/runtime/strings.ts +++ b/src/mono/browser/runtime/strings.ts @@ -7,7 +7,7 @@ import { mono_wasm_new_root, mono_wasm_new_root_buffer } from "./roots"; import { MonoString, MonoStringNull, WasmRoot, WasmRootBuffer } from "./types/internal"; import { Module } from "./globals"; import cwraps from "./cwraps"; -import { isSharedArrayBuffer, localHeapViewU8, getU32_local, setU16_local, localHeapViewU32, getU16_local, localHeapViewU16, _zero_region } from "./memory"; +import { isSharedArrayBuffer, localHeapViewU8, getU32_local, setU16_local, localHeapViewU32, getU16_local, localHeapViewU16, _zero_region, malloc, free } from "./memory"; import { NativePointer, CharPtr, VoidPtr } from "./types/emscripten"; export const interned_js_string_table = new Map(); @@ -31,7 +31,7 @@ export function strings_init (): void { _text_decoder_utf8_validating = new TextDecoder("utf-8"); _text_encoder_utf8 = new TextEncoder(); } - mono_wasm_string_decoder_buffer = Module._malloc(12); + mono_wasm_string_decoder_buffer = malloc(12); } if (!mono_wasm_string_root) mono_wasm_string_root = mono_wasm_new_root(); @@ -49,7 +49,7 @@ export function stringToUTF8 (str: string): Uint8Array { export function stringToUTF8Ptr (str: string): CharPtr { const size = Module.lengthBytesUTF8(str) + 1; - const ptr = Module._malloc(size) as any; + const ptr = malloc(size) as any; const buffer = localHeapViewU8().subarray(ptr, ptr + size); Module.stringToUTF8Array(str, buffer, 0, size); buffer[size - 1] = 0; @@ -113,7 +113,7 @@ export function stringToUTF16 (dstPtr: number, endPtr: number, text: string) { export function stringToUTF16Ptr (str: string): VoidPtr { const bytes = (str.length + 1) * 2; - const ptr = Module._malloc(bytes) as any; + const ptr = malloc(bytes) as any; _zero_region(ptr, str.length * 2); stringToUTF16(ptr, ptr + bytes, str); return ptr; @@ -264,10 +264,10 @@ function stringToMonoStringNewRoot (string: string, result: WasmRoot // TODO this could be stack allocated for small strings // or temp_malloc/alloca for large strings // or skip the scratch buffer entirely, and make a new MonoString of size string.length, pin it, and then call stringToUTF16 to write directly into the MonoString's chars - const buffer = Module._malloc(bufferLen); + const buffer = malloc(bufferLen); stringToUTF16(buffer as any, buffer as any + bufferLen, string); cwraps.mono_wasm_string_from_utf16_ref(buffer, string.length, result.address); - Module._free(buffer); + free(buffer); } // When threading is enabled, TextDecoder does not accept a view of a diff --git a/src/mono/wasm/Wasm.Build.Tests/TestAppScenarios/MemoryTests.cs b/src/mono/wasm/Wasm.Build.Tests/TestAppScenarios/MemoryTests.cs index 239950d1fd7279..d0589c04f1ddd1 100644 --- a/src/mono/wasm/Wasm.Build.Tests/TestAppScenarios/MemoryTests.cs +++ b/src/mono/wasm/Wasm.Build.Tests/TestAppScenarios/MemoryTests.cs @@ -30,7 +30,7 @@ public async Task AllocateLargeHeapThenRepeatedlyInterop() { string config = "Release"; CopyTestAsset("WasmBasicTestApp", "MemoryTests", "App"); - string extraArgs = BuildTestBase.IsUsingWorkloads ? "-p:EmccMaximumHeapSize=4294901760" : "-p:EmccMaximumHeapSize=4294901760"; + string extraArgs = "-p:EmccMaximumHeapSize=4294901760"; BuildProject(config, assertAppBundle: false, extraArgs: extraArgs, expectSuccess: BuildTestBase.IsUsingWorkloads); if (BuildTestBase.IsUsingWorkloads) diff --git a/src/mono/wasm/testassets/WasmBasicTestApp/App/MemoryTest.cs b/src/mono/wasm/testassets/WasmBasicTestApp/App/MemoryTest.cs index aa9ee1819ae2ad..827c0a151c2c0e 100644 --- a/src/mono/wasm/testassets/WasmBasicTestApp/App/MemoryTest.cs +++ b/src/mono/wasm/testassets/WasmBasicTestApp/App/MemoryTest.cs @@ -15,11 +15,11 @@ public partial class MemoryTest [JSExport] internal static void Run() { - // Allocate over 2GB space, 2 621 440 000 bytes - const int arrayCnt = 25; + // Allocate 250MB managed space above 2GB already wasted before startup + const int arrayCnt = 10; int[][] arrayHolder = new int[arrayCnt][]; string errors = ""; - TestOutput.WriteLine("Starting over 2GB array allocation"); + TestOutput.WriteLine("Starting managed array allocation"); for (int i = 0; i < arrayCnt; i++) { try @@ -31,7 +31,7 @@ internal static void Run() errors += $"Exception {ex} was thrown on i={i}"; } } - TestOutput.WriteLine("Finished over 2GB array allocation"); + TestOutput.WriteLine("Finished managed array allocation"); // call a method many times to trigger tier-up optimization string randomString = GenerateRandomString(1000); diff --git a/src/mono/wasm/testassets/WasmBasicTestApp/App/wwwroot/main.js b/src/mono/wasm/testassets/WasmBasicTestApp/App/wwwroot/main.js index bc9cc730a364e9..b2354f0672c3f1 100644 --- a/src/mono/wasm/testassets/WasmBasicTestApp/App/wwwroot/main.js +++ b/src/mono/wasm/testassets/WasmBasicTestApp/App/wwwroot/main.js @@ -127,6 +127,19 @@ switch (testCase) { }; dotnet.withConfig({ maxParallelDownloads: maxParallelDownloads }); break; + case "AllocateLargeHeapThenInterop": + dotnet.withEnvironmentVariable("MONO_LOG_LEVEL", "debug") + dotnet.withEnvironmentVariable("MONO_LOG_MASK", "gc") + dotnet.withModuleConfig({ + preRun: (Module) => { + // wasting 2GB of memory + for (let i = 0; i < 210; i++) { + testOutput(`wasting 10m ${Module._malloc(10 * 1024 * 1024)}`); + } + testOutput(`WASM ${Module.HEAP32.byteLength} bytes.`); + } + }) + break; case "ProfilerTest": dotnet.withConfig({ logProfilerOptions: {