Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
a8d0c1f
runtime(apple): CLI bundle resolution + shutdown-order hardening
DjDeveloperr Aug 6, 2026
5ccce2e
ffi(bridge): thread-safe, per-runtime object expandos
DjDeveloperr Aug 6, 2026
8577c61
ffi(bridge): RN backend config — callback gate, lazy symbol indexing,…
DjDeveloperr Aug 6, 2026
2706ae2
ffi(interop): associated objects, primitive aliases, class returns, p…
DjDeveloperr Aug 6, 2026
c1fc91b
ffi(subclass): JS-subclass identity & dispatch
DjDeveloperr Aug 6, 2026
a02aef6
ffi(appearance): UIAppearance proxy primitives
DjDeveloperr Aug 6, 2026
f6421ac
ffi(profiling): interop call counters
DjDeveloperr Aug 6, 2026
7088a2e
react-native: RN module (Fabric hosting, adopted controllers, size fe…
DjDeveloperr Aug 6, 2026
c763ca6
react-native: tests re-pinned to the simplified surface
DjDeveloperr Aug 6, 2026
2aba315
react-native(build): fix native-api packaging gaps found verifying th…
DjDeveloperr Aug 6, 2026
8fd9c79
test(react-native): re-pin super-dispatch assertion to the fixed disp…
DjDeveloperr Aug 6, 2026
10fdf25
ffi(interop): fix stale bound-receiver crash on selector-group re-cro…
DjDeveloperr Aug 6, 2026
3c301f5
react-native(fabric): cut redundant cold-launch host-lifecycle crossings
DjDeveloperr Aug 12, 2026
ef84da1
test(react-native): re-pin stale mount-info assertions to 6738e4ef's …
DjDeveloperr Aug 12, 2026
5382338
fix(ffi): stop calling -description on live callback-argument objects
DjDeveloperr Aug 13, 2026
cdf0c9e
fix(ffi): count prepared-invoker crossings in the always-on interop c…
DjDeveloperr Aug 14, 2026
4a087c1
perf(ffi): add the native hosted-subtree fill walker (__nsFillHostedS…
DjDeveloperr Aug 14, 2026
5e1a7a1
fix(ci): stop check_ffi_boundaries.sh false-positive on a comment string
DjDeveloperr Aug 14, 2026
0cb4a4d
perf(ffi): add the native tabs content-presence scan walker (__nsScan…
DjDeveloperr Aug 14, 2026
1ab87f3
fix(ffi): include BatchOps.mm in the V8 backend TU before HostObject.mm
DjDeveloperr Aug 14, 2026
78dbc4f
fix(ffi): guard the UIKit-only walker host functions with TARGET_OS_I…
DjDeveloperr Aug 14, 2026
431a33e
fix(ffi): import UIKit at true global scope in the V8 TU, not inside …
DjDeveloperr Aug 14, 2026
565c637
perf(ffi): native tabs mountChild embedded-navigation-controller reso…
DjDeveloperr Aug 14, 2026
0dca177
perf(fabric): build mutation-records lazily, past the early-return
DjDeveloperr Aug 14, 2026
a9d0192
perf(fabric): single-snapshot committed funnel + hoisted owner handles
DjDeveloperr Aug 14, 2026
ee527de
perf(fabric): coalesce mid-transaction refresh tail for insert-only c…
DjDeveloperr Aug 14, 2026
828bc2b
chore(fabric): remove temporary [FABRIC7] mount-telemetry instrumenta…
DjDeveloperr Aug 14, 2026
2a18217
chore(fabric): restore [FABRIC7] + add [FABRIC10] observer-phase inst…
DjDeveloperr Aug 14, 2026
784382d
perf(fabric): iteration-10 Stage 1 -- nativeCommitObservations, defau…
DjDeveloperr Aug 14, 2026
06003c7
chore(fabric): remove temporary [FABRIC7]/[FABRIC10] measurement inst…
DjDeveloperr Aug 14, 2026
e6a52b4
chore(fabric): add [ITER11-TXN] transaction-sequence instrumentation …
DjDeveloperr Aug 14, 2026
29383d0
perf(fabric): iteration-11 Stage 1 -- nativeHandleAuthority bit, no c…
DjDeveloperr Aug 14, 2026
c9c6846
chore(fabric): remove temporary [ITER11-TXN] instrumentation (iterati…
DjDeveloperr Aug 14, 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
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,5 +53,6 @@ jobs:
IOS_TEST_TIMEOUT_MS: "600000"
IOS_TEST_INACTIVITY_TIMEOUT_MS: "180000"
IOS_LOG_JUNIT: "1"
IOS_TEST_VERBOSE_SPECS: "1"
IOS_SIMCTL_QUERY_TIMEOUT_MS: "10000"
run: npm run test:ios
8 changes: 7 additions & 1 deletion NativeScript/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,13 @@ if(ENABLE_JS_RUNTIME)
runtime/apple/modules/web/Web.mm
runtime/apple/NativeScript.mm
runtime/apple/RuntimeConfig.cpp
# resolveMainPath() (cli/BundleLoader.h) is called directly from
# NativeScript.mm's runMainApplication, so it must be compiled into every
# framework/app target that includes NativeScript.mm -- not only the
# BUILD_CLI_BINARY executable. cli/main.cpp and cli/segappend.cpp stay
# CLI-binary-only below (main.cpp defines main(), which cannot also be
# linked into the NativeScript shared library).
cli/BundleLoader.mm
runtime/modules/url/ada/ada.cpp
runtime/modules/url/URL.cpp
runtime/modules/url/URLSearchParams.cpp
Expand Down Expand Up @@ -450,7 +457,6 @@ if(BUILD_CLI_BINARY)
set(SOURCE_FILES ${SOURCE_FILES}
cli/main.cpp
cli/segappend.cpp
cli/BundleLoader.mm
)
endif()

Expand Down
104 changes: 101 additions & 3 deletions NativeScript/cli/BundleLoader.mm
Original file line number Diff line number Diff line change
@@ -1,50 +1,148 @@
#include "BundleLoader.h"
#include <Foundation/Foundation.h>
#include <mach-o/dyld.h>
#include <stdlib.h>

// Check if Resources/app/ exists, then load package.json["main"] || app/index.js full file path

std::string resolveMainPath() {
static NSString* resourcesPathForExecutable(NSString* executablePath) {
if (executablePath == nil || [executablePath length] == 0) {
return nil;
}

NSString* standardizedPath = [executablePath stringByStandardizingPath];
NSString* macOSPath = [standardizedPath stringByDeletingLastPathComponent];
NSString* contentsPath = [macOSPath stringByDeletingLastPathComponent];
if ([[macOSPath lastPathComponent] isEqualToString:@"MacOS"] &&
[[contentsPath lastPathComponent] isEqualToString:@"Contents"]) {
return [contentsPath stringByAppendingPathComponent:@"Resources"];
}

return nil;
}

static bool shouldLogBundleResolution() {
return getenv("NS_BUNDLE_LOADER_DEBUG") != nullptr;
}

static void addCandidatePath(NSMutableArray<NSString*>* candidates, NSString* path) {
if (path == nil || [path length] == 0) {
return;
}

NSString* standardizedPath = [path stringByStandardizingPath];
if (![candidates containsObject:standardizedPath]) {
[candidates addObject:standardizedPath];
}
}

static std::string resolveMainPathInResources(NSString* resourcesPath) {
NSFileManager* fileManager = [NSFileManager defaultManager];
NSString* resourcesPath = [[NSBundle mainBundle] resourcePath];
NSString* appPath = [resourcesPath stringByAppendingPathComponent:@"app"];
BOOL isDir;

if ([fileManager fileExistsAtPath:appPath isDirectory:&isDir] && isDir) {
if (shouldLogBundleResolution()) {
NSLog(@"NativeScript BundleLoader checking app path: %@", appPath);
}
NSString* packageJsonPath = [appPath stringByAppendingPathComponent:@"package.json"];
if ([fileManager fileExistsAtPath:packageJsonPath]) {
NSData* jsonData = [NSData dataWithContentsOfFile:packageJsonPath];
NSError* error;
NSError* error = nil;
NSDictionary* packageDict = [NSJSONSerialization JSONObjectWithData:jsonData
options:0
error:&error];
if (error == nil) {
NSString* mainEntry = packageDict[@"main"];
if (shouldLogBundleResolution()) {
NSLog(@"NativeScript BundleLoader package main: %@ from %@", mainEntry, packageJsonPath);
}
if (mainEntry != nil) {
NSString* mainPath = [appPath stringByAppendingPathComponent:mainEntry];
if ([fileManager fileExistsAtPath:mainPath]) {
if (shouldLogBundleResolution()) {
NSLog(@"NativeScript BundleLoader resolved main: %@", mainPath);
}
return std::string([mainPath UTF8String]);
}

if ([[mainEntry pathExtension] length] == 0) {
NSString* mainPathMjs = [mainPath stringByAppendingPathExtension:@"mjs"];
if ([fileManager fileExistsAtPath:mainPathMjs]) {
if (shouldLogBundleResolution()) {
NSLog(@"NativeScript BundleLoader resolved main: %@", mainPathMjs);
}
return std::string([mainPathMjs UTF8String]);
}

NSString* mainPathJs = [mainPath stringByAppendingPathExtension:@"js"];
if ([fileManager fileExistsAtPath:mainPathJs]) {
if (shouldLogBundleResolution()) {
NSLog(@"NativeScript BundleLoader resolved main: %@", mainPathJs);
}
return std::string([mainPathJs UTF8String]);
}
}
}
} else if (shouldLogBundleResolution()) {
NSLog(@"NativeScript BundleLoader failed to parse %@: %@", packageJsonPath, error);
}
}

// Fallback to app/index.js
NSString* indexPath = [appPath stringByAppendingPathComponent:@"index.js"];
if ([fileManager fileExistsAtPath:indexPath]) {
if (shouldLogBundleResolution()) {
NSLog(@"NativeScript BundleLoader resolved fallback main: %@", indexPath);
}
return std::string([indexPath UTF8String]);
}
} else if (shouldLogBundleResolution()) {
NSLog(@"NativeScript BundleLoader skipped resources path: %@ appPath=%@ exists=%d isDir=%d",
resourcesPath,
appPath,
[fileManager fileExistsAtPath:appPath],
isDir);
}

return "";
}

std::string resolveMainPath() {
NSMutableArray<NSString*>* candidates = [NSMutableArray array];
addCandidatePath(candidates, [[NSBundle mainBundle] resourcePath]);
addCandidatePath(candidates, resourcesPathForExecutable([[NSBundle mainBundle] executablePath]));

NSArray<NSString*>* arguments = [[NSProcessInfo processInfo] arguments];
if ([arguments count] > 0) {
addCandidatePath(candidates, resourcesPathForExecutable([arguments objectAtIndex:0]));
}

uint32_t executablePathLength = 0;
_NSGetExecutablePath(nullptr, &executablePathLength);
if (executablePathLength > 0) {
char* executablePathBuffer = static_cast<char*>(malloc(executablePathLength));
if (executablePathBuffer != nullptr) {
if (_NSGetExecutablePath(executablePathBuffer, &executablePathLength) == 0) {
addCandidatePath(candidates, resourcesPathForExecutable([NSString stringWithUTF8String:executablePathBuffer]));
}
free(executablePathBuffer);
}
}

NSString* currentDirectory = [[NSFileManager defaultManager] currentDirectoryPath];
addCandidatePath(candidates, currentDirectory);
addCandidatePath(candidates, [currentDirectory stringByAppendingPathComponent:@"Resources"]);
addCandidatePath(candidates, [[currentDirectory stringByDeletingLastPathComponent] stringByAppendingPathComponent:@"Resources"]);

for (NSString* resourcesPath in candidates) {
if (shouldLogBundleResolution()) {
NSLog(@"NativeScript BundleLoader candidate resources: %@", resourcesPath);
}
std::string mainPath = resolveMainPathInResources(resourcesPath);
if (!mainPath.empty()) {
return mainPath;
}
}

return "";
Expand Down
56 changes: 50 additions & 6 deletions NativeScript/ffi/objc/hermes/NativeApiJsi.mm
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ @protocol NativeApiClassBuilderProtocol
extern const unsigned char embedded_metadata[EMBED_METADATA_SIZE];
#endif

#include "../shared/bridge/InteropProfiler.h"

namespace nativescript {
namespace {

Expand Down Expand Up @@ -86,6 +88,7 @@ void SetNativeApiObjectPrototype(Runtime& runtime, Object& object,
#include "../shared/bridge/TypeConv.mm"
#include "../shared/bridge/Invocation.mm"
#include "../shared/bridge/ClassBuilder.mm"
#include "../shared/bridge/BatchOps.mm"
#include "../shared/bridge/HostObject.mm"
// clang-format on

Expand All @@ -111,7 +114,12 @@ bool tryCallGeneratedEngineObjCSelector(
auto invoker = reinterpret_cast<ObjCGsdInvoker>(prepared.engineInvoker);
GsdObjCContext ctx{runtime, bridge, receiver, prepared.selector, args,
prepared.signature.returnType};
if (!invoker(ctx)) {
bool invoked;
{
NativeScriptInteropCallTimer nsInteropTimer;
invoked = invoker(ctx);
}
if (!invoked) {
return false;
}
*result = std::move(ctx.result);
Expand Down Expand Up @@ -144,7 +152,26 @@ NativeApiSelectorGroupState state(
}
if (state.boundReceiverState != nullptr) {
receiverHostObject = state.boundReceiver.lock();
} else if (thisValue.isObject()) {
if (receiverHostObject) {
return receiverHostObject;
}
// The bound receiver's wrapper has already been torn down (its
// owning JS proxy was collected) since this selector-group
// function was minted and cached as a native-object expando
// (Object.mm's `bridge_->setObjectExpando(..., methodFunction)`).
// The expando itself is keyed by the native pointer and survives
// wrapper churn, so a LATER crossing that re-wraps the SAME
// native object in a fresh `NativeApiObjectHostObject` (this
// runtime mints a new wrapper per crossing) finds the stale
// cached function still bound to the dead original -- every call
// through it then resolves a nil receiver and throws "Objective-C
// selector requires a native receiver" even though the method is
// being invoked on a perfectly live object right now. Fall
// through to `thisValue` exactly like the unbound path below:
// this IS a method call (`receiver.method(...)`), so `thisValue`
// is always the correct, live receiver for this invocation.
}
if (thisValue.isObject()) {
Object receiverObject = thisValue.asObject(runtime);
if (receiverObject.isHostObject<NativeApiObjectHostObject>(
runtime)) {
Expand Down Expand Up @@ -172,16 +199,26 @@ NativeApiSelectorGroupState state(
// GSD fast path: read jsi args directly, call objc_msgSend with a
// typed cast, produce the jsi return value — bypassing all generic
// marshalling. Only engages for plain calls (no super dispatch, init
// disown handling, or implicit NSError-out argument).
// disown handling, implicit NSError-out argument, or appearance
// static selector — those need the generic path's proxy tagging).
if (call.prepared->gsdEngineCallable && call.dispatchClass == Nil &&
count == call.prepared->gsdEngineArgumentCount &&
!(!state.receiverIsClass && call.prepared->isInitMethod)) {
!(!state.receiverIsClass && call.prepared->isInitMethod) &&
call.gsdAllowed) {
auto invoker =
reinterpret_cast<ObjCGsdInvoker>(call.prepared->engineInvoker);
GsdObjCContext ctx{runtime, state.bridge, call.receiver,
call.prepared->selector, args,
call.prepared->signature.returnType};
if (invoker(ctx)) {
bool gsdInvoked;
{
NativeScriptInteropCallTimer nsInteropTimer;
gsdInvoked = invoker(ctx);
}
if (gsdInvoked) {
cachePreparedAppearanceProxySetterValue(
runtime, state.bridge, call.receiver, *call.prepared, args,
count);
return std::move(ctx.result);
}
}
Expand All @@ -198,8 +235,15 @@ NativeApiSelectorGroupState state(
throw JSError(runtime,
"Objective-C selector requires a native receiver.");
}
return receiverHostObject->callPreparedObjectSelector(
Value result = receiverHostObject->callPreparedObjectSelector(
runtime, *call.prepared, args, count, call.dispatchClass);
if (!state.receiverIsClass && call.prepared->isInitMethod) {
if (auto preserved = preservedNativeApiInitializerSelfReturn(
runtime, state.bridge, call.receiver, result, thisValue)) {
return std::move(*preserved);
}
}
return result;
});
}

Expand Down
5 changes: 4 additions & 1 deletion NativeScript/ffi/objc/hermes/NativeApiJsiReactNative.h
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,10 @@ inline NativeApiJsiConfig MakeReactNativeNativeApiJsiConfig(
config.metadataPath = metadataPath;
config.metadataPtr = metadataPtr;
config.globalName = globalName;
config.installGlobalSymbols = true;
// RN launch cost: don't eagerly install the aggregate global surface or
// realize every class/protocol runtime pointer at symbol-index time.
config.installGlobalSymbols = false;
config.indexRuntimePointers = false;
config.invokeCallbacksOnNativeCallerThread = true;
config.scheduler = std::make_shared<ReactNativeCallInvokerScheduler>(
std::move(jsInvoker), std::move(uiInvoker));
Expand Down
2 changes: 2 additions & 0 deletions NativeScript/ffi/objc/jsc/NativeApiJSC.mm
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
#include "NativeApiJSCRuntime.h"
#include "SignatureDispatch.h"

#include "../shared/bridge/InteropProfiler.h"

namespace nativescript {

namespace {
Expand Down
34 changes: 31 additions & 3 deletions NativeScript/ffi/objc/jsc/NativeApiJSCSelectorGroups.mm
Original file line number Diff line number Diff line change
Expand Up @@ -67,14 +67,21 @@ throw JSError(

// GSD fast path: the generated invoker reads args directly from the JSC
// arguments, calls objc_msgSend with a typed cast, and produces the JS
// return value — bypassing all generic marshalling.
// return value — bypassing all generic marshalling. Excludes appearance
// static selectors — those need the generic path's proxy tagging.
if (prepared.gsdEngineCallable && dispatchSuperClass == Nil &&
providedCount == prepared.gsdEngineArgumentCount &&
!initializerClassWrapper && !isNSErrorOutMethod) {
!initializerClassWrapper && !isNSErrorOutMethod &&
!isPreparedStaticAppearanceSelector(prepared)) {
auto invoker = reinterpret_cast<ObjCGsdInvoker>(prepared.engineInvoker);
GsdObjCContext ctx{runtime, bridge, receiver, prepared.selector,
runtime.context(), arguments, signature.returnType};
if (invoker(ctx)) {
if (providedCount > 0) {
Value setterValue = Value::borrowed(runtime, arguments[0]);
cachePreparedAppearanceProxySetterValue(runtime, bridge, receiver,
prepared, &setterValue, 1);
}
return ctx.result;
}
}
Expand All @@ -89,6 +96,11 @@ throw JSError(
if (tryCallFastEngineObjCSelector(runtime, bridge, receiver, prepared,
fastArgs, providedCount, Nil,
&fastResult)) {
cachePreparedAppearanceProxySetterValue(runtime, bridge, receiver,
prepared, fastArgs,
providedCount);
fastResult = tagPreparedStaticAppearanceSelectorResult(
runtime, bridge, receiver, prepared, std::move(fastResult));
return fastResult.local(runtime);
}
}
Expand Down Expand Up @@ -159,6 +171,11 @@ NativeApiReturnStorage returnStorage(
throw JSError(
runtime, errorMessage != nullptr ? errorMessage : "Unknown NSError");
}
if (providedCount > 0) {
Value setterValue = Value::borrowed(runtime, arguments[0]);
cachePreparedAppearanceProxySetterValue(runtime, bridge, receiver,
prepared, &setterValue, 1);
}
if (initializerClassWrapper) {
id resultObject = nil;
if (isObjectiveCObjectType(returnType)) {
Expand All @@ -173,6 +190,8 @@ throw JSError(
Value(runtime, *initializerClassWrapper));
}
}
tagPreparedStaticAppearanceNativeReturn(
runtime, bridge, receiver, prepared, returnType, returnStorage.data());
return setJSCEngineReturnValue(runtime, bridge, returnType,
returnStorage.data(), prepared.selectorName);
}
Expand Down Expand Up @@ -227,10 +246,19 @@ JSValueRef NativeApiSelectorGroupCall(
if (call.hasImmediateResult) {
return call.immediateResult.local(runtime);
}
return setJSCEnginePreparedObjCResult(
JSValueRef result = setJSCEnginePreparedObjCResult(
runtime, data->bridge, call.receiver, *call.prepared,
call.receiverHostObject, call.initializerClassWrapper, argumentCount,
arguments, call.dispatchClass);
if (!data->receiverIsClass && call.prepared->isInitMethod &&
thisObject != nullptr) {
if (auto preserved = preservedNativeApiInitializerSelfReturn(
runtime, data->bridge, call.receiver, Value::borrowed(runtime, result),
Value::borrowed(runtime, thisObject))) {
return preserved->local(runtime);
}
}
return result;
} catch (const std::exception& error) {
engine::jscengine::setException(context, exception, error);
return JSValueMakeUndefined(context);
Expand Down
2 changes: 2 additions & 0 deletions NativeScript/ffi/objc/quickjs/NativeApiQuickJS.mm
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
#include "NativeApiQuickJSRuntime.h"
#include "SignatureDispatch.h"

#include "../shared/bridge/InteropProfiler.h"

namespace nativescript {

namespace {
Expand Down
Loading
Loading