From b5a9e9725092cba1139210d038e006885e03d7b0 Mon Sep 17 00:00:00 2001 From: Dylan Llewellyn <46717769+herefishyfish@users.noreply.github.com> Date: Fri, 18 Sep 2026 15:13:49 +0800 Subject: [PATCH 1/3] perf(android/napi): faster JNI bridge and bounded wrapper memory Node-API tree only. Measured on x86_64 emulator against this base; the runtime test suite passes at parity on V8, QuickJS, Hermes and JSC. - Object lifecycle: wrappers for Java-returned objects are held weakly (JS-constructed instances stay strong so Java can call back into them), each wrapper reports external memory so the engine collects under native pressure, the wrapper finalizer releases the Java side itself, and the id->ref map entries are released from the looper post-finalizers instead of waiting for a Java-GC notification. Fixes unbounded growth (LMK kill after ~200k returned objects). - No host-object proxy for plain instances (arrays keep it for indexed access); a live proxy for JS-constructed instances is still returned. - Wrapper creation: plain object with a cached class prototype instead of instantiating the JSObject class per wrapper; single napi_wrap attachment. - Return-type cache on MetadataEntry: declared return class resolved once per call site; skips the per-object Class.getName() up-call. - this resolution: weak-ref cache seeded at link time, no IsSameObject probe per hit; V8 resolves plain wrappers with napi_unwrap ahead of the #napi marker probe (V8 only: the QuickJS/JSC shims treat any object opaque as the wrap payload). - Strings stay UTF-16 in both directions; removes a double re-encode, a heap-buffer leak on every string argument and non-BMP mangling. - Metadata-first overload match is skipped once a single-candidate call site is bound (it re-ran a signature parse, napi_typeof and IsInstanceOf on every call). - js_get_array_doubles: bulk array read (V8: v8::Array::Iterate) for double[]/int[] arguments; generic napi_get_element loop elsewhere. - JNIEnv cached per thread; NewLocalRef skips a redundant ExceptionCheck. - JSC shim: JSString::CopyTo copied size bytes instead of size JSChars, so napi_get_value_string_utf16 returned garbage. Java call cost, base -> patched (us/call): V8 add 0.310 -> 0.139, concat 1.77 -> 0.81, sum array 1.96 -> 1.25, object return 11.4 -> 5.4 QuickJS add 0.366 -> 0.207, object return 7.1 -> 5.2 Hermes add 0.568 -> 0.376, object return 47.1 -> 9.4 --- .../callbackhandlers/CallbackHandlers.cpp | 31 ++- .../ffi/jni/napi/conversion/ArgConverter.h | 25 ++- .../jni/napi/conversion/JsArgConverter.cpp | 52 +++-- NativeScript/ffi/jni/napi/jni/JEnv.cpp | 45 ++-- NativeScript/ffi/jni/napi/jni/JEnv.h | 7 + NativeScript/ffi/jni/napi/jni/LRUCache.h | 8 +- .../ffi/jni/napi/metadata/MetadataEntry.h | 9 + .../ffi/jni/napi/metadata/MetadataNode.cpp | 31 ++- .../ffi/jni/napi/metadata/MetadataNode.h | 1 + .../jni/napi/objectmanager/ObjectManager.cpp | 208 ++++++++++++------ .../jni/napi/objectmanager/ObjectManager.h | 18 +- NativeScript/napi/common/jsr_common.h | 3 + NativeScript/napi/hermes/jsr.cpp | 16 ++ NativeScript/napi/jsc/jsr.cpp | 16 ++ NativeScript/napi/primjs/jsr.cpp | 16 ++ NativeScript/napi/quickjs/jsr.cpp | 16 ++ NativeScript/napi/v8/jsr.cpp | 35 +++ .../android/napi/workers/WorkerWrapper.cpp | 1 + vendor/jsc/jsc-api.cpp | 2 +- 19 files changed, 415 insertions(+), 125 deletions(-) diff --git a/NativeScript/ffi/jni/napi/callbackhandlers/CallbackHandlers.cpp b/NativeScript/ffi/jni/napi/callbackhandlers/CallbackHandlers.cpp index f6125f7f8..be3c3a067 100644 --- a/NativeScript/ffi/jni/napi/callbackhandlers/CallbackHandlers.cpp +++ b/NativeScript/ffi/jni/napi/callbackhandlers/CallbackHandlers.cpp @@ -313,11 +313,7 @@ napi_value CallbackHandlers::CallJavaMethod(napi_env env, napi_value caller, con result = jEnv.CallCharMethodA(callerJavaObject, mid, javaArgs); } - JniLocalRef str(jEnv.NewString(&result, 1)); - jboolean bol = true; - const char *resP = jEnv.GetStringUTFChars(str, &bol); - returnValue = ArgConverter::convertToJsString(env, resP, 1); - jEnv.ReleaseStringUTFChars(str, resP); + returnValue = ArgConverter::convertToJsString(env, &result, 1); break; } case MethodReturnType::Short: { @@ -432,8 +428,29 @@ napi_value CallbackHandlers::CallJavaMethod(napi_env env, napi_value caller, con returnValue = objectManager->GetJsObjectByJavaObject(javaObjectID); if (napi_util::is_null_or_undefined(env, returnValue)) { - returnValue = objectManager->CreateJSWrapper(javaObjectID, *returnType, - result); + MetadataNode *returnNode = nullptr; + JniLocalRef runtimeClazz(jEnv.GetObjectClass(result)); + if (entry != nullptr && !isArrayReturn) { + if (!entry->returnClazzResolved) { + entry->returnClazzResolved = true; + + if (returnType->size() > 2 && (*returnType)[0] == 'L') { + std::string declaredName = returnType->substr(1, returnType->size() - 2); + jclass declared = jEnv.FindClass(declaredName); + + if (declared != nullptr) { + entry->returnClazz = declared; + entry->returnNode = MetadataNode::GetOrCreate(declaredName); + } + } + } + if (entry->returnClazz != nullptr && jEnv.isSameObject(runtimeClazz, entry->returnClazz)) { + returnNode = entry->returnNode; + } + } + returnValue = returnNode != nullptr + ? objectManager->CreateJSWrapper(javaObjectID, returnNode, runtimeClazz, result) + : objectManager->CreateJSWrapper(javaObjectID, *returnType, result); } } diff --git a/NativeScript/ffi/jni/napi/conversion/ArgConverter.h b/NativeScript/ffi/jni/napi/conversion/ArgConverter.h index a42a14cf2..b172fdc8f 100644 --- a/NativeScript/ffi/jni/napi/conversion/ArgConverter.h +++ b/NativeScript/ffi/jni/napi/conversion/ArgConverter.h @@ -29,12 +29,12 @@ namespace tns { static napi_value jstringToJsString(napi_env env, jstring value) { if (value == nullptr) return napi_util::null(env); - JEnv jenv; - auto chars = jenv.GetStringUTFChars(value,JNI_FALSE); - auto length = jenv.GetStringUTFLength(value); - auto jsString = convertToJsString(env, chars, length); - jenv.ReleaseStringUTFChars(value, chars); - + JNIEnv *jni = JEnv(); + jsize length = jni->GetStringLength(value); + const jchar *chars = jni->GetStringChars(value, nullptr); + if (chars == nullptr) return napi_util::null(env); + napi_value jsString = convertToJsString(env, chars, length); + jni->ReleaseStringChars(value, chars); return jsString; } @@ -64,8 +64,19 @@ namespace tns { static std::u16string ConvertToUtf16String(napi_env env, napi_value s); inline static jstring ConvertToJavaString(napi_env env, napi_value jsValue) { + size_t length = 0; + if (napi_get_value_string_utf16(env, jsValue, nullptr, 0, &length) != napi_ok) return nullptr; + char16_t stack[256]; + std::u16string heap; + char16_t *buffer = stack; + if (length + 1 > sizeof stack / sizeof stack[0]) { + heap.resize(length + 1); + buffer = heap.data(); + } + size_t copied = 0; + if (napi_get_value_string_utf16(env, jsValue, buffer, length + 1, &copied) != napi_ok) return nullptr; JEnv jenv; - return jenv.NewStringUTF(napi_util::get_string_value(env, jsValue, 0)); + return jenv.NewString(reinterpret_cast(buffer), static_cast(copied)); } inline static napi_value convertToJsString(napi_env env, const jchar *data, int length) { diff --git a/NativeScript/ffi/jni/napi/conversion/JsArgConverter.cpp b/NativeScript/ffi/jni/napi/conversion/JsArgConverter.cpp index 1e9730c23..42d9a1ad3 100644 --- a/NativeScript/ffi/jni/napi/conversion/JsArgConverter.cpp +++ b/NativeScript/ffi/jni/napi/conversion/JsArgConverter.cpp @@ -1,4 +1,5 @@ #include "JsArgConverter.h" +#include "jsr_common.h" #include "ObjectManager.h" #include "JniSignatureParser.h" #include "JsArgToArrayConverter.h" @@ -549,9 +550,7 @@ bool JsArgConverter::ConvertJavaScriptArray(napi_env env, napi_value jsArr, int const auto &arraySignature = (*m_tokens)[index]; - std::string elementType = arraySignature.substr(1); - - const char elementTypePrefix = elementType[0]; + const char elementTypePrefix = arraySignature.size() > 1 ? arraySignature[1] : ' '; jclass elementClass; std::string strippedClassName; @@ -615,13 +614,22 @@ bool JsArgConverter::ConvertJavaScriptArray(napi_env env, napi_value jsArr, int } case 'I': { arr = jenv.NewIntArray(arrLength); + double stackDoubles[64]; + std::vector heapDoubles; + double *doubles = stackDoubles; + if (arrLength > 64) { heapDoubles.resize(arrLength); doubles = heapDoubles.data(); } + uint32_t got = 0; std::vector ints(arrLength); - for (uint32_t i = 0; i < arrLength; i++) { - napi_value element; - NAPI_GUARD(napi_get_element(env, jsArr, i, &element)) {} - int32_t intValue; - NAPI_GUARD(napi_get_value_int32(env, element, &intValue)) {} - ints[i] = (jint) intValue; + if (js_get_array_doubles(env, jsArr, doubles, arrLength, &got) == napi_ok && got == (uint32_t) arrLength) { + for (uint32_t i = 0; i < arrLength; i++) ints[i] = (jint) (int32_t) doubles[i]; + } else { + for (uint32_t i = 0; i < arrLength; i++) { + napi_value element; + NAPI_GUARD(napi_get_element(env, jsArr, i, &element)) {} + int32_t intValue; + NAPI_GUARD(napi_get_value_int32(env, element, &intValue)) {} + ints[i] = (jint) intValue; + } } jenv.SetIntArrayRegion((jintArray) arr, 0, arrLength, ints.data()); break; @@ -654,19 +662,25 @@ bool JsArgConverter::ConvertJavaScriptArray(napi_env env, napi_value jsArr, int } case 'D': { arr = jenv.NewDoubleArray(arrLength); - std::vector doubles(arrLength); - for (uint32_t i = 0; i < arrLength; i++) { - napi_value element; - NAPI_GUARD(napi_get_element(env, jsArr, i, &element)) {} - double doubleValue; - NAPI_GUARD(napi_get_value_double(env, element, &doubleValue)) {} - doubles[i] = (jdouble) doubleValue; + jdouble stackDoubles[64]; + std::vector heapDoubles; + jdouble *doubles = stackDoubles; + if (arrLength > 64) { heapDoubles.resize(arrLength); doubles = heapDoubles.data(); } + uint32_t got = 0; + if (js_get_array_doubles(env, jsArr, doubles, arrLength, &got) != napi_ok || got != (uint32_t) arrLength) { + for (uint32_t i = 0; i < arrLength; i++) { + napi_value element; + NAPI_GUARD(napi_get_element(env, jsArr, i, &element)) {} + double doubleValue; + NAPI_GUARD(napi_get_value_double(env, element, &doubleValue)) {} + doubles[i] = (jdouble) doubleValue; + } } - jenv.SetDoubleArrayRegion((jdoubleArray) arr, 0, arrLength, doubles.data()); + jenv.SetDoubleArrayRegion((jdoubleArray) arr, 0, arrLength, doubles); break; } case 'L': - strippedClassName = elementType.substr(1, elementType.length() - 2); + strippedClassName = arraySignature.substr(2, arraySignature.length() - 3); elementClass = jenv.FindClass(strippedClassName); arr = jenv.NewObjectArray(arrLength, elementClass, nullptr); for (uint32_t i = 0; i < arrLength; i++) { @@ -912,7 +926,7 @@ JniLocalRef JsArgConverter::GetByteBuffer(napi_env env, napi_value object, bool ObjectManager::MarkObject(env, object); - objectManager->Link(object, id, clazz); + objectManager->Link(object, id, clazz, nullptr, buffer); return objectManager->GetJavaObjectByJsObject(object); } diff --git a/NativeScript/ffi/jni/napi/jni/JEnv.cpp b/NativeScript/ffi/jni/napi/jni/JEnv.cpp index 190b2817a..ef936968d 100644 --- a/NativeScript/ffi/jni/napi/jni/JEnv.cpp +++ b/NativeScript/ffi/jni/napi/jni/JEnv.cpp @@ -7,30 +7,37 @@ using namespace tns; using namespace std; -JEnv::JEnv() - : m_env(nullptr) { - JNIEnv *env = nullptr; - jint ret = s_jvm->GetEnv(reinterpret_cast(&env), JNI_VERSION_1_6); +// A JNIEnv* is valid for as long as its thread stays attached, so resolve it once per +// thread instead of asking the JavaVM on every JEnv construction (a Java call builds +// several JEnv objects on its way through the bridge). +static thread_local JNIEnv *t_cachedEnv = nullptr; +static JNIEnv *ResolveEnv() { + JNIEnv *env = nullptr; + jint ret = JEnv::GetJavaVM()->GetEnv(reinterpret_cast(&env), JNI_VERSION_1_6); if ((ret != JNI_OK) || (env == nullptr)) { - ret = s_jvm->AttachCurrentThread(&env, nullptr); + ret = JEnv::GetJavaVM()->AttachCurrentThread(&env, nullptr); assert(ret == JNI_OK); assert(env != nullptr); } - - m_env = env; + t_cachedEnv = env; + return env; } -JEnv::JEnv(JNIEnv *jniEnv) { - jint ret = s_jvm->GetEnv(reinterpret_cast(&jniEnv), JNI_VERSION_1_6); - - if ((ret != JNI_OK) || (jniEnv == nullptr)) { - ret = s_jvm->AttachCurrentThread(&jniEnv, nullptr); - assert(ret == JNI_OK); - assert(jniEnv != nullptr); +JEnv::JEnv() + : m_env(t_cachedEnv) { + if (m_env == nullptr) [[unlikely]] { + m_env = ResolveEnv(); } +} - m_env = jniEnv; +JEnv::JEnv(JNIEnv *jniEnv) + : m_env(jniEnv) { + if (m_env == nullptr) [[unlikely]] { + m_env = ResolveEnv(); + } else { + t_cachedEnv = jniEnv; + } } JEnv::~JEnv() { @@ -571,9 +578,7 @@ void JEnv::DeleteWeakGlobalRef(jweak obj) { } jobject JEnv::NewLocalRef(jobject ref) { - jobject jo = m_env->NewLocalRef(ref); - CheckForJavaException(); - return jo; + return m_env->NewLocalRef(ref); } void JEnv::DeleteLocalRef(jobject localRef) { @@ -899,3 +904,7 @@ JEnv::GetInterfaceStaticMethodIDAndJClass(const std::string &interfaceName, } + +void JEnv::ClearCachedEnv() { + t_cachedEnv = nullptr; +} diff --git a/NativeScript/ffi/jni/napi/jni/JEnv.h b/NativeScript/ffi/jni/napi/jni/JEnv.h index b1a544e4c..d2e830fa6 100644 --- a/NativeScript/ffi/jni/napi/jni/JEnv.h +++ b/NativeScript/ffi/jni/napi/jni/JEnv.h @@ -454,6 +454,13 @@ namespace tns { static JavaVM *s_jvm; + public: + static JavaVM *GetJavaVM() { return s_jvm; } + // Call right before DetachCurrentThread so a re-attached thread does not reuse a stale env. + static void ClearCachedEnv(); + + private: + static jclass RUNTIME_CLASS; static jmethodID GET_CACHED_CLASS_METHOD_ID; diff --git a/NativeScript/ffi/jni/napi/jni/LRUCache.h b/NativeScript/ffi/jni/napi/jni/LRUCache.h index 7b829c3dd..87385bd48 100644 --- a/NativeScript/ffi/jni/napi/jni/LRUCache.h +++ b/NativeScript/ffi/jni/napi/jni/LRUCache.h @@ -100,13 +100,15 @@ class LRUCache { } } + void seed(const key_type& key, const value_type& value) { + if (m_key_to_value.find(key) == m_key_to_value.end()) insert(key, value); + } + void update(const key_type& key, const value_type& value) { jweak ref = m_loadCallback(key, m_state); insert(key, ref); } - private: - // Evict a specific key (used when a cached value is no longer valid). void evictKey(const key_type& key) { auto it = m_key_to_value.find(key); @@ -119,6 +121,8 @@ class LRUCache { } } + private: + // Record a fresh key-value pair in the cache void insert(const key_type& k, const value_type& v) { // Method is only called on cache misses diff --git a/NativeScript/ffi/jni/napi/metadata/MetadataEntry.h b/NativeScript/ffi/jni/napi/metadata/MetadataEntry.h index 6e708526c..10de752d6 100644 --- a/NativeScript/ffi/jni/napi/metadata/MetadataEntry.h +++ b/NativeScript/ffi/jni/napi/metadata/MetadataEntry.h @@ -7,6 +7,8 @@ #include "MetadataMethodInfo.h" #include "MetadataFieldInfo.h" +class MetadataNode; + namespace tns { enum class NodeType { Package, @@ -49,6 +51,9 @@ namespace tns { memberId = other.memberId; clazz = other.clazz; parsedSig = other.parsedSig; + returnClazz = other.returnClazz; + returnClazzResolved = other.returnClazzResolved; + returnNode = other.returnNode; mi = other.mi; fi = other.fi; sfi = other.sfi; @@ -91,6 +96,10 @@ namespace tns { jclass clazz; std::vector parsedSig; + jclass returnClazz = nullptr; + bool returnClazzResolved = false; + ::MetadataNode *returnNode = nullptr; + MethodInfo mi; FieldInfo *fi; StaticFieldInfo *sfi; diff --git a/NativeScript/ffi/jni/napi/metadata/MetadataNode.cpp b/NativeScript/ffi/jni/napi/metadata/MetadataNode.cpp index a7631b637..53c64fb70 100644 --- a/NativeScript/ffi/jni/napi/metadata/MetadataNode.cpp +++ b/NativeScript/ffi/jni/napi/metadata/MetadataNode.cpp @@ -148,10 +148,25 @@ napi_value MetadataNode::CreateJSWrapper(napi_env env, ObjectManager *objectMana if (m_isArray) { obj = CreateArrayWrapper(env); } else { - obj = objectManager->GetEmptyObject(); - napi_value ctorFunc = GetConstructorFunction(env); - NAPI_GUARD(napi_set_named_property(env, obj, CONSTRUCTOR, ctorFunc)) {} - napi_util::setPrototypeOf(env, obj, napi_util::get_prototype(env, ctorFunc)); + napi_value prototype = nullptr; + auto cache = GetMetadataNodeCache(env); + auto itFound = cache->CtorFuncCache.find(m_treeNode); + if (itFound != cache->CtorFuncCache.end() && itFound->second.wrapperPrototype != nullptr) { + prototype = napi_util::get_ref_value(env, itFound->second.wrapperPrototype); + } + if (prototype == nullptr || napi_util::is_null_or_undefined(env, prototype)) { + napi_value ctorFunc = GetConstructorFunction(env); + prototype = napi_util::get_prototype(env, ctorFunc); + itFound = cache->CtorFuncCache.find(m_treeNode); + if (itFound != cache->CtorFuncCache.end()) { + if (itFound->second.wrapperPrototype != nullptr) { + NAPI_GUARD(napi_delete_reference(env, itFound->second.wrapperPrototype)) {} + } + itFound->second.wrapperPrototype = napi_util::make_ref(env, prototype, 1); + } + } + NAPI_GUARD(napi_create_object(env, &obj)) { return nullptr; } + napi_util::setPrototypeOf(env, obj, prototype); SetInstanceMetadata(env, obj, this); } @@ -2757,7 +2772,13 @@ napi_value MetadataNode::MethodCallback(napi_env env, napi_callback_info info) { } return true; }; - if (!first.isStatic && !metadataSignatureIsUnambiguous) { + // The metadata-first match only decides how a not-yet-bound entry gets its jmethodID. + // Once the single candidate of a call site is bound there is nothing left to decide, so + // skip the per-call signature parse / napi_typeof / IsInstanceOf walk entirely. + const bool alreadyBound = entry != nullptr && entry->memberId != nullptr && + initialCallbackData->parent == nullptr && + initialCallbackData->candidates.size() == 1; + if (!first.isStatic && !metadataSignatureIsUnambiguous && !alreadyBound) { for (auto *candidateData = initialCallbackData; candidateData != nullptr; candidateData = candidateData->parent) { diff --git a/NativeScript/ffi/jni/napi/metadata/MetadataNode.h b/NativeScript/ffi/jni/napi/metadata/MetadataNode.h index d78ab9e77..277429290 100644 --- a/NativeScript/ffi/jni/napi/metadata/MetadataNode.h +++ b/NativeScript/ffi/jni/napi/metadata/MetadataNode.h @@ -266,6 +266,7 @@ class MetadataNode { napi_ref constructorFunction; std::vector instanceMethodCallbacks; + napi_ref wrapperPrototype = nullptr; }; struct MethodCallbackData { diff --git a/NativeScript/ffi/jni/napi/objectmanager/ObjectManager.cpp b/NativeScript/ffi/jni/napi/objectmanager/ObjectManager.cpp index a5c67014a..8c79fe23a 100644 --- a/NativeScript/ffi/jni/napi/objectmanager/ObjectManager.cpp +++ b/NativeScript/ffi/jni/napi/objectmanager/ObjectManager.cpp @@ -6,6 +6,7 @@ #include "NativeScriptException.h" #include "Runtime.h" #include "CallbackHandlers.h" +#include "jsr_common.h" #include #include @@ -20,7 +21,7 @@ jmethodID ObjectManager::GET_NAME_METHOD_ID = nullptr; ObjectManager::ObjectManager(jobject javaRuntimeObject) : m_javaRuntimeObject(javaRuntimeObject), - m_cache(NewWeakGlobalRefCallback, DeleteWeakGlobalRefCallback, ValidateWeakGlobalRefCallback, 1000, this), + m_cache(NewWeakGlobalRefCallback, DeleteWeakGlobalRefCallback, nullptr, 1000, this), m_currentObjectId(0), m_jsObjectProxyCreator(nullptr), m_jsObjectCtor(nullptr), @@ -211,6 +212,19 @@ napi_value ObjectManager::GetOrCreateProxy(jint javaObjectID, napi_value instanc } JniLocalRef ObjectManager::GetJavaObjectByJsObject(napi_value object, int *objectId, bool *isSuper) { + int32_t javaObjectId = ResolveJavaObjectId(object, objectId, isSuper); + if (javaObjectId != -1) { + try { + return {GetJavaObjectByID(javaObjectId), true}; + } catch (NativeScriptException &e) { + throw NativeScriptException("Failed to get Java object by ID. id=" + + std::to_string(javaObjectId) + ". " + e.what()); + } + } + return {}; +} + +int ObjectManager::ResolveJavaObjectId(napi_value object, int *objectId, bool *isSuper) { napi_status status; int32_t javaObjectId = (objectId) ? *objectId : -1; // Cache slot for the super-call flag on whichever per-object info we resolve; @@ -218,7 +232,7 @@ JniLocalRef ObjectManager::GetJavaObjectByJsObject(napi_value object, int *objec int8_t *superSlot = nullptr; #ifdef USE_HOST_OBJECT - // Non-host object → miss (an error status on some engines); expected, ignore. + // Non-host object -> miss (an error status on some engines); expected, ignore. void* data = nullptr; napi_get_host_object_data(m_env, object, &data); if (data) { @@ -258,19 +272,7 @@ JniLocalRef ObjectManager::GetJavaObjectByJsObject(napi_value object, int *objec if (objectId) { *objectId = javaObjectId; } - - if (javaObjectId != -1) { - try { - return {GetJavaObjectByID(javaObjectId), true}; - } catch (NativeScriptException &e) { - // Surface which object failed instead of a bare error — this usually - // means the id belongs to a different runtime/thread. - throw NativeScriptException("Failed to get Java object by ID. id=" + - std::to_string(javaObjectId) + ". " + e.what()); - } - } - - return {}; + return javaObjectId; } JniLocalRef ObjectManager::GetJavaObjectByJsObjectFast(napi_value object) { @@ -302,7 +304,9 @@ JniLocalRef ObjectManager::GetJavaObjectByJsObjectFast(napi_value object) { ObjectManager::JSInstanceInfo *ObjectManager::GetJSInstanceInfo(napi_value object) { #ifdef USE_HOST_OBJECT - // Non-host object → miss (an error status on some engines); expected, ignore. + // Host objects must be probed first: the V8 shim's napi_unwrap reads internal field 0 as the + // wrap Reference on any object with internal fields, and a host proxy keeps its own pointer + // there. A non-host object is a miss (an error status on some engines); expected, ignore. void *hostData = nullptr; napi_get_host_object_data(m_env, object, &hostData); if (hostData) { @@ -312,11 +316,20 @@ ObjectManager::JSInstanceInfo *ObjectManager::GetJSInstanceInfo(napi_value objec } } #endif - + +#ifdef __V8__ + // V8 only: its napi_unwrap is safe on arbitrary objects (private symbol lookup), so plain + // wrappers resolve with one read and skip the "#napi" prototype-chain probe. The QuickJS and + // JSC shims treat any object opaque as the wrap payload, so there the marker check must stay + // in front of napi_unwrap (see GetJSInstanceInfoFromRuntimeObject). + void *wrapped = nullptr; + napi_unwrap(m_env, object, &wrapped); + if (wrapped != nullptr) return reinterpret_cast(wrapped); +#endif + if (!IsRuntimeJsObject(object)) return nullptr; return GetJSInstanceInfoFromRuntimeObject(object); } - MetadataNode *ObjectManager::GetInstanceNode(napi_value object) { JSInstanceInfo *info = GetJSInstanceInfo(object); return info != nullptr ? info->node : nullptr; @@ -542,8 +555,7 @@ napi_value ObjectManager::HostObjectIndexedGet(napi_env env, napi_value host, // The proxy already knows the java object id + ObjectManager, so resolve // the backing array directly (no locked env->runtime lookup, no host probe). jobject arr = proxy->instanceInfo - ? (jobject) proxy->objectManager->GetJavaObjectByID( - proxy->instanceInfo->JavaObjectID) + ? (jobject) proxy->objectManager->GetJavaObjectByID(proxy->instanceInfo->JavaObjectID) : nullptr; return CallbackHandlers::GetArrayElement(env, host, index, proxy->arraySignature, proxy->objectManager, arr); @@ -565,8 +577,7 @@ void ObjectManager::HostObjectIndexedSet(napi_env env, napi_value host, auto *proxy = reinterpret_cast(data); try { jobject arr = proxy->instanceInfo - ? (jobject) proxy->objectManager->GetJavaObjectByID( - proxy->instanceInfo->JavaObjectID) + ? (jobject) proxy->objectManager->GetJavaObjectByID(proxy->instanceInfo->JavaObjectID) : nullptr; CallbackHandlers::SetArrayElement(env, host, index, proxy->arraySignature, value, proxy->objectManager, arr); @@ -638,6 +649,18 @@ void ObjectManager::HostObjectProxyPostFinalizer(napi_env env, void *data, NAPI_GUARD(napi_delete_reference(env, proxy->target)) {} } + if (proxy->isPrimary && proxy->instanceInfo && !destroying) { + auto objManager = rt->GetObjectManager(); + auto it = objManager->m_idToProxy.find(proxy->instanceInfo->JavaObjectID); + if (it != objManager->m_idToProxy.end() && it->second != nullptr) { + napi_value current = napi_util::get_ref_value(env, it->second); + if (napi_util::is_null_or_undefined(env, current)) { + NAPI_GUARD(napi_delete_reference(env, it->second)) {} + objManager->m_idToProxy.erase(it); + } + } + } + // Primary (cached) proxies own their JSInstanceInfo and mark the java // instance weak on collection (the old JSObjectProxyFinalizerCallback role). if (proxy->isPrimary && proxy->instanceInfo) { @@ -715,25 +738,16 @@ napi_value ObjectManager::CreateHostObjectProxy(napi_value instance, ObjectManager::JSInstanceInfo * ObjectManager::GetJSInstanceInfoFromRuntimeObject(napi_value object) { - napi_status status; - napi_value jsInfo; - NAPI_GUARD(napi_get_named_property(m_env, object, PRIVATE_JSINFO, &jsInfo)) {} - - if (napi_util::is_null_or_undefined(m_env, jsInfo)) { - napi_value proto = napi_util::get__proto__(m_env, object); - //Typescript object layout has an object instance as child of the actual registered instance. checking for that - if (!napi_util::is_null_or_undefined(m_env, proto)) { - if (IsRuntimeJsObject(proto)) { - NAPI_GUARD(napi_get_named_property(m_env, proto, PRIVATE_JSINFO, &jsInfo)) {} - } - } - } + // The info lives in the napi_wrap slot. A miss is expected for plain objects; ignore status. + void *data = nullptr; + napi_unwrap(m_env, object, &data); + if (data != nullptr) return reinterpret_cast(data); - if (!napi_util::is_null_or_undefined(m_env, jsInfo)) { - void *data = nullptr; - NAPI_GUARD(napi_get_value_external(m_env, jsInfo, &data)) {} - auto info = reinterpret_cast(data); - return info; + // TypeScript object layout has an object instance as child of the actual registered instance. + napi_value proto = napi_util::get__proto__(m_env, object); + if (!napi_util::is_null_or_undefined(m_env, proto) && IsRuntimeJsObject(proto)) { + napi_unwrap(m_env, proto, &data); + if (data != nullptr) return reinterpret_cast(data); } return nullptr; } @@ -750,9 +764,12 @@ bool ObjectManager::IsRuntimeJsObject(napi_value object) { } jweak ObjectManager::GetJavaObjectByID(uint32_t javaObjectID) { + // The weak global is handed out unowned: Java holds every linked instance strongly until its + // wrapper finalizes, and release paths evict the entry, so no IsSameObject probe per hit. return m_cache(javaObjectID); } + jobject ObjectManager::GetJavaObjectByIDImpl(uint32_t javaObjectID) { JEnv env; jobject object = env.CallObjectMethod(m_javaRuntimeObject, GET_JAVAOBJECT_BY_ID_METHOD_ID, @@ -791,6 +808,17 @@ napi_value ObjectManager::GetJsObjectByJavaObject(int javaObjectID) { napi_value instance = napi_util::get_ref_value(m_env, it->second); if (napi_util::is_null_or_undefined(m_env, instance)) return nullptr; + // Identity: if JS already holds a proxy for this id (instances constructed from JS are + // handed out as proxies by RegisterInstance), that proxy is the object JS knows. + auto proxyIt = m_idToProxy.find(javaObjectID); + if (proxyIt != m_idToProxy.end() && proxyIt->second != nullptr) { + napi_value proxy = napi_util::get_ref_value(m_env, proxyIt->second); + if (!napi_util::is_null_or_undefined(m_env, proxy)) return proxy; + } + // Otherwise plain instances are handed out directly: the wrapper is weakly tracked and + // finalizes itself, so the host-object proxy only earns its keep for arrays (indexed access). + MetadataNode *node = GetInstanceNode(instance); + if (node != nullptr && !node->isArray()) return instance; return GetOrCreateProxy(javaObjectID, instance); } @@ -805,15 +833,28 @@ ObjectManager::CreateJSWrapper(jint javaObjectID, const std::string &typeName, j JEnv jenv; JniLocalRef clazz(jenv.GetObjectClass(instance)); - return CreateJSWrapperHelper(javaObjectID, typeName, clazz); + return CreateJSWrapperHelper(javaObjectID, typeName, clazz, instance); } napi_value -ObjectManager::CreateJSWrapperHelper(jint javaObjectID, const std::string &typeName, jclass clazz) { - napi_status status; - auto className = (clazz != nullptr) ? GetClassName(clazz) : typeName; +ObjectManager::CreateJSWrapper(jint javaObjectID, MetadataNode *node, jclass clazz, jobject instance) { + return CreateJSWrapperForNode(javaObjectID, node, clazz, instance); +} +napi_value +ObjectManager::CreateJSWrapperHelper(jint javaObjectID, const std::string &typeName, jclass clazz, jobject instance) { + auto className = (clazz != nullptr) ? GetClassName(clazz) : typeName; auto node = MetadataNode::GetOrCreate(className); + if (clazz == nullptr) { + JEnv jenv; + clazz = jenv.FindClass(className); + } + return CreateJSWrapperForNode(javaObjectID, node, clazz, instance); +} + +napi_value +ObjectManager::CreateJSWrapperForNode(jint javaObjectID, MetadataNode *node, jclass clazz, jobject instance) { + napi_status status; napi_value proxy = nullptr; napi_value jsWrapper = node->CreateJSWrapper(m_env, this); if (jsWrapper != nullptr) { @@ -822,25 +863,28 @@ ObjectManager::CreateJSWrapperHelper(jint javaObjectID, const std::string &typeN // stored on JSInstanceInfo::ObjectClazz, which nothing on this path reads, // so a fresh FindClass is pure overhead; only fall back to it for the // typeName-only overload where no instance class was available. - jclass linkClazz = clazz; - if (linkClazz == nullptr) { - JEnv jenv; - linkClazz = jenv.FindClass(className); - } - Link(jsWrapper, javaObjectID, linkClazz, node); + Link(jsWrapper, javaObjectID, clazz, node, instance, /*strongRef*/ false, /*verified*/ true); if (node->isArray()) { NAPI_GUARD(napi_set_named_property(m_env, jsWrapper, "__is__javaArray", napi_util::get_true(m_env))) {} } - proxy = GetOrCreateProxy(javaObjectID, jsWrapper); + proxy = node->isArray() ? GetOrCreateProxy(javaObjectID, jsWrapper) : jsWrapper; } return proxy; } void ObjectManager::Link(napi_value object, uint32_t javaObjectID, jclass clazz, - MetadataNode *node) { - if (!IsRuntimeJsObject(object)) { + MetadataNode *node, jobject instance, bool strongRef, bool verified) { + if (instance != nullptr) { + // Seed the id->object cache so the first method/field access on this wrapper does not + // round-trip into Java (getJavaObjectByID) to fetch an object we are holding right now. + JEnv jenv; + m_cache.seed(javaObjectID, jenv.NewWeakGlobalRef(instance)); + } + // Callers that just built the wrapper themselves pass verified=true and skip the + // prototype-chain marker probe. + if (!verified && !IsRuntimeJsObject(object)) { std::string errMsg("Trying to link invalid 'this' to a Java object"); throw NativeScriptException(errMsg); } @@ -851,17 +895,20 @@ void ObjectManager::Link(napi_value object, uint32_t javaObjectID, jclass clazz, auto jsInstanceInfo = new JSInstanceInfo(javaObjectID, clazz); jsInstanceInfo->node = node; - napi_ref objectHandle = napi_util::make_ref(m_env, object, 1); + napi_ref objectHandle = napi_util::make_ref(m_env, object, strongRef ? 1 : 0); - napi_value jsInfo; - NAPI_GUARD(napi_create_external(m_env, jsInstanceInfo, JSObjectFinalizerCallback, jsInstanceInfo, &jsInfo)) {} - NAPI_GUARD(napi_set_named_property(m_env, object, PRIVATE_JSINFO, jsInfo)) {} + int64_t externalMemory = 0; + js_adjust_external_memory(m_env, kWrapperExternalCost, &externalMemory); - // Wrapped but does not handle data lifecycle. only used for fast access. - NAPI_GUARD(napi_wrap(m_env, object, jsInstanceInfo, [](napi_env env, void *data, void *hint) {}, jsInstanceInfo, - nullptr)) {} + NAPI_GUARD(napi_wrap(m_env, object, jsInstanceInfo, JSObjectFinalizerCallback, jsInstanceInfo, nullptr)) {} - m_idToObject.emplace(javaObjectID, objectHandle); + auto existing = m_idToObject.find(javaObjectID); + if (existing != m_idToObject.end()) { + NAPI_GUARD(napi_delete_reference(m_env, existing->second)) {} + existing->second = objectHandle; + } else { + m_idToObject.emplace(javaObjectID, objectHandle); + } } bool ObjectManager::CloneLink(napi_value src, napi_value dest) { @@ -871,9 +918,6 @@ bool ObjectManager::CloneLink(napi_value src, napi_value dest) { auto success = jsInfo != nullptr; if (success) { - napi_value external; - NAPI_GUARD(napi_create_external(m_env, jsInfo, [](napi_env env, void* d1, void*d2) {}, jsInfo, &external)) {} - NAPI_GUARD(napi_set_named_property(m_env, dest, PRIVATE_JSINFO, external)) {} NAPI_GUARD(napi_wrap(m_env, dest, jsInfo, [](napi_env env, void *data, void *hint) {}, jsInfo, nullptr)) {} } @@ -910,6 +954,39 @@ ObjectManager::JSObjectFinalizerCallback(napi_env env, void *finalizeData, void #endif DEBUG_WRITE("JS Object finalizer called for object id: %d", data->JavaObjectID); + + auto rt = Runtime::GetRuntimeUnchecked(env); + if (rt && !rt->is_destroying) { + auto objManager = rt->GetObjectManager(); + if (objManager->m_weakObjectIds.find(data->JavaObjectID) == objManager->m_weakObjectIds.end()) { + objManager->m_weakObjectIds.emplace(data->JavaObjectID); + JEnv jEnv; + jEnv.CallVoidMethod(objManager->m_javaRuntimeObject, objManager->MAKE_INSTANCE_WEAK_METHOD_ID, + data->JavaObjectID); + } + } + Runtime::PostFinalizer(env, JSObjectPostFinalizerCallback, data, nullptr); +} + +void ObjectManager::JSObjectPostFinalizerCallback(napi_env env, void *finalizeData, void *) { + napi_status status; + auto data = reinterpret_cast(finalizeData); + if (data == nullptr) return; + auto rt = Runtime::GetRuntimeUnchecked(env); + if (rt && !rt->is_destroying) { + int64_t externalMemory = 0; + js_adjust_external_memory(env, -kWrapperExternalCost, &externalMemory); + + auto objManager = rt->GetObjectManager(); + auto it = objManager->m_idToObject.find(data->JavaObjectID); + if (it != objManager->m_idToObject.end()) { + napi_value current = napi_util::get_ref_value(env, it->second); + if (napi_util::is_null_or_undefined(env, current)) { + NAPI_GUARD(napi_delete_reference(env, it->second)) {} + objManager->m_idToObject.erase(it); + } + } + } delete data; } @@ -1013,6 +1090,8 @@ void ObjectManager::ReleaseObjectNow(napi_env env, int javaObjectId) { if (!rt || rt->is_destroying) return; ObjectManager *objMgr = rt->GetObjectManager(); + objMgr->m_cache.evictKey(javaObjectId); + auto itFound = objMgr->m_weakObjectIds.find(javaObjectId); if (itFound == objMgr->m_weakObjectIds.end()) { JEnv jEnv; @@ -1075,6 +1154,7 @@ void ObjectManager::OnGarbageCollected(JNIEnv *jEnv, jintArray object_ids) { auto rt = Runtime::GetRuntimeUnchecked(m_env); if (rt && rt->is_destroying) return; int javaObjectId = cppArray[i]; + this->m_cache.evictKey(javaObjectId); auto itFound = this->m_idToObject.find(javaObjectId); if (itFound != this->m_idToObject.end()) { NAPI_GUARD(napi_delete_reference(m_env, itFound->second)) {} diff --git a/NativeScript/ffi/jni/napi/objectmanager/ObjectManager.h b/NativeScript/ffi/jni/napi/objectmanager/ObjectManager.h index b8e4b3938..2709bf00e 100644 --- a/NativeScript/ffi/jni/napi/objectmanager/ObjectManager.h +++ b/NativeScript/ffi/jni/napi/objectmanager/ObjectManager.h @@ -27,6 +27,7 @@ namespace tns { JniLocalRef GetJavaObjectByJsObject(napi_value object, int *objectId = nullptr, bool *isSuper = nullptr); + int ResolveJavaObjectId(napi_value object, int *objectId, bool *isSuper); JniLocalRef GetJavaObjectByJsObjectFast(napi_value object); @@ -47,12 +48,19 @@ namespace tns { napi_value CreateJSWrapper(jint javaObjectID, const std::string &typeName, jobject instance); + napi_value + CreateJSWrapper(jint javaObjectID, MetadataNode *node, jclass clazz, jobject instance); + napi_value GetOrCreateProxy(jint javaObjectID, napi_value instance); napi_value GetOrCreateProxyWeak(jint javaObjectID, napi_value instance); void Link(napi_value object, uint32_t javaObjectID, jclass clazz, - MetadataNode *node = nullptr); + MetadataNode *node = nullptr, + jobject instance = nullptr, + bool strongRef = true, + bool verified = false); + // Returns the class metadata stored on the per-instance JSInstanceInfo // (host proxy's, or the raw instance's wrap). Used by @@ -163,15 +171,19 @@ namespace tns { JSInstanceInfo *GetJSInstanceInfoFromRuntimeObject(napi_value object); napi_value - CreateJSWrapperHelper(jint javaObjectID, const std::string &typeName, jclass clazz); + CreateJSWrapperHelper(jint javaObjectID, const std::string &typeName, jclass clazz, jobject instance = nullptr); + napi_value + CreateJSWrapperForNode(jint javaObjectID, MetadataNode *node, jclass clazz, jobject instance); static void JSObjectFinalizerCallback(napi_env env, void *finalizeData, void *finalizeHint); + static void JSObjectPostFinalizerCallback(napi_env env, void *finalizeData, void *finalizeHint); static void JSObjectProxyFinalizerCallback(napi_env env, void *finalizeData, void *finalizeHint); jweak GetJavaObjectByID(uint32_t javaObjectID); + jobject GetJavaObjectByIDImpl(uint32_t javaObjectID); static jweak NewWeakGlobalRefCallback(const int &javaObjectID, void *state); @@ -191,6 +203,8 @@ namespace tns { LRUCache m_cache; + static constexpr int64_t kWrapperExternalCost = 1024; + volatile int m_currentObjectId; DirectBuffer m_buff; diff --git a/NativeScript/napi/common/jsr_common.h b/NativeScript/napi/common/jsr_common.h index 6c579a1c5..fd63b00e0 100644 --- a/NativeScript/napi/common/jsr_common.h +++ b/NativeScript/napi/common/jsr_common.h @@ -50,6 +50,9 @@ napi_status js_run_bytecode_file(napi_env env, const char *file, napi_value *res napi_status js_get_runtime_version(napi_env env, napi_value* version); +napi_status js_get_array_doubles(napi_env env, napi_value array, double* out, uint32_t capacity, + uint32_t* length); + // Invoked by engine-specific env teardown to execute registered node-api // cleanup hooks for the environment before it is released. void js_run_env_cleanup_hooks(napi_env env); diff --git a/NativeScript/napi/hermes/jsr.cpp b/NativeScript/napi/hermes/jsr.cpp index 0da333809..44b8274a8 100644 --- a/NativeScript/napi/hermes/jsr.cpp +++ b/NativeScript/napi/hermes/jsr.cpp @@ -405,3 +405,19 @@ extern "C" napi_status jsr_drain_microtasks(napi_env env, return napi_ok; } + +napi_status js_get_array_doubles(napi_env env, napi_value array, double* out, uint32_t capacity, + uint32_t* length) { + bool isArray = false; + if (napi_is_array(env, array, &isArray) != napi_ok || !isArray) return napi_array_expected; + uint32_t count = 0; + if (napi_get_array_length(env, array, &count) != napi_ok) return napi_array_expected; + if (count > capacity) count = capacity; + for (uint32_t i = 0; i < count; i++) { + napi_value element; + if (napi_get_element(env, array, i, &element) != napi_ok) return napi_generic_failure; + if (napi_get_value_double(env, element, &out[i]) != napi_ok) return napi_number_expected; + } + *length = count; + return napi_ok; +} diff --git a/NativeScript/napi/jsc/jsr.cpp b/NativeScript/napi/jsc/jsr.cpp index 93b224300..922c59541 100644 --- a/NativeScript/napi/jsc/jsr.cpp +++ b/NativeScript/napi/jsc/jsr.cpp @@ -370,3 +370,19 @@ napi_status js_get_runtime_version(napi_env env, napi_value* version) { return napi_ok; } + +napi_status js_get_array_doubles(napi_env env, napi_value array, double* out, uint32_t capacity, + uint32_t* length) { + bool isArray = false; + if (napi_is_array(env, array, &isArray) != napi_ok || !isArray) return napi_array_expected; + uint32_t count = 0; + if (napi_get_array_length(env, array, &count) != napi_ok) return napi_array_expected; + if (count > capacity) count = capacity; + for (uint32_t i = 0; i < count; i++) { + napi_value element; + if (napi_get_element(env, array, i, &element) != napi_ok) return napi_generic_failure; + if (napi_get_value_double(env, element, &out[i]) != napi_ok) return napi_number_expected; + } + *length = count; + return napi_ok; +} diff --git a/NativeScript/napi/primjs/jsr.cpp b/NativeScript/napi/primjs/jsr.cpp index 2005428d3..252b4ef62 100644 --- a/NativeScript/napi/primjs/jsr.cpp +++ b/NativeScript/napi/primjs/jsr.cpp @@ -177,3 +177,19 @@ napi_status js_get_runtime_version(napi_env env, napi_value *version) { napi_create_string_utf8(env, "PrimJS", NAPI_AUTO_LENGTH, version); return napi_ok; } + +napi_status js_get_array_doubles(napi_env env, napi_value array, double* out, uint32_t capacity, + uint32_t* length) { + bool isArray = false; + if (napi_is_array(env, array, &isArray) != napi_ok || !isArray) return napi_array_expected; + uint32_t count = 0; + if (napi_get_array_length(env, array, &count) != napi_ok) return napi_array_expected; + if (count > capacity) count = capacity; + for (uint32_t i = 0; i < count; i++) { + napi_value element; + if (napi_get_element(env, array, i, &element) != napi_ok) return napi_generic_failure; + if (napi_get_value_double(env, element, &out[i]) != napi_ok) return napi_number_expected; + } + *length = count; + return napi_ok; +} diff --git a/NativeScript/napi/quickjs/jsr.cpp b/NativeScript/napi/quickjs/jsr.cpp index 8f217e7c1..95bbc5be7 100644 --- a/NativeScript/napi/quickjs/jsr.cpp +++ b/NativeScript/napi/quickjs/jsr.cpp @@ -145,3 +145,19 @@ napi_status js_get_runtime_version(napi_env env, napi_value* version) { return napi_ok; } + +napi_status js_get_array_doubles(napi_env env, napi_value array, double* out, uint32_t capacity, + uint32_t* length) { + bool isArray = false; + if (napi_is_array(env, array, &isArray) != napi_ok || !isArray) return napi_array_expected; + uint32_t count = 0; + if (napi_get_array_length(env, array, &count) != napi_ok) return napi_array_expected; + if (count > capacity) count = capacity; + for (uint32_t i = 0; i < count; i++) { + napi_value element; + if (napi_get_element(env, array, i, &element) != napi_ok) return napi_generic_failure; + if (napi_get_value_double(env, element, &out[i]) != napi_ok) return napi_number_expected; + } + *length = count; + return napi_ok; +} diff --git a/NativeScript/napi/v8/jsr.cpp b/NativeScript/napi/v8/jsr.cpp index d5f78d90b..18f6ab33e 100644 --- a/NativeScript/napi/v8/jsr.cpp +++ b/NativeScript/napi/v8/jsr.cpp @@ -563,3 +563,38 @@ napi_status js_get_runtime_version(napi_env env, napi_value* version) { return napi_ok; } + +napi_status js_get_array_doubles(napi_env env, napi_value array, double* out, uint32_t capacity, + uint32_t* length) { + v8::Local value = v8impl::V8LocalValueFromJsValue(array); + if (!value->IsArray()) return napi_array_expected; + v8::Local jsArray = value.As(); + uint32_t count = jsArray->Length(); + if (count > capacity) count = capacity; + + struct State { + double* out; + uint32_t count; + bool numbersOnly; + } state{out, count, true}; + + // Iterate walks the backing store of packed arrays directly: no handle per element and no + // Node-API status plumbing. The callback may not allocate or call back into V8. + v8::Maybe result = jsArray->Iterate( + env->context(), + [](uint32_t index, v8::Local element, void* data) { + auto* s = static_cast(data); + if (index >= s->count) return v8::Array::CallbackResult::kBreak; + if (!element->IsNumber()) { + s->numbersOnly = false; + return v8::Array::CallbackResult::kBreak; + } + s->out[index] = element.As()->Value(); + return v8::Array::CallbackResult::kContinue; + }, + &state); + if (result.IsNothing()) return napi_generic_failure; + if (!state.numbersOnly) return napi_number_expected; + *length = count; + return napi_ok; +} diff --git a/NativeScript/runtime/android/napi/workers/WorkerWrapper.cpp b/NativeScript/runtime/android/napi/workers/WorkerWrapper.cpp index 21fa72e02..09af851e9 100644 --- a/NativeScript/runtime/android/napi/workers/WorkerWrapper.cpp +++ b/NativeScript/runtime/android/napi/workers/WorkerWrapper.cpp @@ -528,6 +528,7 @@ void WorkerWrapper::BackgroundLooper(std::shared_ptr self) { // ART aborts if a native thread exits while still attached. This must be // the very last JNI-touching action on this thread. + JEnv::ClearCachedEnv(); jvm->DetachCurrentThread(); } diff --git a/vendor/jsc/jsc-api.cpp b/vendor/jsc/jsc-api.cpp index d7bacd8c9..431b85627 100644 --- a/vendor/jsc/jsc-api.cpp +++ b/vendor/jsc/jsc-api.cpp @@ -96,7 +96,7 @@ class JSString { size_t length{JSStringGetLength(_string)}; const JSChar* chars{JSStringGetCharactersPtr(_string)}; size_t size{std::min(length, bufsize - 1)}; - std::memcpy(buf, chars, size); + std::memcpy(buf, chars, size * sizeof(JSChar)); buf[size] = 0; if (result != nullptr) { *result = size; From be241ea07b07cd4eb8f5c278c07d2578a3df598e Mon Sep 17 00:00:00 2001 From: Dylan Llewellyn <46717769+herefishyfish@users.noreply.github.com> Date: Fri, 18 Sep 2026 16:54:20 +0800 Subject: [PATCH 2/3] perf(android/jsi): port the JNI bridge optimizations to the direct-engine tree Brings the jsi binding layer in line with the napi tree (previous commit) so both share the same object model and bridge behaviour: - Wrappers for Java-returned objects are held weakly (WrapperHandle), the native-state destructor posts the Java-side release to the looper, and each wrapper is accounted through EngineHost::AdjustExternalMemory. JS-constructed instances keep their strong handle. - No host-object proxy for plain instances; a live proxy for JS-constructed instances is still returned for identity. - Plain-object wrappers with a per-class cached prototype. - Return-type cache on MetadataEntry for monomorphic object returns. - Weak-ref id cache seeded at link time, no IsSameObject probe per hit. - Strings stay UTF-16 in both directions. New engine API String::createFromUtf16 / utf16Length / copyUtf16: native on V8 and JSC, transcoded through jsi/shared/Utf16.h on QuickJS and Hermes. Also fixes non-BMP text being mangled through NewStringUTF. - New engine API Array::copyNumbers for double[]/int[] arguments (V8: v8::Array::Iterate; generic loop elsewhere). - JNIEnv cached per thread. Measured on x86_64 emulator against the same base the jsi tree was already at or ahead of the patched napi tree, so the timing effect here is within noise except QuickJS object returns (4.34 -> 3.68 us). The runtime test suite passes at parity on V8, QuickJS, Hermes and JSC (431 specs each). --- .../jsi/callbackhandlers/CallbackHandlers.cpp | 25 +++- .../ffi/jni/jsi/conversion/ArgConverter.cpp | 41 +---- .../ffi/jni/jsi/conversion/ArgConverter.h | Bin 5178 -> 6067 bytes .../ffi/jni/jsi/conversion/JsArgConverter.cpp | 37 +++-- NativeScript/ffi/jni/jsi/jni/JEnv.cpp | 45 +++--- NativeScript/ffi/jni/jsi/jni/JEnv.h | 6 + NativeScript/ffi/jni/jsi/jni/LRUCache.h | 8 +- .../ffi/jni/jsi/metadata/MetadataEntry.h | 9 ++ .../ffi/jni/jsi/metadata/MetadataNode.cpp | 21 ++- .../ffi/jni/jsi/metadata/MetadataNode.h | 2 + .../jni/jsi/objectmanager/ObjectManager.cpp | 140 ++++++++++++++---- .../ffi/jni/jsi/objectmanager/ObjectManager.h | 30 +++- NativeScript/jsi/hermes/HermesRuntime.h | 32 ++++ NativeScript/jsi/jsc/JSCRuntime.h | 28 ++++ NativeScript/jsi/jsc/JSCValue.cpp | 23 +++ NativeScript/jsi/quickjs/QuickJSRuntime.h | 30 ++++ NativeScript/jsi/shared/Utf16.h | 71 +++++++++ NativeScript/jsi/v8/V8Runtime.h | 46 ++++++ .../android/jsi/workers/WorkerWrapper.cpp | 1 + 19 files changed, 490 insertions(+), 105 deletions(-) create mode 100644 NativeScript/jsi/shared/Utf16.h diff --git a/NativeScript/ffi/jni/jsi/callbackhandlers/CallbackHandlers.cpp b/NativeScript/ffi/jni/jsi/callbackhandlers/CallbackHandlers.cpp index a85648118..c760e7842 100644 --- a/NativeScript/ffi/jni/jsi/callbackhandlers/CallbackHandlers.cpp +++ b/NativeScript/ffi/jni/jsi/callbackhandlers/CallbackHandlers.cpp @@ -423,8 +423,29 @@ JsValue CallbackHandlers::CallJavaMethod(JsRuntime &rt, const JsValue &caller, c returnValue = objectManager->GetJsObjectByJavaObject(javaObjectID); if (js_util::is_null_or_undefined(returnValue)) { - returnValue = objectManager->CreateJSWrapper(javaObjectID, *returnType, - result); + MetadataNode *returnNode = nullptr; + JniLocalRef runtimeClazz(jEnv.GetObjectClass(result)); + if (entry != nullptr && !isArrayReturn) { + if (!entry->returnClazzResolved) { + entry->returnClazzResolved = true; + // returnType is a JNI descriptor (Lpkg/Cls;); FindClass and the metadata want pkg/Cls. + if (returnType->size() > 2 && (*returnType)[0] == 'L') { + std::string declaredName = returnType->substr(1, returnType->size() - 2); + // JEnv::FindClass returns a cached global ref that lives for the process. + jclass declared = jEnv.FindClass(declaredName); + if (declared != nullptr) { + entry->returnClazz = declared; + entry->returnNode = MetadataNode::GetOrCreate(declaredName); + } + } + } + if (entry->returnClazz != nullptr && jEnv.isSameObject(runtimeClazz, entry->returnClazz)) { + returnNode = entry->returnNode; + } + } + returnValue = returnNode != nullptr + ? objectManager->CreateJSWrapper(javaObjectID, returnNode, runtimeClazz, result) + : objectManager->CreateJSWrapper(javaObjectID, *returnType, result); } } diff --git a/NativeScript/ffi/jni/jsi/conversion/ArgConverter.cpp b/NativeScript/ffi/jni/jsi/conversion/ArgConverter.cpp index 13cc59a18..a77643ca8 100644 --- a/NativeScript/ffi/jni/jsi/conversion/ArgConverter.cpp +++ b/NativeScript/ffi/jni/jsi/conversion/ArgConverter.cpp @@ -248,44 +248,9 @@ JsValue ArgConverter::convertToJsString(JsRuntime &rt, const jchar *data, int le return convertToJsString(rt, std::string()); } - // Strict UTF-16 -> UTF-8, matching what napi_create_string_utf16 did inside - // the engine. Unpaired surrogates are emitted as U+FFFD rather than dropped, - // so a lone jchar (Type::Char, which is exactly one code unit) still yields a - // one-character JS string. - std::string utf8; - utf8.reserve((size_t) length); - for (int i = 0; i < length; i++) { - uint32_t cp = data[i]; - if (cp >= 0xD800 && cp <= 0xDBFF && i + 1 < length) { - uint32_t low = data[i + 1]; - if (low >= 0xDC00 && low <= 0xDFFF) { - cp = 0x10000 + ((cp - 0xD800) << 10) + (low - 0xDC00); - i++; - } else { - cp = 0xFFFD; - } - } else if (cp >= 0xD800 && cp <= 0xDFFF) { - cp = 0xFFFD; - } - - if (cp < 0x80) { - utf8.push_back((char) cp); - } else if (cp < 0x800) { - utf8.push_back((char) (0xC0 | (cp >> 6))); - utf8.push_back((char) (0x80 | (cp & 0x3F))); - } else if (cp < 0x10000) { - utf8.push_back((char) (0xE0 | (cp >> 12))); - utf8.push_back((char) (0x80 | ((cp >> 6) & 0x3F))); - utf8.push_back((char) (0x80 | (cp & 0x3F))); - } else { - utf8.push_back((char) (0xF0 | (cp >> 18))); - utf8.push_back((char) (0x80 | ((cp >> 12) & 0x3F))); - utf8.push_back((char) (0x80 | ((cp >> 6) & 0x3F))); - utf8.push_back((char) (0x80 | (cp & 0x3F))); - } - } - - return convertToJsString(rt, utf8); + static_assert(sizeof(jchar) == sizeof(char16_t)); + return JsValue(rt, JsString::createFromUtf16(rt, reinterpret_cast(data), + static_cast(length))); } u16string ArgConverter::ConvertToUtf16String(JsRuntime &rt, const JsValue &s) { diff --git a/NativeScript/ffi/jni/jsi/conversion/ArgConverter.h b/NativeScript/ffi/jni/jsi/conversion/ArgConverter.h index 957e04eb3fd033aecead9147a34bc49c363e5554..ff8d106f7e1dc17e4a9691d9c00cd655f19aac9d 100644 GIT binary patch literal 6067 zcmcIoZExc?628w1?0=YUfz&n}yV>goIbSwFF3s-6mnKD$=1Wlsv_#u-6jDc0&iNMo z?>9qI7AePxi|rZMXeE-vXWpI}a@c$O26}L+W@l0?xiVZ0=Jd&ahBL*P;TcFVf-w{D z{%?JF|NX%aZWQo$TA8_2BhUp~rgOfek>T4nZ{K{IPesP3aC-Ck?E32d{O0cbCi!QA z1Nb!;d~<--h_t9O4)1O%Ve&^ln7<3ZUNMs|`E9E5(wu6Ia9b0`=VBRuI@U(zVs;XU zIeug%zQ27V*dx~^OF0;!;R}5tmC}M~9hrp2Xr!zrMV^k_3w_WA5v8z{c?K6EH=VKe z$YD>J?%!*X?YZ9AW42^ydVMFynx66UJ}l&9!Bcap6k7qq34PG{e|ch{;9_Rx{RRo` z@Y-*z?^#iC=rN_9#a2)7@IEQ|NDwcjm~B8S@FOfq-m{`|VGe&lpk5SexJ6JZg*H&{ zytiFqJ!RoMN;bQ-zTJT4CU|hQ?`q@ZQ|Q=D;NwRSRZ)~ibwP1cDFF+eR3)xqtGR8wd5$y3OrR(!+($5Q}uq6!hq? z-dKJ{U)}2d8hc9&>E~DPqF;!hv9Lpn`y(h~g}~U#^ufySdnMa?SdDLZ!I|bxP6XSo zZLe9I-z^AK`u&*{(k2^?wDmoj+dl84TiNmNf5`b?Vn>ikJfJVX2Tsa~a?o(vAy=mKOp&hhydV7i$ z89AdZcI}TCy*Gdk@J71lq979Y1Q;S&XT0}FM2Q$XalY&Ba|!y59^`d)cQ=K32kr$N>(BR%+L}T{zpi7lvzH_dA9E+JTfuEK$?M^ z;;7jF$4@_l;r}tX1+JaVn_Y1JAl{|se?+b*dz{7tRtJtNz=Ws%QLG;vVpX!b{`&sI z;cr2F)2yhxqEHk)AR}3Sx(FdNxjKxPozaGNUH$s&_rJBHC}fEm3_XdgT`7lS+zcy6 zEd(}Yz7*8N9BYSk6i1(ORbrAPDbwcIC7rJ=Cmo9#{fJL&U)&U1w@y5oZd-vM4G6c* z_a^KqSp3R9RNwvOyY});Gt!bl#tiEq4rhY)BA?=x!gkj!5WKU7QD$TCVwFa68;x9i zL#}-|Q$C>8C|i62m26c=mO+POnOuRj@X%*%6RjxH%$Y&bV%Q^Vm~+LuB-SVk0wx(Mj{0(^CYz$dQD^%(GKu`S2}^&99|T_LXyo*Dj-mb(6Z6RB)*)LTEVunirQON=R>1i`m<$?2RX1>7H@G$eOz zVvK_4KCC!AvIm}MBt+a=AjK2TSs^E^u&NYzC_;FViq7gaq}uwBp``|J%Q^MF!{yQN z{%C!Cv(F;;(on~*3LBzw=xxMhncq6m5xdSk1zCb^I& zUTy?hRJvzV*u~^V@%GwmW)`(BuaSNr*r!T&*YjfsGrtR=0LR!b1OWWkH#2 zyA)A9PLMfe$&o`bAvDn`4QqRC0G(#){z*8nxe&QJedsn=yiRC-Jh9r%?4R*%a(R9A zIk~*}>EaH1hE8Wa)`f#E93R7nzpj;}SVH$!se!vy$uIF3eZ9tdM)jc`!(;ua5-I5d zH2cabcxI%c&#nGttgr2r0bDWRCc5`v8XajiblL``hsTk6%!1qIJ5npoin@1o0Gb1u zyVC&o*cRgTIj-yHh4@4G@s@Szu~jQLv$jf`D{7X-x9$IYin-f~K00+9o>r9&FRaLS zwIk1b35cz^aX4D&yLqm6%~Enf>*qqXHVfYS%$BZo#U2=6(kAz4;Va;6LAsv4qC*o} zEiAjf#+diM+6I2#SnOAtqK@YOw=LSIwb#vcY+<$d7hmD_*lMkl zel*H6?1gi)!pl0km!N^!e%MMb}&ou o8rdJLY?bTvw-S2q1YR!Y$%15>Vy~S(cs5|M z_Q1Lb(u00D85pHxqlP_rYY8Rxx_UO<|Mh^NXu3ihnSvuzqz_@2`uPmNya`Xt!>n}Z zLTOBz62S~N48P1c++)TR>-KIc&rRAv+8l(#!P~)}hsB*!m38>p;+UZ-#TrtjBlFEn zrbgt7HuZ|CSxl72a6{Ts^-ETnFBU&1Bt=8k65OZ%ku^=nd-n|a)H4M4Y?G>vZ)_7f z9e>!?A!lEM=k^kOv*%#gv97Y!az^33BYc(@0gE%J%Vk}idIn*|S*9@$Ubt@Q+-t6J zr#6GO$Sa&oG6)V_PvMGtjuLCR-5`wSfVgT0fJ xMcMi*l}bQ7a1$DVVxK*ID&SLK6#~I_Or12{NV 1 ? arraySignature[1] : ' '; jclass elementClass; std::string strippedClassName; @@ -453,8 +451,18 @@ bool JsArgConverter::ConvertJavaScriptArray(JsRuntime &rt, const JsValue &jsArr, case 'I': { arr = jenv.NewIntArray(arrLength); std::vector ints(arrLength); - for (jsize i = 0; i < arrLength; i++) { - ints[i] = (jint) js_util::get_int32(jsArray.getValueAtIndexBorrowed(rt, i)); + // Bulk read through the engine (V8: Array::Iterate) into a stack buffer for small arrays. + double stackDoubles[64]; + std::vector heapDoubles; + double *doubles = stackDoubles; + if (arrLength > 64) { heapDoubles.resize(arrLength); doubles = heapDoubles.data(); } + size_t got = 0; + if (jsArray.copyNumbers(rt, doubles, (size_t) arrLength, &got) && got == (size_t) arrLength) { + for (jsize i = 0; i < arrLength; i++) ints[i] = (jint) (int32_t) doubles[i]; + } else { + for (jsize i = 0; i < arrLength; i++) { + ints[i] = (jint) js_util::get_int32(jsArray.getValueAtIndexBorrowed(rt, i)); + } } jenv.SetIntArrayRegion((jintArray) arr, 0, arrLength, ints.data()); break; @@ -479,15 +487,22 @@ bool JsArgConverter::ConvertJavaScriptArray(JsRuntime &rt, const JsValue &jsArr, } case 'D': { arr = jenv.NewDoubleArray(arrLength); - std::vector doubles(arrLength); - for (jsize i = 0; i < arrLength; i++) { - doubles[i] = (jdouble) js_util::get_number(jsArray.getValueAtIndexBorrowed(rt, i)); + // Bulk read through the engine (V8: Array::Iterate) into a stack buffer for small arrays. + jdouble stackDoubles[64]; + std::vector heapDoubles; + jdouble *doubles = stackDoubles; + if (arrLength > 64) { heapDoubles.resize(arrLength); doubles = heapDoubles.data(); } + size_t got = 0; + if (!jsArray.copyNumbers(rt, doubles, (size_t) arrLength, &got) || got != (size_t) arrLength) { + for (jsize i = 0; i < arrLength; i++) { + doubles[i] = (jdouble) js_util::get_number(jsArray.getValueAtIndexBorrowed(rt, i)); + } } - jenv.SetDoubleArrayRegion((jdoubleArray) arr, 0, arrLength, doubles.data()); + jenv.SetDoubleArrayRegion((jdoubleArray) arr, 0, arrLength, doubles); break; } case 'L': - strippedClassName = elementType.substr(1, elementType.length() - 2); + strippedClassName = arraySignature.substr(2, arraySignature.length() - 3); elementClass = jenv.FindClass(strippedClassName); arr = jenv.NewObjectArray(arrLength, elementClass, nullptr); for (jsize i = 0; i < arrLength; i++) { @@ -730,7 +745,7 @@ JniLocalRef JsArgConverter::GetByteBuffer(JsRuntime &rt, const JsValue &object, ObjectManager::MarkObject(rt, object); - objectManager->Link(object, id, clazz); + objectManager->Link(object, id, clazz, nullptr, buffer); return objectManager->GetJavaObjectByJsObject(object); } diff --git a/NativeScript/ffi/jni/jsi/jni/JEnv.cpp b/NativeScript/ffi/jni/jsi/jni/JEnv.cpp index 190b2817a..ef936968d 100644 --- a/NativeScript/ffi/jni/jsi/jni/JEnv.cpp +++ b/NativeScript/ffi/jni/jsi/jni/JEnv.cpp @@ -7,30 +7,37 @@ using namespace tns; using namespace std; -JEnv::JEnv() - : m_env(nullptr) { - JNIEnv *env = nullptr; - jint ret = s_jvm->GetEnv(reinterpret_cast(&env), JNI_VERSION_1_6); +// A JNIEnv* is valid for as long as its thread stays attached, so resolve it once per +// thread instead of asking the JavaVM on every JEnv construction (a Java call builds +// several JEnv objects on its way through the bridge). +static thread_local JNIEnv *t_cachedEnv = nullptr; +static JNIEnv *ResolveEnv() { + JNIEnv *env = nullptr; + jint ret = JEnv::GetJavaVM()->GetEnv(reinterpret_cast(&env), JNI_VERSION_1_6); if ((ret != JNI_OK) || (env == nullptr)) { - ret = s_jvm->AttachCurrentThread(&env, nullptr); + ret = JEnv::GetJavaVM()->AttachCurrentThread(&env, nullptr); assert(ret == JNI_OK); assert(env != nullptr); } - - m_env = env; + t_cachedEnv = env; + return env; } -JEnv::JEnv(JNIEnv *jniEnv) { - jint ret = s_jvm->GetEnv(reinterpret_cast(&jniEnv), JNI_VERSION_1_6); - - if ((ret != JNI_OK) || (jniEnv == nullptr)) { - ret = s_jvm->AttachCurrentThread(&jniEnv, nullptr); - assert(ret == JNI_OK); - assert(jniEnv != nullptr); +JEnv::JEnv() + : m_env(t_cachedEnv) { + if (m_env == nullptr) [[unlikely]] { + m_env = ResolveEnv(); } +} - m_env = jniEnv; +JEnv::JEnv(JNIEnv *jniEnv) + : m_env(jniEnv) { + if (m_env == nullptr) [[unlikely]] { + m_env = ResolveEnv(); + } else { + t_cachedEnv = jniEnv; + } } JEnv::~JEnv() { @@ -571,9 +578,7 @@ void JEnv::DeleteWeakGlobalRef(jweak obj) { } jobject JEnv::NewLocalRef(jobject ref) { - jobject jo = m_env->NewLocalRef(ref); - CheckForJavaException(); - return jo; + return m_env->NewLocalRef(ref); } void JEnv::DeleteLocalRef(jobject localRef) { @@ -899,3 +904,7 @@ JEnv::GetInterfaceStaticMethodIDAndJClass(const std::string &interfaceName, } + +void JEnv::ClearCachedEnv() { + t_cachedEnv = nullptr; +} diff --git a/NativeScript/ffi/jni/jsi/jni/JEnv.h b/NativeScript/ffi/jni/jsi/jni/JEnv.h index b1a544e4c..ca17a36fa 100644 --- a/NativeScript/ffi/jni/jsi/jni/JEnv.h +++ b/NativeScript/ffi/jni/jsi/jni/JEnv.h @@ -454,6 +454,12 @@ namespace tns { static JavaVM *s_jvm; + public: + static JavaVM *GetJavaVM() { return s_jvm; } + static void ClearCachedEnv(); + + private: + static jclass RUNTIME_CLASS; static jmethodID GET_CACHED_CLASS_METHOD_ID; diff --git a/NativeScript/ffi/jni/jsi/jni/LRUCache.h b/NativeScript/ffi/jni/jsi/jni/LRUCache.h index 7b829c3dd..87385bd48 100644 --- a/NativeScript/ffi/jni/jsi/jni/LRUCache.h +++ b/NativeScript/ffi/jni/jsi/jni/LRUCache.h @@ -100,13 +100,15 @@ class LRUCache { } } + void seed(const key_type& key, const value_type& value) { + if (m_key_to_value.find(key) == m_key_to_value.end()) insert(key, value); + } + void update(const key_type& key, const value_type& value) { jweak ref = m_loadCallback(key, m_state); insert(key, ref); } - private: - // Evict a specific key (used when a cached value is no longer valid). void evictKey(const key_type& key) { auto it = m_key_to_value.find(key); @@ -119,6 +121,8 @@ class LRUCache { } } + private: + // Record a fresh key-value pair in the cache void insert(const key_type& k, const value_type& v) { // Method is only called on cache misses diff --git a/NativeScript/ffi/jni/jsi/metadata/MetadataEntry.h b/NativeScript/ffi/jni/jsi/metadata/MetadataEntry.h index 6e708526c..10de752d6 100644 --- a/NativeScript/ffi/jni/jsi/metadata/MetadataEntry.h +++ b/NativeScript/ffi/jni/jsi/metadata/MetadataEntry.h @@ -7,6 +7,8 @@ #include "MetadataMethodInfo.h" #include "MetadataFieldInfo.h" +class MetadataNode; + namespace tns { enum class NodeType { Package, @@ -49,6 +51,9 @@ namespace tns { memberId = other.memberId; clazz = other.clazz; parsedSig = other.parsedSig; + returnClazz = other.returnClazz; + returnClazzResolved = other.returnClazzResolved; + returnNode = other.returnNode; mi = other.mi; fi = other.fi; sfi = other.sfi; @@ -91,6 +96,10 @@ namespace tns { jclass clazz; std::vector parsedSig; + jclass returnClazz = nullptr; + bool returnClazzResolved = false; + ::MetadataNode *returnNode = nullptr; + MethodInfo mi; FieldInfo *fi; StaticFieldInfo *sfi; diff --git a/NativeScript/ffi/jni/jsi/metadata/MetadataNode.cpp b/NativeScript/ffi/jni/jsi/metadata/MetadataNode.cpp index 0596dda34..42917065b 100644 --- a/NativeScript/ffi/jni/jsi/metadata/MetadataNode.cpp +++ b/NativeScript/ffi/jni/jsi/metadata/MetadataNode.cpp @@ -173,11 +173,22 @@ JsValue MetadataNode::CreateJSWrapper(JsRuntime &rt, ObjectManager *objectManage return CreateArrayWrapper(rt); } - JsValue obj = objectManager->GetEmptyObject(); - JsValue ctorFunc = GetConstructorFunction(rt); - auto object = obj.asObjectBorrowed(rt); - object.setProperty(rt, "constructor", ctorFunc); - js_util::setPrototypeOf(rt, obj, js_util::get_prototype(rt, ctorFunc)); + auto cache = GetMetadataNodeCache(rt); + auto itFound = cache->CtorFuncCache.find(m_treeNode); + JsValue prototype; + if (itFound != cache->CtorFuncCache.end() && itFound->second.wrapperPrototype.isObject()) { + prototype = JsValue(rt, itFound->second.wrapperPrototype); + } else { + JsValue ctorFunc = GetConstructorFunction(rt); + prototype = js_util::get_prototype(rt, ctorFunc); + itFound = cache->CtorFuncCache.find(m_treeNode); + if (itFound != cache->CtorFuncCache.end()) { + itFound->second.wrapperPrototype = JsValue(rt, prototype); + } + } + JsObject plain(rt); + JsValue obj(rt, plain); + js_util::setPrototypeOf(rt, obj, prototype); SetInstanceMetadata(rt, obj, this); return obj; diff --git a/NativeScript/ffi/jni/jsi/metadata/MetadataNode.h b/NativeScript/ffi/jni/jsi/metadata/MetadataNode.h index f780ae4c4..dd71b8c8e 100644 --- a/NativeScript/ffi/jni/jsi/metadata/MetadataNode.h +++ b/NativeScript/ffi/jni/jsi/metadata/MetadataNode.h @@ -289,6 +289,8 @@ class MetadataNode { JsValue constructorFunction; std::vector instanceMethodCallbacks; + + JsValue wrapperPrototype; }; struct MethodCallbackData { diff --git a/NativeScript/ffi/jni/jsi/objectmanager/ObjectManager.cpp b/NativeScript/ffi/jni/jsi/objectmanager/ObjectManager.cpp index 394a45446..a9ebe8f15 100644 --- a/NativeScript/ffi/jni/jsi/objectmanager/ObjectManager.cpp +++ b/NativeScript/ffi/jni/jsi/objectmanager/ObjectManager.cpp @@ -6,6 +6,7 @@ #include "NativeScriptException.h" #include "Runtime.h" #include "CallbackHandlers.h" +#include "EngineHost.h" #include #include @@ -20,7 +21,7 @@ jmethodID ObjectManager::GET_NAME_METHOD_ID = nullptr; ObjectManager::ObjectManager(jobject javaRuntimeObject) : m_javaRuntimeObject(javaRuntimeObject), - m_cache(NewWeakGlobalRefCallback, DeleteWeakGlobalRefCallback, ValidateWeakGlobalRefCallback, 1000, this), + m_cache(NewWeakGlobalRefCallback, DeleteWeakGlobalRefCallback, nullptr, 1000, this), m_currentObjectId(0), m_rt(nullptr), m_proxyRegistry(std::make_shared()) { @@ -245,7 +246,13 @@ ObjectManager::GetJSInstanceInfoShared(const JsValue &object) { ObjectManager::JSInstanceInfo *ObjectManager::GetJSInstanceInfo(const JsValue &object) { if (!object.isObject()) return nullptr; - auto proxy = object.asObjectBorrowed(*m_rt).getHostObject(*m_rt); + JsObject borrowed = object.asObjectBorrowed(*m_rt); + // Plain wrappers (the common case): the native-state slot is type-token checked, so this is + // safe on any object and avoids the "#napi" prototype-chain probe. + auto direct = borrowed.getNativeState(*m_rt); + if (direct != nullptr) return direct.get(); + + auto proxy = borrowed.getHostObject(*m_rt); if (proxy != nullptr) { if (proxy->instanceInfo) { return proxy->instanceInfo; @@ -598,14 +605,32 @@ int ObjectManager::GetOrCreateObjectId(jobject object) { return javaObjectID; } +JsValue ObjectManager::LockWrapper(const WrapperHandle &handle) { + if (handle.isStrong) return JsValue(*m_rt, handle.strong); + if (handle.weak.empty()) return js_util::undefined(); + return handle.weak.lock(*m_rt); +} + JsValue ObjectManager::GetJsObjectByJavaObject(int javaObjectID) { auto it = m_idToObject.find(javaObjectID); if (it == m_idToObject.end()) { return js_util::undefined(); } - JsValue instance = it->second; + JsValue instance = LockWrapper(it->second); if (js_util::is_null_or_undefined(instance)) return js_util::undefined(); + + // Identity: if JS already holds a proxy for this id (instances constructed from JS are + // handed out as proxies by RegisterInstance), that proxy is the object JS knows. + auto proxyIt = m_idToProxy.find(javaObjectID); + if (proxyIt != m_idToProxy.end() && !proxyIt->second.empty()) { + JsValue proxy = proxyIt->second.lock(*m_rt); + if (!js_util::is_null_or_undefined(proxy)) return proxy; + } + // Otherwise plain instances are handed out directly: the wrapper is weakly tracked and + // finalizes itself, so the host-object proxy only earns its keep for arrays (indexed access). + MetadataNode *node = GetInstanceNode(instance); + if (node != nullptr && !node->isArray()) return instance; return GetOrCreateProxy(javaObjectID, instance); } @@ -618,41 +643,48 @@ JsValue ObjectManager::CreateJSWrapper(jint javaObjectID, const std::string &typ jobject instance) { JEnv jenv; JniLocalRef clazz(jenv.GetObjectClass(instance)); + auto className = GetClassName(static_cast(clazz)); + auto node = MetadataNode::GetOrCreate(className); + return CreateJSWrapperForNode(javaObjectID, node, clazz, instance); +} - return CreateJSWrapperHelper(javaObjectID, typeName, clazz); +JsValue ObjectManager::CreateJSWrapper(jint javaObjectID, MetadataNode *node, jclass clazz, jobject instance) { + return CreateJSWrapperForNode(javaObjectID, node, clazz, instance); } JsValue ObjectManager::CreateJSWrapperHelper(jint javaObjectID, const std::string &typeName, jclass clazz) { auto className = (clazz != nullptr) ? GetClassName(clazz) : typeName; - auto node = MetadataNode::GetOrCreate(className); - JsValue proxy = js_util::undefined(); - JsValue jsWrapper = node->CreateJSWrapper(*m_rt, this); - if (jsWrapper.isObject()) { - // Reuse the class we already resolved via GetObjectClass on the instance - // path instead of re-resolving it with a JNI FindClass. The class is only - // stored on JSInstanceInfo::ObjectClazz, which nothing on this path reads, - // so a fresh FindClass is pure overhead; only fall back to it for the - // typeName-only overload where no instance class was available. - jclass linkClazz = clazz; - if (linkClazz == nullptr) { - JEnv jenv; - linkClazz = jenv.FindClass(className); - } - Link(jsWrapper, javaObjectID, linkClazz, node); - if (node->isArray()) { - jsWrapper.asObject(*m_rt).setProperty(*m_rt, "__is__javaArray", true); - } - proxy = GetOrCreateProxy(javaObjectID, jsWrapper); + if (clazz == nullptr) { + JEnv jenv; + clazz = jenv.FindClass(className); } + return CreateJSWrapperForNode(javaObjectID, node, clazz, nullptr); +} - return proxy; +JsValue ObjectManager::CreateJSWrapperForNode(jint javaObjectID, MetadataNode *node, jclass clazz, jobject instance) { + JsValue jsWrapper = node->CreateJSWrapper(*m_rt, this); + if (!jsWrapper.isObject()) return js_util::undefined(); + + // Java-returned wrappers are held weakly and finalize themselves (see ~JSInstanceInfo). + Link(jsWrapper, javaObjectID, clazz, node, instance, /*strongRef*/ false, /*verified*/ true); + if (node->isArray()) { + jsWrapper.asObject(*m_rt).setProperty(*m_rt, "__is__javaArray", true); + return GetOrCreateProxy(javaObjectID, jsWrapper); + } + return jsWrapper; } void ObjectManager::Link(const JsValue &object, uint32_t javaObjectID, jclass clazz, - MetadataNode *node) { - if (!IsRuntimeJsObject(object)) { + MetadataNode *node, jobject instance, bool strongRef, bool verified) { + if (instance != nullptr) { + // Seed the id->object cache so the first method/field access on this wrapper does not + // round-trip into Java (getJavaObjectByID) to fetch an object we are holding right now. + JEnv jenv; + m_cache.seed(javaObjectID, jenv.NewWeakGlobalRef(instance)); + } + if (!verified && !IsRuntimeJsObject(object)) { std::string errMsg("Trying to link invalid 'this' to a Java object"); throw NativeScriptException(errMsg); } @@ -661,12 +693,64 @@ void ObjectManager::Link(const JsValue &object, uint32_t javaObjectID, jclass cl auto jsInstanceInfo = std::make_shared(javaObjectID, clazz); jsInstanceInfo->node = node; + if (!strongRef) { + jsInstanceInfo->owner = this; + jsInstanceInfo->ownerRuntime = m_rt; + // Tell the engine what this wrapper pins on the Java side so it schedules GCs under + // native pressure instead of only when its own heap grows. + auto rtOwner = Runtime::GetRuntimeUnchecked(*m_rt); + if (rtOwner != nullptr) rtOwner->GetEngineHost()->AdjustExternalMemory(kWrapperExternalCost); + } // One slot, one owner: the native state both carries the record and keeps it // alive, replacing the napi tree's external-plus-wrap pair. object.asObjectBorrowed(*m_rt).setNativeState(*m_rt, jsInstanceInfo); - m_idToObject.emplace(javaObjectID, JsValue(*m_rt, object)); + WrapperHandle handle; + if (strongRef) { + handle.strong = JsValue(*m_rt, object); + handle.isStrong = true; + } else { + handle.weak = engine::WeakObject(*m_rt, object); + } + // A collected wrapper leaves its (now empty) weak handle behind until finalized; replace it. + m_idToObject[javaObjectID] = std::move(handle); +} + +// Weakly held wrappers: the native-state holder dies with the wrapper, inside the engine's GC +// pass, where only JNI and queueing are allowed. The Java-side release and the engine handle +// bookkeeping run on the looper tick. +ObjectManager::JSInstanceInfo::~JSInstanceInfo() { + if (owner == nullptr || ownerRuntime == nullptr) return; + auto *pending = new int(static_cast(JavaObjectID)); + Runtime::PostFinalizer(*ownerRuntime, WrapperPostFinalizer, pending, owner); +} + +void ObjectManager::WrapperPostFinalizer(JsRuntime &rt, void *data, void *hint) { + auto *pending = reinterpret_cast(data); + if (pending == nullptr) return; + int javaObjectID = *pending; + delete pending; + auto rtOwner = Runtime::GetRuntimeUnchecked(rt); + if (rtOwner == nullptr || rtOwner->is_destroying) return; + auto objManager = reinterpret_cast(hint); + if (objManager == nullptr || objManager != rtOwner->GetObjectManager()) return; + + rtOwner->GetEngineHost()->AdjustExternalMemory(-kWrapperExternalCost); + if (objManager->m_weakObjectIds.find(javaObjectID) == objManager->m_weakObjectIds.end()) { + objManager->m_weakObjectIds.emplace(javaObjectID); + JEnv jEnv; + jEnv.CallVoidMethod(objManager->m_javaRuntimeObject, objManager->MAKE_INSTANCE_WEAK_METHOD_ID, + javaObjectID); + } + // Drop the id->wrapper entry now instead of waiting for a Java GC notification that may + // never come; only if it still points at nothing (a new wrapper may have been linked). + auto it = objManager->m_idToObject.find(javaObjectID); + if (it != objManager->m_idToObject.end() && !it->second.isStrong && + js_util::is_null_or_undefined(objManager->LockWrapper(it->second))) { + objManager->m_idToObject.erase(it); + } + objManager->m_cache.evictKey(javaObjectID); } bool ObjectManager::CloneLink(const JsValue &src, const JsValue &dest) { @@ -759,6 +843,7 @@ void ObjectManager::ReleaseObjectNow(JsRuntime &rt, int javaObjectId) { objMgr->m_idToProxy.erase(javaObjectId); objMgr->m_idToObject.erase(javaObjectId); + objMgr->m_cache.evictKey(javaObjectId); Runtime::GetRuntime(rt)->js_method_cache->cleanupObject(javaObjectId); } @@ -787,6 +872,7 @@ void ObjectManager::OnGarbageCollected(JNIEnv *jEnv, jintArray object_ids) { auto rt = Runtime::GetRuntimeUnchecked(*m_rt); if (rt && rt->is_destroying) return; int javaObjectId = cppArray[i]; + this->m_cache.evictKey(javaObjectId); auto itFound = this->m_idToObject.find(javaObjectId); if (itFound != this->m_idToObject.end()) { this->m_idToObject.erase(javaObjectId); diff --git a/NativeScript/ffi/jni/jsi/objectmanager/ObjectManager.h b/NativeScript/ffi/jni/jsi/objectmanager/ObjectManager.h index c59720750..6a28ef152 100644 --- a/NativeScript/ffi/jni/jsi/objectmanager/ObjectManager.h +++ b/NativeScript/ffi/jni/jsi/objectmanager/ObjectManager.h @@ -50,8 +50,18 @@ namespace tns { JsValue GetOrCreateProxyWeak(jint javaObjectID, const JsValue &instance); + // strongRef: JS-constructed instances (extend/implement) must stay alive while Java holds them, + // because Java may call back into them; wrappers for Java-returned objects are held weakly and + // finalize themselves. verified: the caller built the object itself, skip the marker probe. void Link(const JsValue &object, uint32_t javaObjectID, jclass clazz, - MetadataNode *node = nullptr); + MetadataNode *node = nullptr, jobject instance = nullptr, + bool strongRef = true, bool verified = false); + + // Same as CreateJSWrapper(id, typeName, instance) with the class metadata already resolved. + JsValue CreateJSWrapper(jint javaObjectID, MetadataNode *node, jclass clazz, jobject instance); + + // Estimated per-wrapper cost reported to the engine (Java object + runtime bookkeeping). + static constexpr int64_t kWrapperExternalCost = 1024; // Returns the class metadata stored on the per-instance JSInstanceInfo // (host proxy's, or the raw instance's native state). Used by @@ -102,8 +112,15 @@ namespace tns { JSInstanceInfo(uint32_t javaObjectID, jclass claz) : JavaObjectID(javaObjectID), ObjectClazz(claz) { } + // For weakly held (Java-returned) wrappers the native-state holder is collected with the + // wrapper, so this destructor is the finalizer: it posts the Java-side release to the + // looper (see WrapperPostFinalizer). Runs inside the engine's GC pass, so it only enqueues. + ~JSInstanceInfo() override; uint32_t JavaObjectID; + // Set for weakly held wrappers only. + ObjectManager *owner = nullptr; + JsRuntime *ownerRuntime = nullptr; jclass ObjectClazz; // Cached super-call flag (-1 = unresolved, 0 = false, 1 = true). int8_t isSuper = -1; @@ -188,6 +205,8 @@ namespace tns { // Actual cleanup, deferred to the runtime's safe post-GC finalizer drain // (Runtime::PostFinalizer) so its handle-releasing work is legal. static void HostObjectProxyPostFinalizer(JsRuntime &rt, void *data, void *hint); + static void WrapperPostFinalizer(JsRuntime &rt, void *data, void *hint); + JsValue CreateJSWrapperForNode(jint javaObjectID, MetadataNode *node, jclass clazz, jobject instance); std::shared_ptr GetJSInstanceInfoShared(const JsValue &object); @@ -214,7 +233,14 @@ namespace tns { // The napi tree stored a weak napi_ref for proxies and a strong one for // instances; those map onto engine::WeakObject and an owned Value. robin_hood::unordered_map m_idToProxy; - robin_hood::unordered_map m_idToObject; + // Strong handle for JS-constructed instances, weak for Java-returned wrappers. + struct WrapperHandle { + JsValue strong; + engine::WeakObject weak; + bool isStrong = false; + }; + robin_hood::unordered_map m_idToObject; + JsValue LockWrapper(const WrapperHandle &handle); robin_hood::unordered_set m_weakObjectIds; robin_hood::unordered_set m_markedAsWeakIds; diff --git a/NativeScript/jsi/hermes/HermesRuntime.h b/NativeScript/jsi/hermes/HermesRuntime.h index a01a29ea2..6a968bc22 100644 --- a/NativeScript/jsi/hermes/HermesRuntime.h +++ b/NativeScript/jsi/hermes/HermesRuntime.h @@ -55,6 +55,7 @@ #include #include #include +#include "jsi/shared/Utf16.h" namespace nativescript { namespace engine { @@ -404,6 +405,7 @@ class Value { // the same storage, so here they are the old two-step. Declared on every // engine so the shared bridge can call one name. std::string utf8(Runtime& runtime) const; + static Value createStringFromUtf8(Runtime& runtime, const char* data, size_t length); // The jsi handle behind this value, materialising one for the inline scalar @@ -473,6 +475,21 @@ class String { size_t length); std::string utf8(Runtime& runtime) const; + // UTF-16 in and out for the JNI bridge. This backend's native string API is UTF-8, so these + // transcode; V8 and JSC provide them natively. + static String createFromUtf16(Runtime& runtime, const char16_t* value, size_t length) { + return createFromUtf8(runtime, ::nativescript::engine::utf16::toUtf8(value, length)); + } + size_t utf16Length(Runtime& runtime) const { + return ::nativescript::engine::utf16::fromUtf8(utf8(runtime)).size(); + } + // Copies up to `capacity` code units (no terminator); returns the string length. + size_t copyUtf16(Runtime& runtime, char16_t* buffer, size_t capacity) const { + std::u16string units = ::nativescript::engine::utf16::fromUtf8(utf8(runtime)); + size_t count = units.size() < capacity ? units.size() : capacity; + for (size_t i = 0; i < count; i++) buffer[i] = units[i]; + return units.size(); + } operator Value() const { return Value::fromStorage(storage_); } @@ -678,6 +695,21 @@ class Function : public Object { class Array : public Object { public: + + // Bulk-read numeric elements into `out` (at most `capacity`). Returns false if an element is + // not a number, in which case the caller falls back to its per-element conversion. + bool copyNumbers(Runtime& runtime, double* out, size_t capacity, size_t* length) const { + size_t count = size(runtime); + if (count > capacity) count = capacity; + for (size_t i = 0; i < count; i++) { + Value element = getValueAtIndexBorrowed(runtime, i); + if (!element.isNumber()) return false; + out[i] = element.getNumber(); + } + *length = count; + return true; + } + Array() = default; Array(Runtime& runtime, size_t size); explicit Array(Object object) : Object(std::move(object)) {} diff --git a/NativeScript/jsi/jsc/JSCRuntime.h b/NativeScript/jsi/jsc/JSCRuntime.h index c5518a408..059673fef 100644 --- a/NativeScript/jsi/jsc/JSCRuntime.h +++ b/NativeScript/jsi/jsc/JSCRuntime.h @@ -525,6 +525,19 @@ class String { } std::string utf8(Runtime& runtime) const; + + static String createFromUtf16(Runtime& runtime, const char16_t* value, size_t length) { + static_assert(sizeof(char16_t) == sizeof(JSChar)); + JSStringRef string = JSStringCreateWithCharacters(reinterpret_cast(value), length); + String result(runtime, string); + JSStringRelease(string); + return result; + } + + size_t utf16Length(Runtime& runtime) const; + // Copies up to `capacity` code units (no terminator); returns the string length. + size_t copyUtf16(Runtime& runtime, char16_t* buffer, size_t capacity) const; + JSValueRef local(Runtime& runtime) const { return storage_->value; } operator Value() const; @@ -1120,6 +1133,21 @@ class Function : public Object { class Array : public Object { public: + + // Bulk-read numeric elements into `out` (at most `capacity`). Returns false if an element is + // not a number, in which case the caller falls back to its per-element conversion. + bool copyNumbers(Runtime& runtime, double* out, size_t capacity, size_t* length) const { + size_t count = size(runtime); + if (count > capacity) count = capacity; + for (size_t i = 0; i < count; i++) { + Value element = getValueAtIndexBorrowed(runtime, i); + if (!element.isNumber()) return false; + out[i] = element.getNumber(); + } + *length = count; + return true; + } + explicit Array(Runtime& runtime, size_t size) : Object(std::make_shared(jscengine::ValueStorage::Kind::JSC)) { std::vector initial(size, JSValueMakeUndefined(runtime.context())); diff --git a/NativeScript/jsi/jsc/JSCValue.cpp b/NativeScript/jsi/jsc/JSCValue.cpp index e68f07fc6..8bdc63f10 100644 --- a/NativeScript/jsi/jsc/JSCValue.cpp +++ b/NativeScript/jsi/jsc/JSCValue.cpp @@ -1,4 +1,5 @@ #include "jsi/jsc/JSCRuntime.h" +#include #ifdef TARGET_ENGINE_JSC @@ -251,6 +252,28 @@ void Object::setProperty(Runtime& runtime, const char* name, const ArrayBuffer& setProperty(runtime, name, Value(runtime, value)); } +size_t String::utf16Length(Runtime& runtime) const { + JSValueRef exception = nullptr; + JSStringRef string = JSValueToStringCopy(runtime.context(), storage_->value, &exception); + if (string == nullptr) return 0; + size_t length = JSStringGetLength(string); + JSStringRelease(string); + return length; +} + +size_t String::copyUtf16(Runtime& runtime, char16_t* buffer, size_t capacity) const { + JSValueRef exception = nullptr; + JSStringRef string = JSValueToStringCopy(runtime.context(), storage_->value, &exception); + if (string == nullptr) return 0; + size_t length = JSStringGetLength(string); + size_t count = length < capacity ? length : capacity; + if (count > 0) { + std::memcpy(buffer, JSStringGetCharactersPtr(string), count * sizeof(JSChar)); + } + JSStringRelease(string); + return length; +} + } // namespace engine } // namespace nativescript diff --git a/NativeScript/jsi/quickjs/QuickJSRuntime.h b/NativeScript/jsi/quickjs/QuickJSRuntime.h index c2d0f63c0..08e233c57 100644 --- a/NativeScript/jsi/quickjs/QuickJSRuntime.h +++ b/NativeScript/jsi/quickjs/QuickJSRuntime.h @@ -35,6 +35,7 @@ #include #include #include +#include "jsi/shared/Utf16.h" #include "quickjs.h" @@ -428,6 +429,20 @@ class String { JS_NewStringLen(runtime.context(), reinterpret_cast(value), length)); } std::string utf8(Runtime& runtime) const; + + static String createFromUtf16(Runtime& runtime, const char16_t* value, size_t length) { + return createFromUtf8(runtime, ::nativescript::engine::utf16::toUtf8(value, length)); + } + size_t utf16Length(Runtime& runtime) const { + return ::nativescript::engine::utf16::fromUtf8(utf8(runtime)).size(); + } + // Copies up to `capacity` code units (no terminator); returns the string length. + size_t copyUtf16(Runtime& runtime, char16_t* buffer, size_t capacity) const { + std::u16string units = ::nativescript::engine::utf16::fromUtf8(utf8(runtime)); + size_t count = units.size() < capacity ? units.size() : capacity; + for (size_t i = 0; i < count; i++) buffer[i] = units[i]; + return units.size(); + } JSValue local(Runtime& runtime) const; operator Value() const; @@ -1127,6 +1142,21 @@ class Function : public Object { class Array : public Object { public: + + // Bulk-read numeric elements into `out` (at most `capacity`). Returns false if an element is + // not a number, in which case the caller falls back to its per-element conversion. + bool copyNumbers(Runtime& runtime, double* out, size_t capacity, size_t* length) const { + size_t count = size(runtime); + if (count > capacity) count = capacity; + for (size_t i = 0; i < count; i++) { + Value element = getValueAtIndexBorrowed(runtime, i); + if (!element.isNumber()) return false; + out[i] = element.getNumber(); + } + *length = count; + return true; + } + explicit Array(Runtime& runtime, size_t size) : Object(std::make_shared( quickjsengine::ValueStorage::Kind::QuickJS)) { diff --git a/NativeScript/jsi/shared/Utf16.h b/NativeScript/jsi/shared/Utf16.h new file mode 100644 index 000000000..07b214e19 --- /dev/null +++ b/NativeScript/jsi/shared/Utf16.h @@ -0,0 +1,71 @@ +#pragma once +// UTF-16 <-> UTF-8 for engine backends whose native string API is UTF-8 only (QuickJS, Hermes). +// V8 and JSC take and return UTF-16 code units directly, so they never come through here. +#include +#include + +namespace nativescript::engine::utf16 { + +inline std::string toUtf8(const char16_t* data, size_t length) { + std::string out; + out.reserve(length); + for (size_t i = 0; i < length; i++) { + uint32_t cp = data[i]; + if (cp >= 0xD800 && cp <= 0xDBFF && i + 1 < length && data[i + 1] >= 0xDC00 && + data[i + 1] <= 0xDFFF) { + cp = 0x10000 + ((cp - 0xD800) << 10) + (data[i + 1] - 0xDC00); + i++; + } else if (cp >= 0xD800 && cp <= 0xDFFF) { + cp = 0xFFFD; // unpaired surrogate + } + if (cp < 0x80) { + out.push_back(static_cast(cp)); + } else if (cp < 0x800) { + out.push_back(static_cast(0xC0 | (cp >> 6))); + out.push_back(static_cast(0x80 | (cp & 0x3F))); + } else if (cp < 0x10000) { + out.push_back(static_cast(0xE0 | (cp >> 12))); + out.push_back(static_cast(0x80 | ((cp >> 6) & 0x3F))); + out.push_back(static_cast(0x80 | (cp & 0x3F))); + } else { + out.push_back(static_cast(0xF0 | (cp >> 18))); + out.push_back(static_cast(0x80 | ((cp >> 12) & 0x3F))); + out.push_back(static_cast(0x80 | ((cp >> 6) & 0x3F))); + out.push_back(static_cast(0x80 | (cp & 0x3F))); + } + } + return out; +} + +inline std::u16string fromUtf8(const std::string& in) { + std::u16string out; + out.reserve(in.size()); + size_t i = 0, n = in.size(); + while (i < n) { + unsigned char c = static_cast(in[i]); + uint32_t cp; + size_t extra; + if (c < 0x80) { cp = c; extra = 0; } + else if ((c & 0xE0) == 0xC0) { cp = c & 0x1F; extra = 1; } + else if ((c & 0xF0) == 0xE0) { cp = c & 0x0F; extra = 2; } + else if ((c & 0xF8) == 0xF0) { cp = c & 0x07; extra = 3; } + else { cp = 0xFFFD; extra = 0; } + if (i + extra >= n + (extra ? 0 : 1) && extra) { cp = 0xFFFD; extra = 0; } + for (size_t k = 1; k <= extra; k++) { + unsigned char cc = static_cast(in[i + k]); + if ((cc & 0xC0) != 0x80) { cp = 0xFFFD; extra = k - 1; break; } + cp = (cp << 6) | (cc & 0x3F); + } + i += extra + 1; + if (cp >= 0x10000) { + cp -= 0x10000; + out.push_back(static_cast(0xD800 + (cp >> 10))); + out.push_back(static_cast(0xDC00 + (cp & 0x3FF))); + } else { + out.push_back(static_cast(cp)); + } + } + return out; +} + +} // namespace nativescript::engine::utf16 diff --git a/NativeScript/jsi/v8/V8Runtime.h b/NativeScript/jsi/v8/V8Runtime.h index 6815558f3..ba9341c2c 100644 --- a/NativeScript/jsi/v8/V8Runtime.h +++ b/NativeScript/jsi/v8/V8Runtime.h @@ -540,6 +540,28 @@ class String { return v8engine::toUtf8(runtime.isolate(), local(runtime)); } + static String createFromUtf16(Runtime& runtime, const char16_t* value, size_t length) { + return String(runtime, v8::String::NewFromTwoByte( + runtime.isolate(), reinterpret_cast(value), + v8::NewStringType::kNormal, static_cast(length)) + .ToLocalChecked()); + } + + size_t utf16Length(Runtime& runtime) const { + return static_cast(local(runtime)->Length()); + } + + size_t copyUtf16(Runtime& runtime, char16_t* buffer, size_t capacity) const { + v8::Local str = local(runtime); + size_t length = static_cast(str->Length()); + size_t count = length < capacity ? length : capacity; + if (count > 0) { + str->WriteV2(runtime.isolate(), 0, static_cast(count), + reinterpret_cast(buffer), v8::String::WriteFlags::kNone); + } + return length; + } + v8::Local local(Runtime& runtime) const { if (storage_->kind == v8engine::ValueStorage::Kind::V8Borrowed) { return storage_->borrowedValue.As(); @@ -1228,6 +1250,30 @@ class Function : public Object { class Array : public Object { public: + + // Bulk-read numeric elements into `out` (at most `capacity`). Returns false if an element is + // not a number, in which case the caller falls back to its per-element conversion. V8 walks + // the packed backing store with Iterate: no handle and no exception plumbing per element. + bool copyNumbers(Runtime& runtime, double* out, size_t capacity, size_t* length) const { + v8::Local array = local(runtime).As(); + size_t count = static_cast(array->Length()); + if (count > capacity) count = capacity; + struct State { double* out; size_t count; bool ok; } state{out, count, true}; + v8::Maybe result = array->Iterate( + runtime.context(), + [](uint32_t index, v8::Local element, void* data) { + auto* s = static_cast(data); + if (index >= s->count) return v8::Array::CallbackResult::kBreak; + if (!element->IsNumber()) { s->ok = false; return v8::Array::CallbackResult::kBreak; } + s->out[index] = element.As()->Value(); + return v8::Array::CallbackResult::kContinue; + }, + &state); + if (result.IsNothing() || !state.ok) return false; + *length = count; + return true; + } + explicit Array(Runtime& runtime, size_t size) : Object(std::make_shared(v8engine::ValueStorage::Kind::V8)) { storage_->reset(runtime.isolate(), diff --git a/NativeScript/runtime/android/jsi/workers/WorkerWrapper.cpp b/NativeScript/runtime/android/jsi/workers/WorkerWrapper.cpp index 5bd5b3b7f..09010a5c7 100644 --- a/NativeScript/runtime/android/jsi/workers/WorkerWrapper.cpp +++ b/NativeScript/runtime/android/jsi/workers/WorkerWrapper.cpp @@ -451,6 +451,7 @@ void WorkerWrapper::BackgroundLooper(std::shared_ptr self) { // ART aborts if a native thread exits while still attached. This must be // the very last JNI-touching action on this thread. + JEnv::ClearCachedEnv(); jvm->DetachCurrentThread(); } From 5ba2673ab894aabf3df63b2ca583fae1f5dbf0a6 Mon Sep 17 00:00:00 2001 From: Dylan Llewellyn <46717769+herefishyfish@users.noreply.github.com> Date: Fri, 18 Sep 2026 18:26:01 +0800 Subject: [PATCH 3/3] fix(android): address review findings on the bridge and wrapper lifecycle - JEnv::NewLocalRef: restore CheckForJavaException (both trees). - LRUCache::seed: replace an existing entry instead of leaking the caller's fresh weak global ref on wrapper recreate (both trees). - Recreated wrappers left the Java instance weak: Java reuses the id of a weakened instance and only the array/proxy path re-strengthened it. New ObjectManager::EnsureInstanceStrong, called from CreateJSWrapperForNode and GetOrCreateProxy (both trees). - jsi WrapperPostFinalizer: leave the id alone when a new wrapper was linked under it between the GC and the looper drain. - jsi JSInstanceInfo: reach the ObjectManager/runtime through a shared OwnerToken that OnDisposeRuntime clears, so native state the engine destroys after ~Runtime is a no-op. - jsi Hermes/QuickJS: createFromUtf16/utf16Length/copyUtf16 on the engines' own two-byte APIs so unpaired surrogates survive; drop jsi/shared/Utf16.h. - V8 js_get_array_doubles / Array::copyNumbers: fail over to the per-element path when a dictionary-mode array skipped holes. Test suites: napi 503/503 (V8, QuickJS, JSC), 502/502 (Hermes); jsi 431/431 on all four engines. --- NativeScript/ffi/jni/jsi/jni/JEnv.cpp | 4 +- NativeScript/ffi/jni/jsi/jni/LRUCache.h | 6 +- .../jni/jsi/objectmanager/ObjectManager.cpp | 55 +++++++++----- .../ffi/jni/jsi/objectmanager/ObjectManager.h | 16 ++++- NativeScript/ffi/jni/napi/jni/JEnv.cpp | 4 +- NativeScript/ffi/jni/napi/jni/LRUCache.h | 6 +- .../jni/napi/objectmanager/ObjectManager.cpp | 23 +++--- .../jni/napi/objectmanager/ObjectManager.h | 1 + NativeScript/jsi/hermes/HermesRuntime.h | 34 ++++++--- NativeScript/jsi/quickjs/QuickJSRuntime.h | 29 +++++--- NativeScript/jsi/shared/Utf16.h | 71 ------------------- NativeScript/jsi/v8/V8Runtime.h | 6 +- NativeScript/napi/v8/jsr.cpp | 7 +- 13 files changed, 133 insertions(+), 129 deletions(-) delete mode 100644 NativeScript/jsi/shared/Utf16.h diff --git a/NativeScript/ffi/jni/jsi/jni/JEnv.cpp b/NativeScript/ffi/jni/jsi/jni/JEnv.cpp index ef936968d..6ba92ee7e 100644 --- a/NativeScript/ffi/jni/jsi/jni/JEnv.cpp +++ b/NativeScript/ffi/jni/jsi/jni/JEnv.cpp @@ -578,7 +578,9 @@ void JEnv::DeleteWeakGlobalRef(jweak obj) { } jobject JEnv::NewLocalRef(jobject ref) { - return m_env->NewLocalRef(ref); + jobject jo = m_env->NewLocalRef(ref); + CheckForJavaException(); + return jo; } void JEnv::DeleteLocalRef(jobject localRef) { diff --git a/NativeScript/ffi/jni/jsi/jni/LRUCache.h b/NativeScript/ffi/jni/jsi/jni/LRUCache.h index 87385bd48..048ceade6 100644 --- a/NativeScript/ffi/jni/jsi/jni/LRUCache.h +++ b/NativeScript/ffi/jni/jsi/jni/LRUCache.h @@ -100,8 +100,12 @@ class LRUCache { } } + // Record a value the caller already holds (takes ownership of it). An existing entry + // for the key is evicted first: the fresh reference is the one known to be live, and the + // old one would otherwise leak outside the cache's capacity accounting. void seed(const key_type& key, const value_type& value) { - if (m_key_to_value.find(key) == m_key_to_value.end()) insert(key, value); + evictKey(key); + insert(key, value); } void update(const key_type& key, const value_type& value) { diff --git a/NativeScript/ffi/jni/jsi/objectmanager/ObjectManager.cpp b/NativeScript/ffi/jni/jsi/objectmanager/ObjectManager.cpp index a9ebe8f15..4eba3b048 100644 --- a/NativeScript/ffi/jni/jsi/objectmanager/ObjectManager.cpp +++ b/NativeScript/ffi/jni/jsi/objectmanager/ObjectManager.cpp @@ -61,6 +61,9 @@ ObjectManager::ObjectManager(jobject javaRuntimeObject) : void ObjectManager::Init(JsRuntime &rt) { m_rt = &rt; + m_ownerToken = std::make_shared(); + m_ownerToken->manager = this; + m_ownerToken->runtime = &rt; JsFunction jsObjectCtor = JsFunction::createFromHostConstructor( rt, JsPropNameID::forAscii(rt, "JSObject"), 0, @@ -85,6 +88,12 @@ void ObjectManager::OnDisposeRuntime() { m_idToProxy.clear(); m_idToObject.clear(); m_jsObjectCtor = JsFunction(); + // Wrapper native state outlives this object (the engine frees it during its own teardown); + // detach it so ~JSInstanceInfo does not reach a deleted manager or runtime. + if (m_ownerToken != nullptr) { + m_ownerToken->manager = nullptr; + m_ownerToken->runtime = nullptr; + } // The host-object proxies are owned by the *engine*, and each holds an owned // handle to the instance it wraps. Nothing above reaches them: they are only @@ -138,15 +147,7 @@ JsValue ObjectManager::GetOrCreateProxy(jint javaObjectID, const JsValue &instan } JsValue proxy = CreateHostObjectProxy(instance, info, /*isPrimary=*/true); - auto javaObjectIdFound = m_weakObjectIds.find(javaObjectID); - if (javaObjectIdFound != m_weakObjectIds.end()) { - m_weakObjectIds.erase(javaObjectID); - JEnv jenv; - jenv.CallVoidMethod(m_javaRuntimeObject, - MAKE_INSTANCE_STRONG_METHOD_ID, - javaObjectID); - DEBUG_WRITE("Making instance strong: %d", javaObjectID); - } + EnsureInstanceStrong(javaObjectID); m_idToProxy.emplace(javaObjectID, engine::WeakObject(*m_rt, proxy)); @@ -663,12 +664,25 @@ JsValue ObjectManager::CreateJSWrapperHelper(jint javaObjectID, const std::strin return CreateJSWrapperForNode(javaObjectID, node, clazz, nullptr); } +// Java keeps an instance strong while a JS wrapper for it is alive and weak otherwise. The +// Java side reuses the id of a weakened instance (getOrCreateJavaObjectID consults the weak +// table too), so any path that (re)creates a wrapper must move it back to the strong table. +void ObjectManager::EnsureInstanceStrong(int javaObjectID) { + auto it = m_weakObjectIds.find(javaObjectID); + if (it == m_weakObjectIds.end()) return; + m_weakObjectIds.erase(it); + JEnv jenv; + jenv.CallVoidMethod(m_javaRuntimeObject, MAKE_INSTANCE_STRONG_METHOD_ID, javaObjectID); + DEBUG_WRITE("Making instance strong: %d", javaObjectID); +} + JsValue ObjectManager::CreateJSWrapperForNode(jint javaObjectID, MetadataNode *node, jclass clazz, jobject instance) { JsValue jsWrapper = node->CreateJSWrapper(*m_rt, this); if (!jsWrapper.isObject()) return js_util::undefined(); // Java-returned wrappers are held weakly and finalize themselves (see ~JSInstanceInfo). Link(jsWrapper, javaObjectID, clazz, node, instance, /*strongRef*/ false, /*verified*/ true); + EnsureInstanceStrong(javaObjectID); if (node->isArray()) { jsWrapper.asObject(*m_rt).setProperty(*m_rt, "__is__javaArray", true); return GetOrCreateProxy(javaObjectID, jsWrapper); @@ -694,8 +708,7 @@ void ObjectManager::Link(const JsValue &object, uint32_t javaObjectID, jclass cl auto jsInstanceInfo = std::make_shared(javaObjectID, clazz); jsInstanceInfo->node = node; if (!strongRef) { - jsInstanceInfo->owner = this; - jsInstanceInfo->ownerRuntime = m_rt; + jsInstanceInfo->owner = m_ownerToken; // Tell the engine what this wrapper pins on the Java side so it schedules GCs under // native pressure instead of only when its own heap grows. auto rtOwner = Runtime::GetRuntimeUnchecked(*m_rt); @@ -721,9 +734,9 @@ void ObjectManager::Link(const JsValue &object, uint32_t javaObjectID, jclass cl // pass, where only JNI and queueing are allowed. The Java-side release and the engine handle // bookkeeping run on the looper tick. ObjectManager::JSInstanceInfo::~JSInstanceInfo() { - if (owner == nullptr || ownerRuntime == nullptr) return; + if (owner == nullptr || owner->manager == nullptr || owner->runtime == nullptr) return; auto *pending = new int(static_cast(JavaObjectID)); - Runtime::PostFinalizer(*ownerRuntime, WrapperPostFinalizer, pending, owner); + Runtime::PostFinalizer(*owner->runtime, WrapperPostFinalizer, pending, owner->manager); } void ObjectManager::WrapperPostFinalizer(JsRuntime &rt, void *data, void *hint) { @@ -737,6 +750,14 @@ void ObjectManager::WrapperPostFinalizer(JsRuntime &rt, void *data, void *hint) if (objManager == nullptr || objManager != rtOwner->GetObjectManager()) return; rtOwner->GetEngineHost()->AdjustExternalMemory(-kWrapperExternalCost); + // Between the GC that killed the wrapper and this tick, Java may have handed the same + // object out again and a new wrapper may have been linked under this id. Everything below + // is keyed by id, so it belongs to that live wrapper now and must be left alone. + auto it = objManager->m_idToObject.find(javaObjectID); + if (it != objManager->m_idToObject.end() && + !js_util::is_null_or_undefined(objManager->LockWrapper(it->second))) { + return; + } if (objManager->m_weakObjectIds.find(javaObjectID) == objManager->m_weakObjectIds.end()) { objManager->m_weakObjectIds.emplace(javaObjectID); JEnv jEnv; @@ -744,12 +765,8 @@ void ObjectManager::WrapperPostFinalizer(JsRuntime &rt, void *data, void *hint) javaObjectID); } // Drop the id->wrapper entry now instead of waiting for a Java GC notification that may - // never come; only if it still points at nothing (a new wrapper may have been linked). - auto it = objManager->m_idToObject.find(javaObjectID); - if (it != objManager->m_idToObject.end() && !it->second.isStrong && - js_util::is_null_or_undefined(objManager->LockWrapper(it->second))) { - objManager->m_idToObject.erase(it); - } + // never come. + if (it != objManager->m_idToObject.end()) objManager->m_idToObject.erase(it); objManager->m_cache.evictKey(javaObjectID); } diff --git a/NativeScript/ffi/jni/jsi/objectmanager/ObjectManager.h b/NativeScript/ffi/jni/jsi/objectmanager/ObjectManager.h index 6a28ef152..275cc3a24 100644 --- a/NativeScript/ffi/jni/jsi/objectmanager/ObjectManager.h +++ b/NativeScript/ffi/jni/jsi/objectmanager/ObjectManager.h @@ -107,6 +107,13 @@ namespace tns { // It derives from engine::HostObject because that is what the native // state slot stores; it overrides none of the traps and is never exposed // to JS as an object of its own. + // Shared between the ObjectManager and every weakly held wrapper's JSInstanceInfo; + // cleared by OnDisposeRuntime so late native-state destruction becomes a no-op. + struct OwnerToken { + ObjectManager *manager = nullptr; + JsRuntime *runtime = nullptr; + }; + struct JSInstanceInfo : public engine::HostObject { public: JSInstanceInfo(uint32_t javaObjectID, jclass claz) @@ -118,9 +125,10 @@ namespace tns { ~JSInstanceInfo() override; uint32_t JavaObjectID; - // Set for weakly held wrappers only. - ObjectManager *owner = nullptr; - JsRuntime *ownerRuntime = nullptr; + // Set for weakly held wrappers only. The engine destroys native state on its own + // schedule -- possibly after ~Runtime has deleted the ObjectManager -- so the owner is + // reached through a token the manager invalidates on dispose. + std::shared_ptr owner; jclass ObjectClazz; // Cached super-call flag (-1 = unresolved, 0 = false, 1 = true). int8_t isSuper = -1; @@ -199,6 +207,7 @@ namespace tns { std::set proxies; }; + void EnsureInstanceStrong(int javaObjectID); JsValue CreateHostObjectProxy(const JsValue &instance, JSInstanceInfo *instanceInfo, bool isPrimary); @@ -240,6 +249,7 @@ namespace tns { bool isStrong = false; }; robin_hood::unordered_map m_idToObject; + std::shared_ptr m_ownerToken; JsValue LockWrapper(const WrapperHandle &handle); robin_hood::unordered_set m_weakObjectIds; robin_hood::unordered_set m_markedAsWeakIds; diff --git a/NativeScript/ffi/jni/napi/jni/JEnv.cpp b/NativeScript/ffi/jni/napi/jni/JEnv.cpp index ef936968d..6ba92ee7e 100644 --- a/NativeScript/ffi/jni/napi/jni/JEnv.cpp +++ b/NativeScript/ffi/jni/napi/jni/JEnv.cpp @@ -578,7 +578,9 @@ void JEnv::DeleteWeakGlobalRef(jweak obj) { } jobject JEnv::NewLocalRef(jobject ref) { - return m_env->NewLocalRef(ref); + jobject jo = m_env->NewLocalRef(ref); + CheckForJavaException(); + return jo; } void JEnv::DeleteLocalRef(jobject localRef) { diff --git a/NativeScript/ffi/jni/napi/jni/LRUCache.h b/NativeScript/ffi/jni/napi/jni/LRUCache.h index 87385bd48..048ceade6 100644 --- a/NativeScript/ffi/jni/napi/jni/LRUCache.h +++ b/NativeScript/ffi/jni/napi/jni/LRUCache.h @@ -100,8 +100,12 @@ class LRUCache { } } + // Record a value the caller already holds (takes ownership of it). An existing entry + // for the key is evicted first: the fresh reference is the one known to be live, and the + // old one would otherwise leak outside the cache's capacity accounting. void seed(const key_type& key, const value_type& value) { - if (m_key_to_value.find(key) == m_key_to_value.end()) insert(key, value); + evictKey(key); + insert(key, value); } void update(const key_type& key, const value_type& value) { diff --git a/NativeScript/ffi/jni/napi/objectmanager/ObjectManager.cpp b/NativeScript/ffi/jni/napi/objectmanager/ObjectManager.cpp index 8c79fe23a..33954417f 100644 --- a/NativeScript/ffi/jni/napi/objectmanager/ObjectManager.cpp +++ b/NativeScript/ffi/jni/napi/objectmanager/ObjectManager.cpp @@ -196,15 +196,7 @@ napi_value ObjectManager::GetOrCreateProxy(jint javaObjectID, napi_value instanc #endif - auto javaObjectIdFound = m_weakObjectIds.find(javaObjectID); - if (javaObjectIdFound != m_weakObjectIds.end()) { - m_weakObjectIds.erase(javaObjectID); - JEnv jenv; - jenv.CallVoidMethod(m_javaRuntimeObject, - MAKE_INSTANCE_STRONG_METHOD_ID, - javaObjectID); - DEBUG_WRITE("Making instance strong: %d", javaObjectID); - } + EnsureInstanceStrong(javaObjectID); m_idToProxy.emplace(javaObjectID, napi_util::make_ref(m_env, proxy, 0)); @@ -852,6 +844,18 @@ ObjectManager::CreateJSWrapperHelper(jint javaObjectID, const std::string &typeN return CreateJSWrapperForNode(javaObjectID, node, clazz, instance); } +// Java keeps an instance strong while a JS wrapper for it is alive and weak otherwise. The +// Java side reuses the id of a weakened instance (getOrCreateJavaObjectID consults the weak +// table too), so any path that (re)creates a wrapper must move it back to the strong table. +void ObjectManager::EnsureInstanceStrong(int javaObjectID) { + auto it = m_weakObjectIds.find(javaObjectID); + if (it == m_weakObjectIds.end()) return; + m_weakObjectIds.erase(it); + JEnv jenv; + jenv.CallVoidMethod(m_javaRuntimeObject, MAKE_INSTANCE_STRONG_METHOD_ID, javaObjectID); + DEBUG_WRITE("Making instance strong: %d", javaObjectID); +} + napi_value ObjectManager::CreateJSWrapperForNode(jint javaObjectID, MetadataNode *node, jclass clazz, jobject instance) { napi_status status; @@ -864,6 +868,7 @@ ObjectManager::CreateJSWrapperForNode(jint javaObjectID, MetadataNode *node, jcl // so a fresh FindClass is pure overhead; only fall back to it for the // typeName-only overload where no instance class was available. Link(jsWrapper, javaObjectID, clazz, node, instance, /*strongRef*/ false, /*verified*/ true); + EnsureInstanceStrong(javaObjectID); if (node->isArray()) { NAPI_GUARD(napi_set_named_property(m_env, jsWrapper, "__is__javaArray", napi_util::get_true(m_env))) {} diff --git a/NativeScript/ffi/jni/napi/objectmanager/ObjectManager.h b/NativeScript/ffi/jni/napi/objectmanager/ObjectManager.h index 2709bf00e..34f6d281e 100644 --- a/NativeScript/ffi/jni/napi/objectmanager/ObjectManager.h +++ b/NativeScript/ffi/jni/napi/objectmanager/ObjectManager.h @@ -133,6 +133,7 @@ namespace tns { int64_t arrayLength = -1; // cached fixed length (arrays only; -1=unresolved) }; + void EnsureInstanceStrong(int javaObjectID); napi_value CreateHostObjectProxy(napi_value instance, JSInstanceInfo *instanceInfo, bool isPrimary); diff --git a/NativeScript/jsi/hermes/HermesRuntime.h b/NativeScript/jsi/hermes/HermesRuntime.h index 6a968bc22..a3960f275 100644 --- a/NativeScript/jsi/hermes/HermesRuntime.h +++ b/NativeScript/jsi/hermes/HermesRuntime.h @@ -55,7 +55,6 @@ #include #include #include -#include "jsi/shared/Utf16.h" namespace nativescript { namespace engine { @@ -475,19 +474,16 @@ class String { size_t length); std::string utf8(Runtime& runtime) const; - // UTF-16 in and out for the JNI bridge. This backend's native string API is UTF-8, so these - // transcode; V8 and JSC provide them natively. - static String createFromUtf16(Runtime& runtime, const char16_t* value, size_t length) { - return createFromUtf8(runtime, ::nativescript::engine::utf16::toUtf8(value, length)); - } - size_t utf16Length(Runtime& runtime) const { - return ::nativescript::engine::utf16::fromUtf8(utf8(runtime)).size(); - } + // UTF-16 in and out for the JNI bridge, on jsi's own two-byte API so unpaired surrogates + // survive the round trip (a UTF-8 detour would replace them). + static String createFromUtf16(Runtime& runtime, const char16_t* value, size_t length); + std::u16string utf16(Runtime& runtime) const; + size_t utf16Length(Runtime& runtime) const { return utf16(runtime).size(); } // Copies up to `capacity` code units (no terminator); returns the string length. size_t copyUtf16(Runtime& runtime, char16_t* buffer, size_t capacity) const { - std::u16string units = ::nativescript::engine::utf16::fromUtf8(utf8(runtime)); + std::u16string units = utf16(runtime); size_t count = units.size() < capacity ? units.size() : capacity; - for (size_t i = 0; i < count; i++) buffer[i] = units[i]; + if (count > 0) std::memcpy(buffer, units.data(), count * sizeof(char16_t)); return units.size(); } @@ -970,6 +966,22 @@ inline String String::createFromUtf8(Runtime& runtime, const uint8_t* value, }); } +inline String String::createFromUtf16(Runtime& runtime, const char16_t* value, size_t length) { + return hermesengine::guard(runtime, [&] { + ::facebook::jsi::Runtime& rt = runtime.jsi(); + return String::fromStorage(hermesengine::makeStorage( + ::facebook::jsi::Value(::facebook::jsi::String::createFromUtf16( + rt, value != nullptr ? value : u"", length)))); + }); +} + +inline std::u16string String::utf16(Runtime& runtime) const { + if (storage_ == nullptr) return {}; + return hermesengine::guard(runtime, [&] { + return storage_->string(runtime.jsi()).utf16(runtime.jsi()); + }); +} + inline std::string Value::utf8(Runtime& runtime) const { return asString(runtime).utf8(runtime); } diff --git a/NativeScript/jsi/quickjs/QuickJSRuntime.h b/NativeScript/jsi/quickjs/QuickJSRuntime.h index 08e233c57..a41c9db9b 100644 --- a/NativeScript/jsi/quickjs/QuickJSRuntime.h +++ b/NativeScript/jsi/quickjs/QuickJSRuntime.h @@ -35,7 +35,6 @@ #include #include #include -#include "jsi/shared/Utf16.h" #include "quickjs.h" @@ -430,18 +429,30 @@ class String { } std::string utf8(Runtime& runtime) const; + // UTF-16 in and out for the JNI bridge, on the engine's own two-byte API so unpaired + // surrogates survive the round trip (a UTF-8 detour would replace them). static String createFromUtf16(Runtime& runtime, const char16_t* value, size_t length) { - return createFromUtf8(runtime, ::nativescript::engine::utf16::toUtf8(value, length)); - } - size_t utf16Length(Runtime& runtime) const { - return ::nativescript::engine::utf16::fromUtf8(utf8(runtime)).size(); + static const uint16_t empty = 0; + return adopt(runtime, JS_NewStringUTF16(runtime.context(), + value != nullptr ? reinterpret_cast(value) : &empty, + length)); } + size_t utf16Length(Runtime& runtime) const { return copyUtf16(runtime, nullptr, 0); } // Copies up to `capacity` code units (no terminator); returns the string length. size_t copyUtf16(Runtime& runtime, char16_t* buffer, size_t capacity) const { - std::u16string units = ::nativescript::engine::utf16::fromUtf8(utf8(runtime)); - size_t count = units.size() < capacity ? units.size() : capacity; - for (size_t i = 0; i < count; i++) buffer[i] = units[i]; - return units.size(); + JSContext* ctx = runtime.context(); + JSValue value = local(runtime); + size_t length = 0; + const uint16_t* units = JS_ToCStringLenUTF16(ctx, &length, value); + if (units != nullptr) { + size_t count = length < capacity ? length : capacity; + if (count > 0) std::memcpy(buffer, units, count * sizeof(char16_t)); + JS_FreeCStringUTF16(ctx, units); + } else { + length = 0; + } + JS_FreeValue(ctx, value); + return length; } JSValue local(Runtime& runtime) const; operator Value() const; diff --git a/NativeScript/jsi/shared/Utf16.h b/NativeScript/jsi/shared/Utf16.h deleted file mode 100644 index 07b214e19..000000000 --- a/NativeScript/jsi/shared/Utf16.h +++ /dev/null @@ -1,71 +0,0 @@ -#pragma once -// UTF-16 <-> UTF-8 for engine backends whose native string API is UTF-8 only (QuickJS, Hermes). -// V8 and JSC take and return UTF-16 code units directly, so they never come through here. -#include -#include - -namespace nativescript::engine::utf16 { - -inline std::string toUtf8(const char16_t* data, size_t length) { - std::string out; - out.reserve(length); - for (size_t i = 0; i < length; i++) { - uint32_t cp = data[i]; - if (cp >= 0xD800 && cp <= 0xDBFF && i + 1 < length && data[i + 1] >= 0xDC00 && - data[i + 1] <= 0xDFFF) { - cp = 0x10000 + ((cp - 0xD800) << 10) + (data[i + 1] - 0xDC00); - i++; - } else if (cp >= 0xD800 && cp <= 0xDFFF) { - cp = 0xFFFD; // unpaired surrogate - } - if (cp < 0x80) { - out.push_back(static_cast(cp)); - } else if (cp < 0x800) { - out.push_back(static_cast(0xC0 | (cp >> 6))); - out.push_back(static_cast(0x80 | (cp & 0x3F))); - } else if (cp < 0x10000) { - out.push_back(static_cast(0xE0 | (cp >> 12))); - out.push_back(static_cast(0x80 | ((cp >> 6) & 0x3F))); - out.push_back(static_cast(0x80 | (cp & 0x3F))); - } else { - out.push_back(static_cast(0xF0 | (cp >> 18))); - out.push_back(static_cast(0x80 | ((cp >> 12) & 0x3F))); - out.push_back(static_cast(0x80 | ((cp >> 6) & 0x3F))); - out.push_back(static_cast(0x80 | (cp & 0x3F))); - } - } - return out; -} - -inline std::u16string fromUtf8(const std::string& in) { - std::u16string out; - out.reserve(in.size()); - size_t i = 0, n = in.size(); - while (i < n) { - unsigned char c = static_cast(in[i]); - uint32_t cp; - size_t extra; - if (c < 0x80) { cp = c; extra = 0; } - else if ((c & 0xE0) == 0xC0) { cp = c & 0x1F; extra = 1; } - else if ((c & 0xF0) == 0xE0) { cp = c & 0x0F; extra = 2; } - else if ((c & 0xF8) == 0xF0) { cp = c & 0x07; extra = 3; } - else { cp = 0xFFFD; extra = 0; } - if (i + extra >= n + (extra ? 0 : 1) && extra) { cp = 0xFFFD; extra = 0; } - for (size_t k = 1; k <= extra; k++) { - unsigned char cc = static_cast(in[i + k]); - if ((cc & 0xC0) != 0x80) { cp = 0xFFFD; extra = k - 1; break; } - cp = (cp << 6) | (cc & 0x3F); - } - i += extra + 1; - if (cp >= 0x10000) { - cp -= 0x10000; - out.push_back(static_cast(0xD800 + (cp >> 10))); - out.push_back(static_cast(0xDC00 + (cp & 0x3FF))); - } else { - out.push_back(static_cast(cp)); - } - } - return out; -} - -} // namespace nativescript::engine::utf16 diff --git a/NativeScript/jsi/v8/V8Runtime.h b/NativeScript/jsi/v8/V8Runtime.h index ba9341c2c..30ace2638 100644 --- a/NativeScript/jsi/v8/V8Runtime.h +++ b/NativeScript/jsi/v8/V8Runtime.h @@ -1258,7 +1258,7 @@ class Array : public Object { v8::Local array = local(runtime).As(); size_t count = static_cast(array->Length()); if (count > capacity) count = capacity; - struct State { double* out; size_t count; bool ok; } state{out, count, true}; + struct State { double* out; size_t count; size_t written; bool ok; } state{out, count, 0, true}; v8::Maybe result = array->Iterate( runtime.context(), [](uint32_t index, v8::Local element, void* data) { @@ -1266,10 +1266,12 @@ class Array : public Object { if (index >= s->count) return v8::Array::CallbackResult::kBreak; if (!element->IsNumber()) { s->ok = false; return v8::Array::CallbackResult::kBreak; } s->out[index] = element.As()->Value(); + s->written++; return v8::Array::CallbackResult::kContinue; }, &state); - if (result.IsNothing() || !state.ok) return false; + // Dictionary-mode arrays only report present entries; unwritten holes go to the fallback. + if (result.IsNothing() || !state.ok || state.written != count) return false; *length = count; return true; } diff --git a/NativeScript/napi/v8/jsr.cpp b/NativeScript/napi/v8/jsr.cpp index 18f6ab33e..b12dcfda3 100644 --- a/NativeScript/napi/v8/jsr.cpp +++ b/NativeScript/napi/v8/jsr.cpp @@ -575,8 +575,9 @@ napi_status js_get_array_doubles(napi_env env, napi_value array, double* out, ui struct State { double* out; uint32_t count; + uint32_t written; bool numbersOnly; - } state{out, count, true}; + } state{out, count, 0, true}; // Iterate walks the backing store of packed arrays directly: no handle per element and no // Node-API status plumbing. The callback may not allocate or call back into V8. @@ -590,11 +591,15 @@ napi_status js_get_array_doubles(napi_env env, napi_value array, double* out, ui return v8::Array::CallbackResult::kBreak; } s->out[index] = element.As()->Value(); + s->written++; return v8::Array::CallbackResult::kContinue; }, &state); if (result.IsNothing()) return napi_generic_failure; if (!state.numbersOnly) return napi_number_expected; + // Dictionary-mode arrays only report present entries, so holes leave slots unwritten; + // hand those to the per-element path, which sees them as undefined. + if (state.written != count) return napi_number_expected; *length = count; return napi_ok; }