From c53bc6f52074069aa27ecfaef985469c2c3310e4 Mon Sep 17 00:00:00 2001 From: Matt Hargett Date: Sat, 11 Jul 2026 20:53:36 -0700 Subject: [PATCH 1/3] Use Bun's JavaScriptCore build on Android --- .../Source/AppRuntime_JavaScriptCore.cpp | 10 ++ Core/Node-API/CMakeLists.txt | 105 ++++++++++++++++-- .../Include/Engine/JavaScriptCore/napi/env.h | 27 +++++ .../Source/js_native_api_javascriptcore.cc | 4 + .../Source/js_native_api_javascriptcore.h | 73 +++++++++++- Core/Node-API/Source/jsc_android.cpp | 55 +++++++++ Core/Node-API/Source/jsc_android.exports | 8 ++ Core/Node-API/package-jsc.json | 5 - Tests/UnitTests/Android/app/build.gradle | 7 +- 9 files changed, 274 insertions(+), 20 deletions(-) create mode 100644 Core/Node-API/Source/jsc_android.cpp create mode 100644 Core/Node-API/Source/jsc_android.exports delete mode 100644 Core/Node-API/package-jsc.json diff --git a/Core/AppRuntime/Source/AppRuntime_JavaScriptCore.cpp b/Core/AppRuntime/Source/AppRuntime_JavaScriptCore.cpp index b1334c22..31f9d919 100644 --- a/Core/AppRuntime/Source/AppRuntime_JavaScriptCore.cpp +++ b/Core/AppRuntime/Source/AppRuntime_JavaScriptCore.cpp @@ -1,10 +1,17 @@ #include "AppRuntime.h" #include +#if __ANDROID__ +extern "C" void JSCAndroidInitialize(); +#endif + namespace Babylon { void AppRuntime::RunEnvironmentTier(const char*) { +#if __ANDROID__ + JSCAndroidInitialize(); +#endif auto globalContext = JSGlobalContextCreateInGroup(nullptr, nullptr); #if __APPLE__ @@ -18,6 +25,9 @@ namespace Babylon Run(env); +#if __ANDROID__ + Napi::ContextLock contextLock{env}; +#endif JSGlobalContextRelease(globalContext); // Detach must come after JSGlobalContextRelease since it triggers finalizers which require env. diff --git a/Core/Node-API/CMakeLists.txt b/Core/Node-API/CMakeLists.txt index 5f495695..c15d9fd4 100644 --- a/Core/Node-API/CMakeLists.txt +++ b/Core/Node-API/CMakeLists.txt @@ -68,20 +68,109 @@ if(NAPI_BUILD_ABI) "Source/js_native_api_javascriptcore.h") if(ANDROID) - set(V8_PACKAGE_NAME "jsc-android") - set(JSC_ANDROID_DIR "${CMAKE_CURRENT_BINARY_DIR}/${V8_PACKAGE_NAME}") - napi_install_android_package(jsc "dist/org/webkit/android-jsc" ${JSC_ANDROID_DIR}) + # Bun publishes the JavaScriptCore build used by each release as pinned Android static + # archives. Relink the selected archives into the libjsc.so boundary expected by napi, + # instead of installing an npm package only to unpack its embedded AAR. + set(JSR_JSC_BUN_WEBKIT_REVISION "5488984d20e0dbfe4be2c3ba8fb18eb81a5e0e8b") + set(JSR_JSC_BUN_WEBKIT_RELEASE "autobuild-${JSR_JSC_BUN_WEBKIT_REVISION}") + set(JSR_JSC_ANDROID_ROOT "" CACHE PATH + "Path to an extracted Bun Android WebKit 'bun-webkit' directory; empty downloads the pinned release") - # Add `JavaScriptCore` prefix to the include path - file(RENAME "${JSC_ANDROID_DIR}/include" "${JSC_ANDROID_DIR}/JavaScriptCore") - file(MAKE_DIRECTORY "${JSC_ANDROID_DIR}/include") - file(RENAME "${JSC_ANDROID_DIR}/JavaScriptCore" "${JSC_ANDROID_DIR}/include/JavaScriptCore") + string(REGEX REPLACE "^android-" "" JSR_JSC_ANDROID_API_LEVEL "${ANDROID_PLATFORM}") + if(JSR_JSC_ANDROID_API_LEVEL LESS 28) + message(FATAL_ERROR + "Bun's pinned Android WebKit archives target API 28; set Android minSdkVersion to 28 or newer") + endif() + + if(ANDROID_ABI STREQUAL "arm64-v8a") + set(JSR_JSC_BUN_WEBKIT_ASSET "bun-webkit-linux-arm64-android.tar.gz") + set(JSR_JSC_BUN_WEBKIT_SHA256 "63c18a18c89dc3e1c0a93107f95290c27fbe310eb90e30dc197a7936804d034b") + elseif(ANDROID_ABI STREQUAL "x86_64") + set(JSR_JSC_BUN_WEBKIT_ASSET "bun-webkit-linux-amd64-android.tar.gz") + set(JSR_JSC_BUN_WEBKIT_SHA256 "c198efb68c17ae65ff66968f7d04bdab4845a5abaefb941b15e0e2b36aaab774") + else() + message(FATAL_ERROR + "Bun Android JavaScriptCore supports only arm64-v8a and x86_64; got '${ANDROID_ABI}'") + endif() + + if(JSR_JSC_ANDROID_ROOT) + set(JSC_ANDROID_DIR "${JSR_JSC_ANDROID_ROOT}") + else() + FetchContent_Declare(bun_webkit_android + URL + "https://github.com/oven-sh/WebKit/releases/download/${JSR_JSC_BUN_WEBKIT_RELEASE}/${JSR_JSC_BUN_WEBKIT_ASSET}" + URL_HASH "SHA256=${JSR_JSC_BUN_WEBKIT_SHA256}") + FetchContent_GetProperties(bun_webkit_android) + if(NOT bun_webkit_android_POPULATED) + message(STATUS + "Downloading Bun WebKit ${JSR_JSC_BUN_WEBKIT_REVISION} for ${ANDROID_ABI}") + FetchContent_Populate(bun_webkit_android) + endif() + # FetchContent strips the archive's single top-level bun-webkit directory. + set(JSC_ANDROID_DIR "${bun_webkit_android_SOURCE_DIR}") + endif() + + set(JSC_ANDROID_STATIC_LIBRARIES + "${JSC_ANDROID_DIR}/lib/libJavaScriptCore.a" + "${JSC_ANDROID_DIR}/lib/libWTF.a" + "${JSC_ANDROID_DIR}/lib/libbmalloc.a" + "${JSC_ANDROID_DIR}/lib/libicui18n.a" + "${JSC_ANDROID_DIR}/lib/libicuuc.a" + "${JSC_ANDROID_DIR}/lib/libicudata.a") + + foreach(JSC_ANDROID_FILE + "${JSC_ANDROID_DIR}/include/JavaScriptCore/JavaScript.h" + ${JSC_ANDROID_STATIC_LIBRARIES}) + if(NOT EXISTS "${JSC_ANDROID_FILE}") + message(FATAL_ERROR "Bun Android WebKit archive is missing '${JSC_ANDROID_FILE}'") + endif() + endforeach() set(INCLUDE_DIRECTORIES ${INCLUDE_DIRECTORIES} PUBLIC "${JSC_ANDROID_DIR}/include") + add_library(jsc_android SHARED "Source/jsc_android.cpp") + set_target_properties(jsc_android PROPERTIES + OUTPUT_NAME jsc + CXX_STANDARD 23 + CXX_STANDARD_REQUIRED ON + LINK_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/Source/jsc_android.exports") + target_compile_definitions(jsc_android PRIVATE + JSR_JSC_BUN_WEBKIT_REVISION="${JSR_JSC_BUN_WEBKIT_REVISION}" + BUILDING_JSCONLY__ + BUILDING_JavaScriptCore + BUILDING_WEBKIT=1 + BUILDING_WITH_CMAKE=1 + HAVE_CONFIG_H=1 + PAS_BMALLOC=1 + STATICALLY_LINKED_WITH_WTF + STATICALLY_LINKED_WITH_bmalloc + U_STATIC_IMPLEMENTATION=1) + target_include_directories(jsc_android PUBLIC "${JSC_ANDROID_DIR}/include") + target_link_options(jsc_android PRIVATE + "-Wl,--no-undefined" + "-Wl,-Bsymbolic" + "-Wl,--version-script=${CMAKE_CURRENT_SOURCE_DIR}/Source/jsc_android.exports" + "-Wl,-z,max-page-size=16384" + "-Wl,-z,common-page-size=16384") + target_link_libraries(jsc_android PRIVATE + "-Wl,--whole-archive" + "${JSC_ANDROID_DIR}/lib/libJavaScriptCore.a" + "-Wl,--no-whole-archive" + "-Wl,--start-group" + "${JSC_ANDROID_DIR}/lib/libWTF.a" + "${JSC_ANDROID_DIR}/lib/libbmalloc.a" + "${JSC_ANDROID_DIR}/lib/libicui18n.a" + "${JSC_ANDROID_DIR}/lib/libicuuc.a" + "${JSC_ANDROID_DIR}/lib/libicudata.a" + "-Wl,--end-group" + log + dl + m) + set_property(TARGET jsc_android PROPERTY FOLDER Dependencies) + set(LINK_LIBRARIES ${LINK_LIBRARIES} - PUBLIC "${JSC_ANDROID_DIR}/jni/${ANDROID_ABI}/libjsc.so") + PUBLIC jsc_android) elseif(APPLE) find_library(JAVASCRIPTCORE_LIBRARY JavaScriptCore) set(LINK_LIBRARIES ${LINK_LIBRARIES} diff --git a/Core/Node-API/Include/Engine/JavaScriptCore/napi/env.h b/Core/Node-API/Include/Engine/JavaScriptCore/napi/env.h index 64184b36..9d94d78f 100644 --- a/Core/Node-API/Include/Engine/JavaScriptCore/napi/env.h +++ b/Core/Node-API/Include/Engine/JavaScriptCore/napi/env.h @@ -3,6 +3,11 @@ #include #include +#if __ANDROID__ +extern "C" void* JSCAndroidAcquireContextLock(JSGlobalContextRef context); +extern "C" void JSCAndroidReleaseContextLock(void* opaqueLock); +#endif + namespace Napi { Napi::Env Attach(JSGlobalContextRef); @@ -12,4 +17,26 @@ namespace Napi Napi::Value Eval(Napi::Env env, const char* source, const char* sourceUrl); JSGlobalContextRef GetContext(Napi::Env); + +#if __ANDROID__ + class ContextLock final + { + public: + explicit ContextLock(Napi::Env env) + : m_opaqueLock{JSCAndroidAcquireContextLock(GetContext(env))} + { + } + + ~ContextLock() + { + JSCAndroidReleaseContextLock(m_opaqueLock); + } + + ContextLock(const ContextLock&) = delete; + ContextLock& operator=(const ContextLock&) = delete; + + private: + void* m_opaqueLock{}; + }; +#endif } diff --git a/Core/Node-API/Source/js_native_api_javascriptcore.cc b/Core/Node-API/Source/js_native_api_javascriptcore.cc index 2c9e8e80..2a188b61 100644 --- a/Core/Node-API/Source/js_native_api_javascriptcore.cc +++ b/Core/Node-API/Source/js_native_api_javascriptcore.cc @@ -740,6 +740,9 @@ struct napi_ref__ { CHECK_NAPI(ReferenceInfo::GetObjectId(env, _value, &_objectId)); if (_objectId == 0) { CHECK_NAPI(ReferenceInfo::Initialize(env, _value, [value = _value](ReferenceInfo* info) { + if (info->Env()->shutting_down) { + return; + } auto entry{info->Env()->active_ref_values.find(value)}; // NOTE: The finalizer callback is actually on a "sentinel" JS object that is linked to the // actual JS object we are trying to track. This means it is possible for the tracked object @@ -1902,6 +1905,7 @@ napi_status napi_get_value_string_utf16(napi_env env, napi_status napi_coerce_to_bool(napi_env env, napi_value value, napi_value* result) { + CHECK_ENV(env); CHECK_ARG(env, result); *result = ToNapi(JSValueMakeBoolean(env->context, JSValueToBoolean(env->context, ToJSValue(value)))); diff --git a/Core/Node-API/Source/js_native_api_javascriptcore.h b/Core/Node-API/Source/js_native_api_javascriptcore.h index da74596e..9ed8472b 100644 --- a/Core/Node-API/Source/js_native_api_javascriptcore.h +++ b/Core/Node-API/Source/js_native_api_javascriptcore.h @@ -8,12 +8,55 @@ #include #include +#if __ANDROID__ +extern "C" void* JSCAndroidAcquireContextLock(JSGlobalContextRef context); +extern "C" void JSCAndroidReleaseContextLock(void* opaqueLock); +extern "C" bool JSCAndroidLockContext(JSGlobalContextRef context); +extern "C" void JSCAndroidUnlockContext(JSGlobalContextRef context); + +class JSCAndroidContextLock final { + public: + explicit JSCAndroidContextLock(JSGlobalContextRef context) + : lock{JSCAndroidAcquireContextLock(context)} {} + + ~JSCAndroidContextLock() { + JSCAndroidReleaseContextLock(lock); + } + + JSCAndroidContextLock(const JSCAndroidContextLock&) = delete; + JSCAndroidContextLock& operator=(const JSCAndroidContextLock&) = delete; + + private: + void* lock{}; +}; + +class JSCAndroidAPILock final { + public: + explicit JSCAndroidAPILock(JSGlobalContextRef context) + : context{context}, locked{JSCAndroidLockContext(context)} {} + + ~JSCAndroidAPILock() { + if (locked) { + JSCAndroidUnlockContext(context); + } + } + + JSCAndroidAPILock(const JSCAndroidAPILock&) = delete; + JSCAndroidAPILock& operator=(const JSCAndroidAPILock&) = delete; + + private: + JSGlobalContextRef context{}; + bool locked{}; +}; +#endif + struct napi_env__ { JSGlobalContextRef context{}; JSValueRef last_exception{}; napi_extended_error_info last_error{nullptr, nullptr, 0, napi_ok}; std::unordered_map active_ref_values{}; std::list strong_refs{}; + bool shutting_down{false}; JSValueRef constructor_info_symbol{}; JSValueRef function_info_symbol{}; @@ -23,6 +66,9 @@ struct napi_env__ { const std::thread::id thread_id{std::this_thread::get_id()}; napi_env__(JSGlobalContextRef context) : context{context} { +#if __ANDROID__ + JSCAndroidContextLock contextLock{context}; +#endif napi_envs[context] = this; JSGlobalContextRetain(context); init_symbol(constructor_info_symbol, "BabylonNative_ConstructorInfo"); @@ -32,6 +78,10 @@ struct napi_env__ { } ~napi_env__() { +#if __ANDROID__ + JSCAndroidContextLock contextLock{context}; +#endif + shutting_down = true; deinit_refs(); deinit_symbol(wrapper_info_symbol); deinit_symbol(reference_info_symbol); @@ -65,13 +115,24 @@ struct napi_env__ { } \ } while (0) -#define CHECK_ENV(env) \ - do { \ - if ((env) == nullptr) { \ - return napi_invalid_arg; \ - } \ - assert(env->thread_id == std::this_thread::get_id()); \ +#if __ANDROID__ +#define CHECK_ENV(env) \ + do { \ + if ((env) == nullptr) { \ + return napi_invalid_arg; \ + } \ + } while (0); \ + JSCAndroidAPILock jscAndroidAPILock{(env)->context}; \ + assert((env)->thread_id == std::this_thread::get_id()) +#else +#define CHECK_ENV(env) \ + do { \ + if ((env) == nullptr) { \ + return napi_invalid_arg; \ + } \ + assert((env)->thread_id == std::this_thread::get_id()); \ } while (0) +#endif #define CHECK_ARG(env, arg) \ RETURN_STATUS_IF_FALSE((env), ((arg) != nullptr), napi_invalid_arg) diff --git a/Core/Node-API/Source/jsc_android.cpp b/Core/Node-API/Source/jsc_android.cpp new file mode 100644 index 00000000..389dff90 --- /dev/null +++ b/Core/Node-API/Source/jsc_android.cpp @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#ifndef JSR_JSC_BUN_WEBKIT_REVISION +#error JSR_JSC_BUN_WEBKIT_REVISION must identify the linked Bun WebKit build +#endif + +#include +#include +#include +#include + +extern "C" void JSCAndroidInitialize() +{ + // Match the standalone JavaScriptCore C API initialization path. In particular, do not call + // WTF::initializeMainThread(): AppRuntime runs JavaScript on a worker thread, while Android may + // load this library and initialize other WTF thread-local state on the instrumentation thread. + JSC::initialize(); +} + +extern "C" void* JSCAndroidAcquireContextLock(JSGlobalContextRef context) +{ + // Bun's WebKit fork removes the per-call JSLockHolder instances from the C API and expects its + // embedding event loop to hold the VM lock. This holder is used only for lifecycle operations + // that must also retain the VM, such as environment teardown and the final context release. + auto* vm = toJS(JSContextGetGroup(context)); + return new JSC::JSLockHolder(*vm); +} + +extern "C" void JSCAndroidReleaseContextLock(void* opaqueLock) +{ + delete static_cast(opaqueLock); +} + +extern "C" bool JSCAndroidLockContext(JSGlobalContextRef context) +{ + auto* vm = toJS(JSContextGetGroup(context)); + if (vm->currentThreadIsHoldingAPILock()) + { + return false; + } + + vm->apiLock().lock(); + return true; +} + +extern "C" void JSCAndroidUnlockContext(JSGlobalContextRef context) +{ + toJS(JSContextGetGroup(context))->apiLock().unlock(); +} + +extern "C" const char* JSCAndroidGetWebKitRevision() +{ + return JSR_JSC_BUN_WEBKIT_REVISION; +} diff --git a/Core/Node-API/Source/jsc_android.exports b/Core/Node-API/Source/jsc_android.exports new file mode 100644 index 00000000..47b182c0 --- /dev/null +++ b/Core/Node-API/Source/jsc_android.exports @@ -0,0 +1,8 @@ +{ + global: + JS*; + JSCAndroid*; + kJS*; + local: + *; +}; diff --git a/Core/Node-API/package-jsc.json b/Core/Node-API/package-jsc.json deleted file mode 100644 index d9f70a7a..00000000 --- a/Core/Node-API/package-jsc.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "dependencies": { - "jsc-android": "250231.0.0" - } -} diff --git a/Tests/UnitTests/Android/app/build.gradle b/Tests/UnitTests/Android/app/build.gradle index c6137034..b4cc7768 100644 --- a/Tests/UnitTests/Android/app/build.gradle +++ b/Tests/UnitTests/Android/app/build.gradle @@ -7,11 +7,16 @@ if (project.hasProperty("jsEngine")) { jsEngine = project.property("jsEngine") } +def appMinSdk = jsEngine == "JavaScriptCore" ? 28 : 21 + def cmakeArguments = [ "-DANDROID_STL=c++_shared", "-DNAPI_JAVASCRIPT_ENGINE=${jsEngine}", "-DJSRUNTIMEHOST_CORE_APPRUNTIME_V8_INSPECTOR=ON" ] +if (project.hasProperty("jscAndroidRoot")) { + cmakeArguments.add("-DJSR_JSC_ANDROID_ROOT=${project.property('jscAndroidRoot')}") +} // Hermes for Android needs HOST hermesc/shermes (they emit bytecode at build // time and can't run as NDK-cross-compiled binaries). The JsRuntimeHost CMake // bootstraps these host compilers automatically when cross-compiling, so no @@ -32,7 +37,7 @@ android { defaultConfig { applicationId "com.jsruntimehost.unittests" - minSdk 21 + minSdk appMinSdk targetSdk 33 versionCode 1 versionName "1.0" From 85da588a37e71a339de61f29a089a68e3c297d49 Mon Sep 17 00:00:00 2001 From: Matt Hargett Date: Sat, 11 Jul 2026 23:13:39 -0700 Subject: [PATCH 2/3] Use Bun's LTO JavaScriptCore build on Linux --- .github/tsan_suppressions.txt | 12 +- .github/workflows/build-linux.yml | 54 ++- .github/workflows/ci.yml | 16 +- .../Source/AppRuntime_JavaScriptCore.cpp | 21 +- Core/Node-API/CMakeLists.txt | 344 +++++++++++++----- .../Include/Engine/JavaScriptCore/napi/env.h | 12 +- .../Source/js_native_api_javascriptcore.h | 71 ++-- .../Source/{jsc_android.cpp => jsc_bun.cpp} | 24 +- .../{jsc_android.exports => jsc_bun.exports} | 2 +- Tests/UnitTests/Android/app/build.gradle | 6 +- 10 files changed, 394 insertions(+), 168 deletions(-) rename Core/Node-API/Source/{jsc_android.cpp => jsc_bun.cpp} (58%) rename Core/Node-API/Source/{jsc_android.exports => jsc_bun.exports} (75%) diff --git a/.github/tsan_suppressions.txt b/.github/tsan_suppressions.txt index eebd4661..b17ed653 100644 --- a/.github/tsan_suppressions.txt +++ b/.github/tsan_suppressions.txt @@ -2,16 +2,16 @@ # # These suppress data races internal to third-party libraries that we cannot fix. # TSan on macOS (JavaScriptCore via Xcode) passes clean — these suppressions are -# only needed for the Ubuntu JSC build (libjavascriptcoregtk). +# only needed for the Ubuntu Bun JSC build. -# JavaScriptCore internal races in libjavascriptcoregtk. +# Bun's Linux JavaScriptCore is statically linked into the final executable, so +# a called_from_lib suppression cannot identify it. Match the interceptor frames +# previously observed on JSC's JIT worker threads instead. # Races manifest in TSan interceptors (free/malloc/memcpy/close) called from -# JSC's JIT worker threads. function-name suppressions are needed because the -# top frame is a libc interceptor attributed to UnitTests, not libjavascriptcoregtk. -called_from_lib:libjavascriptcoregtk +# JSC's JIT worker threads. race:free race:close race:memcpy # JSC signal handler that doesn't save/restore errno -signal:libjavascriptcoregtk +signal:WTF::jscSignalHandler diff --git a/.github/workflows/build-linux.yml b/.github/workflows/build-linux.yml index 3357bf59..078155d1 100644 --- a/.github/workflows/build-linux.yml +++ b/.github/workflows/build-linux.yml @@ -6,11 +6,11 @@ on: cc: required: false type: string - default: gcc + default: clang-21 cxx: required: false type: string - default: g++ + default: clang++-21 js-engine: required: false type: string @@ -26,8 +26,10 @@ on: jobs: build: - runs-on: ubuntu-latest - timeout-minutes: 15 + # Bun's JSC requires GLIBCXX_3.4.30. Building on 22.04 preserves that practical + # runtime floor instead of accidentally importing newer glibc symbols. + runs-on: ubuntu-22.04 + timeout-minutes: 30 env: CC: ${{ inputs.cc }} CXX: ${{ inputs.cxx }} @@ -37,7 +39,22 @@ jobs: - name: Install packages run: | sudo apt-get update - sudo apt-get install -y libjavascriptcoregtk-4.1-dev libcurl4-openssl-dev ninja-build clang + sudo apt-get install -y --no-install-recommends ca-certificates gnupg libcurl4-openssl-dev libstdc++-12-dev ninja-build wget + + - name: Install LLVM 21 for Bun JavaScriptCore LTO + if: inputs.cc == 'clang-21' || inputs.cxx == 'clang++-21' + run: | + wget -qO- https://apt.llvm.org/llvm-snapshot.gpg.key | sudo gpg --dearmor -o /usr/share/keyrings/apt.llvm.org.gpg + echo "deb [signed-by=/usr/share/keyrings/apt.llvm.org.gpg] https://apt.llvm.org/jammy/ llvm-toolchain-jammy-21 main" | sudo tee /etc/apt/sources.list.d/llvm21.list + sudo apt-get update + sudo apt-get install -y --no-install-recommends clang-21 lld-21 + + - name: Install Clang sanitizer runtimes + if: (inputs.enable-sanitizers || inputs.enable-thread-sanitizer) && (inputs.cc == 'clang-21' || inputs.cxx == 'clang++-21') + run: sudo apt-get install -y --no-install-recommends libclang-rt-21-dev + + - name: Reclaim package indexes + run: sudo rm -rf /var/lib/apt/lists/* - name: Configure CMake run: | @@ -52,6 +69,32 @@ jobs: - name: Build run: ninja -C Build/ubuntu + - name: Audit statically linked Bun JavaScriptCore runtime + if: inputs.js-engine == '' || inputs.js-engine == 'JavaScriptCore' + run: | + jsc_archive=Build/ubuntu/Core/Node-API/libjsruntimehost-jsc.a + executable=Build/ubuntu/Tests/UnitTests/UnitTests + test -f "$jsc_archive" + test -x "$executable" + readelf --file-header "$executable" 2>/dev/null | grep 'DYN (Position-Independent Executable file)' + readelf --dynamic "$executable" 2>/dev/null | grep -E 'FLAGS_1.*PIE' + readelf --syms "$executable" 2>/dev/null | grep JSCBunInitialize + + ldd_output="$(ldd -r "$executable" 2>&1)" + printf '%s\n' "$ldd_output" + if grep -q 'undefined symbol' <<<"$ldd_output"; then + exit 1 + fi + if grep -q 'javascriptcore' <<<"${ldd_output,,}"; then + exit 1 + fi + + max_glibc="$(readelf --version-info "$executable" 2>/dev/null | grep -o 'GLIBC_[0-9.]*' | sort -Vu | tail -n 1)" + max_glibcxx="$(readelf --version-info "$executable" 2>/dev/null | grep -o 'GLIBCXX_[0-9.]*' | sort -Vu | tail -n 1)" + echo "Maximum required symbol versions: $max_glibc, $max_glibcxx" + dpkg --compare-versions "${max_glibc#GLIBC_}" le 2.35 + dpkg --compare-versions "${max_glibcxx#GLIBCXX_}" le 3.4.30 + - name: Run Tests working-directory: Build/ubuntu/Tests/UnitTests run: ./UnitTests @@ -64,4 +107,3 @@ jobs: # on the mutator without cross-thread signals. macOS JSC uses Mach # thread_suspend() and is unaffected. JSC_useConcurrentGC: ${{ inputs.enable-thread-sanitizer && '0' || '' }} - diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fa226b76..5061e9fa 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -128,30 +128,22 @@ jobs: simulator: 'iPhone 17' # Linux - Ubuntu_gcc: + Ubuntu_JSC_LTO: uses: ./.github/workflows/build-linux.yml - Ubuntu_clang: - uses: ./.github/workflows/build-linux.yml - with: - cc: clang - cxx: clang++ - - Ubuntu_QuickJS: + Ubuntu_QuickJS_gcc: uses: ./.github/workflows/build-linux.yml with: + cc: gcc + cxx: g++ js-engine: QuickJS Ubuntu_Sanitizers_clang: uses: ./.github/workflows/build-linux.yml with: - cc: clang - cxx: clang++ enable-sanitizers: true Ubuntu_ThreadSanitizer_clang: uses: ./.github/workflows/build-linux.yml with: - cc: clang - cxx: clang++ enable-thread-sanitizer: true diff --git a/Core/AppRuntime/Source/AppRuntime_JavaScriptCore.cpp b/Core/AppRuntime/Source/AppRuntime_JavaScriptCore.cpp index 31f9d919..59521578 100644 --- a/Core/AppRuntime/Source/AppRuntime_JavaScriptCore.cpp +++ b/Core/AppRuntime/Source/AppRuntime_JavaScriptCore.cpp @@ -1,16 +1,16 @@ #include "AppRuntime.h" #include -#if __ANDROID__ -extern "C" void JSCAndroidInitialize(); +#if defined(JSR_USE_BUN_JSC) +extern "C" void JSCBunInitialize(); #endif namespace Babylon { void AppRuntime::RunEnvironmentTier(const char*) { -#if __ANDROID__ - JSCAndroidInitialize(); +#if defined(JSR_USE_BUN_JSC) + JSCBunInitialize(); #endif auto globalContext = JSGlobalContextCreateInGroup(nullptr, nullptr); @@ -25,10 +25,17 @@ namespace Babylon Run(env); -#if __ANDROID__ - Napi::ContextLock contextLock{env}; +#if defined(JSR_USE_BUN_JSC) + { + // Scope this holder to the host's context reference. Keeping it alive across Detach + // delays VM destruction until after napi_env has been deleted, but JSC's last-chance + // finalizers are allowed to call Node-API with that environment. + Napi::ContextLock contextLock{env}; +#endif + JSGlobalContextRelease(globalContext); +#if defined(JSR_USE_BUN_JSC) + } #endif - JSGlobalContextRelease(globalContext); // Detach must come after JSGlobalContextRelease since it triggers finalizers which require env. Napi::Detach(env); diff --git a/Core/Node-API/CMakeLists.txt b/Core/Node-API/CMakeLists.txt index c15d9fd4..4e1bd160 100644 --- a/Core/Node-API/CMakeLists.txt +++ b/Core/Node-API/CMakeLists.txt @@ -7,7 +7,6 @@ elseif(ANDROID) set(NAPI_JAVASCRIPT_ENGINE "V8" CACHE STRING "JavaScript engine for Node-API") elseif(UNIX) set(NAPI_JAVASCRIPT_ENGINE "JavaScriptCore" CACHE STRING "JavaScript engine for Node-API") - set(JAVASCRIPTCORE_LIBRARY "/usr/lib/x86_64-linux-gnu/libjavascriptcoregtk-4.1.so" CACHE STRING "Path to the JavaScriptCore shared library") else() message(FATAL_ERROR "Unable to select Node-API JavaScript engine for platform") endif() @@ -67,76 +66,209 @@ if(NAPI_BUILD_ABI) "Source/js_native_api_javascriptcore.cc" "Source/js_native_api_javascriptcore.h") - if(ANDROID) - # Bun publishes the JavaScriptCore build used by each release as pinned Android static - # archives. Relink the selected archives into the libjsc.so boundary expected by napi, - # instead of installing an npm package only to unpack its embedded AAR. - set(JSR_JSC_BUN_WEBKIT_REVISION "5488984d20e0dbfe4be2c3ba8fb18eb81a5e0e8b") - set(JSR_JSC_BUN_WEBKIT_RELEASE "autobuild-${JSR_JSC_BUN_WEBKIT_REVISION}") - set(JSR_JSC_ANDROID_ROOT "" CACHE PATH - "Path to an extracted Bun Android WebKit 'bun-webkit' directory; empty downloads the pinned release") - - string(REGEX REPLACE "^android-" "" JSR_JSC_ANDROID_API_LEVEL "${ANDROID_PLATFORM}") - if(JSR_JSC_ANDROID_API_LEVEL LESS 28) - message(FATAL_ERROR - "Bun's pinned Android WebKit archives target API 28; set Android minSdkVersion to 28 or newer") - endif() + if(ANDROID OR CMAKE_SYSTEM_NAME STREQUAL "Linux") + # Bun publishes the JavaScriptCore build used by each release as pinned static + # archives. Download only headers and link inputs, then use Android's libjsc.so + # boundary or Linux's final LTO executable link instead of maintaining a separate + # JSC build/package pipeline. + set(JSR_BUN_JSC_WEBKIT_REVISION "5488984d20e0dbfe4be2c3ba8fb18eb81a5e0e8b") + set(JSR_BUN_JSC_WEBKIT_RELEASE "autobuild-${JSR_BUN_JSC_WEBKIT_REVISION}") + set(JSR_BUN_JSC_ROOT "" CACHE PATH + "Path to an extracted Bun WebKit 'bun-webkit' directory; empty downloads the pinned release") + + if(ANDROID) + set(JSR_BUN_JSC_ANDROID_NDK_VERSION "${CMAKE_ANDROID_NDK_VERSION}") + if(NOT JSR_BUN_JSC_ANDROID_NDK_VERSION) + set(JSR_BUN_JSC_ANDROID_NDK_ROOT "${CMAKE_ANDROID_NDK}") + if(NOT JSR_BUN_JSC_ANDROID_NDK_ROOT) + set(JSR_BUN_JSC_ANDROID_NDK_ROOT "${ANDROID_NDK}") + endif() + if(EXISTS "${JSR_BUN_JSC_ANDROID_NDK_ROOT}/source.properties") + file(STRINGS "${JSR_BUN_JSC_ANDROID_NDK_ROOT}/source.properties" + JSR_BUN_JSC_ANDROID_NDK_REVISION_LINE + REGEX "^Pkg\\.Revision = ") + string(REGEX REPLACE "^Pkg\\.Revision = ([0-9]+\\.[0-9]+).*$" "\\1" + JSR_BUN_JSC_ANDROID_NDK_VERSION + "${JSR_BUN_JSC_ANDROID_NDK_REVISION_LINE}") + endif() + endif() + if(NOT JSR_BUN_JSC_ANDROID_NDK_VERSION OR + JSR_BUN_JSC_ANDROID_NDK_VERSION VERSION_LESS "26.1") + message(FATAL_ERROR + "Bun's current WebKit headers require Android NDK r26b or newer for C++23 library support; found r${JSR_BUN_JSC_ANDROID_NDK_VERSION}") + endif() - if(ANDROID_ABI STREQUAL "arm64-v8a") - set(JSR_JSC_BUN_WEBKIT_ASSET "bun-webkit-linux-arm64-android.tar.gz") - set(JSR_JSC_BUN_WEBKIT_SHA256 "63c18a18c89dc3e1c0a93107f95290c27fbe310eb90e30dc197a7936804d034b") - elseif(ANDROID_ABI STREQUAL "x86_64") - set(JSR_JSC_BUN_WEBKIT_ASSET "bun-webkit-linux-amd64-android.tar.gz") - set(JSR_JSC_BUN_WEBKIT_SHA256 "c198efb68c17ae65ff66968f7d04bdab4845a5abaefb941b15e0e2b36aaab774") + string(REGEX REPLACE "^android-" "" JSR_BUN_JSC_ANDROID_API_LEVEL "${ANDROID_PLATFORM}") + if(JSR_BUN_JSC_ANDROID_API_LEVEL LESS 28) + message(FATAL_ERROR + "Bun's pinned Android WebKit archives target API 28; set Android minSdkVersion to 28 or newer") + endif() + + # Bun does not publish Android LTO archives. These are its optimized native + # arm64 and x86_64 builds; the full-LTO variants are Linux-only. + if(ANDROID_ABI STREQUAL "arm64-v8a") + set(JSR_BUN_JSC_ASSET "bun-webkit-linux-arm64-android.tar.gz") + set(JSR_BUN_JSC_SHA256 "63c18a18c89dc3e1c0a93107f95290c27fbe310eb90e30dc197a7936804d034b") + elseif(ANDROID_ABI STREQUAL "x86_64") + set(JSR_BUN_JSC_ASSET "bun-webkit-linux-amd64-android.tar.gz") + set(JSR_BUN_JSC_SHA256 "c198efb68c17ae65ff66968f7d04bdab4845a5abaefb941b15e0e2b36aaab774") + else() + message(FATAL_ERROR + "Bun Android JavaScriptCore supports only arm64-v8a and x86_64; got '${ANDROID_ABI}'") + endif() + set(JSR_BUN_JSC_OUTPUT_NAME jsc) else() - message(FATAL_ERROR - "Bun Android JavaScriptCore supports only arm64-v8a and x86_64; got '${ANDROID_ABI}'") + include(CheckCXXSourceCompiles) + check_cxx_source_compiles([=[ + #include + #ifndef __GLIBC__ + #error Bun's selected Linux JavaScriptCore archive requires glibc + #endif + int main() { return 0; } + ]=] JSR_BUN_JSC_USES_GLIBC) + if(NOT JSR_BUN_JSC_USES_GLIBC) + message(FATAL_ERROR + "Bun JavaScriptCore currently supports glibc Linux in JsRuntimeHost; musl requires Bun's separate musl archive") + endif() + + if(NOT CMAKE_CXX_COMPILER_ID STREQUAL "Clang" OR + NOT CMAKE_CXX_COMPILER_VERSION MATCHES "^21\\.") + message(FATAL_ERROR + "Bun's Linux JavaScriptCore LTO archives require Clang 21") + endif() + + set(JSR_BUN_JSC_SAVED_CXX_STANDARD "${CMAKE_CXX_STANDARD}") + set(CMAKE_CXX_STANDARD 23) + check_cxx_source_compiles([=[ + #include + int main() { std::expected value{1}; return *value - 1; } + ]=] JSR_BUN_JSC_HAS_STD_EXPECTED) + set(CMAKE_CXX_STANDARD "${JSR_BUN_JSC_SAVED_CXX_STANDARD}") + if(NOT JSR_BUN_JSC_HAS_STD_EXPECTED) + message(FATAL_ERROR + "Bun's current WebKit headers require a C++23 standard library with (install libstdc++-12-dev on Ubuntu 22.04)") + endif() + + find_program(JSR_BUN_JSC_LLD NAMES ld.lld-21 ld.lld REQUIRED) + execute_process( + COMMAND "${JSR_BUN_JSC_LLD}" --version + OUTPUT_VARIABLE JSR_BUN_JSC_LLD_VERSION + OUTPUT_STRIP_TRAILING_WHITESPACE) + if(NOT JSR_BUN_JSC_LLD_VERSION MATCHES "LLD 21\\.") + message(FATAL_ERROR + "Bun's Linux JavaScriptCore LTO archives require LLD 21; found '${JSR_BUN_JSC_LLD_VERSION}'") + endif() + + string(TOLOWER "${CMAKE_SYSTEM_PROCESSOR}" JSR_BUN_JSC_PROCESSOR) + if(JSR_BUN_JSC_PROCESSOR MATCHES "^(x86_64|amd64)$") + # Use Bun's Nehalem-baseline full-LTO variant rather than imposing Haswell on + # every JsRuntimeHost Linux consumer. + set(JSR_BUN_JSC_ASSET "bun-webkit-linux-amd64-baseline-lto.tar.gz") + set(JSR_BUN_JSC_SHA256 "2756c59963525ee01f8878c07d5bceff98f6e926ccbe79dff52dfe44f4a0b5c3") + elseif(JSR_BUN_JSC_PROCESSOR MATCHES "^(aarch64|arm64)$") + set(JSR_BUN_JSC_ASSET "bun-webkit-linux-arm64-lto.tar.gz") + set(JSR_BUN_JSC_SHA256 "5739c4e6b32258f30db22aeeddefe9dfa3b05d6edb5ea92985b8fbefa85a2cfe") + else() + message(FATAL_ERROR + "Bun Linux JavaScriptCore supports only x86_64 and arm64; got '${CMAKE_SYSTEM_PROCESSOR}'") + endif() + set(JSR_BUN_JSC_OUTPUT_NAME jsruntimehost-jsc) endif() - if(JSR_JSC_ANDROID_ROOT) - set(JSC_ANDROID_DIR "${JSR_JSC_ANDROID_ROOT}") + if(JSR_BUN_JSC_ROOT) + set(BUN_JSC_DIR "${JSR_BUN_JSC_ROOT}") else() - FetchContent_Declare(bun_webkit_android - URL - "https://github.com/oven-sh/WebKit/releases/download/${JSR_JSC_BUN_WEBKIT_RELEASE}/${JSR_JSC_BUN_WEBKIT_ASSET}" - URL_HASH "SHA256=${JSR_JSC_BUN_WEBKIT_SHA256}") - FetchContent_GetProperties(bun_webkit_android) - if(NOT bun_webkit_android_POPULATED) + string(REGEX REPLACE "\\.tar\\.gz$" "" JSR_BUN_JSC_ASSET_STEM "${JSR_BUN_JSC_ASSET}") + set(JSR_BUN_JSC_FETCH_DIR + "${CMAKE_CURRENT_BINARY_DIR}/${JSR_BUN_JSC_WEBKIT_REVISION}/${JSR_BUN_JSC_ASSET_STEM}") + set(BUN_JSC_DIR "${JSR_BUN_JSC_FETCH_DIR}/bun-webkit") + set(JSR_BUN_JSC_ARCHIVE "${JSR_BUN_JSC_FETCH_DIR}/${JSR_BUN_JSC_ASSET}") + set(JSR_BUN_JSC_COMPLETE_STAMP + "${BUN_JSC_DIR}/.jsruntimehost-${JSR_BUN_JSC_SHA256}.complete") + + if(NOT EXISTS "${JSR_BUN_JSC_COMPLETE_STAMP}") + file(REMOVE_RECURSE "${BUN_JSC_DIR}") + file(MAKE_DIRECTORY "${JSR_BUN_JSC_FETCH_DIR}") message(STATUS - "Downloading Bun WebKit ${JSR_JSC_BUN_WEBKIT_REVISION} for ${ANDROID_ABI}") - FetchContent_Populate(bun_webkit_android) + "Downloading Bun WebKit ${JSR_BUN_JSC_WEBKIT_REVISION}: ${JSR_BUN_JSC_ASSET}") + file(DOWNLOAD + "https://github.com/oven-sh/WebKit/releases/download/${JSR_BUN_JSC_WEBKIT_RELEASE}/${JSR_BUN_JSC_ASSET}" + "${JSR_BUN_JSC_ARCHIVE}" + EXPECTED_HASH "SHA256=${JSR_BUN_JSC_SHA256}" + TLS_VERIFY ON + SHOW_PROGRESS + STATUS JSR_BUN_JSC_DOWNLOAD_STATUS) + list(GET JSR_BUN_JSC_DOWNLOAD_STATUS 0 JSR_BUN_JSC_DOWNLOAD_CODE) + if(NOT JSR_BUN_JSC_DOWNLOAD_CODE EQUAL 0) + list(GET JSR_BUN_JSC_DOWNLOAD_STATUS 1 JSR_BUN_JSC_DOWNLOAD_MESSAGE) + message(FATAL_ERROR "Unable to download Bun WebKit: ${JSR_BUN_JSC_DOWNLOAD_MESSAGE}") + endif() + file(ARCHIVE_EXTRACT + INPUT "${JSR_BUN_JSC_ARCHIVE}" + DESTINATION "${JSR_BUN_JSC_FETCH_DIR}" + PATTERNS + "bun-webkit/include" + "bun-webkit/lib/libJavaScriptCore.a" + "bun-webkit/lib/libWTF.a" + "bun-webkit/lib/libbmalloc.a" + "bun-webkit/lib/libicui18n.a" + "bun-webkit/lib/libicuuc.a" + "bun-webkit/lib/libicudata.a" + "bun-webkit/package.json") + + foreach(JSR_BUN_JSC_EXTRACTED_FILE + "${BUN_JSC_DIR}/include/JavaScriptCore/JavaScript.h" + "${BUN_JSC_DIR}/lib/libJavaScriptCore.a" + "${BUN_JSC_DIR}/lib/libWTF.a" + "${BUN_JSC_DIR}/lib/libbmalloc.a" + "${BUN_JSC_DIR}/lib/libicui18n.a" + "${BUN_JSC_DIR}/lib/libicuuc.a" + "${BUN_JSC_DIR}/lib/libicudata.a") + if(NOT EXISTS "${JSR_BUN_JSC_EXTRACTED_FILE}") + message(FATAL_ERROR + "Bun WebKit extraction is missing '${JSR_BUN_JSC_EXTRACTED_FILE}'") + endif() + endforeach() + + file(WRITE "${JSR_BUN_JSC_COMPLETE_STAMP}" + "${JSR_BUN_JSC_WEBKIT_REVISION}\n${JSR_BUN_JSC_SHA256}\n") + file(REMOVE "${JSR_BUN_JSC_ARCHIVE}") endif() - # FetchContent strips the archive's single top-level bun-webkit directory. - set(JSC_ANDROID_DIR "${bun_webkit_android_SOURCE_DIR}") endif() - set(JSC_ANDROID_STATIC_LIBRARIES - "${JSC_ANDROID_DIR}/lib/libJavaScriptCore.a" - "${JSC_ANDROID_DIR}/lib/libWTF.a" - "${JSC_ANDROID_DIR}/lib/libbmalloc.a" - "${JSC_ANDROID_DIR}/lib/libicui18n.a" - "${JSC_ANDROID_DIR}/lib/libicuuc.a" - "${JSC_ANDROID_DIR}/lib/libicudata.a") - - foreach(JSC_ANDROID_FILE - "${JSC_ANDROID_DIR}/include/JavaScriptCore/JavaScript.h" - ${JSC_ANDROID_STATIC_LIBRARIES}) - if(NOT EXISTS "${JSC_ANDROID_FILE}") - message(FATAL_ERROR "Bun Android WebKit archive is missing '${JSC_ANDROID_FILE}'") + set(BUN_JSC_STATIC_LIBRARIES + "${BUN_JSC_DIR}/lib/libJavaScriptCore.a" + "${BUN_JSC_DIR}/lib/libWTF.a" + "${BUN_JSC_DIR}/lib/libbmalloc.a" + "${BUN_JSC_DIR}/lib/libicui18n.a" + "${BUN_JSC_DIR}/lib/libicuuc.a" + "${BUN_JSC_DIR}/lib/libicudata.a") + + foreach(BUN_JSC_FILE + "${BUN_JSC_DIR}/include/JavaScriptCore/JavaScript.h" + ${BUN_JSC_STATIC_LIBRARIES}) + if(NOT EXISTS "${BUN_JSC_FILE}") + message(FATAL_ERROR "Bun WebKit archive is missing '${BUN_JSC_FILE}'") endif() endforeach() set(INCLUDE_DIRECTORIES ${INCLUDE_DIRECTORIES} - PUBLIC "${JSC_ANDROID_DIR}/include") + PUBLIC "${BUN_JSC_DIR}/include") - add_library(jsc_android SHARED "Source/jsc_android.cpp") - set_target_properties(jsc_android PROPERTIES - OUTPUT_NAME jsc + if(ANDROID) + add_library(bun_jsc SHARED "Source/jsc_bun.cpp") + else() + # Bun's Linux LTO archives contain local-exec TLS relocations and are intended to + # be linked into the initial executable. They cannot be repackaged into a shared + # object, so keep this wrapper static and propagate the archives to the final link. + add_library(bun_jsc STATIC "Source/jsc_bun.cpp") + endif() + set_target_properties(bun_jsc PROPERTIES + OUTPUT_NAME "${JSR_BUN_JSC_OUTPUT_NAME}" CXX_STANDARD 23 - CXX_STANDARD_REQUIRED ON - LINK_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/Source/jsc_android.exports") - target_compile_definitions(jsc_android PRIVATE - JSR_JSC_BUN_WEBKIT_REVISION="${JSR_JSC_BUN_WEBKIT_REVISION}" + CXX_STANDARD_REQUIRED ON) + target_compile_definitions(bun_jsc PRIVATE + JSR_BUN_JSC_WEBKIT_REVISION="${JSR_BUN_JSC_WEBKIT_REVISION}" BUILDING_JSCONLY__ BUILDING_JavaScriptCore BUILDING_WEBKIT=1 @@ -146,40 +278,73 @@ if(NAPI_BUILD_ABI) STATICALLY_LINKED_WITH_WTF STATICALLY_LINKED_WITH_bmalloc U_STATIC_IMPLEMENTATION=1) - target_include_directories(jsc_android PUBLIC "${JSC_ANDROID_DIR}/include") - target_link_options(jsc_android PRIVATE - "-Wl,--no-undefined" - "-Wl,-Bsymbolic" - "-Wl,--version-script=${CMAKE_CURRENT_SOURCE_DIR}/Source/jsc_android.exports" - "-Wl,-z,max-page-size=16384" - "-Wl,-z,common-page-size=16384") - target_link_libraries(jsc_android PRIVATE - "-Wl,--whole-archive" - "${JSC_ANDROID_DIR}/lib/libJavaScriptCore.a" - "-Wl,--no-whole-archive" - "-Wl,--start-group" - "${JSC_ANDROID_DIR}/lib/libWTF.a" - "${JSC_ANDROID_DIR}/lib/libbmalloc.a" - "${JSC_ANDROID_DIR}/lib/libicui18n.a" - "${JSC_ANDROID_DIR}/lib/libicuuc.a" - "${JSC_ANDROID_DIR}/lib/libicudata.a" - "-Wl,--end-group" - log - dl - m) - set_property(TARGET jsc_android PROPERTY FOLDER Dependencies) + target_include_directories(bun_jsc PUBLIC "${BUN_JSC_DIR}/include") + + if(ANDROID) + set_target_properties(bun_jsc PROPERTIES + LINK_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/Source/jsc_bun.exports") + target_link_options(bun_jsc PRIVATE + "-Wl,--no-undefined" + "-Wl,-Bsymbolic" + "-Wl,--version-script=${CMAKE_CURRENT_SOURCE_DIR}/Source/jsc_bun.exports" + "-Wl,-z,max-page-size=16384" + "-Wl,-z,common-page-size=16384") + target_link_libraries(bun_jsc PRIVATE + "-Wl,--whole-archive" + "${BUN_JSC_DIR}/lib/libJavaScriptCore.a" + "-Wl,--no-whole-archive" + "-Wl,--start-group" + "${BUN_JSC_DIR}/lib/libWTF.a" + "${BUN_JSC_DIR}/lib/libbmalloc.a" + "${BUN_JSC_DIR}/lib/libicui18n.a" + "${BUN_JSC_DIR}/lib/libicuuc.a" + "${BUN_JSC_DIR}/lib/libicudata.a" + "-Wl,--end-group" + log + ${CMAKE_DL_LIBS} + m) + else() + set_property(TARGET bun_jsc PROPERTY INTERFACE_POSITION_INDEPENDENT_CODE ON) + target_compile_options(bun_jsc PRIVATE + -O2 + -flto=full + -fwhole-program-vtables + -fforce-emit-vtables + -fno-sanitize=all) + target_link_options(bun_jsc INTERFACE + -flto=full + -fwhole-program-vtables + -fforce-emit-vtables + -O2 + -pie + "--ld-path=${JSR_BUN_JSC_LLD}" + "-Wl,--gc-sections") + find_package(Threads REQUIRED) + target_link_libraries(bun_jsc PUBLIC + "-Wl,--whole-archive" + "${BUN_JSC_DIR}/lib/libJavaScriptCore.a" + "-Wl,--no-whole-archive" + "-Wl,--start-group" + "${BUN_JSC_DIR}/lib/libWTF.a" + "${BUN_JSC_DIR}/lib/libbmalloc.a" + "${BUN_JSC_DIR}/lib/libicui18n.a" + "${BUN_JSC_DIR}/lib/libicuuc.a" + "${BUN_JSC_DIR}/lib/libicudata.a" + "-Wl,--end-group" + Threads::Threads + ${CMAKE_DL_LIBS} + m + atomic) + endif() + set_property(TARGET bun_jsc PROPERTY FOLDER Dependencies) + set(JSR_USE_BUN_JSC TRUE) set(LINK_LIBRARIES ${LINK_LIBRARIES} - PUBLIC jsc_android) + PUBLIC bun_jsc) elseif(APPLE) find_library(JAVASCRIPTCORE_LIBRARY JavaScriptCore) set(LINK_LIBRARIES ${LINK_LIBRARIES} PUBLIC ${JAVASCRIPTCORE_LIBRARY}) - elseif(UNIX) - set(LINK_LIBRARIES ${LINK_LIBRARIES} - PUBLIC ${JAVASCRIPTCORE_LIBRARY}) - set(INCLUDE_DIRECTORIES ${INCLUDE_DIRECTORIES} - PUBLIC "/usr/include/webkitgtk-4.1/") else() message(FATAL_ERROR "Unsupported JavaScript engine: ${NAPI_JAVASCRIPT_ENGINE}") endif() @@ -355,6 +520,17 @@ add_library(napi ${SOURCES}) target_include_directories(napi ${INCLUDE_DIRECTORIES}) target_link_libraries(napi ${LINK_LIBRARIES}) +if(JSR_USE_BUN_JSC) + target_compile_definitions(napi PUBLIC JSR_USE_BUN_JSC=1) + if(CMAKE_SYSTEM_NAME STREQUAL "Linux") + get_target_property(JSR_BUN_JSC_NAPI_TYPE napi TYPE) + if(NOT JSR_BUN_JSC_NAPI_TYPE STREQUAL "STATIC_LIBRARY") + message(FATAL_ERROR + "Bun's Linux JavaScriptCore LTO archives must reach the final executable; build JsRuntimeHost static (BUILD_SHARED_LIBS=OFF)") + endif() + endif() +endif() + if(NAPI_JAVASCRIPT_ENGINE STREQUAL "Hermes") # Apply Hermes-specific warning suppressions ONLY to env_hermes.cc so # they don't relax the rules for the rest of the napi sources. diff --git a/Core/Node-API/Include/Engine/JavaScriptCore/napi/env.h b/Core/Node-API/Include/Engine/JavaScriptCore/napi/env.h index 9d94d78f..9c1584f6 100644 --- a/Core/Node-API/Include/Engine/JavaScriptCore/napi/env.h +++ b/Core/Node-API/Include/Engine/JavaScriptCore/napi/env.h @@ -3,9 +3,9 @@ #include #include -#if __ANDROID__ -extern "C" void* JSCAndroidAcquireContextLock(JSGlobalContextRef context); -extern "C" void JSCAndroidReleaseContextLock(void* opaqueLock); +#if defined(JSR_USE_BUN_JSC) +extern "C" void* JSCBunAcquireContextLock(JSGlobalContextRef context); +extern "C" void JSCBunReleaseContextLock(void* opaqueLock); #endif namespace Napi @@ -18,18 +18,18 @@ namespace Napi JSGlobalContextRef GetContext(Napi::Env); -#if __ANDROID__ +#if defined(JSR_USE_BUN_JSC) class ContextLock final { public: explicit ContextLock(Napi::Env env) - : m_opaqueLock{JSCAndroidAcquireContextLock(GetContext(env))} + : m_opaqueLock{JSCBunAcquireContextLock(GetContext(env))} { } ~ContextLock() { - JSCAndroidReleaseContextLock(m_opaqueLock); + JSCBunReleaseContextLock(m_opaqueLock); } ContextLock(const ContextLock&) = delete; diff --git a/Core/Node-API/Source/js_native_api_javascriptcore.h b/Core/Node-API/Source/js_native_api_javascriptcore.h index 9ed8472b..f44fcc93 100644 --- a/Core/Node-API/Source/js_native_api_javascriptcore.h +++ b/Core/Node-API/Source/js_native_api_javascriptcore.h @@ -8,41 +8,41 @@ #include #include -#if __ANDROID__ -extern "C" void* JSCAndroidAcquireContextLock(JSGlobalContextRef context); -extern "C" void JSCAndroidReleaseContextLock(void* opaqueLock); -extern "C" bool JSCAndroidLockContext(JSGlobalContextRef context); -extern "C" void JSCAndroidUnlockContext(JSGlobalContextRef context); +#if defined(JSR_USE_BUN_JSC) +extern "C" void* JSCBunAcquireContextLock(JSGlobalContextRef context); +extern "C" void JSCBunReleaseContextLock(void* opaqueLock); +extern "C" bool JSCBunLockContext(JSGlobalContextRef context); +extern "C" void JSCBunUnlockContext(JSGlobalContextRef context); -class JSCAndroidContextLock final { +class JSCBunContextLock final { public: - explicit JSCAndroidContextLock(JSGlobalContextRef context) - : lock{JSCAndroidAcquireContextLock(context)} {} + explicit JSCBunContextLock(JSGlobalContextRef context) + : lock{JSCBunAcquireContextLock(context)} {} - ~JSCAndroidContextLock() { - JSCAndroidReleaseContextLock(lock); + ~JSCBunContextLock() { + JSCBunReleaseContextLock(lock); } - JSCAndroidContextLock(const JSCAndroidContextLock&) = delete; - JSCAndroidContextLock& operator=(const JSCAndroidContextLock&) = delete; + JSCBunContextLock(const JSCBunContextLock&) = delete; + JSCBunContextLock& operator=(const JSCBunContextLock&) = delete; private: void* lock{}; }; -class JSCAndroidAPILock final { +class JSCBunAPILock final { public: - explicit JSCAndroidAPILock(JSGlobalContextRef context) - : context{context}, locked{JSCAndroidLockContext(context)} {} + explicit JSCBunAPILock(JSGlobalContextRef context, bool acquire = true) + : context{context}, locked{acquire && JSCBunLockContext(context)} {} - ~JSCAndroidAPILock() { + ~JSCBunAPILock() { if (locked) { - JSCAndroidUnlockContext(context); + JSCBunUnlockContext(context); } } - JSCAndroidAPILock(const JSCAndroidAPILock&) = delete; - JSCAndroidAPILock& operator=(const JSCAndroidAPILock&) = delete; + JSCBunAPILock(const JSCBunAPILock&) = delete; + JSCBunAPILock& operator=(const JSCBunAPILock&) = delete; private: JSGlobalContextRef context{}; @@ -66,8 +66,8 @@ struct napi_env__ { const std::thread::id thread_id{std::this_thread::get_id()}; napi_env__(JSGlobalContextRef context) : context{context} { -#if __ANDROID__ - JSCAndroidContextLock contextLock{context}; +#if defined(JSR_USE_BUN_JSC) + JSCBunContextLock contextLock{context}; #endif napi_envs[context] = this; JSGlobalContextRetain(context); @@ -78,16 +78,22 @@ struct napi_env__ { } ~napi_env__() { -#if __ANDROID__ - JSCAndroidContextLock contextLock{context}; +#if defined(JSR_USE_BUN_JSC) + { + // Releasing this holder may destroy the VM and run last-chance finalizers. Keep both this + // environment and its context lookup registered until those finalizers have completed. + JSCBunContextLock contextLock{context}; +#endif + shutting_down = true; + deinit_refs(); + deinit_symbol(wrapper_info_symbol); + deinit_symbol(reference_info_symbol); + deinit_symbol(function_info_symbol); + deinit_symbol(constructor_info_symbol); + JSGlobalContextRelease(context); +#if defined(JSR_USE_BUN_JSC) + } #endif - shutting_down = true; - deinit_refs(); - deinit_symbol(wrapper_info_symbol); - deinit_symbol(reference_info_symbol); - deinit_symbol(function_info_symbol); - deinit_symbol(constructor_info_symbol); - JSGlobalContextRelease(context); napi_envs.erase(context); } @@ -115,14 +121,15 @@ struct napi_env__ { } \ } while (0) -#if __ANDROID__ +#if defined(JSR_USE_BUN_JSC) #define CHECK_ENV(env) \ do { \ if ((env) == nullptr) { \ return napi_invalid_arg; \ } \ } while (0); \ - JSCAndroidAPILock jscAndroidAPILock{(env)->context}; \ + /* Last-chance finalizers hold JSC's API lock. Avoid context APIs after shutdown. */ \ + JSCBunAPILock jscBunAPILock{(env)->context, !(env)->shutting_down}; \ assert((env)->thread_id == std::this_thread::get_id()) #else #define CHECK_ENV(env) \ diff --git a/Core/Node-API/Source/jsc_android.cpp b/Core/Node-API/Source/jsc_bun.cpp similarity index 58% rename from Core/Node-API/Source/jsc_android.cpp rename to Core/Node-API/Source/jsc_bun.cpp index 389dff90..b0c366d3 100644 --- a/Core/Node-API/Source/jsc_android.cpp +++ b/Core/Node-API/Source/jsc_bun.cpp @@ -1,8 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -#ifndef JSR_JSC_BUN_WEBKIT_REVISION -#error JSR_JSC_BUN_WEBKIT_REVISION must identify the linked Bun WebKit build +#ifndef JSR_BUN_JSC_WEBKIT_REVISION +#error JSR_BUN_JSC_WEBKIT_REVISION must identify the linked Bun WebKit build #endif #include @@ -10,15 +10,15 @@ #include #include -extern "C" void JSCAndroidInitialize() +extern "C" void JSCBunInitialize() { - // Match the standalone JavaScriptCore C API initialization path. In particular, do not call - // WTF::initializeMainThread(): AppRuntime runs JavaScript on a worker thread, while Android may - // load this library and initialize other WTF thread-local state on the instrumentation thread. + // Match the standalone JavaScriptCore C API initialization path. Do not separately call + // WTF::initializeMainThread(): the host may load the library and initialize other WTF + // thread-local state on a different thread from the AppRuntime JavaScript worker. JSC::initialize(); } -extern "C" void* JSCAndroidAcquireContextLock(JSGlobalContextRef context) +extern "C" void* JSCBunAcquireContextLock(JSGlobalContextRef context) { // Bun's WebKit fork removes the per-call JSLockHolder instances from the C API and expects its // embedding event loop to hold the VM lock. This holder is used only for lifecycle operations @@ -27,12 +27,12 @@ extern "C" void* JSCAndroidAcquireContextLock(JSGlobalContextRef context) return new JSC::JSLockHolder(*vm); } -extern "C" void JSCAndroidReleaseContextLock(void* opaqueLock) +extern "C" void JSCBunReleaseContextLock(void* opaqueLock) { delete static_cast(opaqueLock); } -extern "C" bool JSCAndroidLockContext(JSGlobalContextRef context) +extern "C" bool JSCBunLockContext(JSGlobalContextRef context) { auto* vm = toJS(JSContextGetGroup(context)); if (vm->currentThreadIsHoldingAPILock()) @@ -44,12 +44,12 @@ extern "C" bool JSCAndroidLockContext(JSGlobalContextRef context) return true; } -extern "C" void JSCAndroidUnlockContext(JSGlobalContextRef context) +extern "C" void JSCBunUnlockContext(JSGlobalContextRef context) { toJS(JSContextGetGroup(context))->apiLock().unlock(); } -extern "C" const char* JSCAndroidGetWebKitRevision() +extern "C" const char* JSCBunGetWebKitRevision() { - return JSR_JSC_BUN_WEBKIT_REVISION; + return JSR_BUN_JSC_WEBKIT_REVISION; } diff --git a/Core/Node-API/Source/jsc_android.exports b/Core/Node-API/Source/jsc_bun.exports similarity index 75% rename from Core/Node-API/Source/jsc_android.exports rename to Core/Node-API/Source/jsc_bun.exports index 47b182c0..15968d28 100644 --- a/Core/Node-API/Source/jsc_android.exports +++ b/Core/Node-API/Source/jsc_bun.exports @@ -1,7 +1,7 @@ { global: JS*; - JSCAndroid*; + JSCBun*; kJS*; local: *; diff --git a/Tests/UnitTests/Android/app/build.gradle b/Tests/UnitTests/Android/app/build.gradle index b4cc7768..3cd00d00 100644 --- a/Tests/UnitTests/Android/app/build.gradle +++ b/Tests/UnitTests/Android/app/build.gradle @@ -15,7 +15,7 @@ def cmakeArguments = [ "-DJSRUNTIMEHOST_CORE_APPRUNTIME_V8_INSPECTOR=ON" ] if (project.hasProperty("jscAndroidRoot")) { - cmakeArguments.add("-DJSR_JSC_ANDROID_ROOT=${project.property('jscAndroidRoot')}") + cmakeArguments.add("-DJSR_BUN_JSC_ROOT=${project.property('jscAndroidRoot')}") } // Hermes for Android needs HOST hermesc/shermes (they emit bytecode at build // time and can't run as NDK-cross-compiled binaries). The JsRuntimeHost CMake @@ -30,7 +30,9 @@ if (project.hasProperty("importHostCompilers")) { android { namespace 'com.jsruntimehost.unittests' compileSdk 33 - ndkVersion = "23.1.7779620" + // Bun's current WebKit headers use the C++23 standard library. Keep the existing V8 default, + // but select the same modern NDK used in CI when the JavaScriptCore engine is requested. + ndkVersion = jsEngine == "JavaScriptCore" ? "28.2.13676358" : "23.1.7779620" if (project.hasProperty("ndkVersion")) { ndkVersion = project.property("ndkVersion") } From 8b3eb46f3c4d5c3ca91ebba19ce4db750f6c3193 Mon Sep 17 00:00:00 2001 From: Matt Hargett Date: Sun, 12 Jul 2026 14:57:34 -0700 Subject: [PATCH 3/3] Address JSC review feedback --- .github/tsan_suppressions.txt | 2 +- Core/Node-API/CMakeLists.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/tsan_suppressions.txt b/.github/tsan_suppressions.txt index b17ed653..82fdb8ab 100644 --- a/.github/tsan_suppressions.txt +++ b/.github/tsan_suppressions.txt @@ -7,7 +7,7 @@ # Bun's Linux JavaScriptCore is statically linked into the final executable, so # a called_from_lib suppression cannot identify it. Match the interceptor frames # previously observed on JSC's JIT worker threads instead. -# Races manifest in TSan interceptors (free/malloc/memcpy/close) called from +# Races manifest in TSan interceptors (free/memcpy/close) called from # JSC's JIT worker threads. race:free race:close diff --git a/Core/Node-API/CMakeLists.txt b/Core/Node-API/CMakeLists.txt index 4e1bd160..e0629f0f 100644 --- a/Core/Node-API/CMakeLists.txt +++ b/Core/Node-API/CMakeLists.txt @@ -5,7 +5,7 @@ elseif(APPLE) set(NAPI_JAVASCRIPT_ENGINE "JavaScriptCore" CACHE STRING "JavaScript engine for Node-API") elseif(ANDROID) set(NAPI_JAVASCRIPT_ENGINE "V8" CACHE STRING "JavaScript engine for Node-API") -elseif(UNIX) +elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux") set(NAPI_JAVASCRIPT_ENGINE "JavaScriptCore" CACHE STRING "JavaScript engine for Node-API") else() message(FATAL_ERROR "Unable to select Node-API JavaScript engine for platform")