diff --git a/NativeScript/ffi/jni/jsi/conversion/ArrayElementAccessor.cpp b/NativeScript/ffi/jni/jsi/conversion/ArrayElementAccessor.cpp index 625ba4ca..b42462b3 100644 --- a/NativeScript/ffi/jni/jsi/conversion/ArrayElementAccessor.cpp +++ b/NativeScript/ffi/jni/jsi/conversion/ArrayElementAccessor.cpp @@ -1,6 +1,7 @@ #include "ArrayElementAccessor.h" #include "JsArgToArrayConverter.h" #include "ArgConverter.h" +#include "Coercion.h" #include "Util.h" #include "NativeScriptException.h" #include "Runtime.h" @@ -114,12 +115,12 @@ void ArrayElementAccessor::SetArrayElement(JsRuntime &rt, const JsValue &array, // chain). switch (arraySignature[1]) { case 'Z': { //bool - jboolean v = static_cast(js_util::get_bool(value)); + jboolean v = static_cast(coerce::ToBool(rt, value)); jenv.SetBooleanArrayRegion((jbooleanArray) arr, index, 1, &v); break; } case 'B': { //byte - jbyte v = static_cast(js_util::get_int32(value)); + jbyte v = static_cast(coerce::ToInt32(rt, value)); jenv.SetByteArrayRegion((jbyteArray) arr, index, 1, &v); break; } @@ -134,27 +135,28 @@ void ArrayElementAccessor::SetArrayElement(JsRuntime &rt, const JsValue &array, break; } case 'S': { //short - jshort v = static_cast(js_util::get_int32(value)); + jshort v = static_cast(coerce::ToInt32(rt, value)); jenv.SetShortArrayRegion((jshortArray) arr, index, 1, &v); break; } case 'I': { //int - jint v = static_cast(js_util::get_int32(value)); + jint v = static_cast(coerce::ToInt32(rt, value)); jenv.SetIntArrayRegion((jintArray) arr, index, 1, &v); break; } case 'J': { //long - jlong v = static_cast(js_util::get_number(value)); + jlong v = value.isObject() ? static_cast(ArgConverter::ConvertToJavaLong(rt, value)) + : static_cast(coerce::ToInt64(rt, value)); jenv.SetLongArrayRegion((jlongArray) arr, index, 1, &v); break; } case 'F': { //float - jfloat v = static_cast(js_util::get_number(value)); + jfloat v = static_cast(coerce::ToDouble(rt, value)); jenv.SetFloatArrayRegion((jfloatArray) arr, index, 1, &v); break; } case 'D': { //double - jdouble v = static_cast(js_util::get_number(value)); + jdouble v = static_cast(coerce::ToDouble(rt, value)); jenv.SetDoubleArrayRegion((jdoubleArray) arr, index, 1, &v); break; } diff --git a/NativeScript/ffi/jni/jsi/conversion/Coercion.h b/NativeScript/ffi/jni/jsi/conversion/Coercion.h new file mode 100644 index 00000000..c3ec7980 --- /dev/null +++ b/NativeScript/ffi/jni/jsi/conversion/Coercion.h @@ -0,0 +1,72 @@ +#ifndef COERCION_H_ +#define COERCION_H_ + +#include +#include +#include + +#include "Engine.h" + +namespace tns { +namespace coerce { + +// Each conversion returns a value that is already the right type as it always did. Only when it is +// not is the value coerced the way the classic V8 runtime does (ToNumber, ToInt32, ToBoolean). A +// coercion that throws, such as a Symbol, yields 0 / false and leaves no exception behind. + +inline double CoerceToDouble(JsRuntime &rt, const JsValue &value) { + try { + JsValue ctor = rt.global().getProperty(rt, "Number"); + JsFunction fn = ctor.asObjectBorrowed(rt).asFunction(rt); + JsValue argument(rt, value); + JsValue result = fn.callWithThis(rt, rt.global(), &argument, 1); + return result.isNumber() ? result.getNumber() : std::numeric_limits::quiet_NaN(); + } catch (JsError &) { + return 0; + } +} + +inline bool CoerceToBool(JsRuntime &rt, const JsValue &value) { + try { + JsValue ctor = rt.global().getProperty(rt, "Boolean"); + JsFunction fn = ctor.asObjectBorrowed(rt).asFunction(rt); + JsValue argument(rt, value); + JsValue result = fn.callWithThis(rt, rt.global(), &argument, 1); + return result.isBool() && result.getBool(); + } catch (JsError &) { + return false; + } +} + +inline double ToDouble(JsRuntime &rt, const JsValue &value) { + return value.isNumber() ? value.getNumber() : CoerceToDouble(rt, value); +} + +inline int32_t Int32FromDouble(double d) { + if (d >= -2147483648.0 && d <= 2147483647.0) return static_cast(d); + if (!std::isfinite(d)) return 0; + double wrapped = std::fmod(std::trunc(d), 4294967296.0); + if (wrapped < 0) wrapped += 4294967296.0; + return static_cast(static_cast(wrapped)); +} + +inline int32_t ToInt32(JsRuntime &rt, const JsValue &value) { + return Int32FromDouble(ToDouble(rt, value)); +} + +inline int64_t ToInt64(JsRuntime &rt, const JsValue &value) { + double d = ToDouble(rt, value); + if (std::isnan(d)) return 0; + if (d >= 9223372036854775807.0) return std::numeric_limits::max(); + if (d <= -9223372036854775808.0) return std::numeric_limits::min(); + return static_cast(d); +} + +inline bool ToBool(JsRuntime &rt, const JsValue &value) { + return value.isBool() ? value.getBool() : CoerceToBool(rt, value); +} + +} // namespace coerce +} // namespace tns + +#endif /* COERCION_H_ */ diff --git a/NativeScript/ffi/jni/jsi/conversion/JsArgToArrayConverter.cpp b/NativeScript/ffi/jni/jsi/conversion/JsArgToArrayConverter.cpp index 9995bafd..9f07d94c 100644 --- a/NativeScript/ffi/jni/jsi/conversion/JsArgToArrayConverter.cpp +++ b/NativeScript/ffi/jni/jsi/conversion/JsArgToArrayConverter.cpp @@ -1,4 +1,5 @@ #include "JsArgToArrayConverter.h" +#include "Coercion.h" #include #include "ObjectManager.h" #include "ArgConverter.h" @@ -190,7 +191,7 @@ bool JsArgToArrayConverter::ConvertArg(JsRuntime &rt, const JsValue &arg, int in auto strValue = ArgConverter::ConvertToString(rt, castValue); longValue = atoll(strValue.c_str()); } else { - longValue = (jlong) js_util::get_number(castValue); + longValue = (jlong) coerce::ToInt64(rt, castValue); } } javaObject = JType::NewLong(jEnv, longValue); @@ -202,7 +203,7 @@ bool JsArgToArrayConverter::ConvertArg(JsRuntime &rt, const JsValue &arg, int in castValue = NumericCasts::GetCastValue(rt, jsObj); floatValue = 0; if (!castValue.isUndefined()) { - floatValue = (jfloat) js_util::get_number(castValue); + floatValue = (jfloat) coerce::ToDouble(rt, castValue); } javaObject = JType::NewFloat(jEnv, floatValue); SetConvertedObject(jEnv, index, javaObject); @@ -213,7 +214,7 @@ bool JsArgToArrayConverter::ConvertArg(JsRuntime &rt, const JsValue &arg, int in castValue = NumericCasts::GetCastValue(rt, jsObj); doubleValue = 0; if (!castValue.isUndefined()) { - doubleValue = (jdouble) js_util::get_number(castValue); + doubleValue = (jdouble) coerce::ToDouble(rt, castValue); } javaObject = JType::NewDouble(jEnv, doubleValue); SetConvertedObject(jEnv, index, javaObject); diff --git a/NativeScript/ffi/jni/jsi/metadata/MetadataNode.cpp b/NativeScript/ffi/jni/jsi/metadata/MetadataNode.cpp index 35769276..23bf6760 100644 --- a/NativeScript/ffi/jni/jsi/metadata/MetadataNode.cpp +++ b/NativeScript/ffi/jni/jsi/metadata/MetadataNode.cpp @@ -9,6 +9,7 @@ #include #include "NativeScriptException.h" #include "MetadataNode.h" +#include "Coercion.h" #include "CallbackHandlers.h" #include "NativeScriptAssert.h" #include "File.h" @@ -187,7 +188,7 @@ JsValue MetadataNode::CreateJSWrapper(JsRuntime &rt, ObjectManager *objectManage JsValue MetadataNode::ArrayGetterCallback(JsRuntime &rt, const JsValue &thisVal, const JsValue *args, size_t argc) { return Guarded(rt, [&]() -> JsValue { - int32_t indexValue = argc > 0 ? js_util::get_int32(args[0]) : 0; + int32_t indexValue = argc > 0 ? coerce::ToInt32(rt, args[0]) : 0; auto node = GetInstanceMetadata(rt, thisVal); return CallbackHandlers::GetArrayElement(rt, thisVal, indexValue, node->m_name); @@ -221,7 +222,7 @@ JsValue MetadataNode::ArraySetterCallback(JsRuntime &rt, const JsValue &thisVal, return Guarded(rt, [&]() -> JsValue { if (argc < 2) return js_util::undefined(); - int32_t indexValue = js_util::get_int32(args[0]); + int32_t indexValue = coerce::ToInt32(rt, args[0]); auto node = GetInstanceMetadata(rt, thisVal); CallbackHandlers::SetArrayElement(rt, thisVal, indexValue, node->m_name, args[1]); diff --git a/NativeScript/ffi/jni/napi/callbackhandlers/CallbackHandlers.cpp b/NativeScript/ffi/jni/napi/callbackhandlers/CallbackHandlers.cpp index f050d6af..dfa6efb9 100644 --- a/NativeScript/ffi/jni/napi/callbackhandlers/CallbackHandlers.cpp +++ b/NativeScript/ffi/jni/napi/callbackhandlers/CallbackHandlers.cpp @@ -15,6 +15,7 @@ #include "Util.h" #include "JniLocalRef.h" #include "MetadataNode.h" +#include "Coercion.h" #include "MethodCache.h" #include "ArgConverter.h" #include "JsArgConverter.h" @@ -1833,8 +1834,8 @@ void CallbackHandlers::CallWorkerScopeOnErrorHandle(napi_env env, napi_value err line); } } else if (!napi_util::is_null_or_undefined(env, result)) { - bool handled; - NAPI_GUARD(napi_get_value_bool(env, result, &handled)) {} + bool handled = false; + NAPI_GUARD(coerce::GetBool(env, result, &handled)) {} if (handled) { return; } diff --git a/NativeScript/ffi/jni/napi/conversion/ArrayElementAccessor.cpp b/NativeScript/ffi/jni/napi/conversion/ArrayElementAccessor.cpp index 6d578196..a45c9cdf 100644 --- a/NativeScript/ffi/jni/napi/conversion/ArrayElementAccessor.cpp +++ b/NativeScript/ffi/jni/napi/conversion/ArrayElementAccessor.cpp @@ -1,6 +1,7 @@ #include "ArrayElementAccessor.h" #include "JsArgToArrayConverter.h" #include "ArgConverter.h" +#include "Coercion.h" #include "Util.h" #include "NativeScriptException.h" #include "Runtime.h" @@ -142,7 +143,7 @@ void ArrayElementAccessor::SetArrayElement(napi_env env, napi_value array, uint3 switch (arraySignature[1]) { case 'Z': { //bool bool b; - NAPI_GUARD(napi_get_value_bool(env, value, &b)) { + NAPI_GUARD(coerce::GetBool(env, value, &b)) { return; } jboolean v = static_cast(b); @@ -151,7 +152,7 @@ void ArrayElementAccessor::SetArrayElement(napi_env env, napi_value array, uint3 } case 'B': { //byte int32_t i; - NAPI_GUARD(napi_get_value_int32(env, value, &i)) { + NAPI_GUARD(coerce::GetInt32(env, value, &i)) { return; } jbyte v = static_cast(i); @@ -160,8 +161,16 @@ void ArrayElementAccessor::SetArrayElement(napi_env env, napi_value array, uint3 } case 'C': { //char size_t str_len; - NAPI_GUARD(napi_get_value_string_utf8(env, value, nullptr, 0, &str_len)) { - return; + status = napi_get_value_string_utf8(env, value, nullptr, 0, &str_len); + if (status != napi_ok) { + napi_value coerced; + NAPI_GUARD(coerce::ToString(env, value, &coerced)) { + return; + } + value = coerced; + NAPI_GUARD(napi_get_value_string_utf8(env, value, nullptr, 0, &str_len)) { + return; + } } string str(str_len, '\0'); NAPI_GUARD(napi_get_value_string_utf8(env, value, &str[0], str_len + 1, &str_len)) { @@ -177,7 +186,7 @@ void ArrayElementAccessor::SetArrayElement(napi_env env, napi_value array, uint3 } case 'S': { //short int32_t i; - NAPI_GUARD(napi_get_value_int32(env, value, &i)) { + NAPI_GUARD(coerce::GetInt32(env, value, &i)) { return; } jshort v = static_cast(i); @@ -186,7 +195,7 @@ void ArrayElementAccessor::SetArrayElement(napi_env env, napi_value array, uint3 } case 'I': { //int int32_t i; - NAPI_GUARD(napi_get_value_int32(env, value, &i)) { + NAPI_GUARD(coerce::GetInt32(env, value, &i)) { return; } jint v = static_cast(i); @@ -195,8 +204,17 @@ void ArrayElementAccessor::SetArrayElement(napi_env env, napi_value array, uint3 } case 'J': { //long int64_t l; - NAPI_GUARD(napi_get_value_int64(env, value, &l)) { - return; + status = napi_get_value_int64(env, value, &l); + if (status != napi_ok) { + napi_valuetype valueType; + napi_typeof(env, value, &valueType); + if (valueType == napi_object) { + l = ArgConverter::ConvertToJavaLong(env, value); + } else { + NAPI_GUARD(coerce::GetInt64(env, value, &l)) { + return; + } + } } jlong v = static_cast(l); jenv.SetLongArrayRegion((jlongArray) arr, index, 1, &v); @@ -204,7 +222,7 @@ void ArrayElementAccessor::SetArrayElement(napi_env env, napi_value array, uint3 } case 'F': { //float double d; - NAPI_GUARD(napi_get_value_double(env, value, &d)) { + NAPI_GUARD(coerce::GetDouble(env, value, &d)) { return; } jfloat v = static_cast(d); @@ -213,7 +231,7 @@ void ArrayElementAccessor::SetArrayElement(napi_env env, napi_value array, uint3 } case 'D': { //double double d; - NAPI_GUARD(napi_get_value_double(env, value, &d)) { + NAPI_GUARD(coerce::GetDouble(env, value, &d)) { return; } jdouble v = static_cast(d); diff --git a/NativeScript/ffi/jni/napi/conversion/Coercion.h b/NativeScript/ffi/jni/napi/conversion/Coercion.h new file mode 100644 index 00000000..6274cb54 --- /dev/null +++ b/NativeScript/ffi/jni/napi/conversion/Coercion.h @@ -0,0 +1,61 @@ +#ifndef COERCION_H_ +#define COERCION_H_ + +#include "js_native_api.h" + +namespace tns { +namespace coerce { + +// Each getter tries the strict Node-API getter first, so a value of the expected type costs what it +// always did. Only when that fails is the value coerced the way the classic V8 runtime does +// (ToNumber, ToInt32, ToBoolean, ToString). If coercion fails too, the failing status is returned +// and any exception the coercion raised is cleared, so the caller can log it and carry on. + +inline napi_status Recover(napi_env env, napi_status status) { + if (status == napi_pending_exception) { + napi_value ignored; + napi_get_and_clear_last_exception(env, &ignored); + } + return status; +} + +inline napi_status GetDouble(napi_env env, napi_value value, double *out) { + if (napi_get_value_double(env, value, out) == napi_ok) return napi_ok; + napi_value number; + napi_status status = napi_coerce_to_number(env, value, &number); + if (status != napi_ok) return Recover(env, status); + return napi_get_value_double(env, number, out); +} + +inline napi_status GetInt32(napi_env env, napi_value value, int32_t *out) { + if (napi_get_value_int32(env, value, out) == napi_ok) return napi_ok; + napi_value number; + napi_status status = napi_coerce_to_number(env, value, &number); + if (status != napi_ok) return Recover(env, status); + return napi_get_value_int32(env, number, out); +} + +inline napi_status GetInt64(napi_env env, napi_value value, int64_t *out) { + if (napi_get_value_int64(env, value, out) == napi_ok) return napi_ok; + napi_value number; + napi_status status = napi_coerce_to_number(env, value, &number); + if (status != napi_ok) return Recover(env, status); + return napi_get_value_int64(env, number, out); +} + +inline napi_status GetBool(napi_env env, napi_value value, bool *out) { + if (napi_get_value_bool(env, value, out) == napi_ok) return napi_ok; + napi_value boolean; + napi_status status = napi_coerce_to_bool(env, value, &boolean); + if (status != napi_ok) return Recover(env, status); + return napi_get_value_bool(env, boolean, out); +} + +inline napi_status ToString(napi_env env, napi_value value, napi_value *out) { + return Recover(env, napi_coerce_to_string(env, value, out)); +} + +} // namespace coerce +} // namespace tns + +#endif /* COERCION_H_ */ diff --git a/NativeScript/ffi/jni/napi/conversion/JsArgToArrayConverter.cpp b/NativeScript/ffi/jni/napi/conversion/JsArgToArrayConverter.cpp index fff0e76f..f9469bc9 100644 --- a/NativeScript/ffi/jni/napi/conversion/JsArgToArrayConverter.cpp +++ b/NativeScript/ffi/jni/napi/conversion/JsArgToArrayConverter.cpp @@ -1,4 +1,5 @@ #include "JsArgToArrayConverter.h" +#include "Coercion.h" #include #include "ObjectManager.h" #include "ArgConverter.h" @@ -218,7 +219,7 @@ bool JsArgToArrayConverter::ConvertArg(napi_env env, napi_value arg, int index) longValue = atoll(strValue.c_str()); } else { int64_t longArg; - NAPI_GUARD(napi_get_value_int64(env, castValue, &longArg)) { + NAPI_GUARD(coerce::GetInt64(env, castValue, &longArg)) { return false; } longValue = (jlong) longArg; @@ -234,7 +235,7 @@ bool JsArgToArrayConverter::ConvertArg(napi_env env, napi_value arg, int index) floatValue = 0; if (castValue != nullptr) { double floatArg; - NAPI_GUARD(napi_get_value_double(env, castValue, &floatArg)) { + NAPI_GUARD(coerce::GetDouble(env, castValue, &floatArg)) { return false; } floatValue = (jfloat) floatArg; @@ -249,7 +250,7 @@ bool JsArgToArrayConverter::ConvertArg(napi_env env, napi_value arg, int index) doubleValue = 0; if (castValue != nullptr) { double doubleArg; - NAPI_GUARD(napi_get_value_double(env, castValue, &doubleArg)) { + NAPI_GUARD(coerce::GetDouble(env, castValue, &doubleArg)) { return false; } doubleValue = (jdouble) doubleArg; diff --git a/NativeScript/ffi/jni/napi/metadata/MetadataNode.cpp b/NativeScript/ffi/jni/napi/metadata/MetadataNode.cpp index df18bb88..2ebf3ec7 100644 --- a/NativeScript/ffi/jni/napi/metadata/MetadataNode.cpp +++ b/NativeScript/ffi/jni/napi/metadata/MetadataNode.cpp @@ -9,6 +9,7 @@ #include #include "NativeScriptException.h" #include "MetadataNode.h" +#include "Coercion.h" #include "CallbackHandlers.h" #include "NativeScriptAssert.h" #include "File.h" @@ -166,7 +167,7 @@ napi_value MetadataNode::ArrayGetterCallback(napi_env env, napi_callback_info in napi_value index = argv[0]; int32_t indexValue; - NAPI_GUARD(napi_get_value_int32(env, index, &indexValue)) { + NAPI_GUARD(coerce::GetInt32(env, index, &indexValue)) { return nullptr; } auto node = GetInstanceMetadata(env, jsThis); @@ -235,7 +236,7 @@ napi_value MetadataNode::ArraySetterCallback(napi_env env, napi_callback_info in napi_value value = argv[1]; int32_t indexValue; - NAPI_GUARD(napi_get_value_int32(env, index, &indexValue)) { + NAPI_GUARD(coerce::GetInt32(env, index, &indexValue)) { return nullptr; } auto node = GetInstanceMetadata(env, jsThis); diff --git a/platforms/android/test-app/app/src/main/assets/app/tests/testArrays.js b/platforms/android/test-app/app/src/main/assets/app/tests/testArrays.js index 4dc40c59..c330381f 100644 --- a/platforms/android/test-app/app/src/main/assets/app/tests/testArrays.js +++ b/platforms/android/test-app/app/src/main/assets/app/tests/testArrays.js @@ -115,5 +115,84 @@ describe("Tests array operations", function () { expect(count).toBe(expectedLength); }); - -}); \ No newline at end of file + + describe("assigning values of another type to a primitive array element", function () { + + function createArray(wrapperName) { + return java.lang.reflect.Array.newInstance(java.lang[wrapperName].class.getField("TYPE").get(null), 1); + } + + it("converts the value to a number for float and double arrays", function () { + ["Float", "Double"].forEach(function (wrapperName) { + var arr = createArray(wrapperName); + + arr[0] = undefined; + expect(isNaN(arr[0])).toBe(true); + + arr[0] = null; + expect(arr[0]).toBe(0); + + arr[0] = "3.5"; + expect(arr[0]).toBe(3.5); + + arr[0] = true; + expect(arr[0]).toBe(1); + + arr[0] = { valueOf: function () { return 7; } }; + expect(arr[0]).toBe(7); + }); + }); + + it("converts the value to an integer for byte, short, int and long arrays", function () { + ["Byte", "Short", "Integer", "Long"].forEach(function (wrapperName) { + var arr = createArray(wrapperName); + + arr[0] = undefined; + expect(arr[0]).toBe(0); + + arr[0] = null; + expect(arr[0]).toBe(0); + + arr[0] = "42"; + expect(arr[0]).toBe(42); + + arr[0] = true; + expect(arr[0]).toBe(1); + }); + }); + + it("uses valueOf of an object assigned to byte, short and int arrays", function () { + ["Byte", "Short", "Integer"].forEach(function (wrapperName) { + var arr = createArray(wrapperName); + + arr[0] = { valueOf: function () { return 7; } }; + expect(arr[0]).toBe(7); + }); + }); + + it("converts the value to a boolean for boolean arrays", function () { + var arr = createArray("Boolean"); + + arr[0] = "x"; + expect(arr[0]).toBe(true); + + arr[0] = 0; + expect(arr[0]).toBe(false); + + arr[0] = undefined; + expect(arr[0]).toBe(false); + + arr[0] = ""; + expect(arr[0]).toBe(false); + }); + + it("converts the index of getValueAtIndex and setValueAtIndex to a number", function () { + var arr = java.lang.reflect.Array.newInstance(java.lang.Integer.class.getField("TYPE").get(null), 2); + + arr.setValueAtIndex("1", 5); + expect(arr.getValueAtIndex(1)).toBe(5); + expect(arr.getValueAtIndex("1")).toBe(5); + }); + }); + +});