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 NativeScript/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,7 @@ if(ENABLE_JS_RUNTIME)
runtime/modules/url/ada/ada.cpp
runtime/modules/url/URL.cpp
runtime/modules/url/URLSearchParams.cpp
runtime/modules/esm/ESModuleSupport.cpp
)

if(TARGET_ENGINE_V8)
Expand Down
24 changes: 24 additions & 0 deletions NativeScript/ffi/jni/napi/exceptions/NativeScriptException.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,31 @@ void NativeScriptException::CallJsFuncWithErr(napi_env env, napi_value errObj, b
if (napi_util::is_of_type(env, handler, napi_function)) {
napi_value result;
NAPI_GUARD(napi_call_function(env, global, handler, 1, &errObj, &result)) {}
return;
}

// Nothing in JS can observe this error yet: it happened before the app
// installed its handler, typically while the entry module was loading, so
// logcat is the only place left to report it.
std::string report;
for (const char* propertyName : {"stack", "message", "stackTrace"}) {
napi_value value = nullptr;
if (napi_get_named_property(env, errObj, propertyName, &value) != napi_ok ||
!napi_util::is_of_type(env, value, napi_string)) {
continue;
}
std::string text = ArgConverter::ConvertToString(env, value);
if (text.empty() || report.find(text) != std::string::npos) {
continue;
}
if (!report.empty()) {
report += "\n";
}
report += text;
}
__android_log_print(ANDROID_LOG_ERROR, "JS",
"Uncaught error before a JS error handler was installed:\n%s",
report.c_str());
}

napi_value NativeScriptException::WrapJavaToJsException(napi_env env) {
Expand Down
2 changes: 1 addition & 1 deletion NativeScript/ffi/objc/shared/bridge/ClassBuilder.mm
Original file line number Diff line number Diff line change
Expand Up @@ -729,7 +729,7 @@ throw JSError(runtime,
newSymbol.name = className;
newSymbol.runtimeName = className;
newSymbol.superclassOffset = baseSymbol.offset;
return makeNativeClassValue(runtime, bridge, std::move(newSymbol));
return makeExtendedNativeClassValue(runtime, bridge, std::move(newSymbol));
}

Value invokeNativeApiBaseMethod(
Expand Down
3 changes: 3 additions & 0 deletions NativeScript/ffi/objc/shared/bridge/ObjCBridge.mm
Original file line number Diff line number Diff line change
Expand Up @@ -2363,6 +2363,9 @@ Value makeNativeObjectValue(Runtime& runtime,
Value makeNativeClassValue(Runtime& runtime,
const std::shared_ptr<NativeApiBridge>& bridge,
NativeApiSymbol symbol);
Value makeExtendedNativeClassValue(Runtime& runtime,
const std::shared_ptr<NativeApiBridge>& bridge,
NativeApiSymbol symbol);

Object symbolToObject(Runtime& runtime, const NativeApiSymbol& symbol) {
Object result(runtime);
Expand Down
17 changes: 17 additions & 0 deletions NativeScript/ffi/objc/shared/bridge/host_objects/Class.mm
Original file line number Diff line number Diff line change
Expand Up @@ -485,6 +485,23 @@ Value makeNativeClassValue(Runtime& runtime,
std::make_shared<NativeApiClassHostObject>(bridge, std::move(symbol)));
}

// For a class the runtime just registered. Unlike makeNativeClassValue this
// never resolves by name: a global of the same name whose `kind` reads "class"
// may be the JS constructor being extended (it inherits that from its base
// wrapper through extendStatics), and taking it would hand back the base class.
Value makeExtendedNativeClassValue(Runtime& runtime,
const std::shared_ptr<NativeApiBridge>& bridge,
NativeApiSymbol symbol) {
Class cls = objc_lookUpClass(symbol.runtimeName.c_str());
Value cachedClass = bridge->findClassValue(runtime, cls);
if (!cachedClass.isUndefined()) {
return cachedClass;
}
return Object::createFromHostObject(
runtime,
std::make_shared<NativeApiClassHostObject>(bridge, std::move(symbol)));
}

Protocol* lookupProtocolByNativeName(const std::string& name) {
Protocol* protocol = objc_getProtocol(name.c_str());
if (protocol != nullptr) {
Expand Down
17 changes: 15 additions & 2 deletions NativeScript/napi/hermes/jsr.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,8 @@ napi_status js_create_napi_env(napi_env* env, jsr_ns_runtime runtime) {
//
// Apple used to take a different route through a NativeScript-local
// jsi::Runtime::createNodeApiEnv hook. That hook no longer exists upstream,
// and both platforms now build against the same headers, so there is one path.
// and both platforms now build against the same headers, so there is one
// path.
auto hermesInterface =
facebook::jsi::castInterface<facebook::hermes::IHermes>(
runtime->hermes->rt);
Expand Down Expand Up @@ -310,7 +311,19 @@ napi_status js_execute_pending_jobs(napi_env env) {
if (jsr == nullptr) {
return napi_invalid_arg;
}
jsr->rt->drainMicrotasks();
// drainMicrotasks() reports a failing job by throwing a C++ jsi::JSError
// (see jsr_drain_microtasks). Callers sit behind JNI and Looper callbacks,
// where an unwinding C++ exception aborts the process, so it is converted
// into a pending exception they already know how to report.
try {
jsr->rt->drainMicrotasks();
} catch (const facebook::jsi::JSError& e) {
napi_throw_error(env, nullptr, e.getMessage().c_str());
return napi_pending_exception;
} catch (const facebook::jsi::JSIException& e) {
napi_throw_error(env, nullptr, e.what());
return napi_pending_exception;
}
return napi_ok;
#else
bool result;
Expand Down
36 changes: 36 additions & 0 deletions NativeScript/runtime/android/napi/Runtime.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -594,6 +594,42 @@ void Runtime::RunModule(const char *moduleName) {

void Runtime::RunWorker(const std::string &filePath) {
m_module.LoadWorker(env, filePath);
js_execute_pending_jobs(env);
}

void Runtime::EnterJsCall() {
m_jsCallDepth++;
}

void Runtime::LeaveJsCall() {
m_jsCallDepth--;
}

// Engines with an explicit job queue (Hermes, QuickJS, PrimJS) run promise
// reactions only when asked; V8 and JavaScriptCore do it themselves as soon as
// the JS stack empties. This reproduces that moment: the queue is drained after
// the outermost call from Java into JS and never inside a nested one, so jobs
// cannot interleave with a JS frame that is still on the stack.
void Runtime::RunMicrotaskCheckpoint() {
if (m_jsCallDepth > 1) {
return;
}

napi_status status = js_execute_pending_jobs(env);
bool pendingException = false;
napi_is_exception_pending(env, &pendingException);
if (status == napi_ok && !pendingException) {
return;
}

napi_value error = nullptr;
if (pendingException) {
napi_get_and_clear_last_exception(env, &error);
}
if (error != nullptr) {
throw NativeScriptException(env, error, "Error running microtasks");
}
throw NativeScriptException("Error running microtasks");
}

void Runtime::DisposeWorkerRuntime(Runtime *runtime) {
Expand Down
9 changes: 9 additions & 0 deletions NativeScript/runtime/android/napi/Runtime.h
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,14 @@ namespace tns {

std::string ReadFileText(const std::string &filePath);

// Java -> JS transitions are counted so the microtask checkpoint runs
// only when the outermost one returns; see RunMicrotaskCheckpoint.
void EnterJsCall();

void LeaveJsCall();

void RunMicrotaskCheckpoint();

bool NotifyGC(JNIEnv *jEnv, jobject obj, jintArray object_ids);

bool TryCallGC();
Expand Down Expand Up @@ -194,6 +202,7 @@ namespace tns {
ArrayBufferHelper m_arrayBufferHelper;

bool m_isMainThread;
int m_jsCallDepth = 0;

ModuleInternal m_module;

Expand Down
26 changes: 26 additions & 0 deletions NativeScript/runtime/android/napi/com_tns_Runtime.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -129,16 +129,34 @@ Runtime* TryGetRuntime(int runtimeId) {
return runtime;
}

// Brackets a call from Java into JS so the runtime knows when the outermost
// one returns; see Runtime::RunMicrotaskCheckpoint.
class JsCallScope {
public:
explicit JsCallScope(Runtime* runtime) : m_runtime(runtime) {
m_runtime->EnterJsCall();
}

~JsCallScope() {
m_runtime->LeaveJsCall();
}

private:
Runtime* m_runtime;
};

extern "C" JNIEXPORT void Java_com_tns_Runtime_runModule(JNIEnv* _env, jobject obj, jint runtimeId, jstring scriptFile) {
auto runtime = TryGetRuntime(runtimeId);
if (runtime == nullptr) {
return;
}

NapiScope scope(runtime->GetNapiEnv());
JsCallScope call(runtime);

try {
runtime->RunModule(_env, obj, scriptFile);
runtime->RunMicrotaskCheckpoint();
} catch (NativeScriptException& e) {
e.ReThrowToJava(runtime->GetNapiEnv());
} catch (std::exception e) {
Expand All @@ -160,8 +178,10 @@ extern "C" JNIEXPORT jobject Java_com_tns_Runtime_runScript(JNIEnv* _env, jobjec

napi_env napiEnv = runtime->GetNapiEnv();
NapiScope scope(napiEnv);
JsCallScope call(runtime);
try {
result = runtime->RunScript(_env, obj, scriptFile);
runtime->RunMicrotaskCheckpoint();
} catch (NativeScriptException& e) {
e.ReThrowToJava(napiEnv);
} catch (std::exception e) {
Expand All @@ -183,8 +203,10 @@ extern "C" JNIEXPORT jobject Java_com_tns_Runtime_callJSMethodNative(JNIEnv* _en
if (runtime == nullptr) return result;

NapiScope scope(runtime->GetNapiEnv());
JsCallScope call(runtime);
try {
result = runtime->CallJSMethodNative(_env, obj, javaObjectID, claz, methodName, retType, isConstructor, packagedArgs);
runtime->RunMicrotaskCheckpoint();
} catch (NativeScriptException& e) {
e.ReThrowToJava( runtime->GetNapiEnv());
} catch (std::exception e) {
Expand All @@ -206,9 +228,11 @@ extern "C" JNIEXPORT void Java_com_tns_Runtime_createJSInstanceNative(JNIEnv* _e
if (runtime == nullptr) return;

NapiScope scope(runtime->GetNapiEnv());
JsCallScope call(runtime);

try {
runtime->CreateJSInstanceNative(_env, obj, javaObject, javaObjectID, className);
runtime->RunMicrotaskCheckpoint();
} catch (NativeScriptException& e) {
e.ReThrowToJava( runtime->GetNapiEnv());
} catch (std::exception e) {
Expand Down Expand Up @@ -286,9 +310,11 @@ extern "C" JNIEXPORT void Java_com_tns_Runtime_passExceptionToJsNative(JNIEnv* j
if (runtime == nullptr) return;

NapiScope scope(runtime->GetNapiEnv());
JsCallScope call(runtime);

try {
runtime->PassExceptionToJsNative(jEnv, obj, exception, message, fullStackTrace, jsStackTrace, isDiscarded, isPendingError);
runtime->RunMicrotaskCheckpoint();
} catch (NativeScriptException& e) {
e.ReThrowToJava(runtime->GetNapiEnv());
} catch (std::exception e) {
Expand Down
52 changes: 40 additions & 12 deletions NativeScript/runtime/android/napi/modules/module/ModuleInternal.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
#include <sys/stat.h>
#include <ctime>
#include "GlobalHelpers.h"
#include "ESModuleSupport.h"
#include <utime.h>


Expand All @@ -34,6 +35,10 @@ void ThrowFallbackRequireError(napi_env env, const char* message) {
napi_throw_error(env, nullptr, message);
}

bool IsJavaScriptModulePath(const std::string& path) {
return Util::EndsWith(path, ".js") || Util::EndsWith(path, ".mjs") || Util::EndsWith(path, ".cjs");
}

void ReThrowRequireError(napi_env env, NativeScriptException& exception) {
try {
exception.ReThrowToNapi(env);
Expand Down Expand Up @@ -353,7 +358,7 @@ napi_value ModuleInternal::LoadImpl(napi_env env, const std::string& moduleName,
auto it2 = m_loadedModules.find(path);

if (it2 == m_loadedModules.end()) {
if (Util::EndsWith(path, ".js") || Util::EndsWith(path, ".so")) {
if (IsJavaScriptModulePath(path) || Util::EndsWith(path, ".so")) {
isData = false;
result = LoadModule(env, path, cachePathKey);
} else if (Util::EndsWith(path, ".json")) {
Expand Down Expand Up @@ -424,18 +429,26 @@ napi_value ModuleInternal::LoadModule(napi_env env, const std::string& modulePat

napi_value moduleFunc;

if (Util::EndsWith(modulePath, ".js")) {
if (IsJavaScriptModulePath(modulePath)) {
DEBUG_WRITE("%s", modulePath.c_str());

// Fast path: if the build compiled this module to engine bytecode, run it
// directly. This peeks the file header only — the source is never read or
// wrapped for a bytecode module. Bytecode is the compiled form of the
// *wrapped* module content, so it yields the same wrapper function.
status = js_run_bytecode_file(env, EnsureFileProtocol(modulePath).c_str(), &moduleFunc);
if (status == napi_cannot_run_js) {
// Not bytecode — compile and run the wrapped source as usual.
napi_value script = LoadScript(env, modulePath, fullRequiredModulePath);
if (nativescript::esm::IsESModulePath(modulePath)) {
// ES module sources are rewritten to CommonJS at load time, so the
// bytecode compiler never produced a precompiled form to try first.
napi_util::define_property(env, exportsObj, "__esModule", napi_util::get_true(env));
napi_value script = WrapESModuleContent(env, modulePath);
status = js_execute_script(env, script, EnsureFileProtocol(modulePath).c_str(), &moduleFunc);
} else {
// Fast path: if the build compiled this module to engine bytecode, run it
// directly. This peeks the file header only — the source is never read or
// wrapped for a bytecode module. Bytecode is the compiled form of the
// *wrapped* module content, so it yields the same wrapper function.
status = js_run_bytecode_file(env, EnsureFileProtocol(modulePath).c_str(), &moduleFunc);
if (status == napi_cannot_run_js) {
// Not bytecode — compile and run the wrapped source as usual.
napi_value script = LoadScript(env, modulePath, fullRequiredModulePath);
status = js_execute_script(env, script, EnsureFileProtocol(modulePath).c_str(), &moduleFunc);
}
}
if (status != napi_ok) {
bool pendingException;
Expand Down Expand Up @@ -566,12 +579,27 @@ napi_value ModuleInternal::LoadData(napi_env env, const std::string& path) {
}

napi_value ModuleInternal::WrapModuleContent(napi_env env, const std::string& path) {
std::string content = nativescript::esm::RewriteCommonJSDynamicImportsForFallbackEngines(
nativescript::esm::StripShebang(Runtime::GetRuntime(m_env)->ReadFileText(path)));
return WrapWithModuleFunction(env, content, false /* isESModule */);
}

std::string content = Runtime::GetRuntime(m_env)->ReadFileText(path);
napi_value ModuleInternal::WrapESModuleContent(napi_env env, const std::string& path) {
std::string content = nativescript::esm::TransformESModuleForFallbackEngines(
nativescript::esm::StripShebang(Runtime::GetRuntime(m_env)->ReadFileText(path)));
return WrapWithModuleFunction(env, content, true /* isESModule */);
}

// TODO: Use statically allocated buffer for better performance
napi_value ModuleInternal::WrapWithModuleFunction(napi_env env, const std::string& content, bool isESModule) {
// The shims share the prologue's line so source line numbers are unchanged.
// MODULE_PROLOGUE itself stays byte-identical to the one the bytecode
// compiler wraps with; only source-evaluated modules get the shims.
std::string result(MODULE_PROLOGUE);
result.reserve(content.length() + 1024);
result += NS_ESM_FALLBACK_DYNAMIC_IMPORT_SHIM;
if (isESModule) {
result += NS_ESM_FALLBACK_MODULE_SHIM;
}
result += content;
result += MODULE_EPILOGUE;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ class ModuleInternal {
napi_value RequireCallbackImpl(napi_env env, napi_callback_info info);

napi_value WrapModuleContent(napi_env env, const std::string& path);
napi_value WrapESModuleContent(napi_env env, const std::string& path);
napi_value WrapWithModuleFunction(napi_env env, const std::string& content, bool isESModule);

napi_value LoadImpl(napi_env env, const std::string& moduleName, const std::string& baseDir, bool& isData);

Expand Down
10 changes: 10 additions & 0 deletions NativeScript/runtime/android/napi/workers/WorkerWrapper.cpp
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
#include "jsr_common.h"
#include "WorkerWrapper.h"

#include <android/looper.h>
Expand Down Expand Up @@ -180,6 +181,9 @@ void WorkerWrapper::DrainPendingTasks() {
napi_value args[1] = {event};
napi_value result;
status = napi_call_function(env, globalObject, callback, 1, args, &result);
if (status == napi_ok) {
status = js_execute_pending_jobs(env);
}
if (status == napi_pending_exception && !isTerminating_) {
napi_value error;
NAPI_GUARD(napi_get_and_clear_last_exception(env, &error)) {}
Expand Down Expand Up @@ -229,6 +233,9 @@ void WorkerWrapper::FireMessageOnParentWorkerObject(int workerId,
napi_value args[1] = {event};
napi_value result;
status = napi_call_function(env, worker, callback, 1, args, &result);
if (status == napi_ok) {
status = js_execute_pending_jobs(env);
}
if (status == napi_pending_exception) {
napi_value error;
NAPI_GUARD(napi_get_and_clear_last_exception(env, &error)) {}
Expand Down Expand Up @@ -313,6 +320,9 @@ void WorkerWrapper::FireErrorOnParentWorkerObject(int workerId, const std::strin
napi_value args[1] = {errEvent};
napi_value result;
status = napi_call_function(env, worker, callback, 1, args, &result);
if (status == napi_ok) {
status = js_execute_pending_jobs(env);
}

if (status == napi_pending_exception) {
napi_value exception;
Expand Down
Loading
Loading