diff --git a/Core/AppRuntime/Source/AppRuntime_QuickJS.cpp b/Core/AppRuntime/Source/AppRuntime_QuickJS.cpp index 10505fea..ca219021 100644 --- a/Core/AppRuntime/Source/AppRuntime_QuickJS.cpp +++ b/Core/AppRuntime/Source/AppRuntime_QuickJS.cpp @@ -18,37 +18,110 @@ #pragma warning(pop) #endif +#include +#include + +#if defined(__ANDROID__) +#include +#include +#include +#include +#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 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::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) diff --git a/Core/Node-API/Source/env_chakra.cc b/Core/Node-API/Source/env_chakra.cc index 76cdcd55..562f3a8d 100644 --- a/Core/Node-API/Source/env_chakra.cc +++ b/Core/Node-API/Source/env_chakra.cc @@ -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));