Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
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 d251ad44015e1ee6cd071514cb863d8ffc220f16
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