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
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 @@ -23,6 +23,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