diff --git a/src/mono/browser/runtime/crypto.ts b/src/mono/browser/runtime/crypto.ts index a6642d874ffcbb..a782b3a3874771 100644 --- a/src/mono/browser/runtime/crypto.ts +++ b/src/mono/browser/runtime/crypto.ts @@ -10,6 +10,7 @@ export function mono_wasm_browser_entropy (bufferPtr: number, bufferLength: numb return -1; } + bufferPtr = bufferPtr >>> 0; const memoryView = localHeapViewU8(); const targetView = memoryView.subarray(bufferPtr, bufferPtr + bufferLength); diff --git a/src/mono/browser/runtime/debug.ts b/src/mono/browser/runtime/debug.ts index 522c3c41158b28..45d1714cc84a17 100644 --- a/src/mono/browser/runtime/debug.ts +++ b/src/mono/browser/runtime/debug.ts @@ -6,7 +6,7 @@ import { toBase64StringImpl } from "./base64"; import cwraps from "./cwraps"; import { VoidPtr, CharPtr } from "./types/emscripten"; import { mono_log_warn } from "./logging"; -import { forceThreadMemoryViewRefresh, free, localHeapViewU8, malloc } from "./memory"; +import { forceThreadMemoryViewRefresh, fixupPointer, free, localHeapViewU8, malloc } from "./memory"; import { utf8ToString } from "./strings"; const commands_received: any = new Map(); commands_received.remove = function (key: number): CommandResponse { @@ -42,12 +42,12 @@ export function mono_wasm_fire_debugger_agent_message_with_data_to_pause (base64 } export function mono_wasm_fire_debugger_agent_message_with_data (data: number, len: number): void { - const base64String = toBase64StringImpl(new Uint8Array(localHeapViewU8().buffer, data, len)); + const base64String = toBase64StringImpl(new Uint8Array(localHeapViewU8().buffer, fixupPointer(data, 0), len)); mono_wasm_fire_debugger_agent_message_with_data_to_pause(base64String); } export function mono_wasm_add_dbg_command_received (res_ok: boolean, id: number, buffer: number, buffer_len: number): void { - const dbg_command = new Uint8Array(localHeapViewU8().buffer, buffer, buffer_len); + const dbg_command = new Uint8Array(localHeapViewU8().buffer, fixupPointer(buffer, 0), buffer_len); const base64String = toBase64StringImpl(dbg_command); const buffer_obj = { res_ok, diff --git a/src/mono/browser/runtime/diagnostics/common.ts b/src/mono/browser/runtime/diagnostics/common.ts index dab4823b6b78ac..161480a5435962 100644 --- a/src/mono/browser/runtime/diagnostics/common.ts +++ b/src/mono/browser/runtime/diagnostics/common.ts @@ -51,7 +51,7 @@ export class DiagnosticConnectionBase { } const message = this.messagesReceived[0]!; const bytes_read = Math.min(message.length, bytes_to_read); - Module.HEAPU8.set(message.subarray(0, bytes_read), buffer as any); + Module.HEAPU8.set(message.subarray(0, bytes_read), buffer as any >>> 0); if (bytes_read === message.length) { this.messagesReceived.shift(); } else { diff --git a/src/mono/browser/runtime/diagnostics/index.ts b/src/mono/browser/runtime/diagnostics/index.ts index 660ac47d8dcc82..c46fbd7822f077 100644 --- a/src/mono/browser/runtime/diagnostics/index.ts +++ b/src/mono/browser/runtime/diagnostics/index.ts @@ -40,7 +40,7 @@ export function setRuntimeGlobals (globalObjects: GlobalObjects): void { if (!wrapper) { return -1; } - const message = (new Uint8Array(Module.HEAPU8.buffer, buffer as any, bytes_to_write)).slice(); + const message = (new Uint8Array(Module.HEAPU8.buffer, buffer as any >>> 0, bytes_to_write)).slice(); return wrapper.send(message); }; diff --git a/src/mono/browser/runtime/jiterpreter-interp-entry.ts b/src/mono/browser/runtime/jiterpreter-interp-entry.ts index f152a70da25706..176ecec246ac70 100644 --- a/src/mono/browser/runtime/jiterpreter-interp-entry.ts +++ b/src/mono/browser/runtime/jiterpreter-interp-entry.ts @@ -167,7 +167,7 @@ export function mono_jiterp_free_method_data_interp_entry (imethod: number) { // FIXME: move this counter into C and make it thread safe export function mono_interp_record_interp_entry (imethod: number) { // clear the unbox bit - imethod = imethod & ~0x1; + imethod = (imethod & ~0x1) >>> 0; const info = infoTable[imethod]; // This shouldn't happen but it's not worth crashing over @@ -199,6 +199,10 @@ export function mono_interp_jit_wasm_entry_trampoline ( if (argumentCount > maxInlineArgs) return 0; + imethod = imethod >>> 0; + method = method as any >>> 0 as any; + pParamTypes = pParamTypes as any >>> 0 as any; + const info = new TrampolineInfo( imethod, method, argumentCount, pParamTypes, unbox, hasThisReference, hasReturnValue, defaultImplementation @@ -265,10 +269,9 @@ function flush_wasm_entry_trampoline_jit_queue () { // If the function signature contains types that need stackval_from_data, that'll use // some constant slots, so make some extra space - const constantSlots = (4 * jitQueue.length) + 1; let builder = trampBuilder; if (!builder) { - trampBuilder = builder = new WasmBuilder(constantSlots); + trampBuilder = builder = new WasmBuilder(); builder.defineType( "unbox", @@ -303,7 +306,7 @@ function flush_wasm_entry_trampoline_jit_queue () { WasmValtype.void, true ); } else - builder.clear(constantSlots); + builder.clear(); if (builder.options.wasmBytesLimit <= getCounter(JiterpCounter.BytesGenerated)) { return; diff --git a/src/mono/browser/runtime/jiterpreter-jit-call.ts b/src/mono/browser/runtime/jiterpreter-jit-call.ts index 102e402f284e4c..b0516d1e5c8014 100644 --- a/src/mono/browser/runtime/jiterpreter-jit-call.ts +++ b/src/mono/browser/runtime/jiterpreter-jit-call.ts @@ -182,6 +182,11 @@ function getWasmTableEntry (index: number) { export function mono_interp_invoke_wasm_jit_call_trampoline ( thunkIndex: number, ret_sp: number, sp: number, ftndesc: number, thrown: NativePointer ) { + ret_sp = ret_sp as any >>> 0 as any; + sp = sp as any >>> 0 as any; + ftndesc = ftndesc as any >>> 0 as any; + thrown = thrown as any >>> 0 as any; + const thunk = getWasmTableEntry(thunkIndex); try { thunk(ret_sp, sp, ftndesc, thrown); @@ -234,6 +239,11 @@ export function mono_interp_jit_wasm_jit_call_trampoline ( method: MonoMethod, rmethod: VoidPtr, cinfo: VoidPtr, arg_offsets: VoidPtr, catch_exceptions: number ): void { + method = method as any >>> 0 as any; + rmethod = rmethod as any >>> 0 as any; + cinfo = cinfo as any >>> 0 as any; + arg_offsets = arg_offsets as any >>> 0 as any; + // multiple cinfos can share the same target function, so for that scenario we want to // use the same TrampolineInfo for all of them. if that info has already been jitted // we want to immediately store its pointer into the cinfo, otherwise we add it to @@ -296,7 +306,7 @@ export function mono_interp_flush_jitcall_queue (): void { let builder = trampBuilder; if (!builder) { - trampBuilder = builder = new WasmBuilder(0); + trampBuilder = builder = new WasmBuilder(); // Function type for compiled trampolines builder.defineType( "trampoline", @@ -316,7 +326,7 @@ export function mono_interp_flush_jitcall_queue (): void { builder.defineImportedFunction("i", "begin_catch", "begin_catch", true, getRawCwrap("mono_jiterp_begin_catch")); builder.defineImportedFunction("i", "end_catch", "end_catch", true, getRawCwrap("mono_jiterp_end_catch")); } else - builder.clear(0); + builder.clear(); if (builder.options.wasmBytesLimit <= getCounter(JiterpCounter.BytesGenerated)) { cwraps.mono_jiterp_tlqueue_clear(JitQueue.JitCall); diff --git a/src/mono/browser/runtime/jiterpreter-support.ts b/src/mono/browser/runtime/jiterpreter-support.ts index 5bd39ede34563b..8be68c9cfd4484 100644 --- a/src/mono/browser/runtime/jiterpreter-support.ts +++ b/src/mono/browser/runtime/jiterpreter-support.ts @@ -103,10 +103,8 @@ export class WasmBuilder { traceBuf: Array = []; branchTargets = new Set(); options!: JiterpreterOptions; - constantSlots: Array = []; backBranchOffsets: Array = []; callHandlerReturnAddresses: Array = []; - nextConstantSlot = 0; backBranchTraceLevel = 0; containsSimd!: boolean; @@ -115,14 +113,14 @@ export class WasmBuilder { compressImportNames = false; lockImports = false; - constructor (constantSlotCount: number) { + constructor () { this.stack = [new BlobBuilder()]; - this.clear(constantSlotCount); + this.clear(); this.cfg = new Cfg(this); this.defineType("__cpp_exception", { "ptr": WasmValtype.i32 }, WasmValtype.void, true); } - clear (constantSlotCount: number) { + clear () { this.options = getOptions(); if (this.options.maxModuleSize >= blobBuilderCapacity) throw new Error(`blobBuilderCapacity ${blobBuilderCapacity} is not large enough for jiterpreter-max-module-size of ${this.options.maxModuleSize}`); @@ -154,10 +152,6 @@ export class WasmBuilder { this.traceBuf.length = 0; this.branchTargets.clear(); this.activeBlocks = 0; - this.nextConstantSlot = 0; - this.constantSlots.length = this.options.useConstants ? constantSlotCount : 0; - for (let i = 0; i < this.constantSlots.length; i++) - this.constantSlots[i] = 0; this.backBranchOffsets.length = 0; this.callHandlerReturnAddresses.length = 0; @@ -209,7 +203,6 @@ export class WasmBuilder { const exceptionTag = this.getExceptionTag(); const result: any = { - c: this.getConstants(), m: { h: memory }, }; if (exceptionTag) @@ -328,22 +321,10 @@ export class WasmBuilder { } ptr_const (pointer: number | ManagedPointer | NativePointer) { - let idx = this.options.useConstants ? this.constantSlots.indexOf(pointer) : -1; - if ( - this.options.useConstants && - (idx < 0) && (this.nextConstantSlot < this.constantSlots.length) - ) { - idx = this.nextConstantSlot++; - this.constantSlots[idx] = pointer; - } - - if (idx >= 0) { - this.appendU8(WasmOpcode.get_global); - this.appendLeb(idx); - } else { - // mono_log_info(`Warning: no constant slot for ${pointer} (${this.nextConstantSlot} slots used)`); - this.i32_const(pointer); - } + // mono_log_info(`Warning: no constant slot for ${pointer} (${this.nextConstantSlot} slots used)`); + this.appendU8(WasmOpcode.i32_const); + // i32_const is always signed + this.appendLeb((pointer as any) | 0); } ip_const (value: MintOpcodePtr) { @@ -507,7 +488,7 @@ export class WasmBuilder { this.appendULeb( 1 + // memory (enableWasmEh ? 1 : 0) + // c++ exception tag - importsToEmit.length + this.constantSlots.length + + importsToEmit.length + ((includeFunctionTable !== false) ? 1 : 0) ); @@ -521,14 +502,6 @@ export class WasmBuilder { this.appendU8(ifi.typeIndex); } - for (let i = 0; i < this.constantSlots.length; i++) { - this.appendName("c"); - this.appendName(i.toString(shortNameBase)); - this.appendU8(0x03); // global - this.appendU8(WasmValtype.i32); // all constants are pointers right now - this.appendU8(0x00); // constant - } - // import the native heap this.appendName("m"); this.appendName("h"); @@ -909,7 +882,8 @@ export class WasmBuilder { appendMemarg (offset: number, alignPower: number) { this.appendULeb(alignPower); - this.appendULeb(offset); + // u64 + this.appendULeb(offset >>> 0); } /* @@ -919,10 +893,9 @@ export class WasmBuilder { if (typeof (ptr1) === "string") this.local(ptr1); else - this.i32_const(ptr1); + this.ptr_const(ptr1); this.i32_const(offset); - // FIXME: How do we make sure this has correct semantics for pointers over 2gb? this.appendU8(WasmOpcode.i32_add); } @@ -931,13 +904,6 @@ export class WasmBuilder { throw new Error("Jiterpreter block stack not empty"); return this.stack[0].getArrayView(fullCapacity); } - - getConstants () { - const result: { [key: string]: number } = {}; - for (let i = 0; i < this.constantSlots.length; i++) - result[i.toString(shortNameBase)] = this.constantSlots[i]; - return result; - } } export class BlobBuilder { @@ -1617,7 +1583,7 @@ export function append_profiler_event (builder: WasmBuilder, ip: MintOpcodePtr, throw new Error(`Unimplemented profiler event ${opcode}`); } builder.local("frame"); - builder.i32_const(ip); + builder.ptr_const(ip); builder.callImport(event_name); } @@ -1633,7 +1599,7 @@ export function append_safepoint (builder: WasmBuilder, ip: MintOpcodePtr) { builder.block(WasmValtype.void, WasmOpcode.if_); builder.local("frame"); // Not ip_const, because we can't pass relative IP to do_safepoint - builder.i32_const(ip); + builder.ptr_const(ip); builder.callImport("safepoint"); builder.endBlock(); } @@ -2020,8 +1986,6 @@ export type JiterpreterOptions = { countBailouts: boolean; // Dump the wasm blob for all compiled traces dumpTraces: boolean; - // Use runtime imports for pointer constants - useConstants: boolean; // Enable performing backward branches without exiting traces noExitBackwardBranches: boolean; // Unwrap gsharedvt wrappers when compiling jitcalls if possible @@ -2062,7 +2026,6 @@ const optionNames: { [jsName: string]: string } = { "estimateHeat": "jiterpreter-estimate-heat", "countBailouts": "jiterpreter-count-bailouts", "dumpTraces": "jiterpreter-dump-traces", - "useConstants": "jiterpreter-use-constants", "eliminateNullChecks": "jiterpreter-eliminate-null-checks", "noExitBackwardBranches": "jiterpreter-backward-branches-enabled", "directJitCalls": "jiterpreter-direct-jit-calls", diff --git a/src/mono/browser/runtime/jiterpreter-trace-generator.ts b/src/mono/browser/runtime/jiterpreter-trace-generator.ts index f9938164b5e112..842d89aebcbda7 100644 --- a/src/mono/browser/runtime/jiterpreter-trace-generator.ts +++ b/src/mono/browser/runtime/jiterpreter-trace-generator.ts @@ -1115,7 +1115,7 @@ export function generateWasmBody ( // Stash obj->vtable->klass so we can do a fast has_parent check later if (canDoFastCheck) builder.local("src_ptr", WasmOpcode.tee_local); - builder.i32_const(klass); + builder.ptr_const(klass); builder.appendU8(WasmOpcode.i32_eq); builder.block(WasmValtype.void, WasmOpcode.if_); // if A @@ -1206,7 +1206,7 @@ export function generateWasmBody ( elementClassOffset = getMemberOffset(JiterpMember.ClassElementClass), destOffset = getArgU16(ip, 1), // Get the class's element class, which is what we will actually type-check against - elementClass = getU32_unaligned(klass + elementClassOffset); + elementClass = getU32_unaligned(klass + elementClassOffset) >>> 0; if (!klass || !elementClass) { record_abort(builder.traceIndex, ip, traceName, "null-klass"); @@ -1234,7 +1234,7 @@ export function generateWasmBody ( builder.local("src_ptr", WasmOpcode.tee_local); builder.appendU8(WasmOpcode.i32_load); builder.appendMemarg(elementClassOffset, 0); - builder.i32_const(elementClass); + builder.ptr_const(elementClass); builder.appendU8(WasmOpcode.i32_eq); // Check klass->rank == 0 @@ -1285,7 +1285,7 @@ export function generateWasmBody ( builder.block(); append_ldloca(builder, getArgU16(ip, 1), 4); const vtable = get_imethod_data(frame, getArgU16(ip, 3)); - builder.i32_const(vtable); + builder.ptr_const(vtable); append_ldloc(builder, getArgU16(ip, 2), WasmOpcode.i32_load); builder.callImport("newarr"); // If the newarr operation succeeded, continue, otherwise bailout @@ -1973,6 +1973,7 @@ function append_stloc_tail (builder: WasmBuilder, offset: number, opcodeOrPrefix // This looks wrong but I assure you it's correct. builder.appendULeb(simdOpcode); } + offset = offset >>> 0; const alignment = computeMemoryAlignment(offset, opcodeOrPrefix, simdOpcode); builder.appendMemarg(offset, alignment); invalidate_local(offset); @@ -2335,7 +2336,7 @@ function emit_fieldop ( append_ldloc(builder, objectOffset, WasmOpcode.i32_load); append_ldloc(builder, objectOffset, WasmOpcode.i32_load); builder.i32_const(builder.traceIndex); - builder.i32_const(ip); + builder.ptr_const(ip); builder.callImport("notnull"); } } diff --git a/src/mono/browser/runtime/jiterpreter.ts b/src/mono/browser/runtime/jiterpreter.ts index 6ba17a3ab25f30..d427e4eb907d52 100644 --- a/src/mono/browser/runtime/jiterpreter.ts +++ b/src/mono/browser/runtime/jiterpreter.ts @@ -117,7 +117,7 @@ export class TraceInfo { isVerbose: boolean; constructor (ip: MintOpcodePtr, index: number, isVerbose: number) { - this.ip = ip; + this.ip = ip as any >>> 0 as any; this.index = index; this.isVerbose = !!isVerbose; } @@ -731,18 +731,12 @@ function generate_wasm ( traceIndex: number, methodFullName: string | undefined, backwardBranchTable: Uint16Array | null, presetFunctionPointer: number ): number { - // Pre-allocate a decent number of constant slots - this adds fixed size bloat - // to the trace but will make the actual pointer constants in the trace smaller - // If we run out of constant slots it will transparently fall back to i32_const - // For System.Runtime.Tests we only run out of slots ~50 times in 9100 test cases - const constantSlotCount = 8; - let builder = traceBuilder; if (!builder) { - traceBuilder = builder = new WasmBuilder(constantSlotCount); + traceBuilder = builder = new WasmBuilder(); initialize_builder(builder); } else - builder.clear(constantSlotCount); + builder.clear(); mostRecentOptions = builder.options; @@ -1011,6 +1005,10 @@ export function mono_interp_tier_prepare_jiterpreter ( presetFunctionPointer: number ): number { mono_assert(ip, "expected instruction pointer"); + ip = ip as any >>> 0 as any; + frame = frame as any >>> 0 as any; + method = method as any >>> 0 as any; + startOfBody = startOfBody as any >>> 0 as any; if (!mostRecentOptions) mostRecentOptions = getOptions(); @@ -1084,6 +1082,9 @@ export function mono_interp_tier_prepare_jiterpreter ( export function mono_wasm_free_method_data ( method: MonoMethod, imethod: number, traceIndex: number ) { + method = method as any >>> 0 as any; + imethod = imethod >>> 0; + if (runtimeHelpers.emscriptenBuildOptions.enableDevToolsProfiler) { mono_wasm_profiler_free_method(method); } diff --git a/src/mono/browser/runtime/marshal.ts b/src/mono/browser/runtime/marshal.ts index 5d537670c4d9db..d53d3cfa39fa15 100644 --- a/src/mono/browser/runtime/marshal.ts +++ b/src/mono/browser/runtime/marshal.ts @@ -5,7 +5,7 @@ import WasmEnableThreads from "consts:wasmEnableThreads"; import { js_owned_gc_handle_symbol, teardown_managed_proxy } from "./gc-handles"; import { Module, loaderHelpers, mono_assert, runtimeHelpers } from "./globals"; -import { getF32, getF64, getI16, getI32, getI64Big, getU16, getU32, getU8, setF32, setF64, setI16, setI32, setI64Big, setU16, setU32, setU8, localHeapViewF64, localHeapViewI32, localHeapViewU8, _zero_region, forceThreadMemoryViewRefresh, setB8, getB8 } from "./memory"; +import { getF32, getF64, getI16, getI32, getI64Big, getU16, getU32, getU8, setF32, setF64, setI16, setI32, setI64Big, setU16, setU32, setU8, localHeapViewF64, localHeapViewI32, localHeapViewU8, _zero_region, forceThreadMemoryViewRefresh, fixupPointer, setB8, getB8 } from "./memory"; import { mono_wasm_new_external_root } from "./roots"; import { GCHandle, JSHandle, MonoObject, MonoString, GCHandleNull, JSMarshalerArguments, JSFunctionSignature, JSMarshalerType, JSMarshalerArgument, MarshalerToJs, MarshalerToCs, WasmRoot, MarshalerType, PThreadPtr, PThreadPtrNull, VoidPtrNull } from "./types/internal"; import { TypedArray, VoidPtr } from "./types/emscripten"; @@ -93,7 +93,7 @@ export function is_receiver_should_free (args: JSMarshalerArguments): boolean { export function get_sync_done_semaphore_ptr (args: JSMarshalerArguments): VoidPtr { if (!WasmEnableThreads) return VoidPtrNull; mono_assert(args, "Null args"); - return getI32(args + JSMarshalerArgumentOffsets.SyncDoneSemaphorePtr) as any; + return getU32(args + JSMarshalerArgumentOffsets.SyncDoneSemaphorePtr) as any; } export function get_caller_native_tid (args: JSMarshalerArguments): PThreadPtr { @@ -467,6 +467,7 @@ export const enum MemoryViewType { abstract class MemoryView implements IMemoryView { protected constructor (public _pointer: VoidPtr, public _length: number, public _viewType: MemoryViewType) { + this._pointer = fixupPointer(_pointer, 0); } abstract dispose(): void; diff --git a/src/mono/browser/runtime/memory.ts b/src/mono/browser/runtime/memory.ts index 3497f05b8b99bc..06d2bbdda0cd7a 100644 --- a/src/mono/browser/runtime/memory.ts +++ b/src/mono/browser/runtime/memory.ts @@ -65,6 +65,7 @@ function assert_int_in_range (value: Number, min: Number, max: Number) { } export function _zero_region (byteOffset: VoidPtr, sizeBytes: number): void { + byteOffset = fixupPointer(byteOffset, 0); localHeapViewU8().fill(0, byteOffset, byteOffset + sizeBytes); } @@ -82,13 +83,13 @@ export function setB8 (offset: MemOffset, value: number | boolean): void { if (typeof (value) === "number") assert_int_in_range(value, 0, 1); receiveWorkerHeapViews(); - Module.HEAPU8[offset] = boolValue ? 1 : 0; + Module.HEAPU8[offset >>> 0] = boolValue ? 1 : 0; } export function setU8 (offset: MemOffset, value: number): void { assert_int_in_range(value, 0, 0xFF); receiveWorkerHeapViews(); - Module.HEAPU8[offset] = value; + Module.HEAPU8[offset >>> 0] = value; } export function setU16 (offset: MemOffset, value: number): void { @@ -122,7 +123,7 @@ export function setU32 (offset: MemOffset, value: NumberOrPointer): void { export function setI8 (offset: MemOffset, value: number): void { assert_int_in_range(value, -0x80, 0x7F); receiveWorkerHeapViews(); - Module.HEAP8[offset] = value; + Module.HEAP8[offset >>> 0] = value; } export function setI16 (offset: MemOffset, value: number): void { @@ -210,12 +211,12 @@ export function getB32 (offset: MemOffset): boolean { export function getB8 (offset: MemOffset): boolean { receiveWorkerHeapViews(); - return !!(Module.HEAPU8[offset]); + return !!(Module.HEAPU8[offset >>> 0]); } export function getU8 (offset: MemOffset): number { receiveWorkerHeapViews(); - return Module.HEAPU8[offset]; + return Module.HEAPU8[offset >>> 0]; } export function getU16 (offset: MemOffset): number { @@ -256,7 +257,7 @@ export function getF64_unaligned (offset: MemOffset): number { export function getI8 (offset: MemOffset): number { receiveWorkerHeapViews(); - return Module.HEAP8[offset]; + return Module.HEAP8[offset >>> 0]; } export function getI16 (offset: MemOffset): number { diff --git a/src/mono/browser/runtime/roots.ts b/src/mono/browser/runtime/roots.ts index 5b81331c29ff25..1e695c7604d8c9 100644 --- a/src/mono/browser/runtime/roots.ts +++ b/src/mono/browser/runtime/roots.ts @@ -171,7 +171,7 @@ export class WasmRootBufferImpl implements WasmRootBuffer { constructor (offset: VoidPtr, capacity: number, ownsAllocation: boolean, name?: string) { const capacityBytes = capacity * 4; - this.__offset = offset; + this.__offset = offset as any >>> 0 as any; this.__offset32 = offset >>> 2; this.__count = capacity; this.length = capacity; @@ -351,7 +351,7 @@ class WasmExternalRoot implements WasmRoot { } _set_address (address: NativePointer | ManagedPointer): void { - this.__external_address = address; + this.__external_address = address as any >>> 0 as any; this.__external_address_32 = address >>> 2; } diff --git a/src/mono/browser/runtime/startup.ts b/src/mono/browser/runtime/startup.ts index 0bfa9466553abc..e8c7956350f31d 100644 --- a/src/mono/browser/runtime/startup.ts +++ b/src/mono/browser/runtime/startup.ts @@ -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, malloc, setU32 } from "./memory"; +import { localHeapViewU8, malloc, setU32, fixupPointer } from "./memory"; import { assertNoProxies } from "./gc-handles"; import { runtimeList } from "./exports"; import { nativeAbort, nativeExit } from "./run"; @@ -685,12 +685,12 @@ export function mono_wasm_asm_loaded (assembly_name: CharPtr, assembly_ptr: numb return; const heapU8 = localHeapViewU8(); const assembly_name_str = assembly_name !== CharPtrNull ? utf8ToString(assembly_name).concat(".dll") : ""; - const assembly_data = new Uint8Array(heapU8.buffer, assembly_ptr, assembly_len); + const assembly_data = new Uint8Array(heapU8.buffer, fixupPointer(assembly_ptr, 0), assembly_len); const assembly_b64 = toBase64StringImpl(assembly_data); let pdb_b64; if (pdb_ptr) { - const pdb_data = new Uint8Array(heapU8.buffer, pdb_ptr, pdb_len); + const pdb_data = new Uint8Array(heapU8.buffer, fixupPointer(pdb_ptr, 0), pdb_len); pdb_b64 = toBase64StringImpl(pdb_data); } diff --git a/src/mono/browser/runtime/strings.ts b/src/mono/browser/runtime/strings.ts index 2b52f82b0320b8..9ae9799c166a22 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, malloc, free } from "./memory"; +import { isSharedArrayBuffer, localHeapViewU8, getU32_local, setU16_local, localHeapViewU32, getU16_local, localHeapViewU16, _zero_region, malloc, free, fixupPointer } from "./memory"; import { NativePointer, CharPtr, VoidPtr } from "./types/emscripten"; export const interned_js_string_table = new Map(); @@ -65,10 +65,12 @@ export function utf8ToStringRelaxed (buffer: Uint8Array): string { export function utf8ToString (ptr: CharPtr): string { const heapU8 = localHeapViewU8(); - return utf8BufferToString(heapU8, ptr as any, heapU8.length - (ptr as any)); + const fixedPtr = fixupPointer(ptr, 0); + return utf8BufferToString(heapU8, fixedPtr, heapU8.length - fixedPtr); } export function utf8BufferToString (heapOrArray: Uint8Array, idx: number, maxBytesToRead: number): string { + idx = fixupPointer(idx, 0); const endIdx = idx + maxBytesToRead; let endPtr = idx; while (heapOrArray[endPtr] && !(endPtr >= endIdx)) ++endPtr; @@ -83,6 +85,8 @@ export function utf8BufferToString (heapOrArray: Uint8Array, idx: number, maxByt } export function utf16ToString (startPtr: number, endPtr: number): string { + startPtr = fixupPointer(startPtr, 0); + endPtr = fixupPointer(endPtr, 0); if (_text_decoder_utf16) { const subArray = viewOrCopy(localHeapViewU8(), startPtr as any, endPtr as any); return _text_decoder_utf16.decode(subArray); @@ -277,8 +281,8 @@ export function viewOrCopy (view: Uint8Array, start: CharPtr, end: CharPtr): Uin // this condition should be eliminated by rollup on non-threading builds const needsCopy = isSharedArrayBuffer(view.buffer); return needsCopy - ? view.slice(start, end) - : view.subarray(start, end); + ? view.slice(start >>> 0, end >>> 0) + : view.subarray(start >>> 0, end >>> 0); } // below is minimal legacy support for Blazor diff --git a/src/mono/browser/runtime/web-socket.ts b/src/mono/browser/runtime/web-socket.ts index b26d5fff443826..a43a0557b163ab 100644 --- a/src/mono/browser/runtime/web-socket.ts +++ b/src/mono/browser/runtime/web-socket.ts @@ -6,7 +6,7 @@ import WasmEnableThreads from "consts:wasmEnableThreads"; import { prevent_timer_throttling } from "./scheduling"; import { Queue } from "./queue"; import { ENVIRONMENT_IS_NODE, ENVIRONMENT_IS_SHELL, createPromiseController, loaderHelpers, mono_assert, Module } from "./globals"; -import { setI32, localHeapViewU8, forceThreadMemoryViewRefresh } from "./memory"; +import { setI32, localHeapViewU8, forceThreadMemoryViewRefresh, fixupPointer } from "./memory"; import { VoidPtr } from "./types/emscripten"; import { PromiseController } from "./types/internal"; import { mono_log_warn } from "./logging"; @@ -72,7 +72,7 @@ export function ws_wasm_create (uri: string, sub_protocols: string[] | null, rec ws[wasm_ws_pending_open_promise] = open_promise_control; ws[wasm_ws_pending_send_promises] = []; ws[wasm_ws_pending_close_promises] = []; - ws[wasm_ws_receive_status_ptr] = receive_status_ptr; + ws[wasm_ws_receive_status_ptr] = fixupPointer(receive_status_ptr, 0); ws.binaryType = "arraybuffer"; const local_on_open = () => { try { @@ -185,7 +185,7 @@ export function ws_wasm_send (ws: WebSocketExtension, buffer_ptr: VoidPtr, buffe return resolvedPromise(); } - const buffer_view = new Uint8Array(localHeapViewU8().buffer, buffer_ptr, buffer_length); + const buffer_view = new Uint8Array(localHeapViewU8().buffer, fixupPointer(buffer_ptr, 0), buffer_length); const whole_buffer = web_socket_send_buffering(ws, buffer_view, message_type, end_of_message); if (!end_of_message || !whole_buffer) { @@ -232,7 +232,7 @@ export function ws_wasm_receive (ws: WebSocketExtension, buffer_ptr: VoidPtr, bu const { promise, promise_control } = createPromiseController(); const receive_promise_control = promise_control as ReceivePromiseControl; - receive_promise_control.buffer_ptr = buffer_ptr; + receive_promise_control.buffer_ptr = fixupPointer(buffer_ptr, 0); receive_promise_control.buffer_length = buffer_length; receive_promise_queue.enqueue(receive_promise_control); @@ -402,7 +402,7 @@ function web_socket_receive_buffering (ws: WebSocketExtension, event_queue: Queu const count = Math.min(buffer_length, event.data.length - event.offset); if (count > 0) { const sourceView = event.data.subarray(event.offset, event.offset + count); - const bufferView = new Uint8Array(localHeapViewU8().buffer, buffer_ptr, buffer_length); + const bufferView = new Uint8Array(localHeapViewU8().buffer, fixupPointer(buffer_ptr, 0), buffer_length); bufferView.set(sourceView, 0); event.offset += count; } diff --git a/src/mono/wasm/Wasm.Build.Tests/MemoryTests.cs b/src/mono/wasm/Wasm.Build.Tests/MemoryTests.cs index 50283071f30bde..a8c11617cdfe7c 100644 --- a/src/mono/wasm/Wasm.Build.Tests/MemoryTests.cs +++ b/src/mono/wasm/Wasm.Build.Tests/MemoryTests.cs @@ -38,10 +38,14 @@ public async Task AllocateLargeHeapThenRepeatedlyInterop() if (BuildTestBase.IsUsingWorkloads) { - await RunForBuildWithDotnetRun(new BrowserRunOptions( + RunResult result = await RunForBuildWithDotnetRun(new BrowserRunOptions( Configuration: config, TestScenario: "AllocateLargeHeapThenInterop" )); + + Assert.Contains(result.TestOutput, line => line.Contains("Great success, MemoryTest finished without errors.")); + // above the 2GB boundary the jiterpreter used to encode negative pointers and emit invalid wasm modules + Assert.DoesNotContain(result.ConsoleOutput, line => line.Contains("code generation failed")); } } } diff --git a/src/mono/wasm/testassets/WasmBasicTestApp/App/MemoryTest.cs b/src/mono/wasm/testassets/WasmBasicTestApp/App/MemoryTest.cs index 827c0a151c2c0e..0c4fa216fbf28e 100644 --- a/src/mono/wasm/testassets/WasmBasicTestApp/App/MemoryTest.cs +++ b/src/mono/wasm/testassets/WasmBasicTestApp/App/MemoryTest.cs @@ -2,23 +2,137 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; -using System.IO; -using System.Text.Json; -using System.Text; +using System.Collections.Generic; +using System.Globalization; using System.Runtime.InteropServices.JavaScript; +using System.Security.Cryptography; +using System.Text; +using System.Threading.Tasks; public partial class MemoryTest { + private static readonly List s_errors = new(); + [JSImport("countChars", "main.js")] internal static partial int CountChars(string testArray); + [JSImport("echoString", "main.js")] + internal static partial string EchoString(string value); + + [JSImport("createBytes", "main.js")] + internal static partial byte[] CreateBytes(int length); + + [JSImport("sumBytes", "main.js")] + internal static partial int SumBytes(byte[] value); + + [JSImport("sumMemoryView", "main.js")] + internal static partial int SumSpan([JSMarshalAs] Span value); + + [JSImport("sumMemoryView", "main.js")] + internal static partial int SumSegment([JSMarshalAs] ArraySegment value); + + [JSImport("echoInt32Array", "main.js")] + internal static partial int[] EchoInt32Array(int[] value); + + [JSImport("echoDoubleArray", "main.js")] + internal static partial double[] EchoDoubleArray(double[] value); + + [JSImport("createObject", "main.js")] + internal static partial JSObject CreateObject(string name, int value); + + [JSImport("throwError", "main.js")] + internal static partial void ThrowError(string message); + + [JSImport("delayedSum", "main.js")] + internal static partial Task DelayedSum(int a, int b); + + [JSImport("callManagedExports", "main.js")] + internal static partial string CallManagedExports(); + + [JSExport] + internal static int SumBytesManaged(byte[] value) + { + int sum = 0; + foreach (byte b in value) + { + sum += b; + } + + return sum; + } + + [JSExport] + internal static byte[] CreateBytesManaged(int length) + { + byte[] result = new byte[length]; + for (int i = 0; i < length; i++) + { + result[i] = unchecked((byte)(i * 3)); + } + + return result; + } + [JSExport] - internal static void Run() + internal static string EchoStringManaged(string value) => value; + + [JSExport] + internal static async Task RunAsync() + { + ReportAllocationAddress(); + AllocateManagedArrays(); + TierUpInterop(); + + Run("string round trip", StringRoundTrip); + Run("large string marshaling", LargeStringMarshaling); + Run("byte[] JS to C#", BytesFromJs); + Run("byte[] C# to JS", BytesToJs); + Run("int[] round trip", Int32ArrayRoundTrip); + Run("double[] round trip", DoubleArrayRoundTrip); + Run("Span memory view", SpanMemoryView); + Run("ArraySegment memory view", ArraySegmentMemoryView); + Run("JSObject properties", JSObjectProperties); + Run("exception propagation", ExceptionPropagation); + Run("crypto RandomNumberGenerator", CryptoRandomBytes); + Run("globalization", Globalization); + Run("JSExport round trips", ManagedExports); + + await RunAsync("Task round trip", TaskRoundTrip); + + if (s_errors.Count != 0) + { + string message = string.Join(Environment.NewLine, s_errors); + TestOutput.WriteLine(message); + throw new Exception(message); + } + + TestOutput.WriteLine("Great success, MemoryTest finished without errors."); + } + + private static void ReportAllocationAddress() + { + byte[] pinned = GC.AllocateArray(1024, pinned: true); + ulong address; + unsafe + { + fixed (byte* p = pinned) + { + address = (ulong)(nuint)p; + } + } + + TestOutput.WriteLine($"Pinned buffer allocated at 0x{address:X}"); + if (address < 0x8000_0000) + { + s_errors.Add($"Allocations are below the 2GB boundary (0x{address:X}), the test is not exercising >2GB pointers."); + } + } + + private static void AllocateManagedArrays() { // Allocate 250MB managed space above 2GB already wasted before startup const int arrayCnt = 10; int[][] arrayHolder = new int[arrayCnt][]; - string errors = ""; TestOutput.WriteLine("Starting managed array allocation"); for (int i = 0; i < arrayCnt; i++) { @@ -28,11 +142,15 @@ internal static void Run() } catch (Exception ex) { - errors += $"Exception {ex} was thrown on i={i}"; + s_errors.Add($"Exception {ex} was thrown on i={i}"); } } + TestOutput.WriteLine("Finished managed array allocation"); + } + private static void TierUpInterop() + { // call a method many times to trigger tier-up optimization string randomString = GenerateRandomString(1000); try @@ -41,22 +159,218 @@ internal static void Run() { int count = CountChars(randomString); if (count != randomString.Length) - errors += $"CountChars returned {count} instead of {randomString.Length} for {i}-th string."; + { + s_errors.Add($"CountChars returned {count} instead of {randomString.Length} for {i}-th string."); + } } } catch (Exception ex) { - errors += $"Exception {ex} was thrown when CountChars was called in a loop"; + s_errors.Add($"Exception {ex} was thrown when CountChars was called in a loop"); + } + } + + private static void Run(string name, Action test) + { + try + { + test(); + TestOutput.WriteLine($"PASS {name}"); + } + catch (Exception ex) + { + s_errors.Add($"FAIL {name}: {ex.GetType().Name}: {ex.Message}"); + } + } + + private static async Task RunAsync(string name, Func test) + { + try + { + await test(); + TestOutput.WriteLine($"PASS {name}"); + } + catch (Exception ex) + { + s_errors.Add($"FAIL {name}: {ex.GetType().Name}: {ex.Message}"); + } + } + + private static void Assert(bool condition, string message) + { + if (!condition) + { + throw new Exception(message); + } + } + + private static void StringRoundTrip() + { + foreach (string value in new[] { "", "a", "Příliš žluťoučký kůň", "🔥 emoji \u00e9\u00e8" }) + { + string result = EchoString(value); + Assert(result == value, $"echo mismatch for '{value}', got '{result}'"); + } + } + + private static void LargeStringMarshaling() + { + string value = string.Concat(new string('ě', 500_000), new string('a', 500_000)); + string result = EchoString(value); + Assert(result.Length == value.Length, $"expected {value.Length} chars, got {result.Length}"); + Assert(result == value, "large string content mismatch"); + } + + private static void BytesFromJs() + { + const int length = 64 * 1024; + byte[] bytes = CreateBytes(length); + Assert(bytes.Length == length, $"expected {length} bytes, got {bytes.Length}"); + for (int i = 0; i < length; i++) + { + Assert(bytes[i] == unchecked((byte)i), $"byte[{i}] expected {unchecked((byte)i)}, got {bytes[i]}"); + } + } + + private static void BytesToJs() + { + byte[] bytes = new byte[64 * 1024]; + int expected = 0; + for (int i = 0; i < bytes.Length; i++) + { + bytes[i] = unchecked((byte)(i * 7)); + expected += bytes[i]; + } + + int actual = SumBytes(bytes); + Assert(actual == expected, $"expected sum {expected}, got {actual}"); + } + + private static void Int32ArrayRoundTrip() + { + int[] values = new int[10_000]; + for (int i = 0; i < values.Length; i++) + { + values[i] = i * 3 - 5; + } + + int[] result = EchoInt32Array(values); + Assert(result.Length == values.Length, $"expected {values.Length} items, got {result.Length}"); + for (int i = 0; i < values.Length; i++) + { + Assert(result[i] == values[i], $"int[{i}] expected {values[i]}, got {result[i]}"); + } + } + + private static void DoubleArrayRoundTrip() + { + double[] values = new double[10_000]; + for (int i = 0; i < values.Length; i++) + { + values[i] = i / 3.0; + } + + double[] result = EchoDoubleArray(values); + Assert(result.Length == values.Length, $"expected {values.Length} items, got {result.Length}"); + for (int i = 0; i < values.Length; i++) + { + Assert(result[i] == values[i], $"double[{i}] expected {values[i]}, got {result[i]}"); + } + } + + private static void SpanMemoryView() + { + byte[] bytes = new byte[32 * 1024]; + int expected = 0; + for (int i = 0; i < bytes.Length; i++) + { + bytes[i] = unchecked((byte)(i * 11)); + expected += bytes[i]; + } + + int actual = SumSpan(bytes.AsSpan()); + Assert(actual == expected, $"expected sum {expected}, got {actual}"); + } + + private static void ArraySegmentMemoryView() + { + byte[] bytes = new byte[32 * 1024]; + for (int i = 0; i < bytes.Length; i++) + { + bytes[i] = unchecked((byte)(i * 13)); + } + + var segment = new ArraySegment(bytes, 1024, 4096); + int expected = 0; + for (int i = 0; i < segment.Count; i++) + { + expected += segment[i]; + } + + int actual = SumSegment(segment); + Assert(actual == expected, $"expected sum {expected}, got {actual}"); + } + + private static void JSObjectProperties() + { + using JSObject obj = CreateObject("answer", 42); + Assert(obj.GetPropertyAsString("name") == "answer", "name property mismatch"); + Assert(obj.GetPropertyAsInt32("value") == 42, "value property mismatch"); + obj.SetProperty("value", 43); + Assert(obj.GetPropertyAsInt32("value") == 43, "value property was not updated"); + } + + private static void ExceptionPropagation() + { + try + { + ThrowError("boom"); } - if (!string.IsNullOrEmpty(errors)) + catch (JSException ex) { - TestOutput.WriteLine(errors); - throw new Exception(errors); + Assert(ex.Message.Contains("boom"), $"unexpected message: {ex.Message}"); + return; } - else + + throw new Exception("expected a JSException"); + } + + private static void CryptoRandomBytes() + { + // the buffer is filled by JavaScript, writing to a wrong offset leaves it silently zeroed + byte[] buffer = new byte[4096]; + RandomNumberGenerator.Fill(buffer); + + int zeros = 0; + foreach (byte b in buffer) { - TestOutput.WriteLine("Great success, MemoryTest finished without errors."); + if (b == 0) + { + zeros++; + } } + + Assert(zeros < buffer.Length / 4, $"buffer looks unfilled, {zeros} zero bytes out of {buffer.Length}"); + } + + private static void Globalization() + { + var culture = new CultureInfo("cs-CZ"); + Assert(string.Compare("a", "b", culture, CompareOptions.None) < 0, "unexpected compare result"); + Assert(new DateTime(2026, 1, 2).ToString("MMMM", culture).Length > 0, "empty month name"); + Assert(Encoding.UTF8.GetString(Encoding.UTF8.GetBytes("žluťoučký")) == "žluťoučký", "UTF8 round trip failed"); + } + + private static void ManagedExports() + { + string errors = CallManagedExports(); + Assert(string.IsNullOrEmpty(errors), errors); + } + + private static async Task TaskRoundTrip() + { + int result = await DelayedSum(20, 22); + Assert(result == 42, $"expected 42, got {result}"); } private static Random random = new Random(); @@ -69,6 +383,7 @@ private static string GenerateRandomString(int stringLength) { stringBuilder.Append(chars[random.Next(chars.Length)]); } + return stringBuilder.ToString(); } } diff --git a/src/mono/wasm/testassets/WasmBasicTestApp/App/wwwroot/main.js b/src/mono/wasm/testassets/WasmBasicTestApp/App/wwwroot/main.js index b2de9206dd3842..9f31e38ec693d2 100644 --- a/src/mono/wasm/testassets/WasmBasicTestApp/App/wwwroot/main.js +++ b/src/mono/wasm/testassets/WasmBasicTestApp/App/wwwroot/main.js @@ -20,6 +20,44 @@ function countChars(str) { return length; } +const memoryTestImports = { + countChars, + echoString: (value) => value, + createBytes: (length) => { + const result = new Uint8Array(length); + for (let i = 0; i < length; i++) { + result[i] = i & 0xFF; + } + return result; + }, + sumBytes: (value) => { + let sum = 0; + for (let i = 0; i < value.length; i++) { + sum += value[i]; + } + return sum; + }, + sumMemoryView: (view) => { + const copy = view.slice(); + let sum = 0; + for (let i = 0; i < copy.length; i++) { + sum += copy[i]; + } + return sum; + }, + echoInt32Array: (value) => value, + echoDoubleArray: (value) => value, + createObject: (name, value) => ({ name, value }), + throwError: (message) => { throw new Error(message); }, + delayedSum: async (a, b) => { + await new Promise(resolve => setTimeout(resolve, 10)); + return a + b; + }, + callManagedExports: () => memoryTestCallManagedExports(), +}; + +let memoryTestCallManagedExports = () => "managed exports were not initialized"; + // Prepare base runtime parameters dotnet .withElementOnExit() @@ -270,10 +308,38 @@ try { exit(0); break; case "AllocateLargeHeapThenInterop": - setModuleImports('main.js', { - countChars - }); - exports.MemoryTest.Run(); + setModuleImports('main.js', memoryTestImports); + memoryTestCallManagedExports = () => { + const errors = []; + const check = (condition, message) => { + if (!condition) { + errors.push(message); + } + }; + + const bytes = new Uint8Array(64 * 1024); + let expected = 0; + for (let i = 0; i < bytes.length; i++) { + bytes[i] = (i * 5) & 0xFF; + expected += bytes[i]; + } + check(exports.MemoryTest.SumBytesManaged(bytes) === expected, "SumBytesManaged returned a wrong sum"); + + const made = exports.MemoryTest.CreateBytesManaged(64 * 1024); + check(made.length === 64 * 1024, `CreateBytesManaged returned ${made.length} bytes`); + for (let i = 0; i < made.length; i++) { + if (made[i] !== ((i * 3) & 0xFF)) { + check(false, `CreateBytesManaged[${i}] is ${made[i]}`); + break; + } + } + + const text = 'ě'.repeat(200000) + 'abc'; + check(exports.MemoryTest.EchoStringManaged(text) === text, "EchoStringManaged returned a different string"); + + return errors.join('; '); + }; + await exports.MemoryTest.RunAsync(); exit(0); break; case "HttpNoStreamingTest":