Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
74f1123
fix(react-native/ios): attach a recycled NativeScriptUIView's handle …
NathanWalker Oct 18, 2026
cf2393e
fix(ffi/objc): never release a Value whose runtime has been torn down
NathanWalker Oct 18, 2026
5809837
fix(android): re-attach NativeScript after a JavaScript reload
NathanWalker Oct 18, 2026
16c448a
fix(ffi/objc): stop defining short enum member names as globals
NathanWalker Oct 18, 2026
ae72cd9
fix(react-native): import without the native module; init() on Android
NathanWalker Oct 18, 2026
3837fda
fix(react-native): configure the Worklets Babel plugin with strictGlobal
NathanWalker Oct 18, 2026
0220fd2
fix(types): nullable blocks as (() => T) | null; declare NativeObject…
NathanWalker Oct 18, 2026
4dfc13d
fix(ffi/objc): key thread_local bridge caches by a per-bridge id
NathanWalker Oct 18, 2026
4129065
Merge remote-tracking branch 'origin/android-react-native' into fix/r…
NathanWalker Oct 18, 2026
4dc6860
docs(react-native): rewrite README for npm readers
NathanWalker Oct 18, 2026
932467d
fix(react-native): keep metadata.android when writing nativescript.re…
NathanWalker Oct 18, 2026
c0db43d
fix(react-native): say Android typings are generated by default in co…
NathanWalker Oct 18, 2026
cb89fd9
feat(android): run JavaScript on the main thread under a host runtime
NathanWalker Oct 22, 2026
72713f5
feat(react-native/android): NativeScriptUIView shows an android.view.…
NathanWalker Oct 22, 2026
1050be2
feat(react-native): runOnUI, nativeViewHandle and androidContext on A…
NathanWalker Oct 22, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions NativeScript/ffi/jni/jsi/jni/LRUCache.h
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,16 @@ class LRUCache {
}
}

void clear() {
if (m_evictCallback != nullptr) {
for (auto& entry : m_key_to_value) {
m_evictCallback(entry.second.first, m_state);
}
}
m_key_to_value.clear();
m_key_tracker.clear();
}

void update(const key_type& key, const value_type& value) {
jweak ref = m_loadCallback(key, m_state);
insert(key, ref);
Expand Down
4 changes: 4 additions & 0 deletions NativeScript/ffi/jni/jsi/objectmanager/ObjectManager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,10 @@ void ObjectManager::OnDisposeRuntime() {
m_idToObject.clear();
m_jsObjectCtor = JsFunction();

// The cache's destructor does not delete its weak global refs, and the
// process-wide weak reference table is finite.
m_cache.clear();

// 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
// destroyed when the engine tears its heap down, at which point
Expand Down
5 changes: 5 additions & 0 deletions NativeScript/ffi/objc/shared/bridge/HostObject.mm
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ inline bool InstallNativeApiLazyGlobal(
explicit NativeApiHostObject(std::shared_ptr<NativeApiBridge> bridge)
: bridge_(std::move(bridge)) {}

// Each bridge has exactly one API object and it lives on its runtime's
// global, so this runs as that runtime is finalized. Running earlier would
// only leak the Values the bridge drops afterwards.
~NativeApiHostObject() override { bridge_->markRuntimeTornDown(); }

Value get(Runtime& runtime, const PropNameID& name) override {
std::string property = name.utf8(runtime);
if (property == "runtime") {
Expand Down
20 changes: 19 additions & 1 deletion NativeScript/ffi/objc/shared/bridge/Install.mm
Original file line number Diff line number Diff line change
Expand Up @@ -1699,6 +1699,24 @@ function nameSet(values) {
var protocolNameSet = nameSet(protocolNameList);
var enumNameSet = nameSet(enumNameList);

// Only full C constant names (NSTextAlignmentCenter, kCLAuthorizationStatus...)
// become globals. The enum object also carries short member names (Center,
// CloudKit's Zone), which as globals shadow what libraries feature-detect.
function isEnumMemberGlobalName(enumName, memberName) {
if (/^-?\d+$/.test(memberName)) {
return false;
}
if (/^k[A-Z]/.test(memberName)) {
return true;
}
var match = /^[A-Z]+(?=[A-Z][a-z])/.exec(enumName);
var prefix = match ? match[0] : '';
return prefix.length >= 2 &&
memberName.length > prefix.length &&
memberName.indexOf(prefix) === 0 &&
/[A-Z]/.test(memberName.charAt(prefix.length));
}

function resolveNativeApiEnum(enumName) {
return (api.getEnum && api.getEnum(enumName)) || api[enumName];
}
Expand Down Expand Up @@ -1759,7 +1777,7 @@ function resolveNativeApiEnum(enumName) {
return;
}
Object.keys(enumValue).forEach(function(memberName) {
if (/^-?\d+$/.test(memberName)) {
if (!isEnumMemberGlobalName(name, memberName)) {
return;
}
defineLazyGlobal(memberName, function() {
Expand Down
113 changes: 96 additions & 17 deletions NativeScript/ffi/objc/shared/bridge/ObjCBridge.mm
Original file line number Diff line number Diff line change
Expand Up @@ -540,11 +540,33 @@ explicit NativeApiBridge(const NativeApiConfig& config)
}

~NativeApiBridge() {
if (runtimeTornDown_.load(std::memory_order_acquire)) {
abandonRuntimeValues(roundTripValues_);
abandonRuntimeValues(roundTripCacheFramesByThread_);
abandonRuntimeValues(recentRoundTripValues_);
#ifdef TARGET_ENGINE_HERMES
abandonRuntimeValues(roundTripRootCache_);
#endif
abandonRuntimeValues(classValues_);
abandonRuntimeValues(classPrototypes_);
abandonRuntimeValues(pointerValues_);
abandonRuntimeValues(objectExpandos_);
abandonRuntimeValues(retainedLifetimes_);
}
if (selfDl_ != nullptr) {
dlclose(selfDl_);
}
}

// Called when the runtime this bridge was installed into is torn down. Host
// objects can be shared into another runtime (react-native-worklets does so
// for values crossing runOnUI) and finalized there after this runtime is
// gone; releasing a Value whose runtime is destroyed is a use-after-free, so
// from here on every Value the bridge drops is leaked instead.
void markRuntimeTornDown() {
runtimeTornDown_.store(true, std::memory_order_release);
}

MDMetadataReader* metadata() const { return metadata_.get(); }

void* selfDl() const { return selfDl_; }
Expand Down Expand Up @@ -708,7 +730,7 @@ Value findRoundTripValue(Runtime& runtime, const void* native,
? validationKey
: (nativeIsObject ? nativeObjectClassKey(native) : 0);
struct RoundTripCacheEntry {
const NativeApiBridge* bridge = nullptr;
uint64_t bridgeId = 0;
uintptr_t key = 0;
uint64_t generation = 0;
std::weak_ptr<Value> value;
Expand All @@ -722,7 +744,7 @@ Value findRoundTripValue(Runtime& runtime, const void* native,
const size_t firstSlot = (key >> 4) & 3;
for (size_t i = 0; i < 4; i++) {
RoundTripCacheEntry& entry = cache[(firstSlot + i) & 3];
if (entry.bridge == this && entry.key == key &&
if (entry.bridgeId == instanceId_ && entry.key == key &&
entry.generation == generation) {
if (entry.validationKey != expectedValidationKey) {
break;
Expand Down Expand Up @@ -777,13 +799,13 @@ Value findRoundTripValue(Runtime& runtime, const void* native,
}
if (entry == nullptr) {
cache[firstSlot] = RoundTripCacheEntry{
this, key, generation, {}, true, false, expectedValidationKey};
instanceId_, key, generation, {}, true, false, expectedValidationKey};
return Value::undefined();
}
storedValue = entry->value;
cachedStringLike = entry->stringLikeNative;
cache[firstSlot] = RoundTripCacheEntry{
this, key, generation, storedValue, false, cachedStringLike,
instanceId_, key, generation, storedValue, false, cachedStringLike,
entry->validationKey};
}
if (stringLikeNative != nullptr) {
Expand Down Expand Up @@ -842,6 +864,7 @@ void forgetRoundTripValue(const void* native) {
eraseRoundTripKeyFromScopedCaches(key, releaseAfterUnlock);
roundTripValuesGeneration_.fetch_add(1, std::memory_order_release);
}
abandonIfRuntimeTornDown(releaseAfterUnlock);
}

uint64_t roundTripValuesGeneration() const {
Expand Down Expand Up @@ -973,7 +996,7 @@ Value findObjectExpando(Runtime& runtime, const void* native,
return Value::undefined();
}
struct ObjectExpandoCacheEntry {
const NativeApiBridge* bridge = nullptr;
uint64_t bridgeId = 0;
uintptr_t key = 0;
uint64_t generation = 0;
std::string property;
Expand All @@ -988,7 +1011,7 @@ Value findObjectExpando(Runtime& runtime, const void* native,
const uint64_t generation =
objectExpandosGeneration_.load(std::memory_order_acquire);
for (auto& entry : cache) {
if (entry.bridge == this && entry.key == key &&
if (entry.bridgeId == instanceId_ && entry.key == key &&
entry.generation == generation && entry.property == property) {
if (entry.miss) {
return Value::undefined();
Expand All @@ -1004,17 +1027,17 @@ Value findObjectExpando(Runtime& runtime, const void* native,
const size_t slot = nextSlot++ & 7;
if (objectIt == objectExpandos_.end()) {
cache[slot] =
ObjectExpandoCacheEntry{this, key, generation, property, {}, true};
ObjectExpandoCacheEntry{instanceId_, key, generation, property, {}, true};
return Value::undefined();
}
auto propertyIt = objectIt->second.find(property);
if (propertyIt == objectIt->second.end() || propertyIt->second == nullptr) {
cache[slot] =
ObjectExpandoCacheEntry{this, key, generation, property, {}, true};
ObjectExpandoCacheEntry{instanceId_, key, generation, property, {}, true};
return Value::undefined();
}
cache[slot] = ObjectExpandoCacheEntry{
this, key, generation, property, propertyIt->second, false};
instanceId_, key, generation, property, propertyIt->second, false};
return Value(runtime, *propertyIt->second);
}

Expand All @@ -1023,8 +1046,11 @@ void forgetObjectExpandos(const void* native) {
return;
}
auto key = normalizeRuntimePointer(reinterpret_cast<uintptr_t>(native));
objectExpandos_.erase(
key);
auto it = objectExpandos_.find(key);
if (it != objectExpandos_.end()) {
abandonIfRuntimeTornDown(it->second);
objectExpandos_.erase(it);
}
objectExpandosGeneration_.fetch_add(1, std::memory_order_release);
}

Expand All @@ -1045,7 +1071,7 @@ void forgetObjectExpandos(const void* native) {
return nullptr;
}
struct PropertyGetterCacheEntry {
const NativeApiBridge* bridge = nullptr;
uint64_t bridgeId = 0;
Class cls = Nil;
uint64_t generation = 0;
std::string property;
Expand All @@ -1058,7 +1084,7 @@ void forgetObjectExpandos(const void* native) {
const uint64_t generation =
propertyGetterCacheGeneration_.load(std::memory_order_acquire);
for (auto& entry : cache) {
if (entry.bridge == this && entry.cls == cls &&
if (entry.bridgeId == instanceId_ && entry.cls == cls &&
entry.generation == generation && entry.property == property) {
return entry.miss ? nullptr : entry.getter;
}
Expand All @@ -1068,17 +1094,17 @@ void forgetObjectExpandos(const void* native) {
const size_t slot = nextSlot++ & 7;
if (classIt == propertyGetterCache_.end()) {
cache[slot] = PropertyGetterCacheEntry{
this, cls, generation, property, nullptr, true};
instanceId_, cls, generation, property, nullptr, true};
return nullptr;
}
auto propIt = classIt->second.find(property);
if (propIt == classIt->second.end()) {
cache[slot] = PropertyGetterCacheEntry{
this, cls, generation, property, nullptr, true};
instanceId_, cls, generation, property, nullptr, true};
return nullptr;
}
cache[slot] =
PropertyGetterCacheEntry{this, cls, generation, property,
PropertyGetterCacheEntry{instanceId_, cls, generation, property,
&propIt->second, false};
return &propIt->second;
}
Expand Down Expand Up @@ -1111,7 +1137,11 @@ void forgetPointerValue(const void* native) {
if (native == nullptr) {
return;
}
pointerValues_.erase(reinterpret_cast<uintptr_t>(native));
auto it = pointerValues_.find(reinterpret_cast<uintptr_t>(native));
if (it != pointerValues_.end()) {
abandonIfRuntimeTornDown(it->second);
pointerValues_.erase(it);
}
}

const NativeApiSymbol* findConstant(const std::string& name) const {
Expand Down Expand Up @@ -2101,6 +2131,47 @@ static void appendSurfaceMember(
return members;
}

template <typename T>
void abandonIfRuntimeTornDown(T& values) {
if (runtimeTornDown_.load(std::memory_order_acquire)) {
abandonRuntimeValues(values);
}
}

// Moves each Value out into a deliberately leaked allocation so destroying
// the container never reaches the runtime. Leak the Value, not its
// shared_ptr: the thread_local lookup caches hold weak_ptrs and rely on them
// expiring once this bridge is gone.
static void abandonRuntimeValues(std::shared_ptr<Value>& value) {
if (value != nullptr) {
new Value(std::move(*value));
}
}

static void abandonRuntimeValues(std::shared_ptr<void>& lifetime) {
if (lifetime != nullptr) {
new std::shared_ptr<void>(std::move(lifetime));
}
}

static void abandonRuntimeValues(NativeApiRoundTripValue& entry) {
abandonRuntimeValues(entry.value);
}

template <typename Key, typename Mapped>
static void abandonRuntimeValues(std::unordered_map<Key, Mapped>& values) {
for (auto& entry : values) {
abandonRuntimeValues(entry.second);
}
}

template <typename Element>
static void abandonRuntimeValues(std::vector<Element>& values) {
for (auto& value : values) {
abandonRuntimeValues(value);
}
}

std::unique_ptr<MDMetadataReader> metadata_;
void* selfDl_ = nullptr;
std::unordered_map<std::string, NativeApiSymbol> symbolsByName_;
Expand Down Expand Up @@ -2133,6 +2204,14 @@ static void appendSurfaceMember(
objectExpandos_;
std::unordered_map<uintptr_t, size_t> objectExpandoOwnerCounts_;
std::atomic<uint64_t> objectExpandosGeneration_{1};
std::atomic<bool> runtimeTornDown_{false};
// Identifies this bridge in the thread_local lookup caches. Its address
// cannot: a bridge allocated where a destroyed one lived would match that
// one's entries, and the property-getter cache holds raw pointers into it.
const uint64_t instanceId_ = [] {
static std::atomic<uint64_t> next{1};
return next.fetch_add(1, std::memory_order_relaxed);
}();
std::unordered_map<Class, std::unordered_map<std::string, CachedPropertyGetter>>
propertyGetterCache_;
std::atomic<uint64_t> propertyGetterCacheGeneration_{1};
Expand Down
Loading
Loading