From d119e502c2d00dd5481f873353ebd455e958013f Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Fri, 13 May 2022 23:30:14 +0200 Subject: [PATCH 01/12] * new memory accessors setI52, setI64Big, getI52, getI64Big * removed support for long form automatic marshaler. It has impact to mono_bind_static_method --- ...me.InteropServices.JavaScript.Tests.csproj | 1 + .../InteropServices/JavaScript/MemoryTests.cs | 44 ++++++++++++ src/mono/wasm/runtime/cjs/dotnet.cjs.lib.js | 3 +- src/mono/wasm/runtime/dotnet.d.ts | 18 +++-- src/mono/wasm/runtime/es6/dotnet.es6.lib.js | 3 +- src/mono/wasm/runtime/exports.ts | 19 +++-- src/mono/wasm/runtime/memory.ts | 70 ++++++++++++++++--- src/mono/wasm/runtime/method-binding.ts | 13 ++-- 8 files changed, 143 insertions(+), 28 deletions(-) create mode 100644 src/libraries/System.Private.Runtime.InteropServices.JavaScript/tests/System/Runtime/InteropServices/JavaScript/MemoryTests.cs diff --git a/src/libraries/System.Private.Runtime.InteropServices.JavaScript/tests/System.Private.Runtime.InteropServices.JavaScript.Tests.csproj b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/tests/System.Private.Runtime.InteropServices.JavaScript.Tests.csproj index 871f62d5d16d7b..5bfcbdfe2a41e8 100644 --- a/src/libraries/System.Private.Runtime.InteropServices.JavaScript/tests/System.Private.Runtime.InteropServices.JavaScript.Tests.csproj +++ b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/tests/System.Private.Runtime.InteropServices.JavaScript.Tests.csproj @@ -8,6 +8,7 @@ + diff --git a/src/libraries/System.Private.Runtime.InteropServices.JavaScript/tests/System/Runtime/InteropServices/JavaScript/MemoryTests.cs b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/tests/System/Runtime/InteropServices/JavaScript/MemoryTests.cs new file mode 100644 index 00000000000000..3f054473ae285e --- /dev/null +++ b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/tests/System/Runtime/InteropServices/JavaScript/MemoryTests.cs @@ -0,0 +1,44 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.CompilerServices; +using Xunit; + +namespace System.Runtime.InteropServices.JavaScript.Tests +{ + public class MemoryTests + { + [Theory] + [InlineData(-1L)] + [InlineData(-42L)] + [InlineData(int.MinValue)] + [InlineData(-9007199254740991L)]//MIN_SAFE_INTEGER + [InlineData(1L)] + [InlineData(0L)] + [InlineData(42L)] + [InlineData(int.MaxValue)] + [InlineData(0xF_FFFF_FFFFL)] + [InlineData(9007199254740991L)]//MAX_SAFE_INTEGER + public static unsafe void Int52TestOK(long value) + { + long expected = value; + long actual2 = value; + var bagFn = new Function("ptr", "ptr2", @" + const value=globalThis.App.MONO.getI52(ptr); + globalThis.App.MONO.setI52(ptr2, value); + return value;"); + + uint ptr = (uint)Unsafe.AsPointer(ref expected); + uint ptr2 = (uint)Unsafe.AsPointer(ref actual2); + + object o = bagFn.Call(null, ptr, ptr2); + if (value < int.MaxValue && value > int.MinValue) + { + Assert.IsType(o); + long actual = (int)o; + Assert.Equal(expected, actual); + } + Assert.Equal(expected, actual2); + } + } +} diff --git a/src/mono/wasm/runtime/cjs/dotnet.cjs.lib.js b/src/mono/wasm/runtime/cjs/dotnet.cjs.lib.js index de07811bdd6211..79116860e1ce8b 100644 --- a/src/mono/wasm/runtime/cjs/dotnet.cjs.lib.js +++ b/src/mono/wasm/runtime/cjs/dotnet.cjs.lib.js @@ -10,11 +10,12 @@ const DotnetSupportLib = { // we replace implementation of readAsync and fetch // replacement of require is there for consistency with ES6 code $DOTNET__postset: ` -let __dotnet_replacements = {readAsync, fetch: globalThis.fetch, require}; +let __dotnet_replacements = {readAsync, fetch: globalThis.fetch, require, updateGlobalBufferAndViews}; let __dotnet_exportedAPI = __dotnet_runtime.__initializeImportsAndExports( { isESM:false, isGlobal:ENVIRONMENT_IS_GLOBAL, isNode:ENVIRONMENT_IS_NODE, isShell:ENVIRONMENT_IS_SHELL, isWeb:ENVIRONMENT_IS_WEB, locateFile, quit_, ExitStatus, requirePromise:Promise.resolve(require)}, { mono:MONO, binding:BINDING, internal:INTERNAL, module:Module }, __dotnet_replacements); +updateGlobalBufferAndViews = __dotnet_replacements.updateGlobalBufferAndViews; readAsync = __dotnet_replacements.readAsync; var fetch = __dotnet_replacements.fetch; require = __dotnet_replacements.requireOut; diff --git a/src/mono/wasm/runtime/dotnet.d.ts b/src/mono/wasm/runtime/dotnet.d.ts index e31742188e500a..66bc03c3b6d3c4 100644 --- a/src/mono/wasm/runtime/dotnet.d.ts +++ b/src/mono/wasm/runtime/dotnet.d.ts @@ -292,7 +292,11 @@ declare function setU32(offset: _MemOffset, value: _NumberOrPointer): void; declare function setI8(offset: _MemOffset, value: number): void; declare function setI16(offset: _MemOffset, value: number): void; declare function setI32(offset: _MemOffset, value: _NumberOrPointer): void; -declare function setI64(offset: _MemOffset, value: number): void; +/** + * Throws for values which are not integer. See Number.isInteger() + */ +declare function setI52(offset: _MemOffset, value: number): void; +declare function setI64Big(offset: _MemOffset, value: bigint): void; declare function setF32(offset: _MemOffset, value: number): void; declare function setF64(offset: _MemOffset, value: number): void; declare function getU8(offset: _MemOffset): number; @@ -301,7 +305,11 @@ declare function getU32(offset: _MemOffset): number; declare function getI8(offset: _MemOffset): number; declare function getI16(offset: _MemOffset): number; declare function getI32(offset: _MemOffset): number; -declare function getI64(offset: _MemOffset): number; +/** + * Throws for Number.MIN_SAFE_INTEGER < value < Number.MAX_SAFE_INTEGER + */ +declare function getI52(offset: _MemOffset): number; +declare function getI64Big(offset: _MemOffset): bigint; declare function getF32(offset: _MemOffset): number; declare function getF64(offset: _MemOffset): number; @@ -329,7 +337,8 @@ declare const MONO: { setI8: typeof setI8; setI16: typeof setI16; setI32: typeof setI32; - setI64: typeof setI64; + setI52: typeof setI52; + setI64Big: typeof setI64Big; setU8: typeof setU8; setU16: typeof setU16; setU32: typeof setU32; @@ -338,7 +347,8 @@ declare const MONO: { getI8: typeof getI8; getI16: typeof getI16; getI32: typeof getI32; - getI64: typeof getI64; + getI52: typeof getI52; + getI64Big: typeof getI64Big; getU8: typeof getU8; getU16: typeof getU16; getU32: typeof getU32; diff --git a/src/mono/wasm/runtime/es6/dotnet.es6.lib.js b/src/mono/wasm/runtime/es6/dotnet.es6.lib.js index a110268beeb525..f886800ab508f0 100644 --- a/src/mono/wasm/runtime/es6/dotnet.es6.lib.js +++ b/src/mono/wasm/runtime/es6/dotnet.es6.lib.js @@ -14,7 +14,7 @@ const DotnetSupportLib = { // Emscripten's getBinaryPromise is not async for NodeJs, but we would like to have it async, so we replace it. // We also replace implementation of readAsync and fetch $DOTNET__postset: ` -let __dotnet_replacements = {readAsync, fetch: globalThis.fetch, require}; +let __dotnet_replacements = {readAsync, fetch: globalThis.fetch, require, updateGlobalBufferAndViews}; if (ENVIRONMENT_IS_NODE) { __dotnet_replacements.requirePromise = import('module').then(mod => { const require = mod.createRequire(import.meta.url); @@ -52,6 +52,7 @@ let __dotnet_exportedAPI = __dotnet_runtime.__initializeImportsAndExports( { isESM:true, isGlobal:false, isNode:ENVIRONMENT_IS_NODE, isShell:ENVIRONMENT_IS_SHELL, isWeb:ENVIRONMENT_IS_WEB, locateFile, quit_, ExitStatus, requirePromise:__dotnet_replacements.requirePromise }, { mono:MONO, binding:BINDING, internal:INTERNAL, module:Module }, __dotnet_replacements); +updateGlobalBufferAndViews = __dotnet_replacements.updateGlobalBufferAndViews; readAsync = __dotnet_replacements.readAsync; var fetch = __dotnet_replacements.fetch; require = __dotnet_replacements.requireOut; diff --git a/src/mono/wasm/runtime/exports.ts b/src/mono/wasm/runtime/exports.ts index 66fe698af5dc09..afc376ba6caff5 100644 --- a/src/mono/wasm/runtime/exports.ts +++ b/src/mono/wasm/runtime/exports.ts @@ -59,10 +59,10 @@ import { mono_wasm_release_cs_owned_object } from "./gc-handles"; import { mono_wasm_web_socket_open_ref, mono_wasm_web_socket_send, mono_wasm_web_socket_receive, mono_wasm_web_socket_close_ref, mono_wasm_web_socket_abort } from "./web-socket"; import cwraps from "./cwraps"; import { - setI8, setI16, setI32, setI64, + setI8, setI16, setI32, setI52, setU8, setU16, setU32, setF32, setF64, - getI8, getI16, getI32, getI64, - getU8, getU16, getU32, getF32, getF64, + getI8, getI16, getI32, getI52, + getU8, getU16, getU32, getF32, getF64, afterUpdateGlobalBufferAndViews, getI64Big, setI64Big, } from "./memory"; import { create_weak_ref } from "./weak-ref"; import { fetch_like, readAsync_like } from "./polyfills"; @@ -96,7 +96,8 @@ const MONO = { setI8, setI16, setI32, - setI64, + setI52, + setI64Big, setU8, setU16, setU32, @@ -105,7 +106,8 @@ const MONO = { getI8, getI16, getI32, - getI64, + getI52, + getI64Big, getU8, getU16, getU32, @@ -176,7 +178,7 @@ let exportedAPI: DotnetPublicAPI; function initializeImportsAndExports( imports: { isESM: boolean, isGlobal: boolean, isNode: boolean, isShell: boolean, isWeb: boolean, locateFile: Function, quit_: Function, ExitStatus: ExitStatusError, requirePromise: Promise }, exports: { mono: any, binding: any, internal: any, module: any }, - replacements: { fetch: any, readAsync: any, require: any, requireOut: any, noExitRuntime: boolean }, + replacements: { fetch: any, readAsync: any, require: any, requireOut: any, noExitRuntime: boolean, updateGlobalBufferAndViews: Function }, ): DotnetPublicAPI { const module = exports.module as DotnetModule; const globalThisAny = globalThis as any; @@ -233,6 +235,11 @@ function initializeImportsAndExports( replacements.fetch = runtimeHelpers.fetch; replacements.readAsync = readAsync_like; replacements.requireOut = module.imports.require; + const originalUpdateGlobalBufferAndViews = replacements.updateGlobalBufferAndViews; + replacements.updateGlobalBufferAndViews = (buffer: Buffer) => { + originalUpdateGlobalBufferAndViews(buffer); + afterUpdateGlobalBufferAndViews(buffer); + }; replacements.noExitRuntime = ENVIRONMENT_IS_WEB; diff --git a/src/mono/wasm/runtime/memory.ts b/src/mono/wasm/runtime/memory.ts index b61b379e87e8c5..b5584f14dceb74 100644 --- a/src/mono/wasm/runtime/memory.ts +++ b/src/mono/wasm/runtime/memory.ts @@ -1,9 +1,11 @@ import { Module } from "./imports"; +import { assert } from "./types"; import { VoidPtr, NativePointer, ManagedPointer } from "./types/emscripten"; const alloca_stack: Array = []; const alloca_buffer_size = 32 * 1024; let alloca_base: VoidPtr, alloca_offset: VoidPtr, alloca_limit: VoidPtr; +let HEAPI64: BigInt64Array = null; function _ensure_allocated(): void { if (alloca_base) @@ -13,6 +15,10 @@ function _ensure_allocated(): void { alloca_limit = (alloca_base + alloca_buffer_size); } +function is_bingint_supported() { + return typeof BigInt !== "undefined" && typeof BigInt64Array !== "undefined"; +} + export function temp_malloc(size: number): VoidPtr { _ensure_allocated(); if (!alloca_stack.length) @@ -48,7 +54,7 @@ export function setU16(offset: _MemOffset, value: number): void { Module.HEAPU16[offset >>> 1] = value; } -export function setU32 (offset: _MemOffset, value: _NumberOrPointer) : void { +export function setU32(offset: _MemOffset, value: _NumberOrPointer): void { Module.HEAPU32[offset >>> 2] = value; } @@ -60,13 +66,34 @@ export function setI16(offset: _MemOffset, value: number): void { Module.HEAP16[offset >>> 1] = value; } -export function setI32 (offset: _MemOffset, value: _NumberOrPointer) : void { +export function setI32(offset: _MemOffset, value: _NumberOrPointer): void { Module.HEAP32[offset >>> 2] = value; } -// NOTE: Accepts a number, not a BigInt, so values over Number.MAX_SAFE_INTEGER will be corrupted -export function setI64(offset: _MemOffset, value: number): void { - Module.setValue(offset, value, "i64"); +/** + * Throws for values which are not integer. See Number.isInteger() + */ +export function setI52(offset: _MemOffset, value: number): void { + // 52 bits = 0x1F_FFFF_FFFF_FFFF + assert(Number.isSafeInteger(value), "Int64 value out of JavaScript Number safe integer range"); + let hi: number; + let lo: number; + if (value < 0) { + value = -1 - value; + hi = 0x8000_0000 + ((value >>> 32) ^ 0x001F_FFFF); + lo = (value & 0xFFFF_FFFF) ^ 0xFFFF_FFFF; + } + else { + hi = value >>> 32; + lo = value & 0xFFFF_FFFF; + } + Module.HEAPU32[1 + offset >>> 2] = hi; + Module.HEAPU32[offset >>> 2] = lo; +} + +export function setI64Big(offset: _MemOffset, value: bigint): void { + assert(is_bingint_supported(), "BigInt is not supported."); + HEAPI64[offset >>> 3] = value; } export function setF32(offset: _MemOffset, value: number): void { @@ -102,9 +129,30 @@ export function getI32(offset: _MemOffset): number { return Module.HEAP32[offset >>> 2]; } -// NOTE: Returns a number, not a BigInt. This means values over Number.MAX_SAFE_INTEGER will be corrupted -export function getI64(offset: _MemOffset): number { - return Module.getValue(offset, "i64"); +/** + * Throws for Number.MIN_SAFE_INTEGER < value < Number.MAX_SAFE_INTEGER + */ +export function getI52(offset: _MemOffset): number { + // 52 bits = 0x1F_FFFF_FFFF_FFFF + const hi = Module.HEAPU32[1 + (offset >>> 2)]; + const lo = Module.HEAPU32[offset >>> 2]; + const sign = hi & 0x8000_0000; + const exp = hi & 0x7FE0_0000; + if (sign) { + assert(exp === 0x7FE0_0000, "Int64 value out of JavaScript Number safe integer range"); + const nhi = (hi & 0x000F_FFFF) ^ 0x000F_FFFF; + const nlo = lo ^ 0xFFFF_FFFF; + return -1 - ((nhi * 0x1_0000_0000) + nlo); + } + else { + assert(exp === 0, "Int64 value out of JavaScript Number safe integer range"); + return (hi * 0x1_0000_0000) + lo; + } +} + +export function getI64Big(offset: _MemOffset): bigint { + assert(is_bingint_supported(), "BigInt is not supported."); + return HEAPI64[offset >>> 3]; } export function getF32(offset: _MemOffset): number { @@ -114,3 +162,9 @@ export function getF32(offset: _MemOffset): number { export function getF64(offset: _MemOffset): number { return Module.HEAPF64[offset >>> 3]; } + +export function afterUpdateGlobalBufferAndViews(buffer: Buffer): void { + if (is_bingint_supported()) { + HEAPI64 = new BigInt64Array(buffer); + } +} \ No newline at end of file diff --git a/src/mono/wasm/runtime/method-binding.ts b/src/mono/wasm/runtime/method-binding.ts index 6dc3daec9f7c42..95970bbf23557d 100644 --- a/src/mono/wasm/runtime/method-binding.ts +++ b/src/mono/wasm/runtime/method-binding.ts @@ -10,7 +10,7 @@ import { _unbox_mono_obj_root_with_known_nonprimitive_type } from "./cs-to-js"; import { _create_temp_frame, getI32, getU32, getF32, getF64, - setI32, setU32, setF32, setF64, setI64, + setI32, setU32, setF32, setF64 } from "./memory"; import { _get_args_root_buffer_for_method_call, _get_buffer_for_method_call, @@ -131,7 +131,7 @@ export function _create_primitive_converters(): void { result.set("j", { steps: [{ convert: js_to_mono_enum.bind(BINDING), indirect: "i32" }], size: 8 }); result.set("i", { steps: [{ indirect: "i32" }], size: 8 }); - result.set("l", { steps: [{ indirect: "i64" }], size: 8 }); + // result.set("l", { steps: [{ indirect: "i64" }], size: 8 }); result.set("f", { steps: [{ indirect: "float" }], size: 8 }); result.set("d", { steps: [{ indirect: "double" }], size: 8 }); } @@ -218,7 +218,6 @@ export function _compile_converter_for_marshal_string(args_marshal: string/*Args setU32, setF32, setF64, - setI64, scratchValueRoot: converter.scratchValueRoot }; let indirectLocalOffset = 0; @@ -288,8 +287,6 @@ export function _compile_converter_for_marshal_string(args_marshal: string/*Args case "double": body.push(`setF64(${offsetText}, ${valueKey});`); break; - case "i64": - body.push(`setI64(${offsetText}, ${valueKey});`); break; default: throw new Error("Unimplemented indirect type: " + step.indirect); @@ -564,8 +561,8 @@ We currently don't use these types because it makes typeScript compiler very slo declare const enum ArgsMarshal { Int32 = "i", // int32 Int32Enum = "j", // int32 - Enum with underlying type of int32 - Int64 = "l", // int64 - Int64Enum = "k", // int64 - Enum with underlying type of int64 + // Int64 = "l", // int64 + // Int64Enum = "k", // int64 - Enum with underlying type of int64 Float32 = "f", // float Float64 = "d", // double String = "s", // string @@ -584,7 +581,7 @@ export type ArgsMarshalString = "" | `${ArgsMarshal}${ArgsMarshal}${ArgsMarshal}${ArgsMarshal}${_ExtraArgsMarshalOperators}`; */ -type ConverterStepIndirects = "u32" | "i32" | "float" | "double" | "i64" | "reference" +type ConverterStepIndirects = "u32" | "i32" | "float" | "double" | "reference" export type Converter = { steps: { From a9a8bccd7dd76a67e9abf1da29435e59888a6c22 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Fri, 13 May 2022 23:47:32 +0200 Subject: [PATCH 02/12] doc --- src/mono/wasm/runtime/dotnet.d.ts | 2 +- src/mono/wasm/runtime/memory.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/mono/wasm/runtime/dotnet.d.ts b/src/mono/wasm/runtime/dotnet.d.ts index 66bc03c3b6d3c4..abbb5768eb25fc 100644 --- a/src/mono/wasm/runtime/dotnet.d.ts +++ b/src/mono/wasm/runtime/dotnet.d.ts @@ -293,7 +293,7 @@ declare function setI8(offset: _MemOffset, value: number): void; declare function setI16(offset: _MemOffset, value: number): void; declare function setI32(offset: _MemOffset, value: _NumberOrPointer): void; /** - * Throws for values which are not integer. See Number.isInteger() + * Throws for values which are not 52 bit integer. See Number.isSafeInteger() */ declare function setI52(offset: _MemOffset, value: number): void; declare function setI64Big(offset: _MemOffset, value: bigint): void; diff --git a/src/mono/wasm/runtime/memory.ts b/src/mono/wasm/runtime/memory.ts index b5584f14dceb74..6159295457b6ec 100644 --- a/src/mono/wasm/runtime/memory.ts +++ b/src/mono/wasm/runtime/memory.ts @@ -71,7 +71,7 @@ export function setI32(offset: _MemOffset, value: _NumberOrPointer): void { } /** - * Throws for values which are not integer. See Number.isInteger() + * Throws for values which are not 52 bit integer. See Number.isSafeInteger() */ export function setI52(offset: _MemOffset, value: number): void { // 52 bits = 0x1F_FFFF_FFFF_FFFF From e6c06c40f7f3d949772be3d7eb9990cf003f4d40 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Mon, 16 May 2022 10:29:52 +0200 Subject: [PATCH 03/12] tests --- .../InteropServices/JavaScript/MarshalTests.cs | 4 ---- .../InteropServices/JavaScript/MemoryTests.cs | 18 ++++++++++++++++++ 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/src/libraries/System.Private.Runtime.InteropServices.JavaScript/tests/System/Runtime/InteropServices/JavaScript/MarshalTests.cs b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/tests/System/Runtime/InteropServices/JavaScript/MarshalTests.cs index 16b8dfb3f8d07b..89a7d9e2dc4312 100644 --- a/src/libraries/System.Private.Runtime.InteropServices.JavaScript/tests/System/Runtime/InteropServices/JavaScript/MarshalTests.cs +++ b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/tests/System/Runtime/InteropServices/JavaScript/MarshalTests.cs @@ -22,10 +22,6 @@ public static void MarshalPrimitivesToCS() HelperMarshal._f64Value = 0; Runtime.InvokeJS("App.call_test_method (\"InvokeDouble\", [4.5])"); Assert.Equal(4.5, HelperMarshal._f64Value); - - HelperMarshal._i64Value = 0; - Runtime.InvokeJS("App.call_test_method (\"InvokeLong\", [99])"); - Assert.Equal(99, HelperMarshal._i64Value); } [Fact] diff --git a/src/libraries/System.Private.Runtime.InteropServices.JavaScript/tests/System/Runtime/InteropServices/JavaScript/MemoryTests.cs b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/tests/System/Runtime/InteropServices/JavaScript/MemoryTests.cs index 3f054473ae285e..05232a8d72a08a 100644 --- a/src/libraries/System.Private.Runtime.InteropServices.JavaScript/tests/System/Runtime/InteropServices/JavaScript/MemoryTests.cs +++ b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/tests/System/Runtime/InteropServices/JavaScript/MemoryTests.cs @@ -40,5 +40,23 @@ public static unsafe void Int52TestOK(long value) } Assert.Equal(expected, actual2); } + + [Theory] + [InlineData(double.NaN)] + [InlineData(double.NegativeInfinity)] + [InlineData(double.PositiveInfinity)] + [InlineData(double.MinValue)] + [InlineData(double.MaxValue)] + [InlineData(double.Pi)] + [InlineData(9007199254740993.0)]//MAX_SAFE_INTEGER +2 + public static unsafe void Int52TestInvalid(double value) + { + long actual = 0; + uint ptr = (uint)Unsafe.AsPointer(ref actual); + var bagFn = new Function("ptr", "value", @" + globalThis.App.MONO.setI52(ptr, value);"); + var ex=Assert.Throws(() => bagFn.Call(null, ptr, value)); + Assert.Contains("Int64 value out of JavaScript Number safe integer range", ex.Message); + } } } From 27cf6b8d2b7e4d55f5c0d08e0e9525adf80c7226 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Mon, 16 May 2022 17:31:28 +0200 Subject: [PATCH 04/12] feedback from @kg --- .../InteropServices/JavaScript/MarshalTests.cs | 4 ++++ src/mono/wasm/runtime/method-binding.ts | 13 ++++++++----- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/libraries/System.Private.Runtime.InteropServices.JavaScript/tests/System/Runtime/InteropServices/JavaScript/MarshalTests.cs b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/tests/System/Runtime/InteropServices/JavaScript/MarshalTests.cs index 89a7d9e2dc4312..16b8dfb3f8d07b 100644 --- a/src/libraries/System.Private.Runtime.InteropServices.JavaScript/tests/System/Runtime/InteropServices/JavaScript/MarshalTests.cs +++ b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/tests/System/Runtime/InteropServices/JavaScript/MarshalTests.cs @@ -22,6 +22,10 @@ public static void MarshalPrimitivesToCS() HelperMarshal._f64Value = 0; Runtime.InvokeJS("App.call_test_method (\"InvokeDouble\", [4.5])"); Assert.Equal(4.5, HelperMarshal._f64Value); + + HelperMarshal._i64Value = 0; + Runtime.InvokeJS("App.call_test_method (\"InvokeLong\", [99])"); + Assert.Equal(99, HelperMarshal._i64Value); } [Fact] diff --git a/src/mono/wasm/runtime/method-binding.ts b/src/mono/wasm/runtime/method-binding.ts index 95970bbf23557d..6473e3a77e7d96 100644 --- a/src/mono/wasm/runtime/method-binding.ts +++ b/src/mono/wasm/runtime/method-binding.ts @@ -10,7 +10,7 @@ import { _unbox_mono_obj_root_with_known_nonprimitive_type } from "./cs-to-js"; import { _create_temp_frame, getI32, getU32, getF32, getF64, - setI32, setU32, setF32, setF64 + setI32, setU32, setF32, setF64, setI52 } from "./memory"; import { _get_args_root_buffer_for_method_call, _get_buffer_for_method_call, @@ -131,7 +131,7 @@ export function _create_primitive_converters(): void { result.set("j", { steps: [{ convert: js_to_mono_enum.bind(BINDING), indirect: "i32" }], size: 8 }); result.set("i", { steps: [{ indirect: "i32" }], size: 8 }); - // result.set("l", { steps: [{ indirect: "i64" }], size: 8 }); + result.set("l", { steps: [{ indirect: "i52" }], size: 8 }); result.set("f", { steps: [{ indirect: "float" }], size: 8 }); result.set("d", { steps: [{ indirect: "double" }], size: 8 }); } @@ -218,6 +218,7 @@ export function _compile_converter_for_marshal_string(args_marshal: string/*Args setU32, setF32, setF64, + setI52, scratchValueRoot: converter.scratchValueRoot }; let indirectLocalOffset = 0; @@ -287,6 +288,8 @@ export function _compile_converter_for_marshal_string(args_marshal: string/*Args case "double": body.push(`setF64(${offsetText}, ${valueKey});`); break; + case "i52": + body.push(`setI52(${offsetText}, ${valueKey});`); break; default: throw new Error("Unimplemented indirect type: " + step.indirect); @@ -561,8 +564,8 @@ We currently don't use these types because it makes typeScript compiler very slo declare const enum ArgsMarshal { Int32 = "i", // int32 Int32Enum = "j", // int32 - Enum with underlying type of int32 - // Int64 = "l", // int64 - // Int64Enum = "k", // int64 - Enum with underlying type of int64 + Int64 = "l", // int64 + Int64Enum = "k", // int64 - Enum with underlying type of int64 Float32 = "f", // float Float64 = "d", // double String = "s", // string @@ -581,7 +584,7 @@ export type ArgsMarshalString = "" | `${ArgsMarshal}${ArgsMarshal}${ArgsMarshal}${ArgsMarshal}${_ExtraArgsMarshalOperators}`; */ -type ConverterStepIndirects = "u32" | "i32" | "float" | "double" | "reference" +type ConverterStepIndirects = "u32" | "i32" | "float" | "double" | "i52" | "reference" export type Converter = { steps: { From b6f033c059cdf675386fa2ce8fcd7f61aa74f335 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Mon, 16 May 2022 17:37:04 +0200 Subject: [PATCH 05/12] fix --- src/mono/wasm/runtime/memory.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/mono/wasm/runtime/memory.ts b/src/mono/wasm/runtime/memory.ts index 6159295457b6ec..623d8414cb2bf3 100644 --- a/src/mono/wasm/runtime/memory.ts +++ b/src/mono/wasm/runtime/memory.ts @@ -15,9 +15,7 @@ function _ensure_allocated(): void { alloca_limit = (alloca_base + alloca_buffer_size); } -function is_bingint_supported() { - return typeof BigInt !== "undefined" && typeof BigInt64Array !== "undefined"; -} +const is_bingint_supported = typeof BigInt !== "undefined" && typeof BigInt64Array !== "undefined"; export function temp_malloc(size: number): VoidPtr { _ensure_allocated(); @@ -92,7 +90,7 @@ export function setI52(offset: _MemOffset, value: number): void { } export function setI64Big(offset: _MemOffset, value: bigint): void { - assert(is_bingint_supported(), "BigInt is not supported."); + assert(is_bingint_supported, "BigInt is not supported."); HEAPI64[offset >>> 3] = value; } @@ -151,7 +149,7 @@ export function getI52(offset: _MemOffset): number { } export function getI64Big(offset: _MemOffset): bigint { - assert(is_bingint_supported(), "BigInt is not supported."); + assert(is_bingint_supported, "BigInt is not supported."); return HEAPI64[offset >>> 3]; } @@ -164,7 +162,7 @@ export function getF64(offset: _MemOffset): number { } export function afterUpdateGlobalBufferAndViews(buffer: Buffer): void { - if (is_bingint_supported()) { + if (is_bingint_supported) { HEAPI64 = new BigInt64Array(buffer); } } \ No newline at end of file From 5b5e6b5103ba164014a2472464815884a06586a4 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Thu, 19 May 2022 12:10:32 +0200 Subject: [PATCH 06/12] - made assert silent on Release config feedback from @kg - test for not marshaling long from C# to JS - negative test for marshaling NaN as long --- .../JavaScript/HelperMarshal.cs | 15 ++++++++++ .../JavaScript/MarshalTests.cs | 29 +++++++++++++++++++ .../InteropServices/JavaScript/MemoryTests.cs | 23 +++++++++++++-- src/mono/wasm/runtime/dotnet.d.ts | 2 +- src/mono/wasm/runtime/memory.ts | 9 +++--- src/mono/wasm/runtime/types.ts | 9 ++++-- 6 files changed, 76 insertions(+), 11 deletions(-) diff --git a/src/libraries/System.Private.Runtime.InteropServices.JavaScript/tests/System/Runtime/InteropServices/JavaScript/HelperMarshal.cs b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/tests/System/Runtime/InteropServices/JavaScript/HelperMarshal.cs index cf2a8a7d81bc66..6d290ca4d71d62 100644 --- a/src/libraries/System.Private.Runtime.InteropServices.JavaScript/tests/System/Runtime/InteropServices/JavaScript/HelperMarshal.cs +++ b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/tests/System/Runtime/InteropServices/JavaScript/HelperMarshal.cs @@ -105,6 +105,21 @@ private static object InvokeReturnMarshalObj() return _marshaledObject; } + private static int InvokeReturnInt() + { + return 42; + } + + private static long InvokeReturnLong() + { + return 42L; + } + + private static double InvokeReturnDouble() + { + return double.Pi; + } + internal static int _valOne, _valTwo; private static void ManipulateObject(JSObject obj) { diff --git a/src/libraries/System.Private.Runtime.InteropServices.JavaScript/tests/System/Runtime/InteropServices/JavaScript/MarshalTests.cs b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/tests/System/Runtime/InteropServices/JavaScript/MarshalTests.cs index 16b8dfb3f8d07b..711d94bcd04272 100644 --- a/src/libraries/System.Private.Runtime.InteropServices.JavaScript/tests/System/Runtime/InteropServices/JavaScript/MarshalTests.cs +++ b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/tests/System/Runtime/InteropServices/JavaScript/MarshalTests.cs @@ -160,6 +160,35 @@ public static void InvokeUnboxNumber(object o, object expected = null) Assert.Equal(expected ?? o, HelperMarshal._object1); } + [Fact] + public static void InvokeUnboxInt() + { + Runtime.InvokeJS(@" + var obj = App.call_test_method (""InvokeReturnInt""); + var res = App.call_test_method (""InvokeObj1"", [ obj ]); + "); + + Assert.Equal(42, HelperMarshal._object1); + } + + [Fact] + public static void InvokeUnboxDouble() + { + Runtime.InvokeJS(@" + var obj = App.call_test_method (""InvokeReturnDouble""); + var res = App.call_test_method (""InvokeObj1"", [ obj ]); + "); + + Assert.Equal(double.Pi, HelperMarshal._object1); + } + + [Fact] + public static void InvokeUnboxLongFail() + { + var ex = Assert.Throws(() => Runtime.InvokeJS(@"App.call_test_method (""InvokeReturnLong"");")); + Assert.Contains("int64 not available", ex.Message); + } + [Theory] [InlineData(byte.MinValue, 0)] [InlineData(byte.MaxValue, 255)] diff --git a/src/libraries/System.Private.Runtime.InteropServices.JavaScript/tests/System/Runtime/InteropServices/JavaScript/MemoryTests.cs b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/tests/System/Runtime/InteropServices/JavaScript/MemoryTests.cs index 05232a8d72a08a..3195886fbdd3cd 100644 --- a/src/libraries/System.Private.Runtime.InteropServices.JavaScript/tests/System/Runtime/InteropServices/JavaScript/MemoryTests.cs +++ b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/tests/System/Runtime/InteropServices/JavaScript/MemoryTests.cs @@ -42,21 +42,38 @@ public static unsafe void Int52TestOK(long value) } [Theory] - [InlineData(double.NaN)] [InlineData(double.NegativeInfinity)] [InlineData(double.PositiveInfinity)] [InlineData(double.MinValue)] [InlineData(double.MaxValue)] [InlineData(double.Pi)] [InlineData(9007199254740993.0)]//MAX_SAFE_INTEGER +2 - public static unsafe void Int52TestInvalid(double value) + public static unsafe void Int52TestRange(double value) { long actual = 0; uint ptr = (uint)Unsafe.AsPointer(ref actual); var bagFn = new Function("ptr", "value", @" globalThis.App.MONO.setI52(ptr, value);"); var ex=Assert.Throws(() => bagFn.Call(null, ptr, value)); - Assert.Contains("Int64 value out of JavaScript Number safe integer range", ex.Message); + Assert.Contains("Overflow: value out of Number.isSafeInteger range", ex.Message); + + double expectedD = value; + uint ptrD = (uint)Unsafe.AsPointer(ref expectedD); + var bagFnD = new Function("ptr", "value", @" + globalThis.App.MONO.getI52(ptr);"); + var exD = Assert.Throws(() => bagFn.Call(null, ptr, value)); + Assert.Contains("Overflow: value out of Number.isSafeInteger range", ex.Message); + } + + [Fact] + public static unsafe void Int52TestNaN() + { + long actual = 0; + uint ptr = (uint)Unsafe.AsPointer(ref actual); + var bagFn = new Function("ptr", "value", @" + globalThis.App.MONO.setI52(ptr, value);"); + var ex=Assert.Throws(() => bagFn.Call(null, ptr, double.NaN)); + Assert.Contains("Can't convert Number.Nan into Int64", ex.Message); } } } diff --git a/src/mono/wasm/runtime/dotnet.d.ts b/src/mono/wasm/runtime/dotnet.d.ts index 69d069f9d87096..2702cd67d17990 100644 --- a/src/mono/wasm/runtime/dotnet.d.ts +++ b/src/mono/wasm/runtime/dotnet.d.ts @@ -323,7 +323,7 @@ declare function getI8(offset: _MemOffset): number; declare function getI16(offset: _MemOffset): number; declare function getI32(offset: _MemOffset): number; /** - * Throws for Number.MIN_SAFE_INTEGER < value < Number.MAX_SAFE_INTEGER + * Throws for Number.MIN_SAFE_INTEGER > value > Number.MAX_SAFE_INTEGER */ declare function getI52(offset: _MemOffset): number; declare function getI64Big(offset: _MemOffset): bigint; diff --git a/src/mono/wasm/runtime/memory.ts b/src/mono/wasm/runtime/memory.ts index c0facd5eea0f78..bb7e4243703147 100644 --- a/src/mono/wasm/runtime/memory.ts +++ b/src/mono/wasm/runtime/memory.ts @@ -74,7 +74,8 @@ export function setI32(offset: _MemOffset, value: _NumberOrPointer): void { */ export function setI52(offset: _MemOffset, value: number): void { // 52 bits = 0x1F_FFFF_FFFF_FFFF - assert(Number.isSafeInteger(value), "Int64 value out of JavaScript Number safe integer range"); + assert(!Number.isNaN(value), "Can't convert Number.Nan into Int64"); + assert(Number.isSafeInteger(value), "Overflow: value out of Number.isSafeInteger range"); let hi: number; let lo: number; if (value < 0) { @@ -129,7 +130,7 @@ export function getI32(offset: _MemOffset): number { } /** - * Throws for Number.MIN_SAFE_INTEGER < value < Number.MAX_SAFE_INTEGER + * Throws for Number.MIN_SAFE_INTEGER > value > Number.MAX_SAFE_INTEGER */ export function getI52(offset: _MemOffset): number { // 52 bits = 0x1F_FFFF_FFFF_FFFF @@ -138,13 +139,13 @@ export function getI52(offset: _MemOffset): number { const sign = hi & 0x8000_0000; const exp = hi & 0x7FE0_0000; if (sign) { - assert(exp === 0x7FE0_0000, "Int64 value out of JavaScript Number safe integer range"); + assert(exp === 0x7FE0_0000, "Overflow: value out of Number.isSafeInteger range"); const nhi = (hi & 0x000F_FFFF) ^ 0x000F_FFFF; const nlo = lo ^ 0xFFFF_FFFF; return -1 - ((nhi * 0x1_0000_0000) + nlo); } else { - assert(exp === 0, "Int64 value out of JavaScript Number safe integer range"); + assert(exp === 0, "Overflow: value out of Number.isSafeInteger range"); return (hi * 0x1_0000_0000) + lo; } } diff --git a/src/mono/wasm/runtime/types.ts b/src/mono/wasm/runtime/types.ts index d26277e38fb60f..c0df597b942adb 100644 --- a/src/mono/wasm/runtime/types.ts +++ b/src/mono/wasm/runtime/types.ts @@ -3,6 +3,7 @@ import { bind_runtime_method } from "./method-binding"; import { CharPtr, EmscriptenModule, ManagedPointer, NativePointer, VoidPtr } from "./types/emscripten"; +import Configuration from "consts:configuration"; export type GCHandle = { __brand: "GCHandle" @@ -82,7 +83,7 @@ export type MonoConfig = { aot_profiler_options?: AOTProfilerOptions, // dictionary-style Object. If omitted, aot profiler will not be initialized. coverage_profiler_options?: CoverageProfilerOptions, // dictionary-style Object. If omitted, coverage profiler will not be initialized. ignore_pdb_load_errors?: boolean, - wait_for_debugger ?: number + wait_for_debugger?: number }; export type MonoConfigError = { @@ -225,7 +226,9 @@ export function assert(condition: unknown, messageFactory: string | (() => strin const message = typeof messageFactory === "string" ? messageFactory : messageFactory(); - console.error(`Assert failed: ${message}`); + if (Configuration === "Debug") { + console.error(`Assert failed: ${message}`); + } throw new Error(`Assert failed: ${message}`); } } @@ -276,6 +279,6 @@ export const enum MarshalError { // Evaluates whether a value is nullish (same definition used as the ?? operator, // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Nullish_coalescing_operator) -export function is_nullish (value: T | null | undefined): value is null | undefined { +export function is_nullish(value: T | null | undefined): value is null | undefined { return (value === undefined) || (value === null); } From 7982e54157edc6bdff642df997990af584330efd Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Thu, 19 May 2022 15:18:01 +0200 Subject: [PATCH 07/12] - fixed marshaling of uint32, uint16 and byte to be unsigned - implemented range check on all set memory operations - implemented also uint52 - differentiated bool marshaling because it shoud have different validation and message --- .../InteropServices/JavaScript/Runtime.cs | 13 ++-- .../JavaScript/DelegateTests.cs | 7 +- .../JavaScript/MarshalTests.cs | 71 ++++++++----------- .../InteropServices/JavaScript/MemoryTests.cs | 49 +++++++++++++ src/mono/wasm/runtime/dotnet.d.ts | 16 ++++- src/mono/wasm/runtime/driver.c | 4 +- src/mono/wasm/runtime/exports.ts | 6 +- src/mono/wasm/runtime/memory.ts | 51 ++++++++++++- src/mono/wasm/runtime/method-binding.ts | 18 ++++- 9 files changed, 175 insertions(+), 60 deletions(-) diff --git a/src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Runtime.cs b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Runtime.cs index 8817723b6c3c73..bb909a4d5e1694 100644 --- a/src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Runtime.cs +++ b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Runtime.cs @@ -154,12 +154,12 @@ internal static MarshalType GetMarshalTypeFromType(Type? type) switch (typeCode) { - case TypeCode.Byte: case TypeCode.SByte: case TypeCode.Int16: - case TypeCode.UInt16: case TypeCode.Int32: return MarshalType.INT; + case TypeCode.Byte: + case TypeCode.UInt16: case TypeCode.UInt32: return MarshalType.UINT32; case TypeCode.Boolean: @@ -232,11 +232,14 @@ internal static char GetCallSignatureCharacterForMarshalType(MarshalType t, char switch (t) { case MarshalType.BOOL: - case MarshalType.INT: + return 'b'; case MarshalType.UINT32: case MarshalType.POINTER: + return 'I'; + case MarshalType.INT: return 'i'; case MarshalType.UINT64: + return 'L'; case MarshalType.INT64: return 'l'; case MarshalType.FP32: @@ -250,9 +253,9 @@ internal static char GetCallSignatureCharacterForMarshalType(MarshalType t, char case MarshalType.SAFEHANDLE: return 'h'; case MarshalType.ENUM: - return 'j'; + return 'j'; // this is wrong for uint enums case MarshalType.ENUM64: - return 'k'; + return 'k'; // this is wrong for ulong enums case MarshalType.TASK: case MarshalType.DELEGATE: case MarshalType.OBJECT: diff --git a/src/libraries/System.Private.Runtime.InteropServices.JavaScript/tests/System/Runtime/InteropServices/JavaScript/DelegateTests.cs b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/tests/System/Runtime/InteropServices/JavaScript/DelegateTests.cs index 44939de5769c7a..3f6ff49fc313bb 100644 --- a/src/libraries/System.Private.Runtime.InteropServices.JavaScript/tests/System/Runtime/InteropServices/JavaScript/DelegateTests.cs +++ b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/tests/System/Runtime/InteropServices/JavaScript/DelegateTests.cs @@ -99,12 +99,13 @@ public static void InvokeActionIntInt() public static void InvokeActionFloatIntToIntInt() { HelperMarshal._actionResultValue = 0; - Runtime.InvokeJS(@" + var ex = Assert.Throws(()=>Runtime.InvokeJS(@" var actionDelegate = App.call_test_method (""CreateActionDelegate"", [ ]); actionDelegate(3.14,40); - "); + ")); - Assert.Equal(43, HelperMarshal._actionResultValue); + Assert.Contains("Value is not integer but float", ex.Message); + Assert.Equal(0, HelperMarshal._actionResultValue); } [Fact] diff --git a/src/libraries/System.Private.Runtime.InteropServices.JavaScript/tests/System/Runtime/InteropServices/JavaScript/MarshalTests.cs b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/tests/System/Runtime/InteropServices/JavaScript/MarshalTests.cs index 711d94bcd04272..47f3cf1555bd69 100644 --- a/src/libraries/System.Private.Runtime.InteropServices.JavaScript/tests/System/Runtime/InteropServices/JavaScript/MarshalTests.cs +++ b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/tests/System/Runtime/InteropServices/JavaScript/MarshalTests.cs @@ -450,16 +450,13 @@ public static void TestFunctionApply() [Fact] public static void BoundStaticMethodMissingArgs() { - // TODO: We currently have code that relies on this behavior (missing args default to 0) but - // it would be better if it threw an exception about the missing arguments. This test is here - // to ensure we do not break things by accidentally changing this behavior -kg - HelperMarshal._intValue = 1; - Runtime.InvokeJS(@$" + var ex = Assert.Throws(() => Runtime.InvokeJS(@$" var invoke_int = INTERNAL.mono_bind_static_method (""{HelperMarshal.INTEROP_CLASS}InvokeInt""); invoke_int (); - "); - Assert.Equal(0, HelperMarshal._intValue); + ")); + Assert.Contains("Value is not integer but undefined", ex.Message); + Assert.Equal(1, HelperMarshal._intValue); } [Fact] @@ -474,40 +471,41 @@ public static void BoundStaticMethodExtraArgs() } [Fact] - public static void BoundStaticMethodArgumentTypeCoercion() + public static void RangeCheckInt() { - // TODO: As above, the type coercion behavior on display in this test is not ideal, but - // changing it risks breakage in existing code so for now it is verified by a test -kg - HelperMarshal._intValue = 0; - Runtime.InvokeJS(@$" + // no numbers bigger than 32 bits + var ex = Assert.Throws(() => Runtime.InvokeJS(@$" var invoke_int = INTERNAL.mono_bind_static_method (""{HelperMarshal.INTEROP_CLASS}InvokeInt""); - invoke_int (""200""); - "); - Assert.Equal(200, HelperMarshal._intValue); - - Runtime.InvokeJS(@$" - var invoke_int = INTERNAL.mono_bind_static_method (""{HelperMarshal.INTEROP_CLASS}InvokeInt""); - invoke_int (400.5); - "); - Assert.Equal(400, HelperMarshal._intValue); + invoke_int (Number.MAX_SAFE_INTEGER); + ")); + Assert.Contains("Overflow: value 9007199254740991 is out of -2147483648 2147483647 range", ex.Message); + Assert.Equal(0, HelperMarshal._intValue); } [Fact] - public static void BoundStaticMethodUnpleasantArgumentTypeCoercion() + public static void IntegerCheckInt() { - HelperMarshal._intValue = 100; - Runtime.InvokeJS(@$" + HelperMarshal._intValue = 0; + // no floating point rounding + var ex = Assert.Throws(() => Runtime.InvokeJS(@$" var invoke_int = INTERNAL.mono_bind_static_method (""{HelperMarshal.INTEROP_CLASS}InvokeInt""); - invoke_int (""hello""); - "); + invoke_int (3.14); + ")); + Assert.Contains("Value is not integer but float", ex.Message); Assert.Equal(0, HelperMarshal._intValue); + } - // In this case at the very least, the leading "7" is not turned into the number 7 - Runtime.InvokeJS(@$" + [Fact] + public static void TypeCheckInt() + { + HelperMarshal._intValue = 0; + // no string conversion + var ex = Assert.Throws(() => Runtime.InvokeJS(@$" var invoke_int = INTERNAL.mono_bind_static_method (""{HelperMarshal.INTEROP_CLASS}InvokeInt""); - invoke_int (""7apples""); - "); + invoke_int (""200""); + ")); + Assert.Contains("Value is not integer but string", ex.Message); Assert.Equal(0, HelperMarshal._intValue); } @@ -548,19 +546,6 @@ public static void PassUintEnumByValue() Assert.Equal(TestEnum.BigValue, HelperMarshal._enumValue); } - [Fact] - public static void PassUintEnumByValueMasqueradingAsInt() - { - HelperMarshal._enumValue = TestEnum.Zero; - // HACK: We're explicitly telling the bindings layer to pass an int here, not an enum - // Because we know the enum is : uint, this is compatible, so it works. - Runtime.InvokeJS(@$" - var set_enum = INTERNAL.mono_bind_static_method (""{HelperMarshal.INTEROP_CLASS}SetEnumValue"", ""i""); - set_enum (0xFFFFFFFE); - "); - Assert.Equal(TestEnum.BigValue, HelperMarshal._enumValue); - } - [Fact] public static void PassUintEnumByNameIsNotImplemented() { diff --git a/src/libraries/System.Private.Runtime.InteropServices.JavaScript/tests/System/Runtime/InteropServices/JavaScript/MemoryTests.cs b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/tests/System/Runtime/InteropServices/JavaScript/MemoryTests.cs index 3195886fbdd3cd..2b39b6f6d0a74b 100644 --- a/src/libraries/System.Private.Runtime.InteropServices.JavaScript/tests/System/Runtime/InteropServices/JavaScript/MemoryTests.cs +++ b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/tests/System/Runtime/InteropServices/JavaScript/MemoryTests.cs @@ -41,6 +41,36 @@ public static unsafe void Int52TestOK(long value) Assert.Equal(expected, actual2); } + [Theory] + [InlineData(uint.MinValue)] + [InlineData(1L)] + [InlineData(0L)] + [InlineData(42L)] + [InlineData(uint.MaxValue)] + [InlineData(0xF_FFFF_FFFFL)] + [InlineData(9007199254740991L)]//MAX_SAFE_INTEGER + public static unsafe void UInt52TestOK(ulong value) + { + ulong expected = value; + ulong actual2 = value; + var bagFn = new Function("ptr", "ptr2", @" + const value=globalThis.App.MONO.getI52(ptr); + globalThis.App.MONO.setU52(ptr2, value); + return value;"); + + uint ptr = (uint)Unsafe.AsPointer(ref expected); + uint ptr2 = (uint)Unsafe.AsPointer(ref actual2); + + object o = bagFn.Call(null, ptr, ptr2); + if (value < int.MaxValue) + { + Assert.IsType(o); + ulong actual = (uint)o; + Assert.Equal(expected, actual); + } + Assert.Equal(expected, actual2); + } + [Theory] [InlineData(double.NegativeInfinity)] [InlineData(double.PositiveInfinity)] @@ -65,6 +95,25 @@ public static unsafe void Int52TestRange(double value) Assert.Contains("Overflow: value out of Number.isSafeInteger range", ex.Message); } + [Theory] + [InlineData(-1.0)] + public static unsafe void UInt52TestRange(double value) + { + long actual = 0; + uint ptr = (uint)Unsafe.AsPointer(ref actual); + var bagFn = new Function("ptr", "value", @" + globalThis.App.MONO.setU52(ptr, value);"); + var ex=Assert.Throws(() => bagFn.Call(null, ptr, value)); + Assert.Contains("Can't convert negative Number into UInt64", ex.Message); + + double expectedD = value; + uint ptrD = (uint)Unsafe.AsPointer(ref expectedD); + var bagFnD = new Function("ptr", "value", @" + globalThis.App.MONO.getU52(ptr);"); + var exD = Assert.Throws(() => bagFn.Call(null, ptr, value)); + Assert.Contains("Can't convert negative Number into UInt64", ex.Message); + } + [Fact] public static unsafe void Int52TestNaN() { diff --git a/src/mono/wasm/runtime/dotnet.d.ts b/src/mono/wasm/runtime/dotnet.d.ts index 2702cd67d17990..3c4c491a34010b 100644 --- a/src/mono/wasm/runtime/dotnet.d.ts +++ b/src/mono/wasm/runtime/dotnet.d.ts @@ -303,19 +303,25 @@ declare function mono_wasm_load_bytes_into_heap(bytes: Uint8Array): VoidPtr; declare type _MemOffset = number | VoidPtr | NativePointer | ManagedPointer; declare type _NumberOrPointer = number | VoidPtr | NativePointer | ManagedPointer; +declare function setB32(offset: _MemOffset, value: number | boolean): void; declare function setU8(offset: _MemOffset, value: number): void; declare function setU16(offset: _MemOffset, value: number): void; declare function setU32(offset: _MemOffset, value: _NumberOrPointer): void; declare function setI8(offset: _MemOffset, value: number): void; declare function setI16(offset: _MemOffset, value: number): void; -declare function setI32(offset: _MemOffset, value: _NumberOrPointer): void; +declare function setI32(offset: _MemOffset, value: number): void; /** * Throws for values which are not 52 bit integer. See Number.isSafeInteger() */ declare function setI52(offset: _MemOffset, value: number): void; +/** + * Throws for values which are not 52 bit integer or are negative. See Number.isSafeInteger(). + */ +declare function setU52(offset: _MemOffset, value: number): void; declare function setI64Big(offset: _MemOffset, value: bigint): void; declare function setF32(offset: _MemOffset, value: number): void; declare function setF64(offset: _MemOffset, value: number): void; +declare function getB32(offset: _MemOffset): boolean; declare function getU8(offset: _MemOffset): number; declare function getU16(offset: _MemOffset): number; declare function getU32(offset: _MemOffset): number; @@ -326,6 +332,10 @@ declare function getI32(offset: _MemOffset): number; * Throws for Number.MIN_SAFE_INTEGER > value > Number.MAX_SAFE_INTEGER */ declare function getI52(offset: _MemOffset): number; +/** + * Throws for Number.MIN_SAFE_INTEGER > value > Number.MAX_SAFE_INTEGER + */ +declare function getU52(offset: _MemOffset): number; declare function getI64Big(offset: _MemOffset): bigint; declare function getF32(offset: _MemOffset): number; declare function getF64(offset: _MemOffset): number; @@ -351,20 +361,24 @@ declare const MONO: { mono_wasm_load_runtime: (unused: string, debug_level: number) => void; config: MonoConfig | MonoConfigError; loaded_files: string[]; + setB32: typeof setB32; setI8: typeof setI8; setI16: typeof setI16; setI32: typeof setI32; setI52: typeof setI52; + setU52: typeof setU52; setI64Big: typeof setI64Big; setU8: typeof setU8; setU16: typeof setU16; setU32: typeof setU32; setF32: typeof setF32; setF64: typeof setF64; + getB32: typeof getB32; getI8: typeof getI8; getI16: typeof getI16; getI32: typeof getI32; getI52: typeof getI52; + getU52: typeof getU52; getI64Big: typeof getI64Big; getU8: typeof getU8; getU16: typeof getU16; diff --git a/src/mono/wasm/runtime/driver.c b/src/mono/wasm/runtime/driver.c index 3a5977ee2c9657..596e4f05f3c97f 100644 --- a/src/mono/wasm/runtime/driver.c +++ b/src/mono/wasm/runtime/driver.c @@ -875,13 +875,13 @@ _marshal_type_from_mono_type (int mono_type, MonoClass *klass, MonoType *type) case MONO_TYPE_PTR: return MARSHAL_TYPE_POINTER; case MONO_TYPE_I1: - case MONO_TYPE_U1: case MONO_TYPE_I2: - case MONO_TYPE_U2: case MONO_TYPE_I4: return MARSHAL_TYPE_INT; case MONO_TYPE_CHAR: return MARSHAL_TYPE_CHAR; + case MONO_TYPE_U1: + case MONO_TYPE_U2: case MONO_TYPE_U4: // The distinction between this and signed int is // important due to how numbers work in JavaScript return MARSHAL_TYPE_UINT32; diff --git a/src/mono/wasm/runtime/exports.ts b/src/mono/wasm/runtime/exports.ts index 31c15e4ad1e674..ae7efdfab333e3 100644 --- a/src/mono/wasm/runtime/exports.ts +++ b/src/mono/wasm/runtime/exports.ts @@ -61,7 +61,7 @@ import { setI8, setI16, setI32, setI52, setU8, setU16, setU32, setF32, setF64, getI8, getI16, getI32, getI52, - getU8, getU16, getU32, getF32, getF64, afterUpdateGlobalBufferAndViews, getI64Big, setI64Big, + getU8, getU16, getU32, getF32, getF64, afterUpdateGlobalBufferAndViews, getI64Big, setI64Big, getU52, setU52, setB32, getB32, } from "./memory"; import { create_weak_ref } from "./weak-ref"; import { fetch_like, readAsync_like } from "./polyfills"; @@ -93,20 +93,24 @@ const MONO = { loaded_files: [], // memory accessors + setB32, setI8, setI16, setI32, setI52, + setU52, setI64Big, setU8, setU16, setU32, setF32, setF64, + getB32, getI8, getI16, getI32, getI52, + getU52, getI64Big, getU8, getU16, diff --git a/src/mono/wasm/runtime/memory.ts b/src/mono/wasm/runtime/memory.ts index bb7e4243703147..09164690cc68e4 100644 --- a/src/mono/wasm/runtime/memory.ts +++ b/src/mono/wasm/runtime/memory.ts @@ -45,28 +45,45 @@ export function _release_temp_frame(): void { type _MemOffset = number | VoidPtr | NativePointer | ManagedPointer; type _NumberOrPointer = number | VoidPtr | NativePointer | ManagedPointer; +function is_int_in_range(value: Number, min: Number, max: Number) { + assert(typeof value === "number", () => `Value is not integer but ${typeof value}`); + assert(Number.isInteger(value), "Value is not integer but float"); + assert(value >= min && value <= max, () => `Overflow: value ${value} is out of ${min} ${max} range`); +} + +export function setB32(offset: _MemOffset, value: number | boolean): void { + assert(typeof value === "boolean", () => `Value is not boolean but ${typeof value}`); + Module.HEAP32[offset >>> 2] = !!value; +} + export function setU8(offset: _MemOffset, value: number): void { + is_int_in_range(value, 0, 0xFF); Module.HEAPU8[offset] = value; } export function setU16(offset: _MemOffset, value: number): void { + is_int_in_range(value, 0, 0xFFFF); Module.HEAPU16[offset >>> 1] = value; } export function setU32(offset: _MemOffset, value: _NumberOrPointer): void { + is_int_in_range(value, 0, 0xFFFF_FFFF); Module.HEAPU32[offset >>> 2] = value; } export function setI8(offset: _MemOffset, value: number): void { + is_int_in_range(value, -0x80, 0x7F); Module.HEAP8[offset] = value; } export function setI16(offset: _MemOffset, value: number): void { + is_int_in_range(value, -0x8000, 0x7FFF); Module.HEAP16[offset >>> 1] = value; } -export function setI32(offset: _MemOffset, value: _NumberOrPointer): void { - Module.HEAP32[offset >>> 2] = value; +export function setI32(offset: _MemOffset, value: number): void { + is_int_in_range(value, -0x8000_0000, 0x7FFF_FFFF); + Module.HEAP32[offset >>> 2] = value; } /** @@ -91,6 +108,20 @@ export function setI52(offset: _MemOffset, value: number): void { Module.HEAPU32[offset >>> 2] = lo; } +/** + * Throws for values which are not 52 bit integer or are negative. See Number.isSafeInteger(). + */ +export function setU52(offset: _MemOffset, value: number): void { + // 52 bits = 0x1F_FFFF_FFFF_FFFF + assert(!Number.isNaN(value), "Can't convert Number.Nan into UInt64"); + assert(Number.isSafeInteger(value), "Overflow: value out of Number.isSafeInteger range"); + assert(value >= 0, "Can't convert negative Number into UInt64"); + const hi = value >>> 32; + const lo = value & 0xFFFF_FFFF; + Module.HEAPU32[1 + offset >>> 2] = hi; + Module.HEAPU32[offset >>> 2] = lo; +} + export function setI64Big(offset: _MemOffset, value: bigint): void { assert(is_bingint_supported, "BigInt is not supported."); HEAPI64[offset >>> 3] = value; @@ -105,6 +136,10 @@ export function setF64(offset: _MemOffset, value: number): void { } +export function getB32(offset: _MemOffset): boolean { + return !!(Module.HEAP32[offset >>> 2]); +} + export function getU8(offset: _MemOffset): number { return Module.HEAPU8[offset]; } @@ -150,6 +185,18 @@ export function getI52(offset: _MemOffset): number { } } +/** + * Throws for Number.MIN_SAFE_INTEGER > value > Number.MAX_SAFE_INTEGER + */ +export function getU52(offset: _MemOffset): number { + // 52 bits = 0x1F_FFFF_FFFF_FFFF + const hi = Module.HEAPU32[1 + (offset >>> 2)]; + const lo = Module.HEAPU32[offset >>> 2]; + const exp_sign = hi & 0xFFE0_0000; + assert(exp_sign === 0, "Overflow: value out of Number.isSafeInteger range"); + return (hi * 0x1_0000_0000) + lo; +} + export function getI64Big(offset: _MemOffset): bigint { assert(is_bingint_supported, "BigInt is not supported."); return HEAPI64[offset >>> 3]; diff --git a/src/mono/wasm/runtime/method-binding.ts b/src/mono/wasm/runtime/method-binding.ts index 6473e3a77e7d96..d5cea1c6f0dac3 100644 --- a/src/mono/wasm/runtime/method-binding.ts +++ b/src/mono/wasm/runtime/method-binding.ts @@ -10,7 +10,7 @@ import { _unbox_mono_obj_root_with_known_nonprimitive_type } from "./cs-to-js"; import { _create_temp_frame, getI32, getU32, getF32, getF64, - setI32, setU32, setF32, setF64, setI52 + setI32, setU32, setF32, setF64, setI52, setU52, setB32, getB32 } from "./memory"; import { _get_args_root_buffer_for_method_call, _get_buffer_for_method_call, @@ -130,8 +130,11 @@ export function _create_primitive_converters(): void { // result.set ('k', { steps: [{ convert: js_to_mono_enum.bind (this), indirect: 'i64'}], size: 8}); result.set("j", { steps: [{ convert: js_to_mono_enum.bind(BINDING), indirect: "i32" }], size: 8 }); + result.set("b", { steps: [{ indirect: "bool" }], size: 8 }); result.set("i", { steps: [{ indirect: "i32" }], size: 8 }); + result.set("I", { steps: [{ indirect: "u32" }], size: 8 }); result.set("l", { steps: [{ indirect: "i52" }], size: 8 }); + result.set("L", { steps: [{ indirect: "u52" }], size: 8 }); result.set("f", { steps: [{ indirect: "float" }], size: 8 }); result.set("d", { steps: [{ indirect: "double" }], size: 8 }); } @@ -218,7 +221,9 @@ export function _compile_converter_for_marshal_string(args_marshal: string/*Args setU32, setF32, setF64, + setU52, setI52, + setB32, scratchValueRoot: converter.scratchValueRoot }; let indirectLocalOffset = 0; @@ -276,6 +281,9 @@ export function _compile_converter_for_marshal_string(args_marshal: string/*Args const offsetText = `(indirectStart + ${indirectLocalOffset})`; switch (step.indirect) { + case "bool": + body.push(`setB32(${offsetText}, ${valueKey});`); + break; case "u32": body.push(`setU32(${offsetText}, ${valueKey});`); break; @@ -291,6 +299,9 @@ export function _compile_converter_for_marshal_string(args_marshal: string/*Args case "i52": body.push(`setI52(${offsetText}, ${valueKey});`); break; + case "u52": + body.push(`setU52(${offsetText}, ${valueKey});`); + break; default: throw new Error("Unimplemented indirect type: " + step.indirect); } @@ -421,6 +432,7 @@ export function mono_bind_method(method: MonoMethod, this_arg: null, args_marsha token, unbox_buffer, unbox_buffer_size, + getB32, getI32, getU32, getF32, @@ -522,7 +534,7 @@ export function mono_bind_method(method: MonoMethod, this_arg: null, args_marsha ` case ${MarshalType.FP64}:`, " result = getF64(unbox_buffer); break;", ` case ${MarshalType.BOOL}:`, - " result = getI32(unbox_buffer) !== 0; break;", + " result = getB32(unbox_buffer); break;", ` case ${MarshalType.CHAR}:`, " result = String.fromCharCode(getI32(unbox_buffer)); break;", ` case ${MarshalType.NULL}:`, @@ -584,7 +596,7 @@ export type ArgsMarshalString = "" | `${ArgsMarshal}${ArgsMarshal}${ArgsMarshal}${ArgsMarshal}${_ExtraArgsMarshalOperators}`; */ -type ConverterStepIndirects = "u32" | "i32" | "float" | "double" | "i52" | "reference" +type ConverterStepIndirects = "u32" | "i32" | "float" | "double" | "u52" | "i52" | "reference" | "bool" export type Converter = { steps: { From 3e20335126e9f710174f16d775f087ff4cc8ab7f Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Thu, 19 May 2022 17:14:39 +0200 Subject: [PATCH 08/12] inlined asserts --- src/mono/wasm/runtime/rollup.config.js | 50 ++++++++++++++++++++++++-- src/mono/wasm/runtime/types.ts | 5 ++- 2 files changed, 49 insertions(+), 6 deletions(-) diff --git a/src/mono/wasm/runtime/rollup.config.js b/src/mono/wasm/runtime/rollup.config.js index 7225a2c954e5ff..5b01520c8211c3 100644 --- a/src/mono/wasm/runtime/rollup.config.js +++ b/src/mono/wasm/runtime/rollup.config.js @@ -7,6 +7,7 @@ import * as path from "path"; import { createHash } from "crypto"; import dts from "rollup-plugin-dts"; import consts from "rollup-plugin-consts"; +import { createFilter } from "@rollup/pluginutils"; const configuration = process.env.Configuration; const isDebug = configuration !== "Release"; @@ -45,7 +46,16 @@ const banner_dts = banner + "//!\n//! This is generated file, see src/mono/wasm/ // emcc doesn't know how to load ES6 module, that's why we need the whole rollup.js const format = "iife"; const name = "__dotnet_runtime"; - +const inlineAssertQuotes = { + // eslint-disable-next-line quotes + pattern: /assert\(([^,]*), *("[^"]*")\);/g, + replacement: "if (!($1)) throw new Error($2); // inlined assert" +}; +const inlineAssertInterpolation = { + // eslint-disable-next-line quotes + pattern: /assert\(([^,]*), \(\) => *(`[^`]*`)\);/g, + replacement: "if (!($1)) throw new Error($2); // inlined assert" +}; const iffeConfig = { treeshake: !isDebug, input: "exports.ts", @@ -72,7 +82,7 @@ const iffeConfig = { handler(warning); }, - plugins: [consts({ productVersion, configuration }), typescript()] + plugins: [regexReplace([inlineAssertQuotes, inlineAssertInterpolation]), consts({ productVersion, configuration }), typescript()] }; const typesConfig = { input: "./export-types.ts", @@ -159,4 +169,38 @@ function checkFileExists(file) { return fs.promises.access(file, fs.constants.F_OK) .then(() => true) .catch(() => false); -} \ No newline at end of file +} + +function regexReplace(replacements = []) { + const filter = createFilter("**/*.ts"); + + return { + name: "replace", + + renderChunk(code, chunk) { + const id = chunk.fileName; + if (!filter(id)) return null; + return executeReplacement(this, code, id); + }, + + transform(code, id) { + if (!filter(id)) return null; + return executeReplacement(this, code, id); + } + }; + + function executeReplacement(self, code) { + // TODO use MagicString for sourcemap support + let fixed = code; + for (const rep of replacements) { + const { pattern, replacement } = rep; + fixed = fixed.replace(pattern, replacement); + } + + if (fixed == code) { + return null; + } + + return { code: fixed }; + } +} diff --git a/src/mono/wasm/runtime/types.ts b/src/mono/wasm/runtime/types.ts index c0df597b942adb..8a0d4055416d88 100644 --- a/src/mono/wasm/runtime/types.ts +++ b/src/mono/wasm/runtime/types.ts @@ -221,14 +221,13 @@ export type DotnetModuleConfigImports = { url?: any; } +// see src\mono\wasm\runtime\rollup.config.js +// inline this, because the lambda could allocate closure on hot path otherwise export function assert(condition: unknown, messageFactory: string | (() => string)): asserts condition { if (!condition) { const message = typeof messageFactory === "string" ? messageFactory : messageFactory(); - if (Configuration === "Debug") { - console.error(`Assert failed: ${message}`); - } throw new Error(`Assert failed: ${message}`); } } From 3e3e658b4f0a7a71c69beeaa5735dfbca23f4799 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Thu, 19 May 2022 20:53:55 +0200 Subject: [PATCH 09/12] fix test --- .../System/Runtime/InteropServices/JavaScript/MemoryTests.cs | 2 +- src/mono/wasm/runtime/types.ts | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/libraries/System.Private.Runtime.InteropServices.JavaScript/tests/System/Runtime/InteropServices/JavaScript/MemoryTests.cs b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/tests/System/Runtime/InteropServices/JavaScript/MemoryTests.cs index 2b39b6f6d0a74b..a95d8b3694aafa 100644 --- a/src/libraries/System.Private.Runtime.InteropServices.JavaScript/tests/System/Runtime/InteropServices/JavaScript/MemoryTests.cs +++ b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/tests/System/Runtime/InteropServices/JavaScript/MemoryTests.cs @@ -65,7 +65,7 @@ public static unsafe void UInt52TestOK(ulong value) if (value < int.MaxValue) { Assert.IsType(o); - ulong actual = (uint)o; + ulong actual = (ulong)(long)(int)o; Assert.Equal(expected, actual); } Assert.Equal(expected, actual2); diff --git a/src/mono/wasm/runtime/types.ts b/src/mono/wasm/runtime/types.ts index 8a0d4055416d88..9f6034de0b3419 100644 --- a/src/mono/wasm/runtime/types.ts +++ b/src/mono/wasm/runtime/types.ts @@ -3,7 +3,6 @@ import { bind_runtime_method } from "./method-binding"; import { CharPtr, EmscriptenModule, ManagedPointer, NativePointer, VoidPtr } from "./types/emscripten"; -import Configuration from "consts:configuration"; export type GCHandle = { __brand: "GCHandle" From a11bcf39b7af5075f084c29c9e77626b5532f4bc Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Thu, 19 May 2022 21:02:58 +0200 Subject: [PATCH 10/12] fix message --- src/mono/wasm/runtime/rollup.config.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/mono/wasm/runtime/rollup.config.js b/src/mono/wasm/runtime/rollup.config.js index 5b01520c8211c3..87a23d9f92d720 100644 --- a/src/mono/wasm/runtime/rollup.config.js +++ b/src/mono/wasm/runtime/rollup.config.js @@ -49,12 +49,12 @@ const name = "__dotnet_runtime"; const inlineAssertQuotes = { // eslint-disable-next-line quotes pattern: /assert\(([^,]*), *("[^"]*")\);/g, - replacement: "if (!($1)) throw new Error($2); // inlined assert" + replacement: "if (!($1)) throw new Error(`Assert failed: ${$2}`); // inlined assert" }; const inlineAssertInterpolation = { // eslint-disable-next-line quotes pattern: /assert\(([^,]*), \(\) => *(`[^`]*`)\);/g, - replacement: "if (!($1)) throw new Error($2); // inlined assert" + replacement: "if (!($1)) throw new Error(`Assert failed: ${$2}`); // inlined assert" }; const iffeConfig = { treeshake: !isDebug, From 067e9db297aab8af0665ea3e22fee05531c826c9 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Thu, 19 May 2022 21:17:46 +0200 Subject: [PATCH 11/12] optimize inline --- src/mono/wasm/runtime/rollup.config.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/mono/wasm/runtime/rollup.config.js b/src/mono/wasm/runtime/rollup.config.js index 87a23d9f92d720..4648e87f1d4946 100644 --- a/src/mono/wasm/runtime/rollup.config.js +++ b/src/mono/wasm/runtime/rollup.config.js @@ -48,13 +48,14 @@ const format = "iife"; const name = "__dotnet_runtime"; const inlineAssertQuotes = { // eslint-disable-next-line quotes - pattern: /assert\(([^,]*), *("[^"]*")\);/g, - replacement: "if (!($1)) throw new Error(`Assert failed: ${$2}`); // inlined assert" + pattern: /assert\(([^,]*), *"([^"]*)"\);/g, + // eslint-disable-next-line quotes + replacement: 'if (!($1)) throw new Error("Assert failed: $2"); // inlined assert' }; const inlineAssertInterpolation = { // eslint-disable-next-line quotes - pattern: /assert\(([^,]*), \(\) => *(`[^`]*`)\);/g, - replacement: "if (!($1)) throw new Error(`Assert failed: ${$2}`); // inlined assert" + pattern: /assert\(([^,]*), \(\) => *`([^`]*)`\);/g, + replacement: "if (!($1)) throw new Error(`Assert failed: $2`); // inlined assert" }; const iffeConfig = { treeshake: !isDebug, From 7b2e2113fcd2bc1563c92e209f80125e5dca49a2 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Fri, 20 May 2022 11:02:12 +0200 Subject: [PATCH 12/12] rename mono_assert and make sure we replace them all --- src/mono/wasm/runtime/cwraps.ts | 4 +-- src/mono/wasm/runtime/memory.ts | 30 +++++++++---------- src/mono/wasm/runtime/rollup.config.js | 41 ++++++++++++++++---------- src/mono/wasm/runtime/startup.ts | 18 +++++------ src/mono/wasm/runtime/types.ts | 2 +- 5 files changed, 53 insertions(+), 42 deletions(-) diff --git a/src/mono/wasm/runtime/cwraps.ts b/src/mono/wasm/runtime/cwraps.ts index 52663f7f39aa6b..0c205b47e492df 100644 --- a/src/mono/wasm/runtime/cwraps.ts +++ b/src/mono/wasm/runtime/cwraps.ts @@ -2,7 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. import { - assert, + mono_assert, MonoArray, MonoAssembly, MonoClass, MonoMethod, MonoObject, MonoString, MonoType, MonoObjectRef, MonoStringRef @@ -197,7 +197,7 @@ export default wrapped_c_functions; export function wrap_c_function(name: string): Function { const wf: any = wrapped_c_functions; const sig = fn_signatures.find(s => s[0] === name); - assert(sig, () => `Function ${name} not found`); + mono_assert(sig, () => `Function ${name} not found`); const fce = Module.cwrap(sig[0], sig[1], sig[2], sig[3]); wf[sig[0]] = fce; return fce; diff --git a/src/mono/wasm/runtime/memory.ts b/src/mono/wasm/runtime/memory.ts index 09164690cc68e4..8e43d133e47df9 100644 --- a/src/mono/wasm/runtime/memory.ts +++ b/src/mono/wasm/runtime/memory.ts @@ -1,5 +1,5 @@ import { Module } from "./imports"; -import { assert } from "./types"; +import { mono_assert } from "./types"; import { VoidPtr, NativePointer, ManagedPointer } from "./types/emscripten"; import * as cuint64 from "./cuint64"; @@ -46,13 +46,13 @@ type _MemOffset = number | VoidPtr | NativePointer | ManagedPointer; type _NumberOrPointer = number | VoidPtr | NativePointer | ManagedPointer; function is_int_in_range(value: Number, min: Number, max: Number) { - assert(typeof value === "number", () => `Value is not integer but ${typeof value}`); - assert(Number.isInteger(value), "Value is not integer but float"); - assert(value >= min && value <= max, () => `Overflow: value ${value} is out of ${min} ${max} range`); + mono_assert(typeof value === "number", () => `Value is not integer but ${typeof value}`); + mono_assert(Number.isInteger(value), "Value is not integer but float"); + mono_assert(value >= min && value <= max, () => `Overflow: value ${value} is out of ${min} ${max} range`); } export function setB32(offset: _MemOffset, value: number | boolean): void { - assert(typeof value === "boolean", () => `Value is not boolean but ${typeof value}`); + mono_assert(typeof value === "boolean", () => `Value is not boolean but ${typeof value}`); Module.HEAP32[offset >>> 2] = !!value; } @@ -91,8 +91,8 @@ export function setI32(offset: _MemOffset, value: number): void { */ export function setI52(offset: _MemOffset, value: number): void { // 52 bits = 0x1F_FFFF_FFFF_FFFF - assert(!Number.isNaN(value), "Can't convert Number.Nan into Int64"); - assert(Number.isSafeInteger(value), "Overflow: value out of Number.isSafeInteger range"); + mono_assert(!Number.isNaN(value), "Can't convert Number.Nan into Int64"); + mono_assert(Number.isSafeInteger(value), "Overflow: value out of Number.isSafeInteger range"); let hi: number; let lo: number; if (value < 0) { @@ -113,9 +113,9 @@ export function setI52(offset: _MemOffset, value: number): void { */ export function setU52(offset: _MemOffset, value: number): void { // 52 bits = 0x1F_FFFF_FFFF_FFFF - assert(!Number.isNaN(value), "Can't convert Number.Nan into UInt64"); - assert(Number.isSafeInteger(value), "Overflow: value out of Number.isSafeInteger range"); - assert(value >= 0, "Can't convert negative Number into UInt64"); + mono_assert(!Number.isNaN(value), "Can't convert Number.Nan into UInt64"); + mono_assert(Number.isSafeInteger(value), "Overflow: value out of Number.isSafeInteger range"); + mono_assert(value >= 0, "Can't convert negative Number into UInt64"); const hi = value >>> 32; const lo = value & 0xFFFF_FFFF; Module.HEAPU32[1 + offset >>> 2] = hi; @@ -123,7 +123,7 @@ export function setU52(offset: _MemOffset, value: number): void { } export function setI64Big(offset: _MemOffset, value: bigint): void { - assert(is_bingint_supported, "BigInt is not supported."); + mono_assert(is_bingint_supported, "BigInt is not supported."); HEAPI64[offset >>> 3] = value; } @@ -174,13 +174,13 @@ export function getI52(offset: _MemOffset): number { const sign = hi & 0x8000_0000; const exp = hi & 0x7FE0_0000; if (sign) { - assert(exp === 0x7FE0_0000, "Overflow: value out of Number.isSafeInteger range"); + mono_assert(exp === 0x7FE0_0000, "Overflow: value out of Number.isSafeInteger range"); const nhi = (hi & 0x000F_FFFF) ^ 0x000F_FFFF; const nlo = lo ^ 0xFFFF_FFFF; return -1 - ((nhi * 0x1_0000_0000) + nlo); } else { - assert(exp === 0, "Overflow: value out of Number.isSafeInteger range"); + mono_assert(exp === 0, "Overflow: value out of Number.isSafeInteger range"); return (hi * 0x1_0000_0000) + lo; } } @@ -193,12 +193,12 @@ export function getU52(offset: _MemOffset): number { const hi = Module.HEAPU32[1 + (offset >>> 2)]; const lo = Module.HEAPU32[offset >>> 2]; const exp_sign = hi & 0xFFE0_0000; - assert(exp_sign === 0, "Overflow: value out of Number.isSafeInteger range"); + mono_assert(exp_sign === 0, "Overflow: value out of Number.isSafeInteger range"); return (hi * 0x1_0000_0000) + lo; } export function getI64Big(offset: _MemOffset): bigint { - assert(is_bingint_supported, "BigInt is not supported."); + mono_assert(is_bingint_supported, "BigInt is not supported."); return HEAPI64[offset >>> 3]; } diff --git a/src/mono/wasm/runtime/rollup.config.js b/src/mono/wasm/runtime/rollup.config.js index 4648e87f1d4946..f37712aa1556df 100644 --- a/src/mono/wasm/runtime/rollup.config.js +++ b/src/mono/wasm/runtime/rollup.config.js @@ -46,17 +46,19 @@ const banner_dts = banner + "//!\n//! This is generated file, see src/mono/wasm/ // emcc doesn't know how to load ES6 module, that's why we need the whole rollup.js const format = "iife"; const name = "__dotnet_runtime"; -const inlineAssertQuotes = { - // eslint-disable-next-line quotes - pattern: /assert\(([^,]*), *"([^"]*)"\);/g, - // eslint-disable-next-line quotes - replacement: 'if (!($1)) throw new Error("Assert failed: $2"); // inlined assert' -}; -const inlineAssertInterpolation = { - // eslint-disable-next-line quotes - pattern: /assert\(([^,]*), \(\) => *`([^`]*)`\);/g, - replacement: "if (!($1)) throw new Error(`Assert failed: $2`); // inlined assert" -}; +const inlineAssert = [ + { + pattern: /mono_assert\(([^,]*), *"([^"]*)"\);/gm, + // eslint-disable-next-line quotes + replacement: 'if (!($1)) throw new Error("Assert failed: $2"); // inlined mono_assert' + }, + { + pattern: /mono_assert\(([^,]*), \(\) => *`([^`]*)`\);/gm, + replacement: "if (!($1)) throw new Error(`Assert failed: $2`); // inlined mono_assert" + }, { + pattern: /^\s*mono_assert/gm, + failure: "previous regexp didn't inline all mono_assert statements" + }]; const iffeConfig = { treeshake: !isDebug, input: "exports.ts", @@ -83,7 +85,7 @@ const iffeConfig = { handler(warning); }, - plugins: [regexReplace([inlineAssertQuotes, inlineAssertInterpolation]), consts({ productVersion, configuration }), typescript()] + plugins: [regexReplace(inlineAssert), consts({ productVersion, configuration }), typescript()] }; const typesConfig = { input: "./export-types.ts", @@ -190,12 +192,21 @@ function regexReplace(replacements = []) { } }; - function executeReplacement(self, code) { + function executeReplacement(self, code, id) { // TODO use MagicString for sourcemap support let fixed = code; for (const rep of replacements) { - const { pattern, replacement } = rep; - fixed = fixed.replace(pattern, replacement); + const { pattern, replacement, failure } = rep; + if (failure) { + const match = pattern.test(fixed); + if (match) { + self.error(failure + " " + id, pattern.lastIndex); + return null; + } + } + else { + fixed = fixed.replace(pattern, replacement); + } } if (fixed == code) { diff --git a/src/mono/wasm/runtime/startup.ts b/src/mono/wasm/runtime/startup.ts index 3871aa5fb166d4..3a34af2244f447 100644 --- a/src/mono/wasm/runtime/startup.ts +++ b/src/mono/wasm/runtime/startup.ts @@ -1,7 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -import { AllAssetEntryTypes, assert, AssetEntry, CharPtrNull, DotnetModule, GlobalizationMode, MonoConfig, MonoConfigError, wasm_type_symbol, MonoObject } from "./types"; +import { AllAssetEntryTypes, mono_assert, AssetEntry, CharPtrNull, DotnetModule, GlobalizationMode, MonoConfig, MonoConfigError, wasm_type_symbol, MonoObject } from "./types"; import { ENVIRONMENT_IS_ESM, ENVIRONMENT_IS_NODE, ENVIRONMENT_IS_SHELL, INTERNAL, locateFile, Module, MONO, requirePromise, runtimeHelpers } from "./imports"; import cwraps from "./cwraps"; import { mono_wasm_raise_debug_event, mono_wasm_runtime_ready } from "./debug"; @@ -36,7 +36,7 @@ export function configure_emscripten_startup(module: DotnetModule, exportedAPI: (typeof (globalThis.document.createElement) === "function") ) { // blazor injects a module preload link element for dotnet.[version].[sha].js - const blazorDotNetJS = Array.from (document.head.getElementsByTagName("link")).filter(elt => elt.rel !== undefined && elt.rel == "modulepreload" && elt.href !== undefined && elt.href.indexOf("dotnet") != -1 && elt.href.indexOf (".js") != -1); + const blazorDotNetJS = Array.from(document.head.getElementsByTagName("link")).filter(elt => elt.rel !== undefined && elt.rel == "modulepreload" && elt.href !== undefined && elt.href.indexOf("dotnet") != -1 && elt.href.indexOf(".js") != -1); if (blazorDotNetJS.length == 1) { const hr = blazorDotNetJS[0].href; console.log("determined url of main script to be " + hr); @@ -191,8 +191,8 @@ export function mono_wasm_set_runtime_options(options: string[]): void { // this need to be run only after onRuntimeInitialized event, when the memory is ready function _handle_fetched_asset(asset: AssetEntry, url?: string) { - assert(ctx, "Context is expected"); - assert(asset.buffer, "asset.buffer is expected"); + mono_assert(ctx, "Context is expected"); + mono_assert(asset.buffer, "asset.buffer is expected"); const bytes = new Uint8Array(asset.buffer); if (ctx.tracing) @@ -304,7 +304,7 @@ function finalize_startup(config: MonoConfig | MonoConfigError | undefined): voi const moduleExt = Module as DotnetModule; - if(!Module.disableDotnet6Compatibility && Module.exports){ + if (!Module.disableDotnet6Compatibility && Module.exports) { // Export emscripten defined in module through EXPORTED_RUNTIME_METHODS // Useful to export IDBFS or other similar types generally exposed as // global types when emscripten is not modularized. @@ -312,10 +312,10 @@ function finalize_startup(config: MonoConfig | MonoConfigError | undefined): voi const exportName = Module.exports[i]; const exportValue = (Module)[exportName]; - if(exportValue) { + if (exportValue) { globalThisAny[exportName] = exportValue; } - else{ + else { console.warn(`MONO_WASM: The exported symbol ${exportName} could not be found in the emscripten module`); } } @@ -586,8 +586,8 @@ async function mono_download_assets(config: MonoConfig | MonoConfigError | undef } function finalize_assets(config: MonoConfig | MonoConfigError | undefined): void { - assert(config && !config.isError, "Expected config"); - assert(ctx && ctx.downloading_count == 0, "Expected assets to be downloaded"); + mono_assert(config && !config.isError, "Expected config"); + mono_assert(ctx && ctx.downloading_count == 0, "Expected assets to be downloaded"); try { for (const fetch_result of ctx.resolved_promises!) { diff --git a/src/mono/wasm/runtime/types.ts b/src/mono/wasm/runtime/types.ts index 9f6034de0b3419..e649ebc0f494df 100644 --- a/src/mono/wasm/runtime/types.ts +++ b/src/mono/wasm/runtime/types.ts @@ -222,7 +222,7 @@ export type DotnetModuleConfigImports = { // see src\mono\wasm\runtime\rollup.config.js // inline this, because the lambda could allocate closure on hot path otherwise -export function assert(condition: unknown, messageFactory: string | (() => string)): asserts condition { +export function mono_assert(condition: unknown, messageFactory: string | (() => string)): asserts condition { if (!condition) { const message = typeof messageFactory === "string" ? messageFactory