Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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"];
Expand Down
16 changes: 14 additions & 2 deletions test-app/app/src/main/java/com/tns/tests/PrimitiveReturnTest.java
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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) {
Expand All @@ -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);
}
Expand Down
90 changes: 59 additions & 31 deletions test-app/runtime/src/main/cpp/Runtime.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@

#include <cfloat>
#include <climits>
#include <cstdint>
#include <cmath>
#include <cstring>

Expand Down Expand Up @@ -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>& value, Type type,
Expand All @@ -516,62 +519,87 @@ 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<BooleanObject>()->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<Int32>()->Value();
if (type == Type::Char) {
Local<String> str;
if (value->IsString()) {
str = value.As<String>();
} else if (value->IsStringObject()) {
str = value.As<StringObject>()->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<Number>()->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<Number>()->Value();
} else if (value->IsNumberObject()) {
d = value.As<NumberObject>()->ValueOf();
} else {
return false;
}
bool isWholeNumber =
std::isfinite(d) && d == static_cast<double>(static_cast<int64_t>(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<jbyte>(d);
return true;
}
return false;
case Type::Short:
if (isWhole && d >= INT16_MIN && d <= INT16_MAX) {
bits = static_cast<jshort>(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<jint>(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<jlong>(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<jfloat>(d);
int32_t raw;
memcpy(&raw, &f, sizeof(raw));
bits = raw;
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;
}
Expand Down
17 changes: 11 additions & 6 deletions test-app/runtime/src/main/java/com/tns/Runtime.java
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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) {
Expand Down
Loading