Skip to content
115 changes: 94 additions & 21 deletions Core/AppRuntime/Source/AppRuntime_QuickJS.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,37 +18,110 @@
#pragma warning(pop)
#endif

#include <stdexcept>
#include <cstddef>

#if defined(__ANDROID__)
#include <pthread.h>
#include <exception>
#include <functional>
#include <utility>
#endif

namespace Babylon
{
void AppRuntime::RunEnvironmentTier(const char* /*executablePath*/)
namespace
{
// Create the runtime.
JSRuntime* runtime = JS_NewRuntime();
if (!runtime)
// Runs the QuickJS environment on the calling thread. QuickJS's interpreter recurses in C
// (one JS_CallInternal frame per JS call), so deep JS call stacks need a comparably deep C
// stack; QuickJS's own limit (JS_DEFAULT_STACK_SIZE, 1 MiB) guards against overrun.
// jsStackLimit: value for JS_SetMaxStackSize, or 0 to keep QuickJS's default (1 MiB). A
// non-default limit is only safe when the caller guarantees a stack large enough to hold it
// below the guard page -- see the Android nested-thread path below.
void RunQuickJSEnvironment(AppRuntime& appRuntime, void (AppRuntime::*run)(Napi::Env), size_t jsStackLimit)
{
throw std::runtime_error{"Failed to create QuickJS runtime"};
}
JSRuntime* runtime = JS_NewRuntime();
if (!runtime)
{
throw std::runtime_error{"Failed to create QuickJS runtime"};
}
if (jsStackLimit != 0)
{
JS_SetMaxStackSize(runtime, jsStackLimit);
}

// Create the context.
JSContext* context = JS_NewContext(runtime);
if (!context)
{
JSContext* context = JS_NewContext(runtime);
if (!context)
{
JS_FreeRuntime(runtime);
throw std::runtime_error{"Failed to create QuickJS context"};
}

{
Napi::Env env = Napi::Attach(context);
(appRuntime.*run)(env);
Napi::Detach(env);
}

JS_FreeContext(context);
JS_FreeRuntime(runtime);
throw std::runtime_error{"Failed to create QuickJS context"};
}
}

// Use the context within a scope.
{
Napi::Env env = Napi::Attach(context);

Run(env);
void AppRuntime::RunEnvironmentTier(const char* /*executablePath*/)
{
#if defined(__ANDROID__)
// bionic gives this worker thread ~1 MiB of stack, at or below QuickJS's default 1 MiB
// recursion limit -- so deep-but-legal JS recursion faults the guard page (SIGSEGV) before
// QuickJS can raise a catchable "stack overflow", while clamping the limit below 1 MiB
// instead rejects call depths that every other engine (and desktop QuickJS on its ~8 MiB
// stack) accepts. Run the environment on a nested thread with a desktop-sized stack so
// QuickJS's raised limit sits safely below the guard page and deep recursion fits. The
// stack must be generous because connectedAndroidTest is an unoptimized Debug build, whose
// JS_CallInternal frames are several times larger than a release build's -- depth-128
// recursion (which release QuickJS clears within the 1 MiB default) needs well over 1 MiB
// here, so QuickJS's default limit would still reject it on any thread size.
constexpr size_t NestedStackSize{8 * 1024 * 1024};
constexpr size_t JsStackLimit{6 * 1024 * 1024}; // < NestedStackSize guard; > debug depth-128 need
std::function<void()> body{[this] { RunQuickJSEnvironment(*this, &AppRuntime::Run, JsStackLimit); }};
std::exception_ptr thrown{};
auto payload = std::make_pair(&body, &thrown);
auto trampoline = [](void* arg) -> void* {
auto* p = static_cast<std::pair<std::function<void()>*, std::exception_ptr*>*>(arg);
try
{
(*p->first)();
}
catch (...)
{
*p->second = std::current_exception();
}
return nullptr;
};

Napi::Detach(env);
pthread_attr_t attr;
if (pthread_attr_init(&attr) == 0)
{
pthread_attr_setstacksize(&attr, NestedStackSize);
pthread_t tid{};
const int created = pthread_create(&tid, &attr, trampoline, &payload);
pthread_attr_destroy(&attr);
if (created == 0)
{
pthread_join(tid, nullptr);
if (thrown)
{
std::rethrow_exception(thrown);
}
return;
}
}

// Destroy the context and runtime.
JS_FreeContext(context);
JS_FreeRuntime(runtime);
// Thread creation failed: fall back to the current (small) worker thread, where only
// QuickJS's default limit is safe.
RunQuickJSEnvironment(*this, &AppRuntime::Run, 0);
#else
RunQuickJSEnvironment(*this, &AppRuntime::Run, 0);
#endif
}

void AppRuntime::ShutdownEnvironment(Napi::Env)
Expand Down
31 changes: 31 additions & 0 deletions Core/Node-API/Source/env_chakra.cc
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,37 @@ namespace Napi
JsValueRef global;
ThrowIfFailed(JsGetGlobalObject(&global));
JsPropertyIdRef propertyId;

// The Windows 10 Chakra predates ES2020 and has no `globalThis`; scripts written against
// browsers (and the polyfills in this repo) reference it. Define it as a plain, writable,
// configurable property of the global object, exactly as the spec describes.
ThrowIfFailed(JsGetPropertyIdFromName(L"globalThis", &propertyId));
JsValueRef existingGlobalThis;
ThrowIfFailed(JsGetProperty(global, propertyId, &existingGlobalThis));
JsValueType existingType;
ThrowIfFailed(JsGetValueType(existingGlobalThis, &existingType));
if (existingType == JsUndefined)
{
// { value: globalThis, writable: true, enumerable: false, configurable: true } -- the
// spec's own data property; plain assignment would make it enumerable.
JsValueRef descriptor;
ThrowIfFailed(JsCreateObject(&descriptor));
JsValueRef trueValue;
ThrowIfFailed(JsGetTrueValue(&trueValue));
JsValueRef falseValue;
ThrowIfFailed(JsGetFalseValue(&falseValue));
JsPropertyIdRef descriptorPropertyId;
ThrowIfFailed(JsGetPropertyIdFromName(L"value", &descriptorPropertyId));
ThrowIfFailed(JsSetProperty(descriptor, descriptorPropertyId, global, true));
ThrowIfFailed(JsGetPropertyIdFromName(L"writable", &descriptorPropertyId));
ThrowIfFailed(JsSetProperty(descriptor, descriptorPropertyId, trueValue, true));
ThrowIfFailed(JsGetPropertyIdFromName(L"enumerable", &descriptorPropertyId));
ThrowIfFailed(JsSetProperty(descriptor, descriptorPropertyId, falseValue, true));
ThrowIfFailed(JsGetPropertyIdFromName(L"configurable", &descriptorPropertyId));
ThrowIfFailed(JsSetProperty(descriptor, descriptorPropertyId, trueValue, true));
bool defined;
ThrowIfFailed(JsDefineProperty(global, propertyId, descriptor, &defined));
}
ThrowIfFailed(JsGetPropertyIdFromName(L"Object", &propertyId));
JsValueRef object;
ThrowIfFailed(JsGetProperty(global, propertyId, &object));
Expand Down