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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions NativeScript/NativeScript.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,10 @@
- (bool)liveSync;

@end

@interface NativeScriptRuntime : NSObject

+ (BOOL)reloadApplication;
+ (BOOL)reloadApplication:(NSString*)baseDir;

@end
56 changes: 56 additions & 0 deletions NativeScript/NativeScript.mm
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,21 @@ @implementation Config

@end

static Config* CopyConfig(Config* config) {
Config* copy = [[Config alloc] init];
copy.BaseDir = config.BaseDir;
copy.ApplicationPath = config.ApplicationPath;
copy.MetadataPtr = config.MetadataPtr;
copy.IsDebug = config.IsDebug;
copy.LogToSystemConsole = config.LogToSystemConsole;
copy.ArgumentsCount = config.ArgumentsCount;
copy.Arguments = config.Arguments;
return copy;
}

static NativeScript* currentNativeScript;
static Config* currentConfig;

@implementation NativeScript

extern char defaultStartOfMetadataSection __asm("section$start$__DATA$__TNSMetadata");
Expand Down Expand Up @@ -82,6 +97,9 @@ - (void)shutdownRuntime {

- (instancetype)initializeWithConfig:(Config*)config {
if (self = [super init]) {
currentNativeScript = self;
currentConfig = CopyConfig(config);

RuntimeConfig.BaseDir = [config.BaseDir UTF8String];
if (config.ApplicationPath != nil) {
RuntimeConfig.ApplicationPath =
Expand All @@ -98,6 +116,15 @@ - (instancetype)initializeWithConfig:(Config*)config {
RuntimeConfig.IsDebug = [config IsDebug];
RuntimeConfig.LogToSystemConsole = [config LogToSystemConsole];

// Connect the JS-exposed `NativeScriptRuntime.reloadApplication(baseDir?)`
// global (registered by the runtime) to the Objective-C implementation below.
tns::SetReloadApplicationHook([](const std::string& baseDir) -> bool {
NSString* dir = baseDir.empty()
? nil
: [NSString stringWithUTF8String:baseDir.c_str()];
return [NativeScriptRuntime reloadApplication:dir] == YES;
});

Runtime::Initialize();
runtime_ = nullptr;
runtime_ = std::make_unique<Runtime>();
Expand Down Expand Up @@ -130,8 +157,37 @@ - (instancetype)initWithConfig:(Config*)config {
}

- (void)restartWithConfig:(Config*)config {
// Incremented before the new isolate boots so its global template bakes in
// the correct `NativeScriptRuntime.reloadCount` value.
tns::IncrementRuntimeReloadCount();
[self shutdownRuntime];
[self initializeWithConfig:config];
}

@end

@implementation NativeScriptRuntime

+ (BOOL)reloadApplication {
return [self reloadApplication:nil];
}

+ (BOOL)reloadApplication:(NSString*)baseDir {
if (currentNativeScript == nil || currentConfig == nil) {
return NO;
}

Config* config = CopyConfig(currentConfig);
if (baseDir != nil && [baseDir length] > 0) {
config.BaseDir = baseDir;
}

dispatch_async(dispatch_get_main_queue(), ^{
[currentNativeScript restartWithConfig:config];
[currentNativeScript runMainApplication];
Comment on lines +185 to +187

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Reset stale JS error state before starting the new bundle.

jsErrorOccurred is process-global and survives reloads. After a previous debug error, the new runMainApplication can enter the debug error loop even if the reload was meant to provide a clean isolate restart.

Proposed fix
   dispatch_async(dispatch_get_main_queue(), ^{
+    tns::jsErrorOccurred = false;
     [currentNativeScript restartWithConfig:config];
     [currentNativeScript runMainApplication];
   });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
dispatch_async(dispatch_get_main_queue(), ^{
[currentNativeScript restartWithConfig:config];
[currentNativeScript runMainApplication];
dispatch_async(dispatch_get_main_queue(), ^{
tns::jsErrorOccurred = false;
[currentNativeScript restartWithConfig:config];
[currentNativeScript runMainApplication];
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@NativeScript/NativeScript.mm` around lines 182 - 184, The reload path in
NativeScript’s restart flow still carries over the process-global
jsErrorOccurred flag, which can make the next bundle start in the old debug
error loop. Update the restart sequence around currentNativeScript
restartWithConfig: and runMainApplication to clear or reinitialize
jsErrorOccurred before launching the new isolate, so a fresh reload starts from
a clean JS error state.

});

return YES;
}

@end
10 changes: 10 additions & 0 deletions NativeScript/runtime/ArgConverter.mm
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#include "ArgConverter.h"
#include <Foundation/Foundation.h>
#include <sstream>
#include "DevFlags.h"
#include "DictionaryAdapter.h"
#include "Helpers.h"
#include "Interop.h"
Expand Down Expand Up @@ -134,6 +135,15 @@
Isolate* isolate = data->isolateWrapper_.Isolate();

if (!data->isolateWrapper_.IsValid()) {
// Instance methods carry (self, _cmd) as the first two ffi args; blocks and
// function pointers do not, so only dereference them when present.
if (data->initialParamIndex_ == 2) {
id self_ = *static_cast<const id*>(argValues[0]);
SEL cmd = *static_cast<const SEL*>(argValues[1]);
LogDroppedDeadIsolateCallback((__bridge void*)self_, (void*)cmd);
} else {
LogDroppedDeadIsolateCallback(nullptr, nullptr);
}
memset(retValue, 0, cif->rtype->size);
return;
}
Expand Down
3 changes: 3 additions & 0 deletions NativeScript/runtime/ClassBuilder.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
#define ClassBuilder_h

#include "Common.h"
#include "IsolateWrapper.h"
#include "Metadata.h"

namespace tns {
Expand All @@ -10,11 +11,13 @@ struct PropertyCallbackContext {
public:
PropertyCallbackContext(v8::Isolate* isolate, std::shared_ptr<v8::Persistent<v8::Function>> callback, std::shared_ptr<v8::Persistent<v8::Object>> implementationObject, const PropertyMeta* meta)
: isolate_(isolate),
isolateWrapper_(isolate),
callback_(callback),
implementationObject_(implementationObject),
meta_(meta) {
}
v8::Isolate* isolate_;
IsolateWrapper isolateWrapper_;
std::shared_ptr<v8::Persistent<v8::Function>> callback_;
std::shared_ptr<v8::Persistent<v8::Object>> implementationObject_;
const PropertyMeta* meta_;
Expand Down
18 changes: 18 additions & 0 deletions NativeScript/runtime/ClassBuilder.mm
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#include <sstream>
#include "ArgConverter.h"
#include "Caches.h"
#include "DevFlags.h"
#include "FastEnumerationAdapter.h"
#include "Helpers.h"
#include "Interop.h"
Expand Down Expand Up @@ -892,6 +893,17 @@
FFIMethodCallback getterCallback = [](ffi_cif* cif, void* retValue, void** argValues,
void* userData) {
PropertyCallbackContext* context = static_cast<PropertyCallbackContext*>(userData);
if (!context->isolateWrapper_.IsValid()) {
// The isolate that implemented this property is gone (e.g. after
// reloadApplication). UIKit may still query stale delegate instances
// (like `window` on a JS-extended UIApplicationDelegate); bail with a
// zeroed return instead of locking a disposed isolate.
id self_ = *static_cast<const id*>(argValues[0]);
SEL cmd = *static_cast<const SEL*>(argValues[1]);
LogDroppedDeadIsolateCallback((__bridge void*)self_, (void*)cmd);
memset(retValue, 0, cif->rtype->size);
return;
}
v8::Locker locker(context->isolate_);
Isolate::Scope isolate_scope(context->isolate_);
HandleScope handle_scope(context->isolate_);
Expand Down Expand Up @@ -926,6 +938,12 @@
FFIMethodCallback setterCallback = [](ffi_cif* cif, void* retValue, void** argValues,
void* userData) {
PropertyCallbackContext* context = static_cast<PropertyCallbackContext*>(userData);
if (!context->isolateWrapper_.IsValid()) {
id self_ = *static_cast<const id*>(argValues[0]);
SEL cmd = *static_cast<const SEL*>(argValues[1]);
LogDroppedDeadIsolateCallback((__bridge void*)self_, (void*)cmd);
return;
}
v8::Locker locker(context->isolate_);
Isolate::Scope isolate_scope(context->isolate_);
HandleScope handle_scope(context->isolate_);
Expand Down
9 changes: 9 additions & 0 deletions NativeScript/runtime/DevFlags.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,15 @@ namespace tns {
// Controlled by package.json setting: "logScriptLoading": true|false
bool IsScriptLoadingLogEnabled();

// Logs (behind IsScriptLoadingLogEnabled) that a native->JS callback was
// dropped because the isolate that created it has been disposed (e.g. after
// NativeScriptRuntime.reloadApplication / restartWithConfig). Without this,
// UIKit dispatch to instances of classes built by a previous isolate (such as
// a JS-extended UIApplicationDelegate or UISceneDelegate) silently no-ops.
// `target` is an ObjC `id` and `selector` a `SEL`; either may be null when
// the callback is a block or C function pointer rather than a method.
void LogDroppedDeadIsolateCallback(void* target, void* selector);

// Security config

// In debug mode (RuntimeConfig.IsDebug): always returns true.
Expand Down
19 changes: 19 additions & 0 deletions NativeScript/runtime/DevFlags.mm
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#import <Foundation/Foundation.h>
#import <objc/runtime.h>

#include "DevFlags.h"
#include "Runtime.h"
Expand All @@ -13,6 +14,24 @@ bool IsScriptLoadingLogEnabled() {
return value ? [value boolValue] : false;
}

void LogDroppedDeadIsolateCallback(void* target, void* selector) {
if (!IsScriptLoadingLogEnabled()) {
return;
}

if (target != nullptr && selector != nullptr) {
id self_ = (__bridge id)target;
SEL cmd = (SEL)selector;
NSLog(@"NativeScript: dropping call to -[%s %s] because the JS isolate that implemented it "
@"was disposed (e.g. after reloadApplication); reassign the delegate/callback from the "
@"new bundle to restore dispatch",
object_getClassName(self_), sel_getName(cmd));
} else {
NSLog(@"NativeScript: dropping a native callback (block or function pointer) because the JS "
@"isolate that implemented it was disposed (e.g. after reloadApplication)");
}
}

// Security config

static std::once_flag s_securityConfigInitFlag;
Expand Down
18 changes: 18 additions & 0 deletions NativeScript/runtime/Runtime.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,24 @@
#include "SpinLock.h"
#include "libplatform/libplatform.h"

#include <functional>
#include <string>

namespace tns {

using ReloadApplicationHook = std::function<bool(const std::string& baseDir)>;
void SetReloadApplicationHook(ReloadApplicationHook hook);
bool InvokeReloadApplicationHook(const std::string& baseDir);

// Number of times the runtime has been soft-rebooted in this process
// (restartWithConfig / reloadApplication). 0 for the first boot. Exposed to JS
// as `NativeScriptRuntime.reloadCount` so application code (e.g.
// @nativescript/core) can detect a reloaded instance and reattach native
// delegates (UIApplication delegate, UIScene delegates) that are still pinned
// to classes created by the previous isolate.
void IncrementRuntimeReloadCount();
int GetRuntimeReloadCount();

class Runtime {
public:
Runtime();
Expand Down Expand Up @@ -67,6 +83,8 @@ class Runtime {
void DefineCollectFunction(v8::Local<v8::Context> context);
void DefineNativeScriptVersion(v8::Isolate* isolate,
v8::Local<v8::ObjectTemplate> globalTemplate);
void DefineNativeScriptRuntime(v8::Isolate* isolate,
v8::Local<v8::ObjectTemplate> globalTemplate);
void DefinePerformanceObject(v8::Isolate* isolate,
v8::Local<v8::ObjectTemplate> globalTemplate);
void DefineTimeMethod(v8::Isolate* isolate,
Expand Down
49 changes: 49 additions & 0 deletions NativeScript/runtime/Runtime.mm
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,7 @@ void DisposeIsolateWhenPossible(Isolate* isolate) {
tns::binding::CreateInternalBindingTemplates(isolate, globalTemplateFunction);
Local<ObjectTemplate> globalTemplate = ObjectTemplate::New(isolate, globalTemplateFunction);
DefineNativeScriptVersion(isolate, globalTemplate);
DefineNativeScriptRuntime(isolate, globalTemplate);

// Worker::Init(isolate, globalTemplate, isWorker);
DefinePerformanceObject(isolate, globalTemplate);
Expand Down Expand Up @@ -573,6 +574,54 @@ void DisposeIsolateWhenPossible(Isolate* isolate) {
ToV8String(isolate, STRINGIZE_VALUE_OF(NATIVESCRIPT_VERSION)), readOnlyFlags);
}

static ReloadApplicationHook reloadApplicationHook_;

void SetReloadApplicationHook(ReloadApplicationHook hook) {
reloadApplicationHook_ = std::move(hook);
}

bool InvokeReloadApplicationHook(const std::string& baseDir) {
if (!reloadApplicationHook_) {
return false;
}
return reloadApplicationHook_(baseDir);
}
Comment on lines +577 to +588

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify all reload hook access sites and whether they can be reached from runtime initialization / JS callbacks.
rg -n -C4 '\b(SetReloadApplicationHook|InvokeReloadApplicationHook|DefineNativeScriptRuntime)\b' NativeScript

Repository: NativeScript/ios

Length of output: 5712


🏁 Script executed:

#!/bin/bash
# Check if <mutex> is included in Runtime.mm
head -50 NativeScript/runtime/Runtime.mm | rg "include.*mutex"

Repository: NativeScript/ios

Length of output: 171


Serialize access to the global reload hook.

reloadApplicationHook_ is a process-global std::function modified by SetReloadApplicationHook during initialization while InvokeReloadApplicationHook may be called concurrently from JavaScript execution threads. Read and write access must be synchronized, and the hook invocation must occur outside the lock to prevent deadlocks or holding locks during user code execution.

Proposed fix
 static ReloadApplicationHook reloadApplicationHook_;
+static std::mutex reloadApplicationHookMutex_;
 
 void SetReloadApplicationHook(ReloadApplicationHook hook) {
+  std::lock_guard<std::mutex> lock(reloadApplicationHookMutex_);
   reloadApplicationHook_ = std::move(hook);
 }
 
 bool InvokeReloadApplicationHook(const std::string& baseDir) {
-  if (!reloadApplicationHook_) {
+  ReloadApplicationHook hook;
+  {
+    std::lock_guard<std::mutex> lock(reloadApplicationHookMutex_);
+    hook = reloadApplicationHook_;
+  }
+  if (!hook) {
     return false;
   }
-  return reloadApplicationHook_(baseDir);
+  return hook(baseDir);
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
static ReloadApplicationHook reloadApplicationHook_;
void SetReloadApplicationHook(ReloadApplicationHook hook) {
reloadApplicationHook_ = std::move(hook);
}
bool InvokeReloadApplicationHook(const std::string& baseDir) {
if (!reloadApplicationHook_) {
return false;
}
return reloadApplicationHook_(baseDir);
}
static ReloadApplicationHook reloadApplicationHook_;
static std::mutex reloadApplicationHookMutex_;
void SetReloadApplicationHook(ReloadApplicationHook hook) {
std::lock_guard<std::mutex> lock(reloadApplicationHookMutex_);
reloadApplicationHook_ = std::move(hook);
}
bool InvokeReloadApplicationHook(const std::string& baseDir) {
ReloadApplicationHook hook;
{
std::lock_guard<std::mutex> lock(reloadApplicationHookMutex_);
hook = reloadApplicationHook_;
}
if (!hook) {
return false;
}
return hook(baseDir);
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@NativeScript/runtime/Runtime.mm` around lines 577 - 588, Serialize access to
the process-global reloadApplicationHook_ used by SetReloadApplicationHook and
InvokeReloadApplicationHook: protect both reads and writes with synchronization
so initialization and concurrent JavaScript-thread calls cannot race. Update
InvokeReloadApplicationHook to copy the current hook under the lock, then
release the lock before invoking it so user code runs outside the critical
section and avoids deadlocks or lock contention.


static std::atomic<int> runtimeReloadCount_{0};

void IncrementRuntimeReloadCount() { runtimeReloadCount_++; }

int GetRuntimeReloadCount() { return runtimeReloadCount_.load(); }

// API to trigger application reload from JS without restarting the application process.
// Exposes `global.NativeScriptRuntime.reloadApplication(baseDir?)` to JS.
// `NativeScriptRuntime` class is part of the runtime framework and
// is intentionally excluded from metadata generation, so it is not reachable
// from JS on its own.
void Runtime::DefineNativeScriptRuntime(Isolate* isolate, Local<ObjectTemplate> globalTemplate) {
Local<ObjectTemplate> runtimeTemplate = ObjectTemplate::New(isolate);

Local<FunctionTemplate> reloadTemplate =
FunctionTemplate::New(isolate, [](const FunctionCallbackInfo<Value>& info) {
Isolate* isolate = info.GetIsolate();
std::string baseDir;
if (info.Length() > 0 && info[0]->IsString()) {
baseDir = tns::ToString(isolate, info[0]);
}
info.GetReturnValue().Set(tns::InvokeReloadApplicationHook(baseDir));
});
runtimeTemplate->Set(ToV8String(isolate, "reloadApplication"), reloadTemplate);

// 0 on first boot, incremented on every in-process runtime restart. Lets JS
// (e.g. @nativescript/core) detect it is running inside a reloaded instance
// and reattach native delegates that UIKit still dispatches to old-bundle
// instances of (UIApplication delegate, UIScene delegates).
runtimeTemplate->Set(ToV8String(isolate, "reloadCount"),
v8::Number::New(isolate, tns::GetRuntimeReloadCount()));

globalTemplate->Set(ToV8String(isolate, "NativeScriptRuntime"), runtimeTemplate);
}

void Runtime::DefineTimeMethod(v8::Isolate* isolate, v8::Local<v8::ObjectTemplate> globalTemplate) {
Local<FunctionTemplate> timeFunctionTemplate =
FunctionTemplate::New(isolate, [](const FunctionCallbackInfo<Value>& info) {
Expand Down
Loading