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 93d24cce4fe2a4..813f07046d0a1d 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 @@ -71,7 +71,16 @@ private static int BindJSObject(int jsId, bool ownsHandle, int mappedType) WeakReference? reference; lock (_boundObjects) { - if (!_boundObjects.TryGetValue(jsId, out reference)) + if (_boundObjects.TryGetValue(jsId, out reference)) + { + if ((reference.Target == null) || ((reference.Target as JSObject)?.IsDisposed == true)) + { + _boundObjects.Remove(jsId); + reference = null; + } + } + + if (reference == null) { IntPtr jsIntPtr = (IntPtr)jsId; reference = new WeakReference(mappedType > 0 ? BindJSType(jsIntPtr, ownsHandle, mappedType) : new JSObject(jsIntPtr, ownsHandle), true); @@ -229,21 +238,6 @@ private static int GetJSObjectId(object rawObj) js.GetWrappedObject() ?? h.Target : h.Target; } - private static object BoxInt(int i) - { - return i; - } - - private static object BoxDouble(double d) - { - return d; - } - - private static object BoxBool(int b) - { - return b == 0 ? false : true; - } - private static bool IsSimpleArray(object a) { return a is System.Array arr && arr.Rank == 1 && arr.GetLowerBound(0) == 0; 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 3c359f2f0b9e49..5957cb4e8d93f4 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 @@ -88,6 +88,11 @@ private static object InvokeMarshalObj() return _marshalledObject; } + private static object InvokeReturnMarshalObj() + { + return _marshalledObject; + } + internal static int _valOne, _valTwo; private static void ManipulateObject(JSObject obj) { @@ -316,6 +321,8 @@ private static void CreateFunctionSum() internal static int _sumValue = 0; private static void CallFunctionSum() { + if (_sumFunction == null) + throw new Exception("_sumFunction is null"); _sumValue = (int)_sumFunction.Call(null, 3, 5); } @@ -323,6 +330,8 @@ private static void CallFunctionSum() private static void CreateFunctionApply() { var math = (JSObject)Runtime.GetGlobalObject("Math"); + if (math == null) + throw new Exception("Runtime.GetGlobalObject(Math) returned null"); _mathMinFunction = (Function)math.GetObjectProperty("min"); } @@ -330,6 +339,8 @@ private static void CreateFunctionApply() internal static int _minValue = 0; private static void CallFunctionApply() { + if (_mathMinFunction == null) + throw new Exception("_mathMinFunction is null"); _minValue = (int)_mathMinFunction.Apply(null, new object[] { 5, 6, 2, 3, 7 }); } @@ -345,5 +356,32 @@ public static void SetBlobAsUri(Uri blobUri) _blobURI = blobUri; } + internal static uint _uintValue; + private static void InvokeUInt(uint value) + { + _uintValue = value; + } + + internal static TestEnum _enumValue; + private static void SetEnumValue(TestEnum value) + { + _enumValue = value; + } + private static TestEnum GetEnumValue() + { + return _enumValue; + } + + private static UInt64 GetUInt64() + { + return UInt64.MaxValue; + } + } + + public enum TestEnum : uint { + FirstValue = 1, + Zero = 0, + Five = 5, + BigValue = 0xFFFFFFFEu } } diff --git a/src/libraries/System.Private.Runtime.InteropServices.JavaScript/tests/System/Runtime/InteropServices/JavaScript/JavaScriptTests.cs b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/tests/System/Runtime/InteropServices/JavaScript/JavaScriptTests.cs index 6fd1a21f7423dd..23d57c25b55391 100644 --- a/src/libraries/System.Private.Runtime.InteropServices.JavaScript/tests/System/Runtime/InteropServices/JavaScript/JavaScriptTests.cs +++ b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/tests/System/Runtime/InteropServices/JavaScript/JavaScriptTests.cs @@ -92,14 +92,13 @@ public static void FunctionSum() } [Fact] - [ActiveIssue("https://github.com/dotnet/runtime/issues/40112")] public static void FunctionMath() { JSObject math = (JSObject)Runtime.GetGlobalObject("Math"); - Assert.NotNull(math); + Assert.True(math != null, "math != null"); Function mathMax = (Function)math.GetObjectProperty("max"); - Assert.NotNull(mathMax); + Assert.True(mathMax != null, "math.max != null"); var maxValue = (int)mathMax.Apply(null, new object[] { 5, 6, 2, 3, 7 }); Assert.Equal(7, maxValue); @@ -108,7 +107,7 @@ public static void FunctionMath() Assert.Equal(7, maxValue); Function mathMin = (Function)((JSObject)Runtime.GetGlobalObject("Math")).GetObjectProperty("min"); - Assert.NotNull(mathMin); + Assert.True(mathMin != null, "math.min != null"); var minValue = (int)mathMin.Apply(null, new object[] { 5, 6, 2, 3, 7 }); Assert.Equal(2, minValue); 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 b07ecbe5fbb709..1c1447201732b5 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 @@ -126,6 +126,73 @@ public static void CSObjectKeepIdentityAcrossCalls() Assert.Same(HelperMarshal._object1, HelperMarshal._object2); } + [Theory] + [InlineData(byte.MinValue)] + [InlineData(byte.MaxValue)] + [InlineData(SByte.MinValue)] + [InlineData(SByte.MaxValue)] + [InlineData(uint.MaxValue)] + [InlineData(uint.MinValue)] + [InlineData(int.MaxValue)] + [InlineData(int.MinValue)] + [InlineData(double.MaxValue)] + [InlineData(double.MinValue)] + public static void InvokeUnboxNumberString(object o) + { + HelperMarshal._marshalledObject = o; + HelperMarshal._object1 = HelperMarshal._object2 = null; + var value = Runtime.InvokeJS(@" + var obj = App.call_test_method (""InvokeReturnMarshalObj""); + var res = App.call_test_method (""InvokeObj1"", [ obj.toString() ]); + "); + + Assert.Equal(o.ToString().ToLower(), HelperMarshal._object1); + } + + [Theory] + [InlineData(byte.MinValue, 0)] + [InlineData(byte.MaxValue, 255)] + [InlineData(SByte.MinValue, -128)] + [InlineData(SByte.MaxValue, 127)] + [InlineData(uint.MaxValue)] + [InlineData(uint.MinValue, 0)] + [InlineData(int.MaxValue)] + [InlineData(int.MinValue)] + [InlineData(double.MaxValue)] + [InlineData(double.MinValue)] + public static void InvokeUnboxNumber(object o, object expected = null) + { + HelperMarshal._marshalledObject = o; + HelperMarshal._object1 = HelperMarshal._object2 = null; + Runtime.InvokeJS(@" + var obj = App.call_test_method (""InvokeReturnMarshalObj""); + var res = App.call_test_method (""InvokeObj1"", [ obj ]); + "); + + Assert.Equal(expected ?? o, HelperMarshal._object1); + } + + [Theory] + [InlineData(byte.MinValue, 0)] + [InlineData(byte.MaxValue, 255)] + [InlineData(SByte.MinValue, -128)] + [InlineData(SByte.MaxValue, 127)] + [InlineData(uint.MaxValue)] + [InlineData(uint.MinValue, 0)] + [InlineData(int.MaxValue)] + [InlineData(int.MinValue)] + [InlineData(double.MaxValue)] + [InlineData(double.MinValue)] + public static void InvokeUnboxStringNumber(object o, object expected = null) + { + HelperMarshal._marshalledObject = HelperMarshal._object1 = HelperMarshal._object2 = null; + Runtime.InvokeJS(String.Format (@" + var res = App.call_test_method (""InvokeObj1"", [ {0} ]); + ", o)); + + Assert.Equal (expected ?? o, HelperMarshal._object1); + } + [Fact] public static void JSInvokeInt() { @@ -200,6 +267,7 @@ public static void MarshalDelegate() Assert.Equal(30, HelperMarshal._functionResultValue); Assert.Equal(60, HelperMarshal._i32Value); } + [Fact] public static void BindStaticMethod() { @@ -322,7 +390,6 @@ public static void SetObjectPropertiesIfNotExistsTrue() } [Fact] - [ActiveIssue("https://github.com/dotnet/runtime/issues/40112")] public static void MarshalTypedArray() { Runtime.InvokeJS(@" @@ -440,29 +507,28 @@ public static void MarshalArrayBuffer2Double2() Assert.Equal(7.5f, HelperMarshal._doubleBuffer[3]); } - [Fact] - public static void MarshalTypedArraySByte() - { + private static void RunMarshalTypedArrayJS(string type) { Runtime.InvokeJS(@" var obj = { }; - App.call_test_method (""SetTypedArraySByte"", [ obj ]); - App.call_test_method (""GetTypedArraySByte"", [ obj ]); + App.call_test_method (""SetTypedArray" + type + @""", [ obj ]); + App.call_test_method (""GetTypedArray" + type + @""", [ obj ]); "); + } + + [Fact] + public static void MarshalTypedArraySByte() + { + RunMarshalTypedArrayJS("SByte"); Assert.Equal(11, HelperMarshal._taSByte.Length); Assert.Equal(32, HelperMarshal._taSByte[0]); Assert.Equal(32, HelperMarshal._taSByte[HelperMarshal._taSByte.Length - 1]); } [Fact] - [ActiveIssue("https://github.com/dotnet/runtime/issues/40112")] public static void MarshalTypedArrayByte() { - Runtime.InvokeJS(@" - var obj = { }; - App.call_test_method (""SetTypedArrayByte"", [ obj ]); - App.call_test_method (""GetTypedArrayByte"", [ obj ]); - "); - Assert.Equal(11, HelperMarshal._taSByte.Length); + RunMarshalTypedArrayJS("Byte"); + Assert.Equal(17, HelperMarshal._taByte.Length); Assert.Equal(104, HelperMarshal._taByte[0]); Assert.Equal(115, HelperMarshal._taByte[HelperMarshal._taByte.Length - 1]); Assert.Equal("hic sunt dracones", System.Text.Encoding.Default.GetString(HelperMarshal._taByte)); @@ -471,11 +537,7 @@ public static void MarshalTypedArrayByte() [Fact] public static void MarshalTypedArrayShort() { - Runtime.InvokeJS(@" - var obj = { }; - App.call_test_method (""SetTypedArrayShort"", [ obj ]); - App.call_test_method (""GetTypedArrayShort"", [ obj ]); - "); + RunMarshalTypedArrayJS("Short"); Assert.Equal(13, HelperMarshal._taShort.Length); Assert.Equal(32, HelperMarshal._taShort[0]); Assert.Equal(32, HelperMarshal._taShort[HelperMarshal._taShort.Length - 1]); @@ -484,11 +546,7 @@ public static void MarshalTypedArrayShort() [Fact] public static void MarshalTypedArrayUShort() { - Runtime.InvokeJS(@" - var obj = { }; - App.call_test_method (""SetTypedArrayUShort"", [ obj ]); - App.call_test_method (""GetTypedArrayUShort"", [ obj ]); - "); + RunMarshalTypedArrayJS("UShort"); Assert.Equal(14, HelperMarshal._taUShort.Length); Assert.Equal(32, HelperMarshal._taUShort[0]); Assert.Equal(32, HelperMarshal._taUShort[HelperMarshal._taUShort.Length - 1]); @@ -497,11 +555,7 @@ public static void MarshalTypedArrayUShort() [Fact] public static void MarshalTypedArrayInt() { - Runtime.InvokeJS(@" - var obj = { }; - App.call_test_method (""SetTypedArrayInt"", ""o"", [ obj ]); - App.call_test_method (""GetTypedArrayInt"", ""o"", [ obj ]); - "); + RunMarshalTypedArrayJS("Int"); Assert.Equal(15, HelperMarshal._taInt.Length); Assert.Equal(32, HelperMarshal._taInt[0]); Assert.Equal(32, HelperMarshal._taInt[HelperMarshal._taInt.Length - 1]); @@ -510,11 +564,7 @@ public static void MarshalTypedArrayInt() [Fact] public static void MarshalTypedArrayUInt() { - Runtime.InvokeJS(@" - var obj = { }; - App.call_test_method (""SetTypedArrayUInt"", [ obj ]); - App.call_test_method (""GetTypedArrayUInt"", [ obj ]); - "); + RunMarshalTypedArrayJS("UInt"); Assert.Equal(16, HelperMarshal._taUInt.Length); Assert.Equal(32, (int)HelperMarshal._taUInt[0]); Assert.Equal(32, (int)HelperMarshal._taUInt[HelperMarshal._taUInt.Length - 1]); @@ -523,11 +573,7 @@ public static void MarshalTypedArrayUInt() [Fact] public static void MarshalTypedArrayFloat() { - Runtime.InvokeJS(@" - var obj = { }; - App.call_test_method (""SetTypedArrayFloat"", [ obj ]); - App.call_test_method (""GetTypedArrayFloat"", [ obj ]); - "); + RunMarshalTypedArrayJS("Float"); Assert.Equal(17, HelperMarshal._taFloat.Length); Assert.Equal(3.14f, HelperMarshal._taFloat[0]); Assert.Equal(3.14f, HelperMarshal._taFloat[HelperMarshal._taFloat.Length - 1]); @@ -536,11 +582,7 @@ public static void MarshalTypedArrayFloat() [Fact] public static void MarshalTypedArrayDouble() { - Runtime.InvokeJS(@" - var obj = { }; - App.call_test_method (""SetTypedArrayDouble"", ""o"", [ obj ]); - App.call_test_method (""GetTypedArrayDouble"", ""o"", [ obj ]); - "); + RunMarshalTypedArrayJS("Double"); Assert.Equal(18, HelperMarshal._taDouble.Length); Assert.Equal(3.14d, HelperMarshal._taDouble[0]); Assert.Equal(3.14d, HelperMarshal._taDouble[HelperMarshal._taDouble.Length - 1]); @@ -551,22 +593,160 @@ public static void TestFunctionSum() { HelperMarshal._sumValue = 0; Runtime.InvokeJS(@" - App.call_test_method (""CreateFunctionSum"", null, [ ]); - App.call_test_method (""CallFunctionSum"", null, [ ]); + App.call_test_method (""CreateFunctionSum"", []); + App.call_test_method (""CallFunctionSum"", []); "); Assert.Equal(8, HelperMarshal._sumValue); } [Fact] - [ActiveIssue("https://github.com/dotnet/runtime/issues/40112")] public static void TestFunctionApply() { HelperMarshal._minValue = 0; Runtime.InvokeJS(@" - App.call_test_method (""CreateFunctionApply"", null, [ ]); - App.call_test_method (""CallFunctionApply"", null, [ ]); + App.call_test_method (""CreateFunctionApply"", []); + App.call_test_method (""CallFunctionApply"", []); "); Assert.Equal(2, HelperMarshal._minValue); } + + [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 invoke_int = Module.mono_bind_static_method (""{HelperMarshal.INTEROP_CLASS}InvokeInt""); + invoke_int (); + "); + Assert.Equal(0, HelperMarshal._intValue); + } + + [Fact] + public static void BoundStaticMethodExtraArgs() + { + HelperMarshal._intValue = 0; + Runtime.InvokeJS(@$" + var invoke_int = Module.mono_bind_static_method (""{HelperMarshal.INTEROP_CLASS}InvokeInt""); + invoke_int (200, 400); + "); + Assert.Equal(200, HelperMarshal._intValue); + } + + [Fact] + public static void BoundStaticMethodArgumentTypeCoercion() + { + // 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(@$" + var invoke_int = Module.mono_bind_static_method (""{HelperMarshal.INTEROP_CLASS}InvokeInt""); + invoke_int (""200""); + "); + Assert.Equal(200, HelperMarshal._intValue); + + Runtime.InvokeJS(@$" + var invoke_int = Module.mono_bind_static_method (""{HelperMarshal.INTEROP_CLASS}InvokeInt""); + invoke_int (400.5); + "); + Assert.Equal(400, HelperMarshal._intValue); + } + + [Fact] + public static void BoundStaticMethodUnpleasantArgumentTypeCoercion() + { + HelperMarshal._intValue = 100; + Runtime.InvokeJS(@$" + var invoke_int = Module.mono_bind_static_method (""{HelperMarshal.INTEROP_CLASS}InvokeInt""); + invoke_int (""hello""); + "); + Assert.Equal(0, HelperMarshal._intValue); + + // In this case at the very least, the leading "7" is not turned into the number 7 + Runtime.InvokeJS(@$" + var invoke_int = Module.mono_bind_static_method (""{HelperMarshal.INTEROP_CLASS}InvokeInt""); + invoke_int (""7apples""); + "); + Assert.Equal(0, HelperMarshal._intValue); + } + + [Fact] + public static void PassUintArgument() + { + HelperMarshal._uintValue = 0; + Runtime.InvokeJS(@$" + var invoke_uint = Module.mono_bind_static_method (""{HelperMarshal.INTEROP_CLASS}InvokeUInt""); + invoke_uint (0xFFFFFFFE); + "); + + Assert.Equal(0xFFFFFFFEu, HelperMarshal._uintValue); + } + + [Fact] + public static void ReturnUintEnum () + { + HelperMarshal._uintValue = 0; + HelperMarshal._enumValue = TestEnum.BigValue; + Runtime.InvokeJS(@$" + var get_value = Module.mono_bind_static_method (""{HelperMarshal.INTEROP_CLASS}GetEnumValue""); + var e = get_value (); + var invoke_uint = Module.mono_bind_static_method (""{HelperMarshal.INTEROP_CLASS}InvokeUInt""); + invoke_uint (e); + "); + Assert.Equal((uint)TestEnum.BigValue, HelperMarshal._uintValue); + } + + [Fact] + public static void PassUintEnumByValue () + { + HelperMarshal._enumValue = TestEnum.Zero; + Runtime.InvokeJS(@$" + var set_enum = Module.mono_bind_static_method (""{HelperMarshal.INTEROP_CLASS}SetEnumValue"", ""j""); + set_enum (0xFFFFFFFE); + "); + 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 = Module.mono_bind_static_method (""{HelperMarshal.INTEROP_CLASS}SetEnumValue"", ""i""); + set_enum (0xFFFFFFFE); + "); + Assert.Equal(TestEnum.BigValue, HelperMarshal._enumValue); + } + + [Fact] + public static void PassUintEnumByNameIsNotImplemented () + { + HelperMarshal._enumValue = TestEnum.Zero; + var exc = Assert.Throws( () => + Runtime.InvokeJS(@$" + var set_enum = Module.mono_bind_static_method (""{HelperMarshal.INTEROP_CLASS}SetEnumValue"", ""j""); + set_enum (""BigValue""); + ") + ); + Assert.StartsWith("Error: Expected numeric value for enum argument, got 'BigValue'", exc.Message); + } + + [Fact] + public static void CannotUnboxUint64 () + { + var exc = Assert.Throws( () => + Runtime.InvokeJS(@$" + var get_u64 = Module.mono_bind_static_method (""{HelperMarshal.INTEROP_CLASS}GetUInt64"", """"); + var u64 = get_u64(); + ") + ); + Assert.StartsWith("Error: int64 not available", exc.Message); + } } } diff --git a/src/libraries/System.Private.Runtime.InteropServices.JavaScript/tests/System/Runtime/InteropServices/JavaScript/TypedArrayTests.cs b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/tests/System/Runtime/InteropServices/JavaScript/TypedArrayTests.cs index 14b31faea0d290..896835795f1986 100644 --- a/src/libraries/System.Private.Runtime.InteropServices.JavaScript/tests/System/Runtime/InteropServices/JavaScript/TypedArrayTests.cs +++ b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/tests/System/Runtime/InteropServices/JavaScript/TypedArrayTests.cs @@ -23,7 +23,7 @@ public static void Uint8ClampedArrayFrom(Function objectPrototype) { var clamped = new byte[50]; Uint8ClampedArray from = Uint8ClampedArray.From(clamped); - Assert.True(from.Length == 50); + Assert.Equal(50, from.Length); Assert.Equal("[object Uint8ClampedArray]", objectPrototype.Call(from)); } @@ -33,7 +33,7 @@ public static void Uint8ArrayFrom(Function objectPrototype) { var array = new byte[50]; Uint8Array from = Uint8Array.From(array); - Assert.True(from.Length == 50); + Assert.Equal(50, from.Length); Assert.Equal("[object Uint8Array]", objectPrototype.Call(from)); } @@ -43,7 +43,7 @@ public static void Uint16ArrayFrom(Function objectPrototype) { var array = new ushort[50]; Uint16Array from = Uint16Array.From(array); - Assert.True(from.Length == 50); + Assert.Equal(50, from.Length); Assert.Equal("[object Uint16Array]", objectPrototype.Call(from)); } @@ -53,7 +53,7 @@ public static void Uint32ArrayFrom(Function objectPrototype) { var array = new uint[50]; Uint32Array from = Uint32Array.From(array); - Assert.True(from.Length == 50); + Assert.Equal(50, from.Length); Assert.Equal("[object Uint32Array]", objectPrototype.Call(from)); } @@ -63,7 +63,7 @@ public static void Int8ArrayFrom(Function objectPrototype) { var array = new sbyte[50]; Int8Array from = Int8Array.From(array); - Assert.True(from.Length == 50); + Assert.Equal(50, from.Length); Assert.Equal("[object Int8Array]", objectPrototype.Call(from)); } @@ -73,7 +73,7 @@ public static void Int16ArrayFrom(Function objectPrototype) { var array = new short[50]; Int16Array from = Int16Array.From(array); - Assert.True(from.Length == 50); + Assert.Equal(50, from.Length); Assert.Equal("[object Int16Array]", objectPrototype.Call(from)); } @@ -83,7 +83,7 @@ public static void Int32ArrayFrom(Function objectPrototype) { var array = new int[50]; Int32Array from = Int32Array.From(array); - Assert.True(from.Length == 50); + Assert.Equal(50, from.Length); Assert.Equal("[object Int32Array]", objectPrototype.Call(from)); } @@ -93,7 +93,7 @@ public static void Float32ArrayFrom(Function objectPrototype) { var array = new float[50]; Float32Array from = Float32Array.From(array); - Assert.True(from.Length == 50); + Assert.Equal(50, from.Length); Assert.Equal("[object Float32Array]", objectPrototype.Call(from)); } @@ -103,7 +103,7 @@ public static void Float64ArrayFrom(Function objectPrototype) { var array = new double[50]; Float64Array from = Float64Array.From(array); - Assert.True(from.Length == 50); + Assert.Equal(50, from.Length); Assert.Equal("[object Float64Array]", objectPrototype.Call(from)); } @@ -112,7 +112,7 @@ public static void Float64ArrayFrom(Function objectPrototype) public static void Uint8ClampedArrayFromSharedArrayBuffer(Function objectPrototype) { Uint8ClampedArray from = new Uint8ClampedArray(new SharedArrayBuffer(50)); - Assert.True(from.Length == 50); + Assert.Equal(50, from.Length); Assert.Equal("[object Uint8ClampedArray]", objectPrototype.Call(from)); } @@ -121,7 +121,7 @@ public static void Uint8ClampedArrayFromSharedArrayBuffer(Function objectPrototy public static void Uint8ArrayFromSharedArrayBuffer(Function objectPrototype) { Uint8Array from = new Uint8Array(new SharedArrayBuffer(50)); - Assert.True(from.Length == 50); + Assert.Equal(50, from.Length); Assert.Equal("[object Uint8Array]", objectPrototype.Call(from)); } @@ -130,7 +130,7 @@ public static void Uint8ArrayFromSharedArrayBuffer(Function objectPrototype) public static void Uint16ArrayFromSharedArrayBuffer(Function objectPrototype) { Uint16Array from = new Uint16Array(new SharedArrayBuffer(50)); - Assert.True(from.Length == 25); + Assert.Equal(25, from.Length); Assert.Equal("[object Uint16Array]", objectPrototype.Call(from)); } @@ -139,7 +139,7 @@ public static void Uint16ArrayFromSharedArrayBuffer(Function objectPrototype) public static void Uint32ArrayFromSharedArrayBuffer(Function objectPrototype) { Uint32Array from = new Uint32Array(new SharedArrayBuffer(40)); - Assert.True(from.Length == 10); + Assert.Equal(10, from.Length); Assert.Equal("[object Uint32Array]", objectPrototype.Call(from)); } diff --git a/src/mono/wasm/runtime-test.js b/src/mono/wasm/runtime-test.js index 51d494eb2c2fa2..14a59c43c6da87 100644 --- a/src/mono/wasm/runtime-test.js +++ b/src/mono/wasm/runtime-test.js @@ -392,6 +392,15 @@ var App = { } }, call_test_method: function (method_name, args) { - return BINDING.call_static_method("[System.Private.Runtime.InteropServices.JavaScript.Tests]System.Runtime.InteropServices.JavaScript.Tests.HelperMarshal:" + method_name, args); + if (arguments.length > 2) + throw new Error("Invalid number of arguments for call_test_method"); + + var fqn = "[System.Private.Runtime.InteropServices.JavaScript.Tests]System.Runtime.InteropServices.JavaScript.Tests.HelperMarshal:" + method_name; + try { + return BINDING.call_static_method(fqn, args || []); + } catch (exc) { + console.error("exception thrown in", fqn); + throw exc; + } } }; diff --git a/src/mono/wasm/runtime/binding_support.js b/src/mono/wasm/runtime/binding_support.js index af6202f5adb439..ac215b10502848 100644 --- a/src/mono/wasm/runtime/binding_support.js +++ b/src/mono/wasm/runtime/binding_support.js @@ -10,13 +10,14 @@ var BindingSupportLib = { mono_wasm_free_list: [], mono_wasm_owned_objects_frames: [], mono_wasm_owned_objects_LMF: [], - mono_wasm_marshal_enum_as_int: false, + mono_wasm_marshal_enum_as_int: true, mono_bindings_init: function (binding_asm) { this.BINDING_ASM = binding_asm; }, export_functions: function (module) { module ["mono_bindings_init"] = BINDING.mono_bindings_init.bind(BINDING); + module ["mono_bind_method"] = BINDING.bind_method.bind(BINDING); module ["mono_method_invoke"] = BINDING.call_method.bind(BINDING); module ["mono_method_get_call_signature"] = BINDING.mono_method_get_call_signature.bind(BINDING); module ["mono_method_resolve"] = BINDING.resolve_method_fqn.bind(BINDING); @@ -29,6 +30,9 @@ var BindingSupportLib = { bindings_lazy_init: function () { if (this.init) return; + + // avoid infinite recursion + this.init = true; Array.prototype[Symbol.for("wasm type")] = 1; ArrayBuffer.prototype[Symbol.for("wasm type")] = 2; @@ -48,21 +52,28 @@ var BindingSupportLib = { Float64Array.prototype[Symbol.for("wasm type")] = 18; this.assembly_load = Module.cwrap ('mono_wasm_assembly_load', 'number', ['string']); + this.find_corlib_class = Module.cwrap ('mono_wasm_find_corlib_class', 'number', ['string', 'string']); this.find_class = Module.cwrap ('mono_wasm_assembly_find_class', 'number', ['number', 'string', 'string']); - this.find_method = Module.cwrap ('mono_wasm_assembly_find_method', 'number', ['number', 'string', 'number']); + this._find_method = Module.cwrap ('mono_wasm_assembly_find_method', 'number', ['number', 'string', 'number']); this.invoke_method = Module.cwrap ('mono_wasm_invoke_method', 'number', ['number', 'number', 'number', 'number']); this.mono_string_get_utf8 = Module.cwrap ('mono_wasm_string_get_utf8', 'number', ['number']); this.mono_wasm_string_from_utf16 = Module.cwrap ('mono_wasm_string_from_utf16', 'number', ['number', 'number']); this.mono_get_obj_type = Module.cwrap ('mono_wasm_get_obj_type', 'number', ['number']); - this.mono_unbox_int = Module.cwrap ('mono_unbox_int', 'number', ['number']); - this.mono_unbox_float = Module.cwrap ('mono_wasm_unbox_float', 'number', ['number']); this.mono_array_length = Module.cwrap ('mono_wasm_array_length', 'number', ['number']); this.mono_array_get = Module.cwrap ('mono_wasm_array_get', 'number', ['number', 'number']); this.mono_obj_array_new = Module.cwrap ('mono_wasm_obj_array_new', 'number', ['number']); this.mono_obj_array_set = Module.cwrap ('mono_wasm_obj_array_set', 'void', ['number', 'number', 'number']); this.mono_wasm_register_bundled_satellite_assemblies = Module.cwrap ('mono_wasm_register_bundled_satellite_assemblies', 'void', [ ]); - this.mono_unbox_enum = Module.cwrap ('mono_wasm_unbox_enum', 'number', ['number']); + this.mono_wasm_try_unbox_primitive_and_get_type = Module.cwrap ('mono_wasm_try_unbox_primitive_and_get_type', 'number', ['number', 'number']); + this.mono_wasm_box_primitive = Module.cwrap ('mono_wasm_box_primitive', 'number', ['number', 'number', 'number']); this.assembly_get_entry_point = Module.cwrap ('mono_wasm_assembly_get_entry_point', 'number', ['number']); + + this._box_buffer = Module._malloc(16); + this._unbox_buffer = Module._malloc(16); + this._class_int32 = this.find_corlib_class ("System", "Int32"); + this._class_uint32 = this.find_corlib_class ("System", "UInt32"); + this._class_double = this.find_corlib_class ("System", "Double"); + this._class_boolean = this.find_corlib_class ("System", "Boolean"); // receives a byteoffset into allocated Heap with a size. this.mono_typed_array_new = Module.cwrap ('mono_wasm_typed_array_new', 'number', ['number','number','number','number']); @@ -74,10 +85,11 @@ var BindingSupportLib = { if (!this.binding_module) throw "Can't find bindings module assembly: " + binding_fqn_asm; + var namespace = null, classname = null; if (binding_fqn_class !== null && typeof binding_fqn_class !== "undefined") { - var namespace = "System.Runtime.InteropServices.JavaScript"; - var classname = binding_fqn_class.length > 0 ? binding_fqn_class : "Runtime"; + namespace = "System.Runtime.InteropServices.JavaScript"; + classname = binding_fqn_class.length > 0 ? binding_fqn_class : "Runtime"; if (binding_fqn_class.indexOf(".") != -1) { var idx = binding_fqn_class.lastIndexOf("."); namespace = binding_fqn_class.substring (0, idx); @@ -85,27 +97,34 @@ var BindingSupportLib = { } } - var wasm_runtime_class = this.find_class (this.binding_module, namespace, classname) + var wasm_runtime_class = this.find_class (this.binding_module, namespace, classname); if (!wasm_runtime_class) throw "Can't find " + binding_fqn_class + " class"; var get_method = function(method_name) { - var res = BINDING.find_method (wasm_runtime_class, method_name, -1) + var res = BINDING.find_method (wasm_runtime_class, method_name, -1); if (!res) throw "Can't find method " + namespace + "." + classname + ":" + method_name; return res; - } - this.bind_js_obj = get_method ("BindJSObject"); - this.bind_core_clr_obj = get_method ("BindCoreCLRObject"); - this.bind_existing_obj = get_method ("BindExistingObject"); - this.unbind_raw_obj_and_free = get_method ("UnBindRawJSObjectAndFree"); - this.get_js_id = get_method ("GetJSObjectId"); - this.get_raw_mono_obj = get_method ("GetDotNetObject"); - - this.box_js_int = get_method ("BoxInt"); - this.box_js_double = get_method ("BoxDouble"); - this.box_js_bool = get_method ("BoxBool"); - this.is_simple_array = get_method ("IsSimpleArray"); + }; + + var bind_runtime_method = function (method_name, signature) { + var method = get_method (method_name); + return BINDING.bind_method (method, 0, signature, "BINDINGS_" + method_name); + }; + + // NOTE: The bound methods have a _ prefix on their names to ensure + // that any code relying on the old get_method/call_method pattern will + // break in a more understandable way. + + this._bind_js_obj = bind_runtime_method ("BindJSObject", "iii"); + this._bind_core_clr_obj = bind_runtime_method ("BindCoreCLRObject", "ii"); + this._bind_existing_obj = bind_runtime_method ("BindExistingObject", "mi"); + this._unbind_raw_obj_and_free = bind_runtime_method ("UnBindRawJSObjectAndFree", "ii"); + this._get_js_id = bind_runtime_method ("GetJSObjectId", "m"); + this._get_raw_mono_obj = bind_runtime_method ("GetDotNetObject", "i!"); + + this._is_simple_array = bind_runtime_method ("IsSimpleArray", "m"); this.setup_js_cont = get_method ("SetupJSContinuation"); this.create_tcs = get_method ("CreateTaskSource"); @@ -114,7 +133,7 @@ var BindingSupportLib = { this.tcs_get_task_and_bind = get_method ("GetTaskAndBind"); this.get_call_sig = get_method ("GetCallSignature"); - this.object_to_string = get_method ("ObjectToString"); + this._object_to_string = bind_runtime_method ("ObjectToString", "m"); this.get_date_value = get_method ("GetDateValue"); this.create_date_time = get_method ("CreateDateTime"); this.create_uri = get_method ("CreateUri"); @@ -124,7 +143,28 @@ var BindingSupportLib = { this.safehandle_get_handle = get_method ("SafeHandleGetHandle"); this.safehandle_release_by_handle = get_method ("SafeHandleReleaseByHandle"); - this.init = true; + this._are_promises_supported = ((typeof Promise === "object") || (typeof Promise === "function")) && (typeof Promise.resolve === "function"); + }, + + js_string_to_mono_string: function (string) { + var buffer = Module._malloc ((string.length + 1) * 2); + var buffer16 = (buffer / 2) | 0; + for (var i = 0; i < string.length; i++) + Module.HEAP16[buffer16 + i] = string.charCodeAt (i); + Module.HEAP16[buffer16 + string.length] = 0; + var result = this.mono_wasm_string_from_utf16 (buffer, string.length); + Module._free (buffer); + return result; + }, + + find_method: function (klass, name, n) { + var result = this._find_method(klass, name, n); + if (result) { + if (!this._method_descriptions) + this._method_descriptions = new Map(); + this._method_descriptions.set(result, name); + } + return result; }, get_js_obj: function (js_handle) { @@ -138,7 +178,7 @@ var BindingSupportLib = { }, is_nested_array: function (ele) { - return this.call_method (this.is_simple_array, null, "mi", [ ele ]); + return this._is_simple_array(ele); }, js_string_to_mono_string: function (string) { @@ -174,19 +214,19 @@ var BindingSupportLib = { let elemRoot = MONO.mono_wasm_new_root (); try { - var res = []; var len = this.mono_array_length (arrayRoot.value); + var res = new Array (len); for (var i = 0; i < len; ++i) { elemRoot.value = this.mono_array_get (arrayRoot.value, i); if (this.is_nested_array (elemRoot.value)) - res.push (this._mono_array_to_js_array_rooted (elemRoot)); + res[i] = this._mono_array_to_js_array_rooted (elemRoot); else - res.push (this._unbox_mono_obj_rooted (elemRoot)); + res[i] = this._unbox_mono_obj_rooted (elemRoot); } } finally { - elemRoot.release(); + elemRoot.release (); } return res; @@ -220,105 +260,113 @@ var BindingSupportLib = { } }, - _unbox_mono_obj_rooted: function (root) { - var mono_obj = root.value; - if (mono_obj === 0) - return undefined; - - var type = this.mono_get_obj_type (mono_obj); - //See MARSHAL_TYPE_ defines in driver.c - switch (type) { - case 1: // int - return this.mono_unbox_int (mono_obj); - case 2: // float - return this.mono_unbox_float (mono_obj); - case 3: //string - return this.conv_string (mono_obj); - case 4: //vts - throw new Error ("no idea on how to unbox value types"); - case 5: { // delegate - var obj = this.extract_js_obj (mono_obj); - obj.__mono_delegate_alive__ = true; - // FIXME: Should we root the object as long as this function has not been GCd? - return function () { - return BINDING.invoke_delegate (obj, arguments); - }; - } - case 6: {// Task - - if (typeof Promise === "undefined" || typeof Promise.resolve === "undefined") - throw new Error ("Promises are not supported thus C# Tasks can not work in this context."); - - var obj = this.extract_js_obj (mono_obj); - var cont_obj = null; - var promise = new Promise (function (resolve, reject) { - cont_obj = { - resolve: resolve, - reject: reject - }; - }); - - this.call_method (this.setup_js_cont, null, "mo", [ mono_obj, cont_obj ]); - obj.__mono_js_cont__ = cont_obj.__mono_gchandle__; - cont_obj.__mono_js_task__ = obj.__mono_gchandle__; - return promise; - } - - case 7: // ref type - return this.extract_js_obj (mono_obj); + _unbox_delegate_rooted: function (mono_obj) { + var obj = this.extract_js_obj (mono_obj); + obj.__mono_delegate_alive__ = true; + // FIXME: Should we root the object as long as this function has not been GCd? + return function () { + // TODO: Just use Function.bind + return BINDING.invoke_delegate (obj, arguments); + }; + }, - case 8: // bool - return this.mono_unbox_int (mono_obj) != 0; + _unbox_task_rooted: function (mono_obj) { + if (!this._are_promises_supported) + throw new Error ("Promises are not supported thus 'System.Threading.Tasks.Task' can not work in this context."); - case 9: // enum + var obj = this.extract_js_obj (mono_obj); + var cont_obj = null; + var promise = new Promise (function (resolve, reject) { + cont_obj = { + resolve: resolve, + reject: reject + }; + }); - if(this.mono_wasm_marshal_enum_as_int) - { - return this.mono_unbox_enum (mono_obj); - } - else - { - enumValue = this.call_method(this.object_to_string, null, "m", [ mono_obj ]); - } + this.call_method (this.setup_js_cont, null, "mo", [ mono_obj, cont_obj ]); + obj.__mono_js_cont__ = cont_obj.__mono_gchandle__; + cont_obj.__mono_js_task__ = obj.__mono_gchandle__; + return promise; + }, - return enumValue; - - case 10: // arrays - case 11: - case 12: - case 13: - case 14: - case 15: - case 16: - case 17: - case 18: + _unbox_safehandle_rooted: function (mono_obj) { + var addRef = true; + var js_handle = this.call_method(this.safehandle_get_handle, null, "mi", [ mono_obj, addRef ]); + var requiredObject = BINDING.mono_wasm_require_handle (js_handle); + if (addRef) { - throw new Error ("Marshalling of primitive arrays are not supported. Use the corresponding TypedArray instead."); + if (typeof this.mono_wasm_owned_objects_LMF === "undefined") + this.mono_wasm_owned_objects_LMF = []; + + this.mono_wasm_owned_objects_LMF.push(js_handle); } - case 20: // clr .NET DateTime - var dateValue = this.call_method(this.get_date_value, null, "md", [ mono_obj ]); - return new Date(dateValue); - case 21: // clr .NET DateTimeOffset - var dateoffsetValue = this.call_method(this.object_to_string, null, "m", [ mono_obj ]); - return dateoffsetValue; - case 22: // clr .NET Uri - var uriValue = this.call_method(this.object_to_string, null, "m", [ mono_obj ]); - return uriValue; - case 23: // clr .NET SafeHandle - var addRef = true; - var js_handle = this.call_method(this.safehandle_get_handle, null, "mii", [ mono_obj, addRef ]); - // FIXME: Is this a GC object that needs to be rooted? - var requiredObject = BINDING.mono_wasm_require_handle (js_handle); - if (addRef) - { - if (typeof this.mono_wasm_owned_objects_LMF === "undefined") - this.mono_wasm_owned_objects_LMF = []; + return requiredObject; + }, - this.mono_wasm_owned_objects_LMF.push(js_handle); - } - return requiredObject; - default: - throw new Error ("no idea on how to unbox object kind " + type + " at offset " + mono_obj); + _unbox_mono_obj_rooted_with_known_nonprimitive_type: function (mono_obj, type) { + //See MARSHAL_TYPE_ defines in driver.c + switch (type) { + case 26: // int64 + case 27: // uint64 + // TODO: Fix this once emscripten offers HEAPI64/HEAPU64 or can return them + throw new Error ("int64 not available"); + case 3: //string + return this.conv_string (mono_obj); + case 4: //vts + throw new Error ("no idea on how to unbox value types"); + case 5: // delegate + return this._unbox_delegate_rooted (mono_obj); + case 6: // Task + return this._unbox_task_rooted (mono_obj); + case 7: // ref type + return this.extract_js_obj (mono_obj); + case 10: // arrays + case 11: + case 12: + case 13: + case 14: + case 15: + case 16: + case 17: + case 18: + throw new Error ("Marshalling of primitive arrays are not supported. Use the corresponding TypedArray instead."); + case 20: // clr .NET DateTime + var dateValue = this.call_method(this.get_date_value, null, "md", [ mono_obj ]); + return new Date(dateValue); + case 21: // clr .NET DateTimeOffset + var dateoffsetValue = this._object_to_string (mono_obj); + return dateoffsetValue; + case 22: // clr .NET Uri + var uriValue = this._object_to_string (mono_obj); + return uriValue; + case 23: // clr .NET SafeHandle + return this._unbox_safehandle_rooted (mono_obj); + default: + throw new Error ("no idea on how to unbox object kind " + type + " at offset " + mono_obj); + } + }, + + _unbox_mono_obj_rooted: function (root) { + var mono_obj = root.value; + if (mono_obj === 0) + return undefined; + + var type = this.mono_wasm_try_unbox_primitive_and_get_type (mono_obj, this._unbox_buffer); + switch (type) { + case 1: // int + return Module.HEAP32[this._unbox_buffer / 4]; + case 25: // uint32 + return Module.HEAPU32[this._unbox_buffer / 4]; + case 24: // float32 + return Module.HEAPF32[this._unbox_buffer / 4]; + case 2: // float64 + return Module.HEAPF64[this._unbox_buffer / 8]; + case 8: // boolean + return (Module.HEAP32[this._unbox_buffer / 4]) !== 0; + case 28: // char + return String.fromCharCode(Module.HEAP32[this._unbox_buffer / 4]); + default: + return this._unbox_mono_obj_rooted_with_known_nonprimitive_type (mono_obj, type); } }, @@ -348,6 +396,27 @@ var BindingSupportLib = { heapBytes.set(new Uint8Array(typedArray.buffer, typedArray.byteOffset, numBytes)); return heapBytes; }, + + _box_js_int: function (js_obj) { + Module.HEAP32[this._box_buffer / 4] = js_obj; + return this.mono_wasm_box_primitive (this._class_int32, this._box_buffer, 4); + }, + + _box_js_uint: function (js_obj) { + Module.HEAPU32[this._box_buffer / 4] = js_obj; + return this.mono_wasm_box_primitive (this._class_uint32, this._box_buffer, 4); + }, + + _box_js_double: function (js_obj) { + Module.HEAPF64[this._box_buffer / 8] = js_obj; + return this.mono_wasm_box_primitive (this._class_double, this._box_buffer, 8); + }, + + _box_js_bool: function (js_obj) { + Module.HEAP32[this._box_buffer / 4] = js_obj ? 1 : 0; + return this.mono_wasm_box_primitive (this._class_boolean, this._box_buffer, 4); + }, + js_to_mono_obj: function (js_obj) { this.bindings_lazy_init (); @@ -368,19 +437,28 @@ var BindingSupportLib = { case js_obj === null: case typeof js_obj === "undefined": return 0; - case typeof js_obj === "number": - if (parseInt(js_obj) == js_obj) - return this.call_method (this.box_js_int, null, "im", [ js_obj ]); - return this.call_method (this.box_js_double, null, "dm", [ js_obj ]); - case typeof js_obj === "string": + case typeof js_obj === "number": { + if ((js_obj | 0) === js_obj) + result = this._box_js_int (js_obj); + else if ((js_obj >>> 0) === js_obj) + result = this._box_js_uint (js_obj); + else + result = this._box_js_double (js_obj); + + if (!result) + throw new Error (`Boxing failed for ${js_obj}`); + + return result; + } case typeof js_obj === "string": return this.js_string_to_mono_string (js_obj); case typeof js_obj === "boolean": - return this.call_method (this.box_js_bool, null, "im", [ js_obj ]); + return this._box_js_bool (js_obj); case isThenable() === true: var the_task = this.try_extract_mono_obj (js_obj); if (the_task) return the_task; - // FIXME: We need to root tcs for an appropriate timespan + // FIXME: We need to root tcs for an appropriate timespan, at least until the Task + // is resolved var tcs = this.create_task_completion_source (); js_obj.then (function (result) { BINDING.set_task_result (tcs, result); @@ -390,7 +468,7 @@ var BindingSupportLib = { return this.get_task_and_bind (tcs, js_obj); case js_obj.constructor.name === "Date": // We may need to take into account the TimeZone Offset - return this.call_method(this.create_date_time, null, "dm", [ js_obj.getTime() ]); + return this.call_method(this.create_date_time, null, "d!", [ js_obj.getTime() ]); default: return this.extract_mono_obj (js_obj); } @@ -403,7 +481,7 @@ var BindingSupportLib = { case typeof js_obj === "undefined": return 0; case typeof js_obj === "string": - return this.call_method(this.create_uri, null, "sm", [ js_obj ]) + return this.call_method(this.create_uri, null, "s!", [ js_obj ]) default: return this.extract_mono_obj (js_obj); } @@ -558,45 +636,36 @@ var BindingSupportLib = { this.typedarray_copy_from(newTypedArray, pinned_array, begin, end, bytes_per_element); return newTypedArray; }, - js_to_mono_enum: function (method, parmIdx, js_obj) { + js_to_mono_enum: function (js_obj, method, parmIdx) { this.bindings_lazy_init (); - if (js_obj === null || typeof js_obj === "undefined") - return 0; + if (typeof (js_obj) !== "number") + throw new Error (`Expected numeric value for enum argument, got '${js_obj}'`); - var monoObj, monoEnum; - try { - monoObj = MONO.mono_wasm_new_root (this.js_to_mono_obj (js_obj)); - // Check enum contract - monoEnum = MONO.mono_wasm_new_root (this.call_method (this.object_to_enum, null, "iimm", [ method, parmIdx, monoObj.value ])) - // return the unboxed enum value. - return this.mono_unbox_enum (monoEnum.value); - } finally { - MONO.mono_wasm_release_roots (monoObj, monoEnum); - } + return js_obj | 0; }, wasm_binding_obj_new: function (js_obj_id, ownsHandle, type) { - return this.call_method (this.bind_js_obj, null, "iii", [js_obj_id, ownsHandle, type]); + return this._bind_js_obj (js_obj_id, ownsHandle, type); }, wasm_bind_existing: function (mono_obj, js_id) { - return this.call_method (this.bind_existing_obj, null, "mi", [mono_obj, js_id]); + return this._bind_existing_obj (mono_obj, js_id); }, wasm_bind_core_clr_obj: function (js_id, gc_handle) { - return this.call_method (this.bind_core_clr_obj, null, "ii", [js_id, gc_handle]); + return this._bind_core_clr_obj (js_id, gc_handle); }, wasm_get_js_id: function (mono_obj) { - return this.call_method (this.get_js_id, null, "m", [mono_obj]); + return this._get_js_id (mono_obj); }, wasm_get_raw_obj: function (gchandle) { - return this.call_method (this.get_raw_mono_obj, null, "im", [gchandle]); + return this._get_raw_mono_obj (gchandle); }, try_extract_mono_obj:function (js_obj) { @@ -626,26 +695,37 @@ var BindingSupportLib = { free_task_completion_source: function (tcs) { if (tcs.is_mono_tcs_result_set) { - this.call_method (this.unbind_raw_obj_and_free, null, "ii", [ tcs.__mono_gchandle__ ]); + this._unbind_raw_obj_and_free (tcs.__mono_gchandle__); } if (tcs.__mono_bound_task__) { - this.call_method (this.unbind_raw_obj_and_free, null, "ii", [ tcs.__mono_bound_task__ ]); + this._unbind_raw_obj_and_free (tcs.__mono_bound_task__); } }, extract_mono_obj: function (js_obj) { - if (js_obj === null || typeof js_obj === "undefined") return 0; - if (!js_obj.is_mono_bridged_obj) { - var gc_handle = this.mono_wasm_register_obj(js_obj); - return this.wasm_get_raw_obj (gc_handle); + var result = null; + var gc_handle = js_obj.__mono_gchandle__; + if (gc_handle) { + result = this.wasm_get_raw_obj (gc_handle); + + // It's possible the managed object corresponding to this JS object was collected, + // in which case we need to make a new one. + if (!result) { + delete js_obj.__mono_gchandle__; + delete js_obj.is_mono_bridged_obj; + } } + if (!result) { + gc_handle = this.mono_wasm_register_obj(js_obj); + result = this.wasm_get_raw_obj (gc_handle); + } - return this.wasm_get_raw_obj (js_obj.__mono_gchandle__); + return result; }, extract_js_obj: function (mono_obj) { @@ -666,6 +746,379 @@ var BindingSupportLib = { return js_obj; }, + _create_named_function: function (name, argumentNames, body, closure) { + var result = null, keys = null, closureArgumentList = null, closureArgumentNames = null; + + if (closure) { + closureArgumentNames = Object.keys (closure); + closureArgumentList = new Array (closureArgumentNames.length); + for (var i = 0, l = closureArgumentNames.length; i < l; i++) + closureArgumentList[i] = closure[closureArgumentNames[i]]; + } + + var constructor = this._create_rebindable_named_function (name, argumentNames, body, closureArgumentNames); + result = constructor.apply (null, closureArgumentList); + + return result; + }, + + _create_rebindable_named_function: function (name, argumentNames, body, closureArgNames) { + var strictPrefix = "\"use strict\";\r\n"; + var uriPrefix = "", escapedFunctionIdentifier = ""; + + if (name) { + uriPrefix = "//# sourceURL=https://mono-wasm.invalid/" + name + "\r\n"; + escapedFunctionIdentifier = name; + } else { + escapedFunctionIdentifier = "unnamed"; + } + + var rawFunctionText = "function " + escapedFunctionIdentifier + "(" + + argumentNames.join(", ") + + ") {\r\n" + + body + + "\r\n};\r\n"; + + var lineBreakRE = /\r(\n?)/g; + + rawFunctionText = + uriPrefix + strictPrefix + + rawFunctionText.replace(lineBreakRE, "\r\n ") + + ` return ${escapedFunctionIdentifier};\r\n`; + + var result = null, keys = null; + + if (closureArgNames) { + keys = closureArgNames.concat ([rawFunctionText]); + } else { + keys = [rawFunctionText]; + } + + result = Function.apply (Function, keys); + return result; + }, + + _create_primitive_converters: function () { + var result = new Map (); + result.set ('m', { steps: [{ }], size: 0}); + result.set ('s', { steps: [{ convert: this.js_string_to_mono_string.bind (this) }], size: 0, needs_root: true }); + result.set ('o', { steps: [{ convert: this.js_to_mono_obj.bind (this) }], size: 0, needs_root: true }); + result.set ('u', { steps: [{ convert: this.js_to_mono_uri.bind (this) }], size: 0, needs_root: true }); + + // result.set ('k', { steps: [{ convert: this.js_to_mono_enum.bind (this), indirect: 'i64'}], size: 8}); + result.set ('j', { steps: [{ convert: this.js_to_mono_enum.bind (this), indirect: 'i32'}], size: 8}); + + result.set ('i', { steps: [{ indirect: 'i32'}], 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}); + + this._primitive_converters = result; + return result; + }, + + _create_converter_for_marshal_string: function (args_marshal) { + var primitiveConverters = this._primitive_converters; + if (!primitiveConverters) + primitiveConverters = this._create_primitive_converters (); + + var steps = []; + var size = 0; + var is_result_definitely_unmarshaled = false, + is_result_possibly_unmarshaled = false, + result_unmarshaled_if_argc = -1, + needs_root_buffer = false; + + for (var i = 0; i < args_marshal.length; ++i) { + var key = args_marshal[i]; + + if (i === args_marshal.length - 1) { + if (key === "!") { + is_result_definitely_unmarshaled = true; + continue; + } else if (key === "m") { + is_result_possibly_unmarshaled = true; + result_unmarshaled_if_argc = args_marshal.length - 1; + } + } else if (key === "!") + throw new Error ("! must be at the end of the signature"); + + var conv = primitiveConverters.get (key); + if (!conv) + throw new Error ("Unknown parameter type " + type); + + var localStep = Object.create (conv.steps[0]); + localStep.size = conv.size; + if (conv.needs_root) + needs_root_buffer = true; + localStep.needs_root = conv.needs_root; + localStep.key = args_marshal[i]; + steps.push (localStep); + size += conv.size; + } + + return { + steps: steps, size: size, args_marshal: args_marshal, + is_result_definitely_unmarshaled: is_result_definitely_unmarshaled, + is_result_possibly_unmarshaled: is_result_possibly_unmarshaled, + result_unmarshaled_if_argc: result_unmarshaled_if_argc, + needs_root_buffer: needs_root_buffer + }; + }, + + _get_converter_for_marshal_string: function (args_marshal) { + if (!this._signature_converters) + this._signature_converters = new Map(); + + var converter = this._signature_converters.get (args_marshal); + if (!converter) { + converter = this._create_converter_for_marshal_string (args_marshal); + this._signature_converters.set (args_marshal, converter); + } + + return converter; + }, + + _compile_converter_for_marshal_string: function (args_marshal) { + var converter = this._get_converter_for_marshal_string (args_marshal); + if (typeof (converter.args_marshal) !== "string") + throw new Error ("Corrupt converter for '" + args_marshal + "'"); + + if (converter.compiled_function && converter.compiled_variadic_function) + return converter; + + var converterName = args_marshal.replace("!", "_result_unmarshaled"); + converter.name = converterName; + + var body = []; + var argumentNames = ["buffer", "rootBuffer", "method"]; + + // worst-case allocation size instead of allocating dynamically, plus padding + var bufferSizeBytes = converter.size + (args_marshal.length * 4) + 16; + var rootBufferSize = args_marshal.length; + // ensure the indirect values are 8-byte aligned so that aligned loads and stores will work + var indirectBaseOffset = ((((args_marshal.length * 4) + 7) / 8) | 0) * 8; + + var closure = {}; + var indirectLocalOffset = 0; + + body.push ( + `if (!buffer) buffer = Module._malloc (${bufferSizeBytes});`, + `var indirectStart = buffer + ${indirectBaseOffset};`, + "var indirect32 = (indirectStart / 4) | 0, indirect64 = (indirectStart / 8) | 0;", + "var buffer32 = (buffer / 4) | 0;", + "" + ); + + for (let i = 0; i < converter.steps.length; i++) { + var step = converter.steps[i]; + var closureKey = "step" + i; + var valueKey = "value" + i; + + var argKey = "arg" + i; + argumentNames.push (argKey); + + if (step.convert) { + closure[closureKey] = step.convert; + body.push (`var ${valueKey} = ${closureKey}(${argKey}, method, ${i});`); + } else { + body.push (`var ${valueKey} = ${argKey};`); + } + + if (step.needs_root) + body.push (`rootBuffer.set (${i}, ${valueKey});`); + + if (step.indirect) { + var heapArrayName = null; + + switch (step.indirect) { + case "u32": + heapArrayName = "HEAPU32"; + break; + case "i32": + heapArrayName = "HEAP32"; + break; + case "float": + heapArrayName = "HEAPF32"; + break; + case "double": + body.push (`Module.HEAPF64[indirect64 + ${(indirectLocalOffset / 8)}] = ${valueKey};`); + break; + case "i64": + body.push (`Module.setValue (indirectStart + ${indirectLocalOffset}, ${valueKey}, 'i64');`); + break; + default: + throw new Error ("Unimplemented indirect type: " + step.indirect); + } + + if (heapArrayName) + body.push (`Module.${heapArrayName}[indirect32 + ${(indirectLocalOffset / 4)}] = ${valueKey};`); + + body.push (`Module.HEAP32[buffer32 + ${i}] = indirectStart + ${indirectLocalOffset};`, ""); + indirectLocalOffset += step.size; + } else { + body.push (`Module.HEAP32[buffer32 + ${i}] = ${valueKey};`, ""); + indirectLocalOffset += 4; + } + } + + body.push ("return buffer;"); + + var bodyJs = body.join ("\r\n"), compiledFunction = null, compiledVariadicFunction = null; + try { + compiledFunction = this._create_named_function("converter_" + converterName, argumentNames, bodyJs, closure); + converter.compiled_function = compiledFunction; + } catch (exc) { + converter.compiled_function = null; + console.warn("compiling converter failed for", bodyJs, "with error", exc); + throw exc; + } + + argumentNames = ["existingBuffer", "rootBuffer", "method", "args"]; + closure = { + converter: compiledFunction + }; + body = [ + "return converter(", + " existingBuffer, rootBuffer, method," + ]; + + for (let i = 0; i < converter.steps.length; i++) { + body.push( + " args[" + i + + ( + (i == converter.steps.length - 1) + ? "]" + : "], " + ) + ); + } + + body.push(");"); + + bodyJs = body.join ("\r\n"); + try { + compiledVariadicFunction = this._create_named_function("variadic_converter_" + converterName, argumentNames, bodyJs, closure); + converter.compiled_variadic_function = compiledVariadicFunction; + } catch (exc) { + converter.compiled_variadic_function = null; + console.warn("compiling converter failed for", bodyJs, "with error", exc); + throw exc; + } + + converter.scratchRootBuffer = null; + converter.scratchBuffer = 0 | 0; + + return converter; + }, + + _verify_args_for_method_call: function (args_marshal, args) { + var has_args = args && (typeof args === "object") && args.length > 0; + var has_args_marshal = typeof args_marshal === "string"; + + if (has_args) { + if (!has_args_marshal) + throw new Error ("No signature provided for method call."); + else if (args.length > args_marshal.length) + throw new Error ("Too many parameter values. Expected at most " + args_marshal.length + " value(s) for signature " + args_marshal); + } + + return has_args_marshal && has_args; + }, + + _get_buffer_for_method_call: function (converter) { + if (!converter) + return 0; + + var result = converter.scratchBuffer; + converter.scratchBuffer = 0; + return result; + }, + + _get_args_root_buffer_for_method_call: function (converter) { + if (!converter) + return null; + + if (!converter.needs_root_buffer) + return null; + + var result; + if (converter.scratchRootBuffer) { + result = converter.scratchRootBuffer; + converter.scratchRootBuffer = null; + } else { + // TODO: Expand the converter's heap allocation and then use + // mono_wasm_new_root_buffer_from_pointer instead. Not that important + // at present because the scratch buffer will be reused unless we are + // recursing through a re-entrant call + result = MONO.mono_wasm_new_root_buffer (converter.steps.length); + result.converter = converter; + } + return result; + }, + + _release_args_root_buffer_from_method_call: function (converter, argsRootBuffer) { + if (!argsRootBuffer || !converter) + return; + + // Store the arguments root buffer for re-use in later calls + if (!converter.scratchRootBuffer) { + argsRootBuffer.clear (); + converter.scratchRootBuffer = argsRootBuffer; + } else { + argsRootBuffer.release (); + } + }, + + _release_buffer_from_method_call: function (converter, buffer) { + if (!converter || !buffer) + return; + + if (!converter.scratchBuffer) + converter.scratchBuffer = buffer | 0; + else + Module._free (buffer | 0); + }, + + _convert_exception_for_method_call: function (result, exception) { + if (exception === 0) + return null; + + var msg = this.conv_string (result); + var err = new Error (msg); //the convention is that invoke_method ToString () any outgoing exception + // console.warn ("error", msg, "at location", err.stack); + return err; + }, + + _maybe_produce_signature_warning: function (converter) { + if (converter.has_warned_about_signature) + return; + + console.warn ("MONO_WASM: Deprecated raw return value signature: '" + converter.args_marshal + "'. End the signature with '!' instead of 'm'."); + converter.has_warned_about_signature = true; + }, + + _decide_if_result_is_marshaled: function (converter, argc) { + if (!converter) + return true; + + if ( + converter.is_result_possibly_unmarshaled && + (argc === converter.result_unmarshaled_if_argc) + ) { + if (argc < converter.result_unmarshaled_if_argc) + throw new Error(["Expected >= ", converter.result_unmarshaled_if_argc, "argument(s) but got", argc, "for signature " + converter.args_marshal].join(" ")); + + this._maybe_produce_signature_warning (converter); + return false; + } else { + if (argc < converter.steps.length) + throw new Error(["Expected", converter.steps.length, "argument(s) but got", argc, "for signature " + converter.args_marshal].join(" ")); + + return !converter.is_result_definitely_unmarshaled; + } + }, + /* args_marshal is a string with one character per parameter that tells how to marshal it, here are the valid values: @@ -679,108 +1132,212 @@ var BindingSupportLib = { o: js object will be converted to a C# object (this will box numbers/bool/promises) m: raw mono object. Don't use it unless you know what you're doing - additionally you can append 'm' to args_marshal beyond `args.length` if you don't want the return value marshaled + to suppress marshaling of the return value, place '!' at the end of args_marshal, i.e. 'ii!' instead of 'ii' */ call_method: function (method, this_arg, args_marshal, args) { this.bindings_lazy_init (); - // Allocate memory for error - var has_args = args !== null && typeof args !== "undefined" && args.length > 0; - var has_args_marshal = args_marshal !== null && typeof args_marshal !== "undefined" && args_marshal.length > 0; + // HACK: Sometimes callers pass null or undefined, coerce it to 0 since that's what wasm expects + this_arg = this_arg | 0; - if (has_args_marshal && (!has_args || args.length > args_marshal.length)) - throw Error("Parameter count mismatch."); + // Detect someone accidentally passing the wrong type of value to method + if ((method | 0) !== method) + throw new Error (`method must be an address in the native heap, but was '${method}'`); + if (!method) + throw new Error ("no method specified"); + + var needs_converter = this._verify_args_for_method_call (args_marshal, args); - var args_start = null; - var buffer = null; - var [resultRoot, exceptionRoot] = MONO.mono_wasm_new_roots (2); - var argsRootBuffer = null; + var buffer = 0, converter = null, argsRootBuffer = null; + var is_result_marshaled = true; // check if the method signature needs argument mashalling - if (has_args_marshal && has_args) { - var i; - - var converters = this.converters; - if (!converters) { - converters = new Map (); - converters.set ('m', { steps: [{ }], size: 0}); - converters.set ('s', { steps: [{ convert: this.js_string_to_mono_string.bind (this)}], size: 0}); - converters.set ('o', { steps: [{ convert: this.js_to_mono_obj.bind (this)}], size: 0}); - converters.set ('u', { steps: [{ convert: this.js_to_mono_uri.bind (this)}], size: 0}); - converters.set ('k', { steps: [{ convert: this.js_to_mono_enum.bind (this), indirect: 'i64'}], size: 8}); - converters.set ('j', { steps: [{ convert: this.js_to_mono_enum.bind (this), indirect: 'i32'}], size: 8}); - converters.set ('i', { steps: [{ indirect: 'i32'}], size: 8}); - converters.set ('l', { steps: [{ indirect: 'i64'}], size: 8}); - converters.set ('f', { steps: [{ indirect: 'float'}], size: 8}); - converters.set ('d', { steps: [{ indirect: 'double'}], size: 8}); - this.converters = converters; - } + if (needs_converter) { + converter = this._compile_converter_for_marshal_string (args_marshal); - var converter = converters.get (args_marshal); - if (!converter) { - var steps = []; - var size = 0; + is_result_marshaled = this._decide_if_result_is_marshaled (converter, args.length); + + argsRootBuffer = this._get_args_root_buffer_for_method_call (converter); - for (i = 0; i < args_marshal.length; ++i) { - var conv = this.converters.get (args_marshal[i]); - if (!conv) - throw Error ("Unknown parameter type " + type); + var scratchBuffer = this._get_buffer_for_method_call (converter); - steps.push (conv.steps[0]); - size += conv.size; - } - converter = { steps: steps, size: size }; - converters.set (args_marshal, converter); - } + buffer = converter.compiled_variadic_function (scratchBuffer, argsRootBuffer, method, args); + } - // FIXME: Allocate a root buffer to contain all the managed objects like strings so that they aren't - // collected until the method call completes. - - // assume at least 8 byte alignment from malloc - var bufferSizeBytes = converter.size + (args.length * 4); - var bufferSizeElements = (bufferSizeBytes / 4) | 0; - argsRootBuffer = MONO.mono_wasm_new_root_buffer (bufferSizeElements); - buffer = Module._malloc (bufferSizeBytes); - var indirect_start = buffer; // buffer + buffer % 8 - args_start = indirect_start + converter.size; - - var slot = args_start; - var indirect_value = indirect_start; - for (i = 0; i < args.length; ++i) { - var handler = converter.steps[i]; - var obj = handler.convert ? handler.convert (args[i], method, i) : args[i]; - - if (handler.indirect) { - Module.setValue (indirect_value, obj, handler.indirect); - obj = indirect_value; - indirect_value += 8; - } else { - argsRootBuffer.set (i, obj); - } + return this._call_method_with_converted_args (method, this_arg, converter, buffer, is_result_marshaled, argsRootBuffer); + }, - Module.setValue (slot, obj, "*"); - slot += 4; - } - } + _handle_exception_for_call: function ( + converter, buffer, resultRoot, exceptionRoot, argsRootBuffer + ) { + var exc = this._convert_exception_for_method_call (resultRoot.value, exceptionRoot.value); + if (!exc) + return; - try { - resultRoot.value = this.invoke_method (method, this_arg, args_start, exceptionRoot.get_address ()); - Module._free (buffer); + this._teardown_after_call (converter, buffer, resultRoot, exceptionRoot, argsRootBuffer); + throw exc; + }, - if (exceptionRoot.value != 0) { - var msg = this.conv_string (resultRoot.value); - throw new Error (msg); //the convention is that invoke_method ToString () any outgoing exception - } + _handle_exception_and_produce_result_for_call: function ( + converter, buffer, resultRoot, exceptionRoot, argsRootBuffer, is_result_marshaled + ) { + this._handle_exception_for_call (converter, buffer, resultRoot, exceptionRoot, argsRootBuffer); - if (has_args_marshal && has_args) { - if (args_marshal.length >= args.length && args_marshal [args.length] === "m") - return resultRoot.value; + if (is_result_marshaled) + result = this._unbox_mono_obj_rooted (resultRoot); + else + result = resultRoot.value; + + this._teardown_after_call (converter, buffer, resultRoot, exceptionRoot, argsRootBuffer); + return result; + }, + + _teardown_after_call: function (converter, buffer, resultRoot, exceptionRoot, argsRootBuffer) { + this._release_args_root_buffer_from_method_call (converter, argsRootBuffer); + this._release_buffer_from_method_call (converter, buffer | 0); + + if (resultRoot) + resultRoot.release (); + if (exceptionRoot) + exceptionRoot.release (); + }, + + _get_method_description: function (method) { + if (!this._method_descriptions) + this._method_descriptions = new Map(); + + var result = this._method_descriptions.get (method); + if (!result) + result = "method#" + method; + return result; + }, + + _call_method_with_converted_args: function (method, this_arg, converter, buffer, is_result_marshaled, argsRootBuffer) { + var resultRoot = MONO.mono_wasm_new_root (), exceptionRoot = MONO.mono_wasm_new_root (); + resultRoot.value = this.invoke_method (method, this_arg, buffer, exceptionRoot.get_address ()); + return this._handle_exception_and_produce_result_for_call (converter, buffer, resultRoot, exceptionRoot, argsRootBuffer, is_result_marshaled); + }, + + bind_method: function (method, this_arg, args_marshal, friendly_name) { + this.bindings_lazy_init (); + + this_arg = this_arg | 0; + + var converter = null; + if (typeof (args_marshal) === "string") + converter = this._compile_converter_for_marshal_string (args_marshal); + + var closure = { + library_mono: MONO, + binding_support: this, + method: method, + this_arg: this_arg + }; + + var converterKey = "converter_" + converter.name; + + if (converter) + closure[converterKey] = converter; + + var argumentNames = []; + var body = [ + "var resultRoot = library_mono.mono_wasm_new_root (), exceptionRoot = library_mono.mono_wasm_new_root ();", + "" + ]; + + if (converter) { + body.push( + `var argsRootBuffer = binding_support._get_args_root_buffer_for_method_call (${converterKey});`, + `var scratchBuffer = binding_support._get_buffer_for_method_call (${converterKey});`, + `var buffer = ${converterKey}.compiled_function (`, + " scratchBuffer, argsRootBuffer, method," + ); + + for (var i = 0; i < converter.steps.length; i++) { + var argName = "arg" + i; + argumentNames.push(argName); + body.push( + " " + argName + + ( + (i == converter.steps.length - 1) + ? "" + : ", " + ) + ); } - return this._unbox_mono_obj_rooted (resultRoot); - } finally { - MONO.mono_wasm_release_roots (resultRoot, exceptionRoot, argsRootBuffer); + body.push(");"); + + } else { + body.push("var argsRootBuffer = null, buffer = 0;"); + } + + if (converter.is_result_definitely_unmarshaled) { + body.push ("var is_result_marshaled = false;"); + } else if (converter.is_result_possibly_unmarshaled) { + body.push (`var is_result_marshaled = arguments.length !== ${converter.result_unmarshaled_if_argc};`); + } else { + body.push ("var is_result_marshaled = true;"); + } + + // We inline a bunch of the invoke and marshaling logic here in order to eliminate the GC pressure normally + // created by the unboxing part of the call process. Because unbox_mono_obj(_rooted) can return non-numeric + // types, v8 and spidermonkey allocate and store its result on the heap (in the nursery, to be fair). + // For a bound method however, we know the result will always be the same type because C# methods have known + // return types. Inlining the invoke and marshaling logic means that even though the bound method has logic + // for handling various types, only one path through the method (for its appropriate return type) will ever + // be taken, and the JIT will see that the 'result' local and thus the return value of this function are + // always of the exact same type. All of the branches related to this end up being predicted and low-cost. + // The end result is that bound method invocations don't always allocate, so no more nursery GCs. Yay! -kg + body.push( + "", + "resultRoot.value = binding_support.invoke_method (method, this_arg, buffer, exceptionRoot.get_address ());", + `binding_support._handle_exception_for_call (${converterKey}, buffer, resultRoot, exceptionRoot, argsRootBuffer);`, + "", + "var resultPtr = resultRoot.value, result = undefined;", + "if (!is_result_marshaled) ", + " result = resultPtr;", + "else if (resultPtr !== 0) {", + // For the common scenario where the return type is a primitive, we want to try and unbox it directly + // into our existing heap allocation and then read it out of the heap. Doing this all in one operation + // means that we only need to enter a gc safe region twice (instead of 3+ times with the normal, + // slower check-type-and-then-unbox flow which has extra checks since unbox verifies the type). + " var resultType = binding_support.mono_wasm_try_unbox_primitive_and_get_type (resultPtr, buffer);", + " switch (resultType) {", + " case 1:", // int + " result = Module.HEAP32[buffer / 4]; break;", + " case 25:", // uint32 + " result = Module.HEAPU32[buffer / 4]; break;", + " case 24:", // float32 + " result = Module.HEAPF32[buffer / 4]; break;", + " case 2:", // float64 + " result = Module.HEAPF64[buffer / 8]; break;", + " case 8:", // boolean + " result = (Module.HEAP32[buffer / 4]) !== 0; break;", + " case 28:", // char + " result = String.fromCharCode(Module.HEAP32[buffer / 4]); break;", + " default:", + " result = binding_support._unbox_mono_obj_rooted_with_known_nonprimitive_type (resultRoot, resultType); break;", + " }", + "}", + "", + `binding_support._teardown_after_call (${converterKey}, buffer, resultRoot, exceptionRoot, argsRootBuffer);`, + "return result;" + ); + + bodyJs = body.join ("\r\n"); + + if (friendly_name) { + var escapeRE = /[^A-Za-z0-9_]/g; + friendly_name = friendly_name.replace(escapeRE, "_"); } + + var displayName = "managed_" + (friendly_name || method); + + if (this_arg) + displayName += "_with_this_" + this_arg; + + return this._create_named_function(displayName, argumentNames, bodyJs, closure); }, invoke_delegate: function (delegate_obj, js_args) { @@ -808,9 +1365,7 @@ var BindingSupportLib = { argsRoot.value = this.js_array_to_mono_array (js_args); if (!this.delegate_dynamic_invoke) throw new Error("System.Delegate.DynamicInvoke method can not be resolved."); - // Note: the single 'm' passed here is causing problems with AOT. Changed to "mo" again. - // This may need more analysis if causes problems again. - return this.call_method (this.delegate_dynamic_invoke, delegateRoot.value, "mo", [ argsRoot.value ]); + return this.call_method (this.delegate_dynamic_invoke, delegateRoot.value, "m", [ argsRoot.value ]); } finally { MONO.mono_wasm_release_roots (delegateRoot, argsRoot); } @@ -839,7 +1394,7 @@ var BindingSupportLib = { var klass = this.find_class(asm, namespace, classname); if (!klass) - throw new Error ("Could not find class: " + namespace + ":" +classname); + throw new Error ("Could not find class: " + namespace + ":" + classname + " in assembly " + assembly); var method = this.find_method (klass, methodname, -1); if (!method) @@ -866,10 +1421,9 @@ var BindingSupportLib = { if (typeof signature === "undefined") signature = Module.mono_method_get_call_signature (method); - return function() { - return BINDING.call_method (method, null, signature, arguments); - }; + return BINDING.bind_method (method, null, signature, fqn); }, + bind_assembly_entry_point: function (assembly) { this.bindings_lazy_init (); @@ -1122,7 +1676,7 @@ var BindingSupportLib = { } BINDING.mono_wasm_unwind_LMF(); - return BINDING.call_method (BINDING.box_js_bool, null, "im", [ result ]); + return BINDING._box_js_bool (result); }, mono_wasm_get_by_index: function(js_handle, property_index, is_exception) { BINDING.bindings_lazy_init (); @@ -1173,7 +1727,7 @@ var BindingSupportLib = { var js_name = BINDING.conv_string (global_name); - var globalObj = undefined; + var globalObj; if (!js_name) { globalObj = globalThis; @@ -1253,8 +1807,9 @@ var BindingSupportLib = { var argsList = new Array(); argsList[0] = constructor; if (js_args) - argsList = argsList.concat(js_args); - var obj = new (constructor.bind.apply(constructor, argsList )); + argsList = argsList.concat (js_args); + var tempCtor = constructor.bind.apply (constructor, argsList); + var obj = new tempCtor (); return obj; }; diff --git a/src/mono/wasm/runtime/driver.c b/src/mono/wasm/runtime/driver.c index 86606ee90a0420..50d0c7868409d8 100644 --- a/src/mono/wasm/runtime/driver.c +++ b/src/mono/wasm/runtime/driver.c @@ -106,10 +106,20 @@ mono_wasm_invoke_js (MonoString *str, int *is_exception) res = res.toString (); setValue ($2, 0, "i32"); } catch (e) { - res = e.toString (); + res = e.toString(); setValue ($2, 1, "i32"); if (res === null || res === undefined) res = "unknown exception"; + + var stack = e.stack; + if (stack) { + // Some JS runtimes insert the error message at the top of the stack, some don't, + // so normalize it by using the stack as the result if it already contains the error + if (stack.startsWith(res)) + res = stack; + else + res += "\n" + stack; + } } var buff = Module._malloc((res.length + 1) * 2); stringToUTF16 (res, buff, (res.length + 1) * 2); @@ -572,6 +582,12 @@ mono_wasm_assembly_load (const char *name) return res; } +EMSCRIPTEN_KEEPALIVE MonoClass* +mono_wasm_find_corlib_class (const char *namespace, const char *name) +{ + return mono_class_from_name (mono_get_corlib (), namespace, name); +} + EMSCRIPTEN_KEEPALIVE MonoClass* mono_wasm_assembly_find_class (MonoAssembly *assembly, const char *namespace, const char *name) { @@ -584,6 +600,21 @@ mono_wasm_assembly_find_method (MonoClass *klass, const char *name, int argument return mono_class_get_method_from_name (klass, name, arguments); } +EMSCRIPTEN_KEEPALIVE MonoObject* +mono_wasm_box_primitive (MonoClass *klass, void *value, int value_size) +{ + if (!klass) + return NULL; + + MonoType *type = mono_class_get_type (klass); + int alignment; + if (mono_type_size (type, &alignment) > value_size) + return NULL; + + // TODO: use mono_value_box_checked and propagate error out + return mono_value_box (root_domain, klass, value); +} + EMSCRIPTEN_KEEPALIVE MonoObject* mono_wasm_invoke_method (MonoMethod *method, MonoObject *this_arg, void *params[], MonoObject **out_exc) { @@ -721,7 +752,7 @@ MonoClass* mono_get_uri_class(MonoException** exc) } #define MARSHAL_TYPE_INT 1 -#define MARSHAL_TYPE_FP 2 +#define MARSHAL_TYPE_FP64 2 #define MARSHAL_TYPE_STRING 3 #define MARSHAL_TYPE_VT 4 #define MARSHAL_TYPE_DELEGATE 5 @@ -745,17 +776,14 @@ MonoClass* mono_get_uri_class(MonoException** exc) #define MARSHAL_ARRAY_FLOAT 17 #define MARSHAL_ARRAY_DOUBLE 18 -EMSCRIPTEN_KEEPALIVE int -mono_wasm_get_obj_type (MonoObject *obj) -{ - if (!obj) - return 0; - - /* Process obj before calling into the runtime, class_from_name () can invoke managed code */ - MonoClass *klass = mono_object_get_class (obj); - MonoType *type = mono_class_get_type (klass); - obj = NULL; +#define MARSHAL_TYPE_FP32 24 +#define MARSHAL_TYPE_UINT32 25 +#define MARSHAL_TYPE_INT64 26 +#define MARSHAL_TYPE_UINT64 27 +#define MARSHAL_TYPE_CHAR 28 +void mono_wasm_ensure_classes_resolved () +{ if (!datetime_class && !resolved_datetime_class) { datetime_class = mono_class_from_name (mono_get_corlib(), "System", "DateTime"); resolved_datetime_class = 1; @@ -773,8 +801,12 @@ mono_wasm_get_obj_type (MonoObject *obj) safehandle_class = mono_class_from_name (mono_get_corlib(), "System.Runtime.InteropServices", "SafeHandle"); resolved_safehandle_class = 1; } +} - switch (mono_type_get_type (type)) { +int +mono_wasm_marshal_type_from_mono_type (int mono_type, MonoClass *klass, MonoType *type) +{ + switch (mono_type) { // case MONO_TYPE_CHAR: prob should be done not as a number? case MONO_TYPE_BOOLEAN: return MARSHAL_TYPE_BOOL; @@ -783,18 +815,25 @@ mono_wasm_get_obj_type (MonoObject *obj) case MONO_TYPE_I2: case MONO_TYPE_U2: case MONO_TYPE_I4: - case MONO_TYPE_U4: - case MONO_TYPE_I8: - case MONO_TYPE_U8: case MONO_TYPE_I: // IntPtr return MARSHAL_TYPE_INT; + case MONO_TYPE_CHAR: + return MARSHAL_TYPE_CHAR; + case MONO_TYPE_U4: // The distinction between this and signed int is + // important due to how numbers work in JavaScript + return MARSHAL_TYPE_UINT32; + case MONO_TYPE_I8: + return MARSHAL_TYPE_INT64; + case MONO_TYPE_U8: + return MARSHAL_TYPE_UINT64; case MONO_TYPE_R4: + return MARSHAL_TYPE_FP32; case MONO_TYPE_R8: - return MARSHAL_TYPE_FP; + return MARSHAL_TYPE_FP64; case MONO_TYPE_STRING: return MARSHAL_TYPE_STRING; case MONO_TYPE_SZARRAY: { // simple zero based one-dim-array - MonoClass *eklass = mono_class_get_element_class(klass); + MonoClass *eklass = mono_class_get_element_class (klass); MonoType *etype = mono_class_get_type (eklass); switch (mono_type_get_type (etype)) { @@ -819,6 +858,8 @@ mono_wasm_get_obj_type (MonoObject *obj) } } default: + mono_wasm_ensure_classes_resolved (); + if (klass == datetime_class) return MARSHAL_TYPE_DATE; if (klass == datetimeoffset_class) @@ -841,6 +882,96 @@ mono_wasm_get_obj_type (MonoObject *obj) } } +EMSCRIPTEN_KEEPALIVE int +mono_wasm_get_obj_type (MonoObject *obj) +{ + if (!obj) + return 0; + + /* Process obj before calling into the runtime, class_from_name () can invoke managed code */ + MonoClass *klass = mono_object_get_class (obj); + MonoType *type = mono_class_get_type (klass); + obj = NULL; + + int mono_type = mono_type_get_type (type); + + return mono_wasm_marshal_type_from_mono_type (mono_type, klass, type); +} + +EMSCRIPTEN_KEEPALIVE int +mono_wasm_try_unbox_primitive_and_get_type (MonoObject *obj, void *result) +{ + int *resultI = result; + int64_t *resultL = result; + float *resultF = result; + double *resultD = result; + + if (!obj) { + *resultL = 0; + return 0; + } + + /* Process obj before calling into the runtime, class_from_name () can invoke managed code */ + MonoClass *klass = mono_object_get_class (obj); + MonoType *type = mono_class_get_type (klass), *original_type = type; + + if (mono_class_is_enum (klass)) + type = mono_type_get_underlying_type (type); + + int mono_type = mono_type_get_type (type); + + // FIXME: We would prefer to unbox once here but it will fail if the value isn't unboxable + + switch (mono_type) { + case MONO_TYPE_I1: + case MONO_TYPE_BOOLEAN: + *resultI = *(signed char*)mono_object_unbox (obj); + break; + case MONO_TYPE_U1: + *resultI = *(unsigned char*)mono_object_unbox (obj); + break; + case MONO_TYPE_I2: + case MONO_TYPE_CHAR: + *resultI = *(short*)mono_object_unbox (obj); + break; + case MONO_TYPE_U2: + *resultI = *(unsigned short*)mono_object_unbox (obj); + break; + case MONO_TYPE_I4: + case MONO_TYPE_I: + *resultI = *(int*)mono_object_unbox (obj); + break; + case MONO_TYPE_U4: + // FIXME: Will this behave the way we want for large unsigned values? + *resultI = *(int*)mono_object_unbox (obj); + break; + case MONO_TYPE_R4: + *resultF = *(float*)mono_object_unbox (obj); + break; + case MONO_TYPE_R8: + *resultD = *(double*)mono_object_unbox (obj); + break; + case MONO_TYPE_I8: + case MONO_TYPE_U8: + // FIXME: At present the javascript side of things can't handle this, + // but there's no reason not to future-proof this API + *resultL = *(int64_t*)mono_object_unbox (obj); + break; + default: + // If we failed to do a fast unboxing, return the original type information so + // that the caller can do a proper, slow unboxing later + *resultL = 0; + obj = NULL; + return mono_wasm_marshal_type_from_mono_type (mono_type, klass, original_type); + } + + // We successfully performed a fast unboxing here so use the type information + // matching what we unboxed (i.e. an enum's underlying type instead of its type) + obj = NULL; + return mono_wasm_marshal_type_from_mono_type (mono_type, klass, type); +} + +// FIXME: This function is retained specifically because runtime-test.js uses it EMSCRIPTEN_KEEPALIVE int mono_unbox_int (MonoObject *obj) { @@ -864,6 +995,8 @@ mono_unbox_int (MonoObject *obj) return *(int*)ptr; case MONO_TYPE_U4: return *(unsigned int*)ptr; + case MONO_TYPE_CHAR: + return *(short*)ptr; // WASM doesn't support returning longs to JS // case MONO_TYPE_I8: // case MONO_TYPE_U8: @@ -873,25 +1006,6 @@ mono_unbox_int (MonoObject *obj) } } -EMSCRIPTEN_KEEPALIVE double -mono_wasm_unbox_float (MonoObject *obj) -{ - if (!obj) - return 0; - MonoType *type = mono_class_get_type (mono_object_get_class(obj)); - - void *ptr = mono_object_unbox (obj); - switch (mono_type_get_type (type)) { - case MONO_TYPE_R4: - return *(float*)ptr; - case MONO_TYPE_R8: - return *(double*)ptr; - default: - printf ("Invalid type %d to mono_wasm_unbox_float\n", mono_type_get_type (type)); - return 0; - } -} - EMSCRIPTEN_KEEPALIVE int mono_wasm_array_length (MonoArray *array) { diff --git a/src/mono/wasm/runtime/library_mono.js b/src/mono/wasm/runtime/library_mono.js index 0f3cb960a7a016..fc212721460640 100644 --- a/src/mono/wasm/runtime/library_mono.js +++ b/src/mono/wasm/runtime/library_mono.js @@ -82,6 +82,7 @@ var MonoSupportLib = { module ["mono_wasm_globalization_init"] = MONO.mono_wasm_globalization_init; module ["mono_wasm_get_loaded_files"] = MONO.mono_wasm_get_loaded_files; module ["mono_wasm_new_root_buffer"] = MONO.mono_wasm_new_root_buffer; + module ["mono_wasm_new_root_buffer_from_pointer"] = MONO.mono_wasm_new_root_buffer_from_pointer; module ["mono_wasm_new_root"] = MONO.mono_wasm_new_root; module ["mono_wasm_new_roots"] = MONO.mono_wasm_new_roots; module ["mono_wasm_release_roots"] = MONO.mono_wasm_release_roots; @@ -195,9 +196,12 @@ var MonoSupportLib = { }, _mono_wasm_root_buffer_prototype: { + _throw_index_out_of_range: function () { + throw new Error ("index out of range"); + }, _check_in_range: function (index) { if ((index >= this.__count) || (index < 0)) - throw new Error ("index out of range"); + this._throw_index_out_of_range(); }, /** @returns {NativePointer} */ get_address: function (index) { @@ -215,23 +219,37 @@ var MonoSupportLib = { return Module.HEAP32[this.get_address_32 (index)]; }, set: function (index, value) { - this._check_in_range (index); Module.HEAP32[this.get_address_32 (index)] = value; return value; }, + _unsafe_get: function (index) { + return Module.HEAP32[this.__offset32 + index]; + }, + _unsafe_set: function (index, value) { + Module.HEAP32[this.__offset32 + index] = value; + }, + clear: function () { + if (this.__offset) + MONO._zero_region (this.__offset, this.__count * 4); + }, release: function () { - if (this.__offset) { + if (this.__offset && this.__ownsAllocation) { MONO.mono_wasm_deregister_root (this.__offset); MONO._zero_region (this.__offset, this.__count * 4); Module._free (this.__offset); } - this.__handle = this.__offset = this.__count = this.__offset32 = undefined; + this.__handle = this.__offset = this.__count = this.__offset32 = 0; }, + toString: function () { + return "[root buffer @" + this.get_address (0) + ", size " + this.__count + "]"; + } }, _scratch_root_buffer: null, _scratch_root_free_indices: null, + _scratch_root_free_indices_count: 0, + _scratch_root_free_instances: [], _mono_wasm_root_prototype: { /** @returns {NativePointer} */ @@ -244,21 +262,33 @@ var MonoSupportLib = { }, /** @returns {ManagedPointer} */ get: function () { - var result = this.__buffer.get (this.__index); + var result = this.__buffer._unsafe_get (this.__index); return result; }, set: function (value) { - this.__buffer.set (this.__index, value); + this.__buffer._unsafe_set (this.__index, value); return value; }, /** @returns {ManagedPointer} */ valueOf: function () { return this.get (); }, + clear: function () { + this.set (0); + }, release: function () { - MONO._mono_wasm_release_scratch_index (this.__index); - this.__buffer = undefined; - this.__index = undefined; + const maxPooledInstances = 128; + if (MONO._scratch_root_free_instances.length > maxPooledInstances) { + MONO._mono_wasm_release_scratch_index (this.__index); + this.__buffer = 0; + this.__index = 0; + } else { + this.set (0); + MONO._scratch_root_free_instances.push (this); + } + }, + toString: function () { + return "[root @" + this.get_address () + "]"; } }, @@ -267,7 +297,8 @@ var MonoSupportLib = { return; this._scratch_root_buffer.set (index, 0); - this._scratch_root_free_indices.push (index); + this._scratch_root_free_indices[this._scratch_root_free_indices_count] = index; + this._scratch_root_free_indices_count++; }, _mono_wasm_claim_scratch_index: function () { @@ -275,10 +306,10 @@ var MonoSupportLib = { const maxScratchRoots = 8192; this._scratch_root_buffer = this.mono_wasm_new_root_buffer (maxScratchRoots, "js roots"); - this._scratch_root_free_indices = new Array (maxScratchRoots); + this._scratch_root_free_indices = new Int32Array (maxScratchRoots); + this._scratch_root_free_indices_count = maxScratchRoots; for (var i = 0; i < maxScratchRoots; i++) - this._scratch_root_free_indices[i] = i; - this._scratch_root_free_indices.reverse (); + this._scratch_root_free_indices[i] = maxScratchRoots - i - 1; Object.defineProperty (this._mono_wasm_root_prototype, "value", { get: this._mono_wasm_root_prototype.get, @@ -287,15 +318,19 @@ var MonoSupportLib = { }); } - if (this._scratch_root_free_indices.length < 1) + if (this._scratch_root_free_indices_count < 1) throw new Error ("Out of scratch root space"); - var result = this._scratch_root_free_indices.pop (); + var result = this._scratch_root_free_indices[this._scratch_root_free_indices_count - 1]; + this._scratch_root_free_indices_count--; return result; }, _zero_region: function (byteOffset, sizeBytes) { - (new Uint8Array (Module.HEAPU8.buffer, byteOffset, sizeBytes)).fill (0); + if (((byteOffset % 4) === 0) && ((sizeBytes % 4) === 0)) + Module.HEAP32.fill(0, byteOffset / 4, sizeBytes / 4); + else + Module.HEAP8.fill(0, byteOffset, sizeBytes); }, /** @@ -331,6 +366,43 @@ var MonoSupportLib = { result.__count = capacity; result.length = capacity; result.__handle = this.mono_wasm_register_root (offset, capacityBytes, msg || 0); + result.__ownsAllocation = true; + + return result; + }, + + /** + * Creates a root buffer object representing an existing allocation in the native heap and registers + * the allocation with the GC. The caller is responsible for managing the lifetime of the allocation. + * @param {NativePointer} offset - the offset of the root buffer in the native heap. + * @param {number} capacity - the maximum number of elements the buffer can hold. + * @param {string} [msg] - a description of the root buffer (for debugging) + * @returns {WasmRootBuffer} + */ + mono_wasm_new_root_buffer_from_pointer: function (offset, capacity, msg) { + if (!this.mono_wasm_register_root || !this.mono_wasm_deregister_root) { + this.mono_wasm_register_root = Module.cwrap ("mono_wasm_register_root", "number", ["number", "number", "string"]); + this.mono_wasm_deregister_root = Module.cwrap ("mono_wasm_deregister_root", null, ["number"]); + } + + if (capacity <= 0) + throw new Error ("capacity >= 1"); + + capacity = capacity | 0; + + var capacityBytes = capacity * 4; + if ((offset % 4) !== 0) + throw new Error ("Unaligned offset"); + + this._zero_region (offset, capacityBytes); + + var result = Object.create (this._mono_wasm_root_buffer_prototype); + result.__offset = offset; + result.__offset32 = (offset / 4) | 0; + result.__count = capacity; + result.length = capacity; + result.__handle = this.mono_wasm_register_root (offset, capacityBytes, msg || 0); + result.__ownsAllocation = false; return result; }, @@ -345,12 +417,18 @@ var MonoSupportLib = { * @returns {WasmRoot} */ mono_wasm_new_root: function (value) { - var index = this._mono_wasm_claim_scratch_index (); - var buffer = this._scratch_root_buffer; + var result; - var result = Object.create (this._mono_wasm_root_prototype); - result.__buffer = buffer; - result.__index = index; + if (this._scratch_root_free_instances.length > 0) { + result = this._scratch_root_free_instances.pop (); + } else { + var index = this._mono_wasm_claim_scratch_index (); + var buffer = this._scratch_root_buffer; + + result = Object.create (this._mono_wasm_root_prototype); + result.__buffer = buffer; + result.__index = index; + } if (value !== undefined) { if (typeof (value) !== "number")