Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 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
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
16 changes: 16 additions & 0 deletions .github/workflows/ci.yml
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

# ── UWP ───────────────────────────────────────────────────────
UWP_x64_Chakra:
uses: ./.github/workflows/build-uwp.yml
Expand Down Expand Up @@ -68,6 +74,11 @@ jobs:
with:
js-engine: V8

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

# ── macOS ─────────────────────────────────────────────────────
macOS_Xcode164:
uses: ./.github/workflows/build-macos.yml
Expand Down Expand Up @@ -134,6 +145,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 @@ -39,6 +39,11 @@ FetchContent_Declare(UrlLib
GIT_REPOSITORY https://github.com/BabylonJS/UrlLib.git
GIT_TAG 16c7dfd88cb5e7121cb5b037b44b796b737955d4
EXCLUDE_FROM_ALL)
FetchContent_Declare(quickjs-ng
GIT_REPOSITORY https://github.com/quickjs-ng/quickjs.git
GIT_TAG c707cf5eda67a97bbff7a60cb2ef124fd4a77420
EXCLUDE_FROM_ALL)

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

FetchContent_MakeAvailable(CMakeExtensions)
Expand Down Expand Up @@ -143,6 +148,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 "V8" AND JSRUNTIMEHOST_CORE_APPRUNTIME_V8_INSPECTOR)
FetchContent_MakeAvailable_With_Message(asio)
add_library(asio INTERFACE)
Expand Down
2 changes: 2 additions & 0 deletions Core/AppRuntime/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,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
1 change: 1 addition & 0 deletions Core/AppRuntime/Include/Babylon/AppRuntime.h
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ namespace Babylon
void RunPlatformTier();
void RunEnvironmentTier(const char* executablePath = ".");
void Run(Napi::Env);
void SetPostTickCallback(std::function<void()> callback);

// This method is called from Dispatch to allow platform-specific code to add
// extra logic around the invocation of a dispatched callback.
Expand Down
10 changes: 10 additions & 0 deletions Core/AppRuntime/Source/AppRuntime.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ namespace Babylon
std::optional<std::scoped_lock<std::mutex>> m_suspensionLock{};
arcana::cancellation_source m_cancelSource{};
arcana::manual_dispatcher<128> m_dispatcher{};
std::function<void()> m_postTickCallback{};
std::thread m_thread;
};

Expand Down Expand Up @@ -85,6 +86,10 @@ namespace Babylon
while (!m_impl->m_cancelSource.cancelled())
{
m_impl->m_dispatcher.blocking_tick(m_impl->m_cancelSource);
if (m_impl->m_postTickCallback)
{
m_impl->m_postTickCallback();
}
}

// The dispatcher can be non-empty if something is dispatched after cancellation.
Expand All @@ -105,6 +110,11 @@ namespace Babylon
m_impl->m_suspensionLock.reset();
}

void AppRuntime::SetPostTickCallback(std::function<void()> callback)
{
m_impl->m_postTickCallback = std::move(callback);
}

void AppRuntime::Dispatch(Dispatchable<void(Napi::Env)> func)
{
m_impl->Append([this, func{std::move(func)}](Napi::Env env) mutable {
Expand Down
55 changes: 55 additions & 0 deletions Core/AppRuntime/Source/AppRuntime_QuickJS.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
#include "AppRuntime.h"
#include <napi/env.h>

#ifdef _WIN32
#pragma warning(push)
// cast from int64 to int32
#pragma warning(disable : 4244)
#endif
#include <quickjs.h>
#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);

// Install microtask processing as a post-tick callback
SetPostTickCallback([runtime]() {
JSContext* pending_ctx;
while (JS_ExecutePendingJob(runtime, &pending_ctx) > 0)
{
}
});

Run(env);

SetPostTickCallback(nullptr);
Napi::Detach(env);
}

// Destroy the context and runtime.
JS_FreeContext(context);
JS_FreeRuntime(runtime);
}
}
8 changes: 7 additions & 1 deletion Core/Node-API/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,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} PRIVATE 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);
}
116 changes: 116 additions & 0 deletions Core/Node-API/Source/env_quickjs.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
#include <napi/env.h>
#include "js_native_api_quickjs.h"
#include <quickjs.h>

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

// Get Object.prototype.hasOwnProperty
JSValue global = JS_GetGlobalObject(context);
JSValue object = JS_GetPropertyStr(context, global, "Object");
if (!JS_IsException(object) && JS_IsObject(object))
{
JSValue prototype = JS_GetPrototype(context, object);
if (!JS_IsException(prototype) && JS_IsObject(prototype))
{
JSValue hasOwnProperty = JS_GetPropertyStr(context, prototype, "hasOwnProperty");
if (!JS_IsException(hasOwnProperty))
{
env_ptr->has_own_property_function = hasOwnProperty;
}
else
{
JS_FreeValue(context, hasOwnProperty);
}
JS_FreeValue(context, prototype);
}
else if (JS_IsException(prototype))
{
JS_FreeValue(context, prototype);
}
JS_FreeValue(context, object);
}
else if (JS_IsException(object))
{
JS_FreeValue(context, object);
}
JS_FreeValue(context, global);

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.
//
// We only release the JS side here. The RefInfo allocations
// themselves are owned by their C++ holders (e.g. an
// Napi::FunctionReference embedded in a polyfill object) and
// will be freed later when those holders are destroyed. We
// zero-out count/value so the subsequent napi_delete_reference
// is a no-op and does not touch the already freed context.
for (void* p : env_ptr->refs_list)
{
auto* info = reinterpret_cast<RefInfo*>(p);
if (info->count > 0)
{
JS_FreeValue(env_ptr->context, info->value);
}
info->count = 0;
info->value = JS_UNDEFINED;
}
env_ptr->refs_list.clear();
env_ptr->detached = true;

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);

// NOTE: we intentionally do NOT `delete env_ptr` here. Some
// native objects (e.g. ObjectWrap instances) have their
// destructors run as a side effect of JS_FreeContext's
// teardown cascade (via napi_wrap finalizers) which happens
// *after* Detach returns. Those destructors reach
// napi_delete_reference on an env pointer that must remain
// valid. The env is leaked, but this is bounded (one per
// AppRuntime environment tier).
}
}

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