Skip to content

Commit 6989c65

Browse files
Hermes + Napi
1 parent 81b01d2 commit 6989c65

14 files changed

Lines changed: 492 additions & 5 deletions

File tree

.github/workflows/build-macos.yml

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,22 @@ on:
1818
required: false
1919
type: boolean
2020
default: false
21+
js-engine:
22+
# When unset we let Core/Node-API/CMakeLists.txt pick the per-platform
23+
# default (JavaScriptCore on macOS). Pass "Hermes" to fetch and link
24+
# against facebook/hermes (static_h branch); pass "V8"/"Chakra"/etc.
25+
# to override.
26+
required: false
27+
type: string
28+
default: ''
2129

2230
jobs:
2331
build:
2432
runs-on: ${{ inputs.runs-on }}
25-
timeout-minutes: 15
33+
# Hermes builds take roughly 4x as long as a JSC build because the
34+
# umbrella `hermesvm_a` static lib drags in the full Hermes compiler
35+
# pipeline. Allow a wider window when Hermes is selected.
36+
timeout-minutes: ${{ inputs.js-engine == 'Hermes' && 60 || 15 }}
2637
steps:
2738
- uses: actions/checkout@v5
2839

@@ -33,7 +44,8 @@ jobs:
3344
run: |
3445
cmake -B Build/macOS -G Xcode \
3546
-D ENABLE_SANITIZERS=${{ inputs.enable-sanitizers && 'ON' || 'OFF' }} \
36-
-D ENABLE_THREAD_SANITIZER=${{ inputs.enable-thread-sanitizer && 'ON' || 'OFF' }}
47+
-D ENABLE_THREAD_SANITIZER=${{ inputs.enable-thread-sanitizer && 'ON' || 'OFF' }} \
48+
${{ inputs.js-engine != '' && format('-D NAPI_JAVASCRIPT_ENGINE={0}', inputs.js-engine) || '' }}
3749
3850
- name: Build
3951
run: cmake --build Build/macOS --target UnitTests --config RelWithDebInfo

.github/workflows/build-win32.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,10 @@ on:
1313
jobs:
1414
build:
1515
runs-on: windows-latest
16-
timeout-minutes: 15
16+
# Hermes pulls in a large LLVH/Boost.Context/BCGen chain through
17+
# `hermesvm_a`; on `windows-latest` the full configure+build comfortably
18+
# exceeds the standard 15-minute window. Keep other engines snappy.
19+
timeout-minutes: ${{ inputs.js-engine == 'Hermes' && 60 || 15 }}
1720
steps:
1821
- uses: actions/checkout@v5
1922

.github/workflows/ci.yml

Lines changed: 17 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_Hermes:
36+
uses: ./.github/workflows/build-win32.yml
37+
with:
38+
platform: x64
39+
js-engine: Hermes
40+
3541
# ── UWP ───────────────────────────────────────────────────────
3642
UWP_x64_Chakra:
3743
uses: ./.github/workflows/build-uwp.yml
@@ -109,6 +115,17 @@ jobs:
109115
runs-on: macos-26
110116
enable-thread-sanitizer: true
111117

118+
# Hermes on macOS: only one variant — Hermes is expensive to build, and
119+
# we already cover both Xcode toolchains for JSC. Pinning to 26.4 keeps
120+
# the matrix on the newer toolchain that downstream embedders care about
121+
# most; bump if needed.
122+
macOS_Xcode264_Hermes:
123+
uses: ./.github/workflows/build-macos.yml
124+
with:
125+
xcode-version: '26.4'
126+
runs-on: macos-26
127+
js-engine: Hermes
128+
112129
# ── iOS ───────────────────────────────────────────────────────
113130
iOS_Xcode164:
114131
uses: ./.github/workflows/build-ios.yml

CMakeLists.txt

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,14 @@ FetchContent_Declare(CMakeExtensions
2828
FetchContent_Declare(googletest
2929
URL "https://github.com/google/googletest/archive/refs/tags/v1.17.0.tar.gz"
3030
EXCLUDE_FROM_ALL)
31+
FetchContent_Declare(hermes
32+
GIT_REPOSITORY https://github.com/facebook/hermes.git
33+
# Pinned to the tip of the `static_h` branch as of 2026-06-03 so CI is
34+
# reproducible. Bump this SHA when you intentionally want to pick up
35+
# upstream Hermes changes — keep it on the static_h branch (Static
36+
# Hermes is the variant our NAPI integration targets).
37+
GIT_TAG 348582831f50954895da8e80cc91112d51036c69
38+
EXCLUDE_FROM_ALL)
3139
FetchContent_Declare(ios-cmake
3240
GIT_REPOSITORY https://github.com/leetal/ios-cmake.git
3341
GIT_TAG 4.4.1
@@ -144,6 +152,33 @@ if(BABYLON_DEBUG_TRACE)
144152
add_definitions(-DBABYLON_DEBUG_TRACE)
145153
endif()
146154

155+
if(NAPI_JAVASCRIPT_ENGINE STREQUAL "Hermes")
156+
# Configure Hermes static_h options BEFORE making it available so that
157+
# cache variables take effect. We disable the parts of Hermes we don't
158+
# need (the unit-test suite, debugger) to keep build time reasonable and
159+
# to avoid pulling extra targets into our build tree. Notably we leave
160+
# HERMES_ENABLE_NAPI ON (the default) — that's the whole point of
161+
# integrating Hermes here.
162+
#
163+
# HERMES_ENABLE_TOOLS must stay ON: even when HERMES_ENABLE_TEST_SUITE is
164+
# OFF, Hermes unconditionally adds external/node-api-cts and
165+
# external/node-api-tests whenever HERMES_ENABLE_NAPI is on, and those
166+
# subdirectories reference the `hermes` CLI tool target via
167+
# $<TARGET_FILE:hermes>. CMake fails to generate if the target doesn't
168+
# exist, so we let Hermes build its tools. None of them ship in our
169+
# final binaries because they're EXCLUDE_FROM_ALL via FetchContent.
170+
set(HERMES_ENABLE_TOOLS ON CACHE BOOL "" FORCE)
171+
set(HERMES_ENABLE_TEST_SUITE OFF CACHE BOOL "" FORCE)
172+
set(HERMES_ENABLE_DEBUGGER OFF CACHE BOOL "" FORCE)
173+
set(HERMES_BUILD_APPLE_FRAMEWORK OFF CACHE BOOL "" FORCE)
174+
set(HERMES_ENABLE_NAPI ON CACHE BOOL "" FORCE)
175+
# Hermes ships its own bundled gtest as `llvh-gtest` (a different
176+
# CMake target name from googletest's `gtest`), so the two coexist
177+
# without clashing. We never link llvh-gtest into our UnitTests
178+
# executable, so there's no duplicate-symbol issue at link time.
179+
FetchContent_MakeAvailable_With_Message(hermes)
180+
endif()
181+
147182
if(NAPI_JAVASCRIPT_ENGINE STREQUAL "V8" AND JSRUNTIMEHOST_CORE_APPRUNTIME_V8_INSPECTOR)
148183
FetchContent_MakeAvailable_With_Message(asio)
149184
add_library(asio INTERFACE)

Core/AppRuntime/Include/Babylon/AppRuntime.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,15 @@ 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.
77+
void DrainMicrotasks(Napi::Env env);
78+
7079
Options m_options;
7180

7281
class Impl;

Core/AppRuntime/Source/AppRuntime.cpp

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,13 @@ namespace Babylon
109109
{
110110
m_impl->Append([this, func{std::move(func)}](Napi::Env env) mutable {
111111
Execute([this, env, func{std::move(func)}]() mutable {
112+
// Some engines (notably Hermes) require an open NAPI handle
113+
// scope before any napi_* call that materializes a value.
114+
// The other engines (V8/Chakra/JSC) already provide an outer
115+
// scope at the RunEnvironmentTier level, so this extra
116+
// scope is harmless there but mandatory for Hermes.
117+
Napi::HandleScope scope{env};
118+
112119
try
113120
{
114121
func(env);
@@ -122,6 +129,13 @@ namespace Babylon
122129
assert(false);
123130
std::abort();
124131
}
132+
133+
// Drain engine-level microtasks/jobs queued during the
134+
// callback (Promise continuations, queueMicrotask, etc.) so
135+
// they run before the next top-level Dispatch. No-op for
136+
// engines that drain automatically; Hermes needs an explicit
137+
// pump.
138+
DrainMicrotasks(env);
125139
});
126140
});
127141
}

Core/AppRuntime/Source/AppRuntime_Chakra.cpp

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,4 +71,11 @@ namespace Babylon
7171
// Detach must come after JsDisposeRuntime since it triggers finalizers which require env.
7272
Napi::Detach(env);
7373
}
74+
75+
void AppRuntime::DrainMicrotasks(Napi::Env)
76+
{
77+
// Chakra drains promise continuations through its
78+
// JsSetPromiseContinuationCallback hook (see RunEnvironmentTier).
79+
// No explicit pump needed here.
80+
}
7481
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
#include "AppRuntime.h"
2+
#include <napi/env.h>
3+
4+
namespace Babylon
5+
{
6+
void AppRuntime::RunEnvironmentTier(const char*)
7+
{
8+
// All Hermes runtime + napi_env setup is encapsulated inside the napi
9+
// library's env_hermes.cc (see Napi::Attach/Detach). Keeping the
10+
// engine-specific machinery there avoids dragging Hermes headers into
11+
// AppRuntime's translation unit.
12+
Napi::Env env = Napi::Attach();
13+
14+
Run(env);
15+
16+
Napi::Detach(env);
17+
}
18+
19+
void AppRuntime::DrainMicrotasks(Napi::Env env)
20+
{
21+
// Hermes does not auto-drain its job queue. Promise continuations,
22+
// queueMicrotask callbacks, and pending NAPI finalizers all run via
23+
// Runtime::drainJobs(). We pump it after each user callback so async
24+
// code (Promises, Mocha's async tests, polyfill schedulers, etc.)
25+
// observes the same "between turns" semantics it gets on V8/Chakra.
26+
Napi::DrainJobs(env);
27+
}
28+
}

Core/AppRuntime/Source/AppRuntime_JSI.cpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,4 +45,9 @@ namespace Babylon
4545

4646
Napi::Detach(env);
4747
}
48+
49+
void AppRuntime::DrainMicrotasks(Napi::Env)
50+
{
51+
// JSI/V8 backed JSI auto-drains microtasks per scope.
52+
}
4853
}

Core/AppRuntime/Source/AppRuntime_JavaScriptCore.cpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,9 @@ namespace Babylon
2323
// Detach must come after JSGlobalContextRelease since it triggers finalizers which require env.
2424
Napi::Detach(env);
2525
}
26+
27+
void AppRuntime::DrainMicrotasks(Napi::Env)
28+
{
29+
// JavaScriptCore drains microtasks automatically at script boundaries.
30+
}
2631
}

0 commit comments

Comments
 (0)