Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
23a3130
QuickJS experiment
CedricGuillemet Jan 15, 2026
2e63f18
removed js
CedricGuillemet Jan 15, 2026
61f1658
cleanup cmake scripts
CedricGuillemet Jan 19, 2026
139a172
warnings
CedricGuillemet Jan 19, 2026
67cacb0
fix prototype
CedricGuillemet Jan 19, 2026
2523e68
forced quickjs promise continuation
CedricGuillemet Jan 20, 2026
3c14c00
fix performance
CedricGuillemet Jan 20, 2026
f32eda9
fix some leaks
CedricGuillemet Jan 21, 2026
af92089
newer quickjs-ng
CedricGuillemet Apr 22, 2026
0f14750
Add QuickJS CI jobs for Win32, Linux, and Android
CedricGuillemet Apr 22, 2026
19be006
Fix multiple bugs in QuickJS NAPI bindings
CedricGuillemet Apr 22, 2026
2f07daf
Move QuickJS microtask processing out of shared code
CedricGuillemet Apr 22, 2026
7d7d077
Upgrade C++ standard from 17 to 20
CedricGuillemet Apr 22, 2026
8657c54
quickjs on android emulator test
CedricGuillemet Apr 22, 2026
fe26c61
Fix JSValue leak in Detach causing JS_FreeRuntime assert on Android
CedricGuillemet Apr 22, 2026
f9e10bb
Fix QuickJS NAPI weak-reference and ExternalCallback self-cycle leaks
CedricGuillemet Apr 24, 2026
a98aabf
quickjs napi: track napi_refs in env and release at Detach
CedricGuillemet Apr 24, 2026
bf4183d
cmake: define NDEBUG on the QuickJS library target
CedricGuillemet Apr 27, 2026
4502c2c
Revert "cmake: define NDEBUG on the QuickJS library target"
CedricGuillemet Apr 27, 2026
5d1fd76
QuickJS: skip newTarget self-dup for non-constructor functions
CedricGuillemet Apr 27, 2026
f370cab
Merge branch 'main' of https://github.com/babylonjs/JsRuntimeHost int…
CedricGuillemet May 26, 2026
ca61e84
Merge BabylonJS/JsRuntimeHost main into quickjs
web-flow Jun 9, 2026
3ae53f8
fix(quickjs): suppress -Wshorten-64-to-32 on Apple Clang
web-flow Jun 9, 2026
1b9b298
Merge remote-tracking branch 'upstream/main' into quickjs
CedricGuillemet Jul 2, 2026
5d8c346
up quickjs
CedricGuillemet Jul 2, 2026
758257f
leak fix iteration
CedricGuillemet Jul 2, 2026
fd593f1
websocket ref
CedricGuillemet Jul 2, 2026
cd76f5d
unref env
CedricGuillemet Jul 2, 2026
172340e
PR feedback
CedricGuillemet Jul 2, 2026
e30c746
feedback
CedricGuillemet Jul 3, 2026
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
5 changes: 5 additions & 0 deletions .github/workflows/build-linux.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ on:
required: false
type: string
default: g++
js-engine:
required: false
type: string
default: ''
enable-sanitizers:
required: false
type: boolean
Expand Down Expand Up @@ -39,6 +43,7 @@ jobs:
run: |
cmake -B Build/ubuntu -G Ninja \
-D CMAKE_BUILD_TYPE=RelWithDebInfo \
${{ inputs.js-engine != '' && format('-D NAPI_JAVASCRIPT_ENGINE={0}', inputs.js-engine) || '' }} \
-D ENABLE_SANITIZERS=${{ inputs.enable-sanitizers && 'ON' || 'OFF' }} \
-D ENABLE_THREAD_SANITIZER=${{ inputs.enable-thread-sanitizer && 'ON' || 'OFF' }} \
-D CMAKE_C_COMPILER=${{ inputs.cc }} \
Expand Down
5 changes: 5 additions & 0 deletions .github/workflows/build-macos.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ on:
required: false
type: string
default: macos-latest
js-engine:
required: false
type: string
default: ''
enable-sanitizers:
required: false
type: boolean
Expand All @@ -32,6 +36,7 @@ jobs:
- name: Configure CMake
run: |
cmake -B Build/macOS -G Xcode \
${{ inputs.js-engine != '' && format('-D NAPI_JAVASCRIPT_ENGINE={0}', inputs.js-engine) || '' }} \
-D ENABLE_SANITIZERS=${{ inputs.enable-sanitizers && 'ON' || 'OFF' }} \
-D ENABLE_THREAD_SANITIZER=${{ inputs.enable-thread-sanitizer && 'ON' || 'OFF' }}

Expand Down
23 changes: 23 additions & 0 deletions .github/workflows/ci.yml
Comment thread
CedricGuillemet marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@ jobs:
platform: x64
js-engine: V8

Win32_x64_QuickJS:
uses: ./.github/workflows/build-win32.yml
with:
platform: x64
js-engine: QuickJS

Win32_x64_Hermes:
uses: ./.github/workflows/build-win32.yml
with:
Expand Down Expand Up @@ -74,6 +80,11 @@ jobs:
with:
js-engine: V8

Android_QuickJS:
uses: ./.github/workflows/build-android.yml
with:
js-engine: QuickJS

Android_Hermes:
uses: ./.github/workflows/build-android.yml
with:
Expand Down Expand Up @@ -101,6 +112,13 @@ jobs:
runs-on: macos-26
enable-thread-sanitizer: true

macOS_Xcode264_QuickJS:
uses: ./.github/workflows/build-macos.yml
with:
xcode-version: '26.4'
runs-on: macos-26
js-engine: QuickJS

# ── iOS ───────────────────────────────────────────────────────
iOS_Xcode264:
uses: ./.github/workflows/build-ios.yml
Expand All @@ -119,6 +137,11 @@ jobs:
cc: clang
cxx: clang++

Ubuntu_QuickJS:
uses: ./.github/workflows/build-linux.yml
with:
js-engine: QuickJS

Ubuntu_Sanitizers_clang:
uses: ./.github/workflows/build-linux.yml
with:
Expand Down
10 changes: 10 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@ FetchContent_Declare(UrlLib
GIT_REPOSITORY https://github.com/BabylonJS/UrlLib.git
GIT_TAG 74985214bd4f83a4906b2c62134ac2f9ab89e1ae
EXCLUDE_FROM_ALL)
FetchContent_Declare(quickjs-ng
GIT_REPOSITORY https://github.com/quickjs-ng/quickjs.git
GIT_TAG 93d3f7df465027f487ed37e175a0bc3012fee79e
EXCLUDE_FROM_ALL)

# --------------------------------------------------

FetchContent_MakeAvailable(CMakeExtensions)
Expand Down Expand Up @@ -165,6 +170,11 @@ if(BABYLON_DEBUG_TRACE)
add_definitions(-DBABYLON_DEBUG_TRACE)
endif()

if(NAPI_JAVASCRIPT_ENGINE STREQUAL "QuickJS")
option(QJS_BUILD_LIBC "Build QuickJS with libc support." ON)
FetchContent_MakeAvailable_With_Message(quickjs-ng)
endif()

if(NAPI_JAVASCRIPT_ENGINE STREQUAL "Hermes")
# Hermes is currently an experimental engine for JsRuntimeHost and is not
# supported on Apple platforms (iOS or macOS). Bail out early with a
Expand Down
2 changes: 2 additions & 0 deletions Core/AppRuntime/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ if(NAPI_JAVASCRIPT_ENGINE STREQUAL "V8" AND JSRUNTIMEHOST_CORE_APPRUNTIME_V8_INS
PRIVATE v8inspector)

set_property(TARGET v8inspector PROPERTY FOLDER Dependencies)
elseif(NAPI_JAVASCRIPT_ENGINE STREQUAL "QuickJS")
target_link_libraries(AppRuntime PRIVATE qjs)
endif()

set_property(TARGET AppRuntime PROPERTY FOLDER Core)
Expand Down
14 changes: 7 additions & 7 deletions Core/AppRuntime/Include/Babylon/AppRuntime.h
Original file line number Diff line number Diff line change
Expand Up @@ -67,13 +67,13 @@ namespace Babylon
// extra logic around the invocation of a dispatched callback.
void Execute(Dispatchable<void()> callback);

// Engine-specific hook called from Dispatch immediately after a user
// callback completes. Most engines auto-drain microtasks at scope
// exit, so the implementation is a no-op for Chakra/V8/JSC/JSI.
// Hermes does NOT auto-drain; its implementation calls
// `Napi::DrainJobs(env)` so Promise continuations and queueMicrotask
// callbacks scheduled during the user callback actually run before
// the next top-level dispatch.
// Engine-specific hook called from Dispatch immediately after each user
// callback completes. Its job is to drain the engine's microtask/job
// queue (Promise continuations, queueMicrotask callbacks, etc.) so they
// run before the next top-level dispatch. Most engines auto-drain at
// scope exit, so the implementation is a no-op for Chakra/V8/JSC/JSI.
// Hermes and QuickJS do NOT auto-drain: their implementations pump the
// queue explicitly (Napi::DrainJobs / JS_ExecutePendingJob).
void DrainMicrotasks(Napi::Env env);

Options m_options;
Expand Down
67 changes: 67 additions & 0 deletions Core/AppRuntime/Source/AppRuntime_QuickJS.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
#include "AppRuntime.h"
#include <napi/env.h>

#ifdef _WIN32
#pragma warning(push)
// cast from int64 to int32
#pragma warning(disable : 4244)
#endif
#if defined(__clang__)
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wshorten-64-to-32"
#endif
#include <quickjs.h>
#if defined(__clang__)
#pragma clang diagnostic pop
#endif
#ifdef _WIN32
#pragma warning(pop)
#endif

namespace Babylon
{
void AppRuntime::RunEnvironmentTier(const char* /*executablePath*/)
{
// Create the runtime.
JSRuntime* runtime = JS_NewRuntime();
if (!runtime)
{
throw std::runtime_error{"Failed to create QuickJS runtime"};
}

// Create the context.
JSContext* context = JS_NewContext(runtime);
if (!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);

Napi::Detach(env);
}

// Destroy the context and runtime.
JS_FreeContext(context);
JS_FreeRuntime(runtime);
}

void AppRuntime::DrainMicrotasks(Napi::Env env)
{
// QuickJS does not auto-drain its job queue. Promise continuations,
// queueMicrotask callbacks, etc. are queued as "pending jobs" and only
// run when explicitly pumped. We drain them here, after each user
// callback, so async code observes the same "between turns" semantics
// it gets on the auto-draining engines (V8/Chakra/JSC).
JSRuntime* runtime = JS_GetRuntime(Napi::GetContext(env));
JSContext* pending_ctx;
while (JS_ExecutePendingJob(runtime, &pending_ctx) > 0)
{
}
}
}
8 changes: 7 additions & 1 deletion Core/Node-API/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,13 @@ if(NAPI_BUILD_ABI)
endfunction()
endif()

if(NAPI_JAVASCRIPT_ENGINE STREQUAL "Chakra")
if(NAPI_JAVASCRIPT_ENGINE STREQUAL "QuickJS")
set(SOURCES ${SOURCES}
"Source/env_quickjs.cc"
"Source/js_native_api_quickjs.cc"
"Source/js_native_api_quickjs.h")
set(LINK_LIBRARIES ${LINK_LIBRARIES} PUBLIC qjs)
elseif(NAPI_JAVASCRIPT_ENGINE STREQUAL "Chakra")
set(SOURCES ${SOURCES}
"Source/env_chakra.cc"
"Source/js_native_api_chakra.cc"
Expand Down
15 changes: 15 additions & 0 deletions Core/Node-API/Include/Engine/QuickJS/napi/env.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
#pragma once

#include <napi/napi.h>
struct JSContext;

namespace Napi
{
Napi::Env Attach(JSContext* context);

void Detach(Napi::Env);

Napi::Value Eval(Napi::Env env, const char* source, const char* sourceUrl);

JSContext* GetContext(Napi::Env);
}
145 changes: 145 additions & 0 deletions Core/Node-API/Source/env_quickjs.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
#include <napi/env.h>
#include "js_native_api_quickjs.h"
#include <stdexcept>
#if defined(__clang__)
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wshorten-64-to-32"
#endif
#include <quickjs.h>
#if defined(__clang__)
#pragma clang diagnostic pop
#endif

namespace Napi
{
Env Attach(JSContext* context)
{
napi_env env_ptr{new napi_env__};
env_ptr->context = context;
env_ptr->current_context = env_ptr->context;

// Cache Object.prototype.hasOwnProperty for napi_has_own_property's
// fast path. These lookups are fundamental to any valid context, so a
// failure here signals a broken context: throw rather than silently
// leaving has_own_property_function undefined (matching how the other
// engines fail Attach loudly).
JSValue global = JS_GetGlobalObject(context);
JSValue object = JS_GetPropertyStr(context, global, "Object");
JS_FreeValue(context, global);
if (JS_IsException(object) || !JS_IsObject(object))
{
JS_FreeValue(context, object);
delete env_ptr;
throw std::runtime_error{"Napi::Attach: failed to resolve the global 'Object' constructor"};
}

// Use the constructor's "prototype" property to get Object.prototype.
// JS_GetPrototype(object) would return the Object *constructor's*
// [[Prototype]] (Function.prototype), from which hasOwnProperty is only
// reachable by inheritance - correct by luck, but not by intent.
JSValue prototype = JS_GetPropertyStr(context, object, "prototype");
JS_FreeValue(context, object);
if (JS_IsException(prototype) || !JS_IsObject(prototype))
{
JS_FreeValue(context, prototype);
delete env_ptr;
throw std::runtime_error{"Napi::Attach: failed to resolve Object.prototype"};
}

JSValue hasOwnProperty = JS_GetPropertyStr(context, prototype, "hasOwnProperty");
JS_FreeValue(context, prototype);
if (JS_IsException(hasOwnProperty) || !JS_IsFunction(context, hasOwnProperty))
{
JS_FreeValue(context, hasOwnProperty);
delete env_ptr;
throw std::runtime_error{"Napi::Attach: failed to resolve Object.prototype.hasOwnProperty"};
}

env_ptr->has_own_property_function = hasOwnProperty;

return {env_ptr};
}

void Detach(Env env)
{
napi_env env_ptr{env};
if (env_ptr)
{
// Release every strong napi_ref still outstanding. This mirrors
// the V8 impl (napi_env__::DeleteMe) and is essential on QuickJS:
// any surviving strong ref pins a JS value from outside the GC
// graph, which prevents the teardown cascade in JS_FreeContext
// from running and triggers list_empty(gc_obj_list) assert in
// JS_FreeRuntime.
//
// Freeing a value can synchronously run a napi_wrap finalizer
// whose C++ destructor releases *other* embedded napi_refs (e.g.
// an AbortController destroying its AbortSignal ObjectReference).
// Those nested napi_delete_reference calls must not perform a real
// JS_FreeValue - otherwise a value can be freed twice - and must
// not mutate refs_list while we iterate it. So we first neutralize
// every ref (count/value zeroed, list cleared) and only then free
// the snapshotted values. Any finalizer-driven
// napi_delete_reference then sees count == 0 and is a safe no-op.
std::vector<JSValue> strongValues;
strongValues.reserve(env_ptr->refs_list.size());
for (void* p : env_ptr->refs_list)
{
auto* info = reinterpret_cast<RefInfo*>(p);
if (info->count > 0)
{
strongValues.push_back(info->value);
}
info->count = 0;
info->value = JS_UNDEFINED;
}
env_ptr->refs_list.clear();
env_ptr->detached = true;

for (JSValue value : strongValues)
{
JS_FreeValue(env_ptr->context, value);
}

if (!JS_IsUndefined(env_ptr->has_own_property_function))
{
JS_FreeValue(env_ptr->context, env_ptr->has_own_property_function);
env_ptr->has_own_property_function = JS_UNDEFINED;
}

// Free all remaining JSValues in the handle scope stack
for (auto& ptr : env_ptr->handle_scope_stack)
{
JS_FreeValue(env_ptr->context, *ptr);
}
env_ptr->handle_scope_stack.clear();

// Run the cycle collector so napi_wrap finalizers (which
// destroy C++ wrapper objects and release any embedded
// napi_refs) get a chance to execute while the env is still
// valid. A second pass picks up anything unpinned by the
// first pass's finalizers.
JSRuntime* rt = JS_GetRuntime(env_ptr->context);
JS_RunGC(rt);
JS_RunGC(rt);

// Drop the initial owner reference taken in Attach. If every
// ExternalData finalizer already ran during the GC passes above,
// this deletes the env now. If some are deferred to the engine's
// JS_FreeContext/JS_FreeRuntime teardown cascade (the common case),
// each still holds a count, so the env survives until the last such
// finalizer completes and drops the final count - deleting the env
// exactly once, after its last use, with no leak.
//
// NOTE: env_ptr must not be touched after this point; it may have
// already been deleted.
env_ptr->Unref();
}
}

JSContext* GetContext(Env env)
{
napi_env env_ptr{env};
return env_ptr->context;
}
}
Loading
Loading