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
6989c65
Hermes + Napi
CedricGuillemet Jun 3, 2026
8283e4b
CI: use Ninja for macOS Hermes builds to bypass Xcode dual-target error
CedricGuillemet Jun 3, 2026
64b45d2
CI: gate Hermes /wdNNNN warning suppressions on MSVC
CedricGuillemet Jun 3, 2026
bc85108
CI: explicitly link Foundation/CoreFoundation on Apple in AppRuntime
CedricGuillemet Jun 3, 2026
d149cca
CI: disable HERMES_SLOW_DEBUG so embed teardown does not SIGABRT
CedricGuillemet Jun 3, 2026
2b68f36
Hermes: swallow Runtime teardown exceptions in Napi::Detach
CedricGuillemet Jun 3, 2026
e7564e3
Hermes: swallow drainJobs() exceptions too
CedricGuillemet Jun 3, 2026
16c0a37
CI: run macOS Hermes UnitTests under lldb to capture backtrace
CedricGuillemet Jun 3, 2026
c26960e
Hermes: route Eval through hermes_run_script's copy path to fix heap …
CedricGuillemet Jun 3, 2026
23e8dec
CI: have lldb intercept SIGABRT to get real backtrace
CedricGuillemet Jun 3, 2026
c823546
CI: use lldb -k (one-line-on-crash) to capture backtrace
CedricGuillemet Jun 3, 2026
047f373
Hermes: patch shutdown() to fix napi_ref double-free during teardown
CedricGuillemet Jun 3, 2026
cdf898b
CI: temporarily narrow matrix to Hermes-only jobs to iterate faster
CedricGuillemet Jun 3, 2026
a48366d
CI: retrigger after startup_failure on previous run
CedricGuillemet Jun 3, 2026
cfc6717
CI: drop Android and iOS Hermes jobs from narrowed matrix
CedricGuillemet Jun 3, 2026
e97292e
Hermes: regenerate shutdown patch via git diff + add marker print
CedricGuillemet Jun 3, 2026
0addb5f
patches: lock patch files to LF line endings
CedricGuillemet Jun 3, 2026
fb5d0e4
CI: restore matrix without Apple Hermes
CedricGuillemet Jun 5, 2026
7b722ae
CI: fix Android and Windows test runs
CedricGuillemet Jun 5, 2026
62abc20
CI: revert Apple workflow cleanup
CedricGuillemet Jun 5, 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
20 changes: 18 additions & 2 deletions .github/workflows/build-android.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ env:
jobs:
build:
runs-on: ubuntu-latest
timeout-minutes: 30
timeout-minutes: ${{ inputs.js-engine == 'Hermes' && 60 || 30 }}
steps:
- uses: actions/checkout@v5

Expand All @@ -29,6 +29,22 @@ jobs:
working-directory: Tests
run: npm install

- name: Install Hermes host build tools
if: inputs.js-engine == 'Hermes'
run: |
sudo apt-get update
sudo apt-get install -y ninja-build

- name: Build Hermes host compilers
if: inputs.js-engine == 'Hermes'
run: |
cmake -B Build/HermesHost -G Ninja \
-D CMAKE_BUILD_TYPE=RelWithDebInfo \
-D NAPI_JAVASCRIPT_ENGINE=Hermes \
-D HERMES_UNICODE_LITE=ON \
-D JSRUNTIMEHOST_TESTS=OFF
cmake --build Build/HermesHost --target hermesc shermes --config RelWithDebInfo

- name: Enable KVM
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
Expand All @@ -42,7 +58,7 @@ jobs:
target: google_apis
arch: x86_64
emulator-options: -no-snapshot -no-window -no-boot-anim -no-audio
script: chmod +x Tests/UnitTests/Android/gradlew && Tests/UnitTests/Android/gradlew -p Tests/UnitTests/Android connectedAndroidTest -PabiFilters=x86_64 -PjsEngine=${{ inputs.js-engine }} -PndkVersion=${{ env.NDK_VERSION }}
script: chmod +x Tests/UnitTests/Android/gradlew && Tests/UnitTests/Android/gradlew -p Tests/UnitTests/Android connectedAndroidTest -PabiFilters=x86_64 -PjsEngine=${{ inputs.js-engine }} -PndkVersion=${{ env.NDK_VERSION }} ${{ inputs.js-engine == 'Hermes' && format('-PimportHostCompilers={0}/Build/HermesHost/ImportHostCompilers.cmake', github.workspace) || '' }}

- name: Dump Test Results
if: always()
Expand Down
5 changes: 4 additions & 1 deletion .github/workflows/build-win32.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@ on:
jobs:
build:
runs-on: windows-latest
timeout-minutes: 15
# Hermes pulls in a large LLVH/Boost.Context/BCGen chain through
# `hermesvm_a`; on `windows-latest` the full configure+build comfortably
# exceeds the standard 15-minute window. Keep other engines snappy.
timeout-minutes: ${{ inputs.js-engine == 'Hermes' && 60 || 15 }}
steps:
- uses: actions/checkout@v5

Expand Down
21 changes: 16 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ on:
branches: [main]

jobs:
# ── Win32 ─────────────────────────────────────────────────────
# Win32
Win32_x86_Chakra:
uses: ./.github/workflows/build-win32.yml
with:
Expand All @@ -32,7 +32,13 @@ jobs:
platform: x64
js-engine: V8

# ── UWP ───────────────────────────────────────────────────────
Win32_x64_Hermes:
uses: ./.github/workflows/build-win32.yml
with:
platform: x64
js-engine: Hermes

# UWP
UWP_x64_Chakra:
uses: ./.github/workflows/build-uwp.yml
with:
Expand All @@ -57,7 +63,7 @@ jobs:
platform: x64
js-engine: V8

# ── Android ───────────────────────────────────────────────────
# Android
Android_JSC:
uses: ./.github/workflows/build-android.yml
with:
Expand All @@ -68,7 +74,12 @@ jobs:
with:
js-engine: V8

# ── macOS ─────────────────────────────────────────────────────
Android_Hermes:
uses: ./.github/workflows/build-android.yml
with:
js-engine: Hermes

# macOS (no Apple + Hermes until the upstream shutdown crash is fixed)
macOS_Xcode164:
uses: ./.github/workflows/build-macos.yml
with:
Expand Down Expand Up @@ -124,7 +135,7 @@ jobs:
runs-on: macos-26
simulator: 'iPhone 17'

# ── Linux ─────────────────────────────────────────────────────
# Linux
Ubuntu_gcc:
uses: ./.github/workflows/build-linux.yml

Expand Down
49 changes: 49 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,14 @@ FetchContent_Declare(CMakeExtensions
FetchContent_Declare(googletest
URL "https://github.com/google/googletest/archive/refs/tags/v1.17.0.tar.gz"
EXCLUDE_FROM_ALL)
FetchContent_Declare(hermes
GIT_REPOSITORY https://github.com/facebook/hermes.git
# Pinned to the tip of the `static_h` branch as of 2026-06-03 so CI is
# reproducible. Bump this SHA when you intentionally want to pick up
# upstream Hermes changes — keep it on the static_h branch (Static
# Hermes is the variant our NAPI integration targets).
GIT_TAG 348582831f50954895da8e80cc91112d51036c69
EXCLUDE_FROM_ALL)
FetchContent_Declare(ios-cmake
GIT_REPOSITORY https://github.com/leetal/ios-cmake.git
GIT_TAG 4.4.1
Expand Down Expand Up @@ -144,6 +152,47 @@ if(BABYLON_DEBUG_TRACE)
add_definitions(-DBABYLON_DEBUG_TRACE)
endif()

if(NAPI_JAVASCRIPT_ENGINE STREQUAL "Hermes")
# Configure Hermes static_h options BEFORE making it available so that
# cache variables take effect. We disable the parts of Hermes we don't
# need (the unit-test suite, debugger) to keep build time reasonable and
# to avoid pulling extra targets into our build tree. Notably we leave
# HERMES_ENABLE_NAPI ON (the default) — that's the whole point of
# integrating Hermes here.
#
# HERMES_ENABLE_TOOLS must stay ON: even when HERMES_ENABLE_TEST_SUITE is
# OFF, Hermes unconditionally adds external/node-api-cts and
# external/node-api-tests whenever HERMES_ENABLE_NAPI is on, and those
# subdirectories reference the `hermes` CLI tool target via
# $<TARGET_FILE:hermes>. CMake fails to generate if the target doesn't
# exist, so we let Hermes build its tools. None of them ship in our
# final binaries because they're EXCLUDE_FROM_ALL via FetchContent.
set(HERMES_ENABLE_TOOLS ON CACHE BOOL "" FORCE)
set(HERMES_ENABLE_TEST_SUITE OFF CACHE BOOL "" FORCE)
set(HERMES_ENABLE_DEBUGGER OFF CACHE BOOL "" FORCE)
set(HERMES_BUILD_APPLE_FRAMEWORK OFF CACHE BOOL "" FORCE)
set(HERMES_ENABLE_NAPI ON CACHE BOOL "" FORCE)
if(ANDROID)
# JsRuntimeHost's Android test app embeds Hermes directly, without
# Hermes's React Native Android/fbjni packaging. Unicode-lite avoids
# pulling ICU/fbjni into this standalone N-API build.
set(HERMES_UNICODE_LITE ON CACHE BOOL "" FORCE)
endif()
# Hermes defaults HERMES_SLOW_DEBUG=ON, which compiles in internal
# sanity checks that fire even in optimized (RelWithDebInfo / Release)
# builds — e.g. heap-state assertions in HadesGC and the finalizer
# chain. These can SIGABRT during Runtime teardown on some platforms
# (observed on macOS arm64 RelWithDebInfo: process exited with code
# 134 right after `145 passing`, before any gtest [OK] line, with no
# diagnostic in stderr). Embedders don't need them.
set(HERMES_SLOW_DEBUG OFF CACHE BOOL "" FORCE)
# Hermes ships its own bundled gtest as `llvh-gtest` (a different
# CMake target name from googletest's `gtest`), so the two coexist
# without clashing. We never link llvh-gtest into our UnitTests
# executable, so there's no duplicate-symbol issue at link time.
FetchContent_MakeAvailable_With_Message(hermes)
endif()

if(NAPI_JAVASCRIPT_ENGINE STREQUAL "V8" AND JSRUNTIMEHOST_CORE_APPRUNTIME_V8_INSPECTOR)
FetchContent_MakeAvailable_With_Message(asio)
add_library(asio INTERFACE)
Expand Down
17 changes: 17 additions & 0 deletions Core/AppRuntime/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,23 @@ target_link_libraries(AppRuntime
PRIVATE arcana
PUBLIC JsRuntime)

# AppRuntime_macOS.mm / AppRuntime_iOS.mm call NSLog (Foundation) and other
# Apple ObjC++ APIs. Xcode's "Link Frameworks Automatically" setting auto-
# links Foundation/CoreFoundation/UIKit for ObjC translation units, so the
# default `-G Xcode` macOS/iOS builds work without an explicit link. Other
# generators (notably Ninja, which we use for the Hermes macOS CI job to
# work around the Xcode "new build system" rejecting Hermes's multi-target
# generated source) leave those frameworks unresolved. Declaring them
# PUBLIC here propagates them onto the final executable's link line for
# every generator, and also covers other Apple-side static deps in the
# tree (e.g. UrlLib's `UrlRequest_Apple.mm` reference to NSBundle) without
# needing to patch the dep itself.
if(APPLE)
target_link_libraries(AppRuntime
PUBLIC "-framework Foundation"
PUBLIC "-framework CoreFoundation")
endif()

if(NAPI_JAVASCRIPT_ENGINE STREQUAL "V8" AND JSRUNTIMEHOST_CORE_APPRUNTIME_V8_INSPECTOR)
add_subdirectory(V8Inspector)

Expand Down
9 changes: 9 additions & 0 deletions Core/AppRuntime/Include/Babylon/AppRuntime.h
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,15 @@ 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.
void DrainMicrotasks(Napi::Env env);

Options m_options;

class Impl;
Expand Down
14 changes: 14 additions & 0 deletions Core/AppRuntime/Source/AppRuntime.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,13 @@ namespace Babylon
{
m_impl->Append([this, func{std::move(func)}](Napi::Env env) mutable {
Execute([this, env, func{std::move(func)}]() mutable {
// Some engines (notably Hermes) require an open NAPI handle
// scope before any napi_* call that materializes a value.
// The other engines (V8/Chakra/JSC) already provide an outer
// scope at the RunEnvironmentTier level, so this extra
// scope is harmless there but mandatory for Hermes.
Napi::HandleScope scope{env};

try
{
func(env);
Expand All @@ -122,6 +129,13 @@ namespace Babylon
assert(false);
std::abort();
}

// Drain engine-level microtasks/jobs queued during the
// callback (Promise continuations, queueMicrotask, etc.) so
// they run before the next top-level Dispatch. No-op for
// engines that drain automatically; Hermes needs an explicit
// pump.
DrainMicrotasks(env);
});
});
}
Expand Down
7 changes: 7 additions & 0 deletions Core/AppRuntime/Source/AppRuntime_Chakra.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -71,4 +71,11 @@ namespace Babylon
// Detach must come after JsDisposeRuntime since it triggers finalizers which require env.
Napi::Detach(env);
}

void AppRuntime::DrainMicrotasks(Napi::Env)
{
// Chakra drains promise continuations through its
// JsSetPromiseContinuationCallback hook (see RunEnvironmentTier).
// No explicit pump needed here.
}
}
28 changes: 28 additions & 0 deletions Core/AppRuntime/Source/AppRuntime_Hermes.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
#include "AppRuntime.h"
#include <napi/env.h>

namespace Babylon
{
void AppRuntime::RunEnvironmentTier(const char*)
{
// All Hermes runtime + napi_env setup is encapsulated inside the napi
// library's env_hermes.cc (see Napi::Attach/Detach). Keeping the
// engine-specific machinery there avoids dragging Hermes headers into
// AppRuntime's translation unit.
Napi::Env env = Napi::Attach();

Run(env);

Napi::Detach(env);
}

void AppRuntime::DrainMicrotasks(Napi::Env env)
{
// Hermes does not auto-drain its job queue. Promise continuations,
// queueMicrotask callbacks, and pending NAPI finalizers all run via
// Runtime::drainJobs(). We pump it after each user callback so async
// code (Promises, Mocha's async tests, polyfill schedulers, etc.)
// observes the same "between turns" semantics it gets on V8/Chakra.
Napi::DrainJobs(env);
}
}
5 changes: 5 additions & 0 deletions Core/AppRuntime/Source/AppRuntime_JSI.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,9 @@ namespace Babylon

Napi::Detach(env);
}

void AppRuntime::DrainMicrotasks(Napi::Env)
{
// JSI/V8 backed JSI auto-drains microtasks per scope.
}
}
5 changes: 5 additions & 0 deletions Core/AppRuntime/Source/AppRuntime_JavaScriptCore.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,9 @@ namespace Babylon
// Detach must come after JSGlobalContextRelease since it triggers finalizers which require env.
Napi::Detach(env);
}

void AppRuntime::DrainMicrotasks(Napi::Env)
{
// JavaScriptCore drains microtasks automatically at script boundaries.
}
}
6 changes: 6 additions & 0 deletions Core/AppRuntime/Source/AppRuntime_V8.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -112,4 +112,10 @@ namespace Babylon
// delete isolate->GetArrayBufferAllocator();
isolate->Dispose();
}

void AppRuntime::DrainMicrotasks(Napi::Env)
{
// V8 auto-drains microtasks at the end of each script/callback when
// using the default MicrotasksPolicy. No explicit pump needed.
}
}
Loading
Loading