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
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
55 changes: 49 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 @@ -111,7 +113,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 +151,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 +198,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 +234,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