Skip to content

Commit 8cd1429

Browse files
CedricGuillemetCopilotweb-flow
authored
Quickjs (#132)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Copilot <noreply@github.com>
1 parent 8c8e32f commit 8cd1429

15 files changed

Lines changed: 3410 additions & 12 deletions

File tree

.github/workflows/build-linux.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ on:
1111
required: false
1212
type: string
1313
default: g++
14+
js-engine:
15+
required: false
16+
type: string
17+
default: ''
1418
enable-sanitizers:
1519
required: false
1620
type: boolean
@@ -39,6 +43,7 @@ jobs:
3943
run: |
4044
cmake -B Build/ubuntu -G Ninja \
4145
-D CMAKE_BUILD_TYPE=RelWithDebInfo \
46+
${{ inputs.js-engine != '' && format('-D NAPI_JAVASCRIPT_ENGINE={0}', inputs.js-engine) || '' }} \
4247
-D ENABLE_SANITIZERS=${{ inputs.enable-sanitizers && 'ON' || 'OFF' }} \
4348
-D ENABLE_THREAD_SANITIZER=${{ inputs.enable-thread-sanitizer && 'ON' || 'OFF' }} \
4449
-D CMAKE_C_COMPILER=${{ inputs.cc }} \

.github/workflows/build-macos.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ on:
1010
required: false
1111
type: string
1212
default: macos-latest
13+
js-engine:
14+
required: false
15+
type: string
16+
default: ''
1317
enable-sanitizers:
1418
required: false
1519
type: boolean
@@ -32,6 +36,7 @@ jobs:
3236
- name: Configure CMake
3337
run: |
3438
cmake -B Build/macOS -G Xcode \
39+
${{ inputs.js-engine != '' && format('-D NAPI_JAVASCRIPT_ENGINE={0}', inputs.js-engine) || '' }} \
3540
-D ENABLE_SANITIZERS=${{ inputs.enable-sanitizers && 'ON' || 'OFF' }} \
3641
-D ENABLE_THREAD_SANITIZER=${{ inputs.enable-thread-sanitizer && 'ON' || 'OFF' }}
3742

.github/workflows/ci.yml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,12 @@ jobs:
3232
platform: x64
3333
js-engine: V8
3434

35+
Win32_x64_QuickJS:
36+
uses: ./.github/workflows/build-win32.yml
37+
with:
38+
platform: x64
39+
js-engine: QuickJS
40+
3541
Win32_x64_Hermes:
3642
uses: ./.github/workflows/build-win32.yml
3743
with:
@@ -74,6 +80,11 @@ jobs:
7480
with:
7581
js-engine: V8
7682

83+
Android_QuickJS:
84+
uses: ./.github/workflows/build-android.yml
85+
with:
86+
js-engine: QuickJS
87+
7788
Android_Hermes:
7889
uses: ./.github/workflows/build-android.yml
7990
with:
@@ -101,6 +112,13 @@ jobs:
101112
runs-on: macos-26
102113
enable-thread-sanitizer: true
103114

115+
macOS_Xcode264_QuickJS:
116+
uses: ./.github/workflows/build-macos.yml
117+
with:
118+
xcode-version: '26.4'
119+
runs-on: macos-26
120+
js-engine: QuickJS
121+
104122
# ── iOS ───────────────────────────────────────────────────────
105123
iOS_Xcode264:
106124
uses: ./.github/workflows/build-ios.yml
@@ -119,6 +137,11 @@ jobs:
119137
cc: clang
120138
cxx: clang++
121139

140+
Ubuntu_QuickJS:
141+
uses: ./.github/workflows/build-linux.yml
142+
with:
143+
js-engine: QuickJS
144+
122145
Ubuntu_Sanitizers_clang:
123146
uses: ./.github/workflows/build-linux.yml
124147
with:

CMakeLists.txt

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ FetchContent_Declare(AndroidExtensions
1515
EXCLUDE_FROM_ALL)
1616
FetchContent_Declare(arcana.cpp
1717
GIT_REPOSITORY https://github.com/microsoft/arcana.cpp.git
18-
GIT_TAG 0b9f9b6d761909fbca9d3ab1f2d8ff1c3d25ed3d
18+
GIT_TAG fe41b17dedea47b5dead98f0271ca284e94ac6a8
1919
EXCLUDE_FROM_ALL)
2020
FetchContent_Declare(asio
2121
GIT_REPOSITORY https://github.com/chriskohlhoff/asio.git
@@ -51,6 +51,11 @@ FetchContent_Declare(UrlLib
5151
GIT_REPOSITORY https://github.com/BabylonJS/UrlLib.git
5252
GIT_TAG e86ffb34e77092266145497681efc74e0a920ffe
5353
EXCLUDE_FROM_ALL)
54+
FetchContent_Declare(quickjs-ng
55+
GIT_REPOSITORY https://github.com/quickjs-ng/quickjs.git
56+
GIT_TAG 93d3f7df465027f487ed37e175a0bc3012fee79e
57+
EXCLUDE_FROM_ALL)
58+
5459
# --------------------------------------------------
5560

5661
FetchContent_MakeAvailable(CMakeExtensions)
@@ -165,6 +170,11 @@ if(BABYLON_DEBUG_TRACE)
165170
add_definitions(-DBABYLON_DEBUG_TRACE)
166171
endif()
167172

173+
if(NAPI_JAVASCRIPT_ENGINE STREQUAL "QuickJS")
174+
option(QJS_BUILD_LIBC "Build QuickJS with libc support." ON)
175+
FetchContent_MakeAvailable_With_Message(quickjs-ng)
176+
endif()
177+
168178
if(NAPI_JAVASCRIPT_ENGINE STREQUAL "Hermes")
169179
# Hermes is currently an experimental engine for JsRuntimeHost and is not
170180
# supported on Apple platforms (iOS or macOS). Bail out early with a

Core/AppRuntime/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ if(NAPI_JAVASCRIPT_ENGINE STREQUAL "V8" AND JSRUNTIMEHOST_CORE_APPRUNTIME_V8_INS
5252
PRIVATE v8inspector)
5353

5454
set_property(TARGET v8inspector PROPERTY FOLDER Dependencies)
55+
elseif(NAPI_JAVASCRIPT_ENGINE STREQUAL "QuickJS")
56+
target_link_libraries(AppRuntime PRIVATE qjs)
5557
endif()
5658

5759
set_property(TARGET AppRuntime PROPERTY FOLDER Core)

Core/AppRuntime/Include/Babylon/AppRuntime.h

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -67,13 +67,13 @@ namespace Babylon
6767
// extra logic around the invocation of a dispatched callback.
6868
void Execute(Dispatchable<void()> callback);
6969

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

7979
Options m_options;
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
#include "AppRuntime.h"
2+
#include <napi/env.h>
3+
4+
#ifdef _WIN32
5+
#pragma warning(push)
6+
// cast from int64 to int32
7+
#pragma warning(disable : 4244)
8+
#endif
9+
#if defined(__clang__)
10+
#pragma clang diagnostic push
11+
#pragma clang diagnostic ignored "-Wshorten-64-to-32"
12+
#endif
13+
#include <quickjs.h>
14+
#if defined(__clang__)
15+
#pragma clang diagnostic pop
16+
#endif
17+
#ifdef _WIN32
18+
#pragma warning(pop)
19+
#endif
20+
21+
namespace Babylon
22+
{
23+
void AppRuntime::RunEnvironmentTier(const char* /*executablePath*/)
24+
{
25+
// Create the runtime.
26+
JSRuntime* runtime = JS_NewRuntime();
27+
if (!runtime)
28+
{
29+
throw std::runtime_error{"Failed to create QuickJS runtime"};
30+
}
31+
32+
// Create the context.
33+
JSContext* context = JS_NewContext(runtime);
34+
if (!context)
35+
{
36+
JS_FreeRuntime(runtime);
37+
throw std::runtime_error{"Failed to create QuickJS context"};
38+
}
39+
40+
// Use the context within a scope.
41+
{
42+
Napi::Env env = Napi::Attach(context);
43+
44+
Run(env);
45+
46+
Napi::Detach(env);
47+
}
48+
49+
// Destroy the context and runtime.
50+
JS_FreeContext(context);
51+
JS_FreeRuntime(runtime);
52+
}
53+
54+
void AppRuntime::DrainMicrotasks(Napi::Env env)
55+
{
56+
// QuickJS does not auto-drain its job queue. Promise continuations,
57+
// queueMicrotask callbacks, etc. are queued as "pending jobs" and only
58+
// run when explicitly pumped. We drain them here, after each user
59+
// callback, so async code observes the same "between turns" semantics
60+
// it gets on the auto-draining engines (V8/Chakra/JSC).
61+
JSRuntime* runtime = JS_GetRuntime(Napi::GetContext(env));
62+
JSContext* pending_ctx;
63+
while (JS_ExecutePendingJob(runtime, &pending_ctx) > 0)
64+
{
65+
}
66+
}
67+
}

Core/Node-API/CMakeLists.txt

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,13 @@ if(NAPI_BUILD_ABI)
4747
endfunction()
4848
endif()
4949

50-
if(NAPI_JAVASCRIPT_ENGINE STREQUAL "Chakra")
50+
if(NAPI_JAVASCRIPT_ENGINE STREQUAL "QuickJS")
51+
set(SOURCES ${SOURCES}
52+
"Source/env_quickjs.cc"
53+
"Source/js_native_api_quickjs.cc"
54+
"Source/js_native_api_quickjs.h")
55+
set(LINK_LIBRARIES ${LINK_LIBRARIES} PUBLIC qjs)
56+
elseif(NAPI_JAVASCRIPT_ENGINE STREQUAL "Chakra")
5157
set(SOURCES ${SOURCES}
5258
"Source/env_chakra.cc"
5359
"Source/js_native_api_chakra.cc"
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
#pragma once
2+
3+
#include <napi/napi.h>
4+
struct JSContext;
5+
6+
namespace Napi
7+
{
8+
Napi::Env Attach(JSContext* context);
9+
10+
void Detach(Napi::Env);
11+
12+
Napi::Value Eval(Napi::Env env, const char* source, const char* sourceUrl);
13+
14+
JSContext* GetContext(Napi::Env);
15+
}
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
#include <napi/env.h>
2+
#include "js_native_api_quickjs.h"
3+
#include <stdexcept>
4+
#if defined(__clang__)
5+
#pragma clang diagnostic push
6+
#pragma clang diagnostic ignored "-Wshorten-64-to-32"
7+
#endif
8+
#include <quickjs.h>
9+
#if defined(__clang__)
10+
#pragma clang diagnostic pop
11+
#endif
12+
13+
namespace Napi
14+
{
15+
Env Attach(JSContext* context)
16+
{
17+
napi_env env_ptr{new napi_env__};
18+
env_ptr->context = context;
19+
env_ptr->current_context = env_ptr->context;
20+
21+
// Cache Object.prototype.hasOwnProperty for napi_has_own_property's
22+
// fast path. These lookups are fundamental to any valid context, so a
23+
// failure here signals a broken context: throw rather than silently
24+
// leaving has_own_property_function undefined (matching how the other
25+
// engines fail Attach loudly).
26+
JSValue global = JS_GetGlobalObject(context);
27+
JSValue object = JS_GetPropertyStr(context, global, "Object");
28+
JS_FreeValue(context, global);
29+
if (JS_IsException(object) || !JS_IsObject(object))
30+
{
31+
JS_FreeValue(context, object);
32+
delete env_ptr;
33+
throw std::runtime_error{"Napi::Attach: failed to resolve the global 'Object' constructor"};
34+
}
35+
36+
// Use the constructor's "prototype" property to get Object.prototype.
37+
// JS_GetPrototype(object) would return the Object *constructor's*
38+
// [[Prototype]] (Function.prototype), from which hasOwnProperty is only
39+
// reachable by inheritance - correct by luck, but not by intent.
40+
JSValue prototype = JS_GetPropertyStr(context, object, "prototype");
41+
JS_FreeValue(context, object);
42+
if (JS_IsException(prototype) || !JS_IsObject(prototype))
43+
{
44+
JS_FreeValue(context, prototype);
45+
delete env_ptr;
46+
throw std::runtime_error{"Napi::Attach: failed to resolve Object.prototype"};
47+
}
48+
49+
JSValue hasOwnProperty = JS_GetPropertyStr(context, prototype, "hasOwnProperty");
50+
JS_FreeValue(context, prototype);
51+
if (JS_IsException(hasOwnProperty) || !JS_IsFunction(context, hasOwnProperty))
52+
{
53+
JS_FreeValue(context, hasOwnProperty);
54+
delete env_ptr;
55+
throw std::runtime_error{"Napi::Attach: failed to resolve Object.prototype.hasOwnProperty"};
56+
}
57+
58+
env_ptr->has_own_property_function = hasOwnProperty;
59+
60+
return {env_ptr};
61+
}
62+
63+
void Detach(Env env)
64+
{
65+
napi_env env_ptr{env};
66+
if (env_ptr)
67+
{
68+
// Release every strong napi_ref still outstanding. This mirrors
69+
// the V8 impl (napi_env__::DeleteMe) and is essential on QuickJS:
70+
// any surviving strong ref pins a JS value from outside the GC
71+
// graph, which prevents the teardown cascade in JS_FreeContext
72+
// from running and triggers list_empty(gc_obj_list) assert in
73+
// JS_FreeRuntime.
74+
//
75+
// Freeing a value can synchronously run a napi_wrap finalizer
76+
// whose C++ destructor releases *other* embedded napi_refs (e.g.
77+
// an AbortController destroying its AbortSignal ObjectReference).
78+
// Those nested napi_delete_reference calls must not perform a real
79+
// JS_FreeValue - otherwise a value can be freed twice - and must
80+
// not mutate refs_list while we iterate it. So we first neutralize
81+
// every ref (count/value zeroed, list cleared) and only then free
82+
// the snapshotted values. Any finalizer-driven
83+
// napi_delete_reference then sees count == 0 and is a safe no-op.
84+
std::vector<JSValue> strongValues;
85+
strongValues.reserve(env_ptr->refs_list.size());
86+
for (void* p : env_ptr->refs_list)
87+
{
88+
auto* info = reinterpret_cast<RefInfo*>(p);
89+
if (info->count > 0)
90+
{
91+
strongValues.push_back(info->value);
92+
}
93+
info->count = 0;
94+
info->value = JS_UNDEFINED;
95+
}
96+
env_ptr->refs_list.clear();
97+
env_ptr->detached = true;
98+
99+
for (JSValue value : strongValues)
100+
{
101+
JS_FreeValue(env_ptr->context, value);
102+
}
103+
104+
if (!JS_IsUndefined(env_ptr->has_own_property_function))
105+
{
106+
JS_FreeValue(env_ptr->context, env_ptr->has_own_property_function);
107+
env_ptr->has_own_property_function = JS_UNDEFINED;
108+
}
109+
110+
// Free all remaining JSValues in the handle scope stack
111+
for (auto& ptr : env_ptr->handle_scope_stack)
112+
{
113+
JS_FreeValue(env_ptr->context, *ptr);
114+
}
115+
env_ptr->handle_scope_stack.clear();
116+
117+
// Run the cycle collector so napi_wrap finalizers (which
118+
// destroy C++ wrapper objects and release any embedded
119+
// napi_refs) get a chance to execute while the env is still
120+
// valid. A second pass picks up anything unpinned by the
121+
// first pass's finalizers.
122+
JSRuntime* rt = JS_GetRuntime(env_ptr->context);
123+
JS_RunGC(rt);
124+
JS_RunGC(rt);
125+
126+
// Drop the initial owner reference taken in Attach. If every
127+
// ExternalData finalizer already ran during the GC passes above,
128+
// this deletes the env now. If some are deferred to the engine's
129+
// JS_FreeContext/JS_FreeRuntime teardown cascade (the common case),
130+
// each still holds a count, so the env survives until the last such
131+
// finalizer completes and drops the final count - deleting the env
132+
// exactly once, after its last use, with no leak.
133+
//
134+
// NOTE: env_ptr must not be touched after this point; it may have
135+
// already been deleted.
136+
env_ptr->Unref();
137+
}
138+
}
139+
140+
JSContext* GetContext(Env env)
141+
{
142+
napi_env env_ptr{env};
143+
return env_ptr->context;
144+
}
145+
}

0 commit comments

Comments
 (0)