diff --git a/test-app/app/src/main/assets/app/tests/testPrimitiveCallbackReturns.js b/test-app/app/src/main/assets/app/tests/testPrimitiveCallbackReturns.js index d206de91f..5fec0b5d4 100644 --- a/test-app/app/src/main/assets/app/tests/testPrimitiveCallbackReturns.js +++ b/test-app/app/src/main/assets/app/tests/testPrimitiveCallbackReturns.js @@ -49,41 +49,43 @@ describe("Primitive return values of JS callbacks", function () { "{}": function () { return {}; } }; - // Captured from the boxed conversion; columns are z b c s i j f d. + // Columns are z b c s i j f d. A value converts only when the return type + // can hold it (whole numbers in range for integer types, float range for + // float, a one-character string for char); otherwise the binding throws. var expected = { - "0": "ex:ClassCastException ex:ClassCastException ex:ClassCastException ex:ClassCastException ok:0 ok:0 ex:ClassCastException ex:ClassCastException", - "1": "ex:ClassCastException ex:ClassCastException ex:ClassCastException ex:ClassCastException ok:1 ok:1 ex:ClassCastException ex:ClassCastException", - "-1": "ex:ClassCastException ex:ClassCastException ex:ClassCastException ex:ClassCastException ok:-1 ok:-1 ex:ClassCastException ex:ClassCastException", - "-0": "ex:ClassCastException ex:ClassCastException ex:ClassCastException ex:ClassCastException ok:0 ok:0 ex:ClassCastException ex:ClassCastException", - "2^31": "ex:ClassCastException ex:ClassCastException ex:ClassCastException ex:ClassCastException ex:ClassCastException ok:2147483648 ex:ClassCastException ex:ClassCastException", - "-2^31-1": "ex:ClassCastException ex:ClassCastException ex:ClassCastException ex:ClassCastException ex:ClassCastException ok:-2147483649 ex:ClassCastException ex:ClassCastException", - "2^40": "ex:ClassCastException ex:ClassCastException ex:ClassCastException ex:ClassCastException ex:ClassCastException ok:1099511627776 ex:ClassCastException ex:ClassCastException", - "2^60": "ex:ClassCastException ex:ClassCastException ex:ClassCastException ex:ClassCastException ex:ClassCastException ok:1152921504606846976 ex:ClassCastException ex:ClassCastException", + "0": "ex:ClassCastException ok:0 ex:ClassCastException ok:0 ok:0 ok:0 ok:0 ok:0", + "1": "ex:ClassCastException ok:1 ex:ClassCastException ok:1 ok:1 ok:1 ok:1 ok:1", + "-1": "ex:ClassCastException ok:-1 ex:ClassCastException ok:-1 ok:-1 ok:-1 ok:-1 ok:-1", + "-0": "ex:ClassCastException ok:0 ex:ClassCastException ok:0 ok:0 ok:0 ok:0 ok:0", + "2^31": "ex:ClassCastException ex:ClassCastException ex:ClassCastException ex:ClassCastException ex:ClassCastException ok:2147483648 ok:2.14748365E+9 ok:2147483648", + "-2^31-1": "ex:ClassCastException ex:ClassCastException ex:ClassCastException ex:ClassCastException ex:ClassCastException ok:-2147483649 ok:-2.14748365E+9 ok:-2147483649", + "2^40": "ex:ClassCastException ex:ClassCastException ex:ClassCastException ex:ClassCastException ex:ClassCastException ok:1099511627776 ok:1.09951163E+12 ok:1099511627776", + "2^60": "ex:ClassCastException ex:ClassCastException ex:ClassCastException ex:ClassCastException ex:ClassCastException ok:1152921504606846976 ok:1.1529215E+18 ok:1.152921504606847E+18", "1.5": "ex:ClassCastException ex:ClassCastException ex:ClassCastException ex:ClassCastException ex:ClassCastException ex:ClassCastException ok:1.5 ok:1.5", - "-1.5": "ex:ClassCastException ex:ClassCastException ex:ClassCastException ex:ClassCastException ex:ClassCastException ex:ClassCastException ex:ClassCastException ok:-1.5", - "1e-40": "ex:ClassCastException ex:ClassCastException ex:ClassCastException ex:ClassCastException ex:ClassCastException ex:ClassCastException ex:ClassCastException ok:1.0E-40", - "3.5e38": "ex:ClassCastException ex:ClassCastException ex:ClassCastException ex:ClassCastException ex:ClassCastException ex:ClassCastException ex:ClassCastException ok:3.5E38", - "NaN": "ex:ClassCastException ex:ClassCastException ex:ClassCastException ex:ClassCastException ex:ClassCastException ex:ClassCastException ex:ClassCastException ok:NaN", - "Infinity": "ex:ClassCastException ex:ClassCastException ex:ClassCastException ex:ClassCastException ex:ClassCastException ex:ClassCastException ex:ClassCastException ok:Infinity", - "-Infinity": "ex:ClassCastException ex:ClassCastException ex:ClassCastException ex:ClassCastException ex:ClassCastException ex:ClassCastException ex:ClassCastException ok:-Infinity", - "-2.5": "ex:ClassCastException ex:ClassCastException ex:ClassCastException ex:ClassCastException ex:ClassCastException ex:ClassCastException ex:ClassCastException ok:-2.5", + "-1.5": "ex:ClassCastException ex:ClassCastException ex:ClassCastException ex:ClassCastException ex:ClassCastException ex:ClassCastException ok:-1.5 ok:-1.5", + "1e-40": "ex:ClassCastException ex:ClassCastException ex:ClassCastException ex:ClassCastException ex:ClassCastException ex:ClassCastException ex:ClassCastException ok:9.9999999999999993E-41", + "3.5e38": "ex:ClassCastException ex:ClassCastException ex:ClassCastException ex:ClassCastException ex:ClassCastException ex:ClassCastException ex:ClassCastException ok:3.5E+38", + "NaN": "ex:ClassCastException ex:ClassCastException ex:ClassCastException ex:ClassCastException ex:ClassCastException ex:ClassCastException ok:NaN ok:NaN", + "Infinity": "ex:ClassCastException ex:ClassCastException ex:ClassCastException ex:ClassCastException ex:ClassCastException ex:ClassCastException ok:Infinity ok:Infinity", + "-Infinity": "ex:ClassCastException ex:ClassCastException ex:ClassCastException ex:ClassCastException ex:ClassCastException ex:ClassCastException ok:-Infinity ok:-Infinity", + "-2.5": "ex:ClassCastException ex:ClassCastException ex:ClassCastException ex:ClassCastException ex:ClassCastException ex:ClassCastException ok:-2.5 ok:-2.5", "true": "ok:true ex:ClassCastException ex:ClassCastException ex:ClassCastException ex:ClassCastException ex:ClassCastException ex:ClassCastException ex:ClassCastException", "false": "ok:false ex:ClassCastException ex:ClassCastException ex:ClassCastException ex:ClassCastException ex:ClassCastException ex:ClassCastException ex:ClassCastException", - "'1'": "ex:ClassCastException ex:ClassCastException ex:ClassCastException ex:ClassCastException ex:ClassCastException ex:ClassCastException ex:ClassCastException ex:ClassCastException", - "'a'": "ex:ClassCastException ex:ClassCastException ex:ClassCastException ex:ClassCastException ex:ClassCastException ex:ClassCastException ex:ClassCastException ex:ClassCastException", - "null": "ok:false ok:0 ok:0 ok:0 ok:0 ok:0 ok:0.0 ok:0.0", - "undefined": "ok:false ok:0 ok:0 ok:0 ok:0 ok:0 ok:0.0 ok:0.0", - "Number(5)": "ex:ClassCastException ex:ClassCastException ex:ClassCastException ex:ClassCastException ok:5 ok:5 ex:ClassCastException ex:ClassCastException", + "'1'": "ex:ClassCastException ex:ClassCastException ok:49 ex:ClassCastException ex:ClassCastException ex:ClassCastException ex:ClassCastException ex:ClassCastException", + "'a'": "ex:ClassCastException ex:ClassCastException ok:97 ex:ClassCastException ex:ClassCastException ex:ClassCastException ex:ClassCastException ex:ClassCastException", + "null": "ok:false ok:0 ok:0 ok:0 ok:0 ok:0 ok:0 ok:0", + "undefined": "ok:false ok:0 ok:0 ok:0 ok:0 ok:0 ok:0 ok:0", + "Number(5)": "ex:ClassCastException ok:5 ex:ClassCastException ok:5 ok:5 ok:5 ok:5 ok:5", "Number(2.5)": "ex:ClassCastException ex:ClassCastException ex:ClassCastException ex:ClassCastException ex:ClassCastException ex:ClassCastException ok:2.5 ok:2.5", "Boolean(true)": "ok:true ex:ClassCastException ex:ClassCastException ex:ClassCastException ex:ClassCastException ex:ClassCastException ex:ClassCastException ex:ClassCastException", - "Boolean(false)": "ok:true ex:ClassCastException ex:ClassCastException ex:ClassCastException ex:ClassCastException ex:ClassCastException ex:ClassCastException ex:ClassCastException", + "Boolean(false)": "ok:false ex:ClassCastException ex:ClassCastException ex:ClassCastException ex:ClassCastException ex:ClassCastException ex:ClassCastException ex:ClassCastException", "long(5)": "ex:ClassCastException ex:ClassCastException ex:ClassCastException ex:ClassCastException ex:ClassCastException ok:5 ex:ClassCastException ex:ClassCastException", "byte(2)": "ex:ClassCastException ok:2 ex:ClassCastException ex:ClassCastException ex:ClassCastException ex:ClassCastException ex:ClassCastException ex:ClassCastException", "short(3)": "ex:ClassCastException ex:ClassCastException ex:ClassCastException ok:3 ex:ClassCastException ex:ClassCastException ex:ClassCastException ex:ClassCastException", "char('a')": "ex:ClassCastException ex:ClassCastException ok:97 ex:ClassCastException ex:ClassCastException ex:ClassCastException ex:ClassCastException ex:ClassCastException", "float(1.5)": "ex:ClassCastException ex:ClassCastException ex:ClassCastException ex:ClassCastException ex:ClassCastException ex:ClassCastException ok:1.5 ex:ClassCastException", - "double(2)": "ex:ClassCastException ex:ClassCastException ex:ClassCastException ex:ClassCastException ex:ClassCastException ex:ClassCastException ex:ClassCastException ok:2.0", - "{}": "ok:false ok:0 ok:0 ok:0 ok:0 ok:0 ok:0.0 ok:0.0" + "double(2)": "ex:ClassCastException ex:ClassCastException ex:ClassCastException ex:ClassCastException ex:ClassCastException ex:ClassCastException ex:ClassCastException ok:2", + "{}": "ok:false ok:0 ok:0 ok:0 ok:0 ok:0 ok:0 ok:0" }; var kinds = ["z", "b", "c", "s", "i", "j", "f", "d"]; diff --git a/test-app/app/src/main/java/com/tns/tests/PrimitiveReturnTest.java b/test-app/app/src/main/java/com/tns/tests/PrimitiveReturnTest.java index 284084e14..96aac8ce7 100644 --- a/test-app/app/src/main/java/com/tns/tests/PrimitiveReturnTest.java +++ b/test-app/app/src/main/java/com/tns/tests/PrimitiveReturnTest.java @@ -1,5 +1,8 @@ package com.tns.tests; +import java.math.BigDecimal; +import java.math.MathContext; + /** * Calls each primitive-returning method of a JS implementation and reports either the * returned value or the class of what was thrown, so a spec can pin how JS return values @@ -24,6 +27,15 @@ public interface Returns { double d(); } + // Float.toString/Double.toString changed their shortest-digits algorithm + // across Android versions; BigDecimal rounding prints the same everywhere. + private static String format(double value, int digits) { + if (Double.isNaN(value) || Double.isInfinite(value)) { + return Double.toString(value); + } + return new BigDecimal(value).round(new MathContext(digits)).stripTrailingZeros().toString(); + } + public static String call(Returns r, String kind) { try { switch (kind) { @@ -40,9 +52,9 @@ public static String call(Returns r, String kind) { case "j": return "ok:" + r.j(); case "f": - return "ok:" + r.f(); + return "ok:" + format(r.f(), 9); case "d": - return "ok:" + r.d(); + return "ok:" + format(r.d(), 17); default: throw new IllegalArgumentException(kind); } diff --git a/test-app/runtime/src/main/cpp/Runtime.cpp b/test-app/runtime/src/main/cpp/Runtime.cpp index 7dca92805..dd45ea3d4 100644 --- a/test-app/runtime/src/main/cpp/Runtime.cpp +++ b/test-app/runtime/src/main/cpp/Runtime.cpp @@ -56,6 +56,7 @@ #include #include +#include #include #include @@ -503,10 +504,12 @@ jobject Runtime::RunScript(JNIEnv* _env, jobject obj, jstring scriptFile) { } /* - * The primitive-returning cases of JsArgToArrayConverter::ConvertArg that box - * into exactly the declared wrapper type, which is all the generated binding's - * cast accepts. Everything else keeps the boxed conversion, including the - * values it turns into a ClassCastException or a defaulted null. + * A JS callback result for a primitive Java return type, converted only when + * the declared type can hold it: whole numbers in range for the integer types, + * a value within float range (or 0, NaN, +-Infinity) for float, any number for + * double, a one-character string for char. Anything else keeps the boxed + * conversion, whose result the generated binding rejects with a + * ClassCastException. */ static bool TryGetPrimitiveReturnBits(Isolate* isolate, const Local& value, Type type, @@ -516,49 +519,76 @@ static bool TryGetPrimitiveReturnBits(Isolate* isolate, } if (type == Type::Boolean) { - if (!value->IsBoolean() && !value->IsBooleanObject()) { - return false; + if (value->IsBoolean()) { + bits = value->IsTrue() ? 1 : 0; + return true; + } + if (value->IsBooleanObject()) { + bits = value.As()->ValueOf() ? 1 : 0; + return true; } - // ToBoolean, as the boxed path does: a Boolean object is always true. - bits = value->BooleanValue(isolate) ? 1 : 0; - return true; - } - - // Number objects are left to the boxed path: reading one calls valueOf, - // which must not run twice if this path then declines. - if (!value->IsNumber()) { return false; } - if (type == Type::Int && value->IsInt32()) { - bits = value.As()->Value(); + if (type == Type::Char) { + Local str; + if (value->IsString()) { + str = value.As(); + } else if (value->IsStringObject()) { + str = value.As()->ValueOf(); + } else { + return false; + } + if (str->Length() != 1) { + return false; + } + uint16_t unit; + str->WriteV2(isolate, 0, 1, &unit); + bits = unit; return true; } - double d = value.As()->Value(); - // Beyond int64 the boxed path's double -> int64 cast is undefined. - constexpr double kTwoPow63 = 9223372036854775808.0; - if (std::isfinite(d) && std::fabs(d) >= kTwoPow63) { + double d; + if (value->IsNumber()) { + d = value.As()->Value(); + } else if (value->IsNumberObject()) { + d = value.As()->ValueOf(); + } else { return false; } - bool isWholeNumber = - std::isfinite(d) && d == static_cast(static_cast(d)); + bool isWhole = std::isfinite(d) && std::trunc(d) == d; switch (type) { + case Type::Byte: + if (isWhole && d >= INT8_MIN && d <= INT8_MAX) { + bits = static_cast(d); + return true; + } + return false; + case Type::Short: + if (isWhole && d >= INT16_MIN && d <= INT16_MAX) { + bits = static_cast(d); + return true; + } + return false; case Type::Int: - if (isWholeNumber && INT_MIN <= d && d <= INT_MAX) { + if (isWhole && d >= INT32_MIN && d <= INT32_MAX) { bits = static_cast(d); return true; } return false; case Type::Long: - if (isWholeNumber) { + // 2^63 itself is not representable; the boxed path's handling of it is + // undefined, so it is left there. + if (isWhole && std::fabs(d) < 9223372036854775808.0) { bits = static_cast(d); return true; } return false; - case Type::Float: - if (!isWholeNumber && FLT_MIN <= d && d <= FLT_MAX) { + case Type::Float: { + double magnitude = std::fabs(d); + if (!std::isfinite(d) || d == 0 || + (magnitude >= FLT_MIN && magnitude <= FLT_MAX)) { float f = static_cast(d); int32_t raw; memcpy(&raw, &f, sizeof(raw)); @@ -566,12 +596,10 @@ static bool TryGetPrimitiveReturnBits(Isolate* isolate, return true; } return false; + } case Type::Double: - if (!isWholeNumber) { - memcpy(&bits, &d, sizeof(bits)); - return true; - } - return false; + memcpy(&bits, &d, sizeof(bits)); + return true; default: return false; } diff --git a/test-app/runtime/src/main/java/com/tns/Runtime.java b/test-app/runtime/src/main/java/com/tns/Runtime.java index 5e1d5e302..3aa1709e4 100644 --- a/test-app/runtime/src/main/java/com/tns/Runtime.java +++ b/test-app/runtime/src/main/java/com/tns/Runtime.java @@ -1537,14 +1537,13 @@ private static Object discardCall(String methodName, Class retType, String re } /* - * For return types the native side can hand back as raw bits instead of a - * boxed object: it writes {bits, 1} here and returns null, or leaves the - * array untouched and returns the boxed value as before. + * For primitive return types the native side hands the converted value back + * as raw bits: it writes {bits, 1} here and returns null, or leaves the + * array untouched and returns a boxed value from the generic conversion. */ private static long[] newPrimitiveResult(int returnType) { - boolean raw = returnType == TypeIDs.Boolean || returnType == TypeIDs.Int || returnType == TypeIDs.Long - || returnType == TypeIDs.Float || returnType == TypeIDs.Double; - return raw ? new long[2] : null; + boolean primitive = returnType >= TypeIDs.Boolean && returnType <= TypeIDs.Double; + return primitive ? new long[2] : null; } private static Object takePrimitiveResult(Object nativeResult, int returnType, long[] primitiveResult) { @@ -1555,6 +1554,12 @@ private static Object takePrimitiveResult(Object nativeResult, int returnType, l long bits = primitiveResult[0]; if (returnType == TypeIDs.Boolean) { return bits != 0; + } else if (returnType == TypeIDs.Char) { + return (char) bits; + } else if (returnType == TypeIDs.Byte) { + return (byte) bits; + } else if (returnType == TypeIDs.Short) { + return (short) bits; } else if (returnType == TypeIDs.Int) { return (int) bits; } else if (returnType == TypeIDs.Long) {