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 957e04eb3..ff8d106f7 100644 Binary files a/NativeScript/ffi/jni/jsi/conversion/ArgConverter.h and b/NativeScript/ffi/jni/jsi/conversion/ArgConverter.h differ diff --git a/NativeScript/ffi/jni/jsi/conversion/JsArgConverter.cpp b/NativeScript/ffi/jni/jsi/conversion/JsArgConverter.cpp index be1cacb50..966000c04 100644 --- a/NativeScript/ffi/jni/jsi/conversion/JsArgConverter.cpp +++ b/NativeScript/ffi/jni/jsi/conversion/JsArgConverter.cpp @@ -403,9 +403,7 @@ bool JsArgConverter::ConvertJavaScriptArray(JsRuntime &rt, const JsValue &jsArr, 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; @@ -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..6ba92ee7e 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() { @@ -899,3 +906,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..048ceade6 100644 --- a/NativeScript/ffi/jni/jsi/jni/LRUCache.h +++ b/NativeScript/ffi/jni/jsi/jni/LRUCache.h @@ -100,13 +100,19 @@ 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) { + evictKey(key); + 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 +125,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..4eba3b048 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()) { @@ -60,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, @@ -84,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 @@ -137,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)); @@ -245,7 +247,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 +606,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 +644,61 @@ 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; +// 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); + } + 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 +707,67 @@ 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 = 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); + 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 || owner->manager == nullptr || owner->runtime == nullptr) return; + auto *pending = new int(static_cast(JavaObjectID)); + Runtime::PostFinalizer(*owner->runtime, WrapperPostFinalizer, pending, owner->manager); +} + +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); + // 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; + 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. + if (it != objManager->m_idToObject.end()) objManager->m_idToObject.erase(it); + objManager->m_cache.evictKey(javaObjectID); } bool ObjectManager::CloneLink(const JsValue &src, const JsValue &dest) { @@ -759,6 +860,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 +889,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..275cc3a24 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 @@ -97,13 +107,28 @@ 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) : 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. 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; @@ -182,12 +207,15 @@ namespace tns { std::set proxies; }; + void EnsureInstanceStrong(int javaObjectID); JsValue CreateHostObjectProxy(const JsValue &instance, JSInstanceInfo *instanceInfo, bool isPrimary); // 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 +242,15 @@ 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; + 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/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..6ba92ee7e 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() { @@ -899,3 +906,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..048ceade6 100644 --- a/NativeScript/ffi/jni/napi/jni/LRUCache.h +++ b/NativeScript/ffi/jni/napi/jni/LRUCache.h @@ -100,13 +100,19 @@ 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) { + evictKey(key); + 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 +125,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..33954417f 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), @@ -195,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)); @@ -211,6 +204,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 +224,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 +264,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 +296,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 +308,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 +547,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 +569,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 +641,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 +730,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 +756,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 +800,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 +825,40 @@ 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); +} + +// 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; napi_value proxy = nullptr; napi_value jsWrapper = node->CreateJSWrapper(m_env, this); if (jsWrapper != nullptr) { @@ -822,25 +867,29 @@ 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); + EnsureInstanceStrong(javaObjectID); 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 +900,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 +923,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 +959,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 +1095,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 +1159,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..34f6d281e 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 @@ -125,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); @@ -163,15 +172,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 +204,8 @@ namespace tns { LRUCache m_cache; + static constexpr int64_t kWrapperExternalCost = 1024; + volatile int m_currentObjectId; DirectBuffer m_buff; diff --git a/NativeScript/jsi/hermes/HermesRuntime.h b/NativeScript/jsi/hermes/HermesRuntime.h index a01a29ea2..a3960f275 100644 --- a/NativeScript/jsi/hermes/HermesRuntime.h +++ b/NativeScript/jsi/hermes/HermesRuntime.h @@ -404,6 +404,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 +474,18 @@ class String { size_t length); std::string utf8(Runtime& runtime) const; + // 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 = utf16(runtime); + size_t count = units.size() < capacity ? units.size() : capacity; + if (count > 0) std::memcpy(buffer, units.data(), count * sizeof(char16_t)); + return units.size(); + } operator Value() const { return Value::fromStorage(storage_); } @@ -678,6 +691,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)) {} @@ -938,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/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..a41c9db9b 100644 --- a/NativeScript/jsi/quickjs/QuickJSRuntime.h +++ b/NativeScript/jsi/quickjs/QuickJSRuntime.h @@ -428,6 +428,32 @@ class String { JS_NewStringLen(runtime.context(), reinterpret_cast(value), length)); } 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) { + 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 { + 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; @@ -1127,6 +1153,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/v8/V8Runtime.h b/NativeScript/jsi/v8/V8Runtime.h index 6815558f3..30ace2638 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,32 @@ 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; 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) { + 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(); + s->written++; + return v8::Array::CallbackResult::kContinue; + }, + &state); + // 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; + } + explicit Array(Runtime& runtime, size_t size) : Object(std::make_shared(v8engine::ValueStorage::Kind::V8)) { storage_->reset(runtime.isolate(), 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..b12dcfda3 100644 --- a/NativeScript/napi/v8/jsr.cpp +++ b/NativeScript/napi/v8/jsr.cpp @@ -563,3 +563,43 @@ 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; + uint32_t written; + bool numbersOnly; + } 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. + 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(); + 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; +} 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(); } 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;