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.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/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/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..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 @@ -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)] @@ -421,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] @@ -445,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); } @@ -519,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 new file mode 100644 index 00000000000000..a95d8b3694aafa --- /dev/null +++ b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/tests/System/Runtime/InteropServices/JavaScript/MemoryTests.cs @@ -0,0 +1,128 @@ +// 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); + } + + [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 = (ulong)(long)(int)o; + Assert.Equal(expected, actual); + } + Assert.Equal(expected, actual2); + } + + [Theory] + [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 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("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); + } + + [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() + { + 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/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/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/dotnet.d.ts b/src/mono/wasm/runtime/dotnet.d.ts index 07c1db8ac7c6f8..3c4c491a34010b 100644 --- a/src/mono/wasm/runtime/dotnet.d.ts +++ b/src/mono/wasm/runtime/dotnet.d.ts @@ -303,22 +303,40 @@ 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 setI64(offset: _MemOffset, value: number): 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; 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; +/** + * 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; @@ -343,19 +361,25 @@ 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; - setI64: typeof setI64; + 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; - getI64: typeof getI64; + getI52: typeof getI52; + getU52: typeof getU52; + getI64Big: typeof getI64Big; getU8: typeof getU8; getU16: typeof getU16; getU32: typeof getU32; 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/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 e370c2e97c3456..ae7efdfab333e3 100644 --- a/src/mono/wasm/runtime/exports.ts +++ b/src/mono/wasm/runtime/exports.ts @@ -58,10 +58,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, getU52, setU52, setB32, getB32, } from "./memory"; import { create_weak_ref } from "./weak-ref"; import { fetch_like, readAsync_like } from "./polyfills"; @@ -93,19 +93,25 @@ const MONO = { loaded_files: [], // memory accessors + setB32, setI8, setI16, setI32, - setI64, + setI52, + setU52, + setI64Big, setU8, setU16, setU32, setF32, setF64, + getB32, getI8, getI16, getI32, - getI64, + getI52, + getU52, + getI64Big, getU8, getU16, getU32, @@ -179,7 +185,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; @@ -236,6 +242,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 e524bb48046e38..8e43d133e47df9 100644 --- a/src/mono/wasm/runtime/memory.ts +++ b/src/mono/wasm/runtime/memory.ts @@ -1,10 +1,12 @@ import { Module } from "./imports"; +import { mono_assert } from "./types"; import { VoidPtr, NativePointer, ManagedPointer } from "./types/emscripten"; import * as cuint64 from "./cuint64"; 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) @@ -14,6 +16,8 @@ function _ensure_allocated(): void { alloca_limit = (alloca_base + alloca_buffer_size); } +const is_bingint_supported = typeof BigInt !== "undefined" && typeof BigInt64Array !== "undefined"; + export function temp_malloc(size: number): VoidPtr { _ensure_allocated(); if (!alloca_stack.length) @@ -41,33 +45,86 @@ 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) { + 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 { + mono_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; +} + +/** + * 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 + 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) { + 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; } -// 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 52 bit integer or are negative. See Number.isSafeInteger(). + */ +export function setU52(offset: _MemOffset, value: number): void { + // 52 bits = 0x1F_FFFF_FFFF_FFFF + 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; + Module.HEAPU32[offset >>> 2] = lo; +} + +export function setI64Big(offset: _MemOffset, value: bigint): void { + mono_assert(is_bingint_supported, "BigInt is not supported."); + HEAPI64[offset >>> 3] = value; } export function setF32(offset: _MemOffset, value: number): void { @@ -79,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]; } @@ -103,9 +164,42 @@ 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) { + 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 { + mono_assert(exp === 0, "Overflow: value out of Number.isSafeInteger range"); + return (hi * 0x1_0000_0000) + lo; + } +} + +/** + * 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; + mono_assert(exp_sign === 0, "Overflow: value out of Number.isSafeInteger range"); + return (hi * 0x1_0000_0000) + lo; +} + +export function getI64Big(offset: _MemOffset): bigint { + mono_assert(is_bingint_supported, "BigInt is not supported."); + return HEAPI64[offset >>> 3]; } export function getF32(offset: _MemOffset): number { @@ -116,6 +210,12 @@ 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); + } +} + export function getCU64(offset: _MemOffset): cuint64.CUInt64 { const lo = getU32(offset); const hi = getU32(offset + 4); diff --git a/src/mono/wasm/runtime/method-binding.ts b/src/mono/wasm/runtime/method-binding.ts index 6dc3daec9f7c42..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, setI64, + 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("l", { steps: [{ indirect: "i64" }], 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, - setI64, + 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; @@ -288,8 +296,11 @@ 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});`); + 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" | "i64" | "reference" +type ConverterStepIndirects = "u32" | "i32" | "float" | "double" | "u52" | "i52" | "reference" | "bool" export type Converter = { steps: { diff --git a/src/mono/wasm/runtime/rollup.config.js b/src/mono/wasm/runtime/rollup.config.js index 7225a2c954e5ff..f37712aa1556df 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,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 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", @@ -72,7 +85,7 @@ const iffeConfig = { handler(warning); }, - plugins: [consts({ productVersion, configuration }), typescript()] + plugins: [regexReplace(inlineAssert), consts({ productVersion, configuration }), typescript()] }; const typesConfig = { input: "./export-types.ts", @@ -159,4 +172,47 @@ 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, id) { + // TODO use MagicString for sourcemap support + let fixed = code; + for (const rep of replacements) { + 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) { + return null; + } + + return { code: fixed }; + } +} 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 d26277e38fb60f..e649ebc0f494df 100644 --- a/src/mono/wasm/runtime/types.ts +++ b/src/mono/wasm/runtime/types.ts @@ -82,7 +82,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 = { @@ -220,12 +220,13 @@ export type DotnetModuleConfigImports = { url?: any; } -export function assert(condition: unknown, messageFactory: string | (() => string)): asserts condition { +// see src\mono\wasm\runtime\rollup.config.js +// inline this, because the lambda could allocate closure on hot path otherwise +export function mono_assert(condition: unknown, messageFactory: string | (() => string)): asserts condition { if (!condition) { const message = typeof messageFactory === "string" ? messageFactory : messageFactory(); - console.error(`Assert failed: ${message}`); throw new Error(`Assert failed: ${message}`); } } @@ -276,6 +277,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); }