diff --git a/.github/tsan_suppressions.txt b/.github/tsan_suppressions.txt index eebd4661..82fdb8ab 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. -# 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 +# 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/memcpy/close) called from +# 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..961503bb 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 @@ -23,11 +23,20 @@ on: required: false type: boolean default: false + bun-jsc-lto: + # Link Bun's ThinLTO JavaScriptCore archives. Needs the LLVM 21 toolchain end-to-end + # (clang-21, lld-21, llvm-ar/ranlib-21) so bitcode archives are indexed and LTO-linked by + # the same LLVM that produced Bun's bitcode (21.1.x). + required: false + type: boolean + default: false 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 +46,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 ${{ (inputs.bun-jsc-lto || inputs.enable-thread-sanitizer) && 'llvm-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: | @@ -47,16 +71,46 @@ jobs: -D ENABLE_SANITIZERS=${{ inputs.enable-sanitizers && 'ON' || 'OFF' }} \ -D ENABLE_THREAD_SANITIZER=${{ inputs.enable-thread-sanitizer && 'ON' || 'OFF' }} \ -D CMAKE_C_COMPILER=${{ inputs.cc }} \ - -D CMAKE_CXX_COMPILER=${{ inputs.cxx }} + -D CMAKE_CXX_COMPILER=${{ inputs.cxx }} \ + -D JSR_BUN_JSC_LTO=${{ inputs.bun-jsc-lto && 'ON' || 'OFF' }} \ + ${{ inputs.bun-jsc-lto && '-D CMAKE_AR=/usr/bin/llvm-ar-21 -D CMAKE_RANLIB=/usr/bin/llvm-ranlib-21 -D CMAKE_NM=/usr/bin/llvm-nm-21' || '' }} - 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 env: - TSAN_OPTIONS: ${{ inputs.enable-thread-sanitizer && format('suppressions={0}/.github/tsan_suppressions.txt', github.workspace) || '' }} + # llvm-21 ships the symbolizer as llvm-symbolizer-21 only; without the explicit path TSan + # prints bare addresses for every frame, which makes a report undiagnosable. + TSAN_OPTIONS: ${{ inputs.enable-thread-sanitizer && format('suppressions={0}/.github/tsan_suppressions.txt:external_symbolizer_path=/usr/bin/llvm-symbolizer-21', github.workspace) || '' }} # JSC's concurrent GC on Linux uses SIGUSR1 + sem_wait to suspend mutator # threads at safepoints. TSan's signal interception delays SIGUSR1 delivery # indefinitely, deadlocking the Collector Thread's sem_wait. Disabling the @@ -64,4 +118,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..bc4f0f1a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -128,30 +128,24 @@ jobs: simulator: 'iPhone 17' # Linux - Ubuntu_gcc: - uses: ./.github/workflows/build-linux.yml - - Ubuntu_clang: + Ubuntu_JSC_LTO: uses: ./.github/workflows/build-linux.yml with: - cc: clang - cxx: clang++ + bun-jsc-lto: true - 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 f2483f41..23c3d317 100644 --- a/Core/AppRuntime/Source/AppRuntime_JavaScriptCore.cpp +++ b/Core/AppRuntime/Source/AppRuntime_JavaScriptCore.cpp @@ -1,10 +1,17 @@ #include "AppRuntime.h" #include +#if defined(JSR_USE_BUN_JSC) +extern "C" void JSCBunInitialize(); +#endif + namespace Babylon { void AppRuntime::RunEnvironmentTier(const char*) { +#if defined(JSR_USE_BUN_JSC) + JSCBunInitialize(); +#endif auto globalContext = JSGlobalContextCreateInGroup(nullptr, nullptr); #if __APPLE__ @@ -18,7 +25,17 @@ namespace Babylon Run(env); - JSGlobalContextRelease(globalContext); +#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 // 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 5f495695..02ecf7c9 100644 --- a/Core/Node-API/CMakeLists.txt +++ b/Core/Node-API/CMakeLists.txt @@ -5,9 +5,8 @@ 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") - 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,30 +66,385 @@ if(NAPI_BUILD_ABI) "Source/js_native_api_javascriptcore.cc" "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}) + 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 "1bd03678132783e3bb98a48205f4e8439253cf90") + 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() + + 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 now also publishes Android ThinLTO archives (bun-webkit-linux-*-android-lto), but + # the NDK toolchain is not LLVM 21, so cross-module LTO with them is not possible here; + # 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 "2e082e6fdad7441c167a5079764a4c989bde73b114115125be10bc821b62d5a9") + elseif(ANDROID_ABI STREQUAL "x86_64") + set(JSR_BUN_JSC_ASSET "bun-webkit-linux-amd64-android.tar.gz") + set(JSR_BUN_JSC_SHA256 "b1bb4bc6c489af5409466a4d31d89550c197baae800c9a2ab42d79158201135f") + 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() + 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() + + # Bun publishes two flavours of its Linux JavaScriptCore: plain native archives, and + # ThinLTO bitcode archives (module summaries, EnableSplitLTOUnit=0) produced by LLVM 21. + # The bitcode flavour lets the final link optimize across JSC and this runtime, but it + # couples the whole build to one LLVM: the same major must compile the bitcode we add + # to the LTO unit, index the archives (llvm-ar, not binutils ar), and run the LTO link + # (lld). Default to the plain archives so a stock toolchain works; opt in to the + # cross-module LTO build with -D JSR_BUN_JSC_LTO=ON on an LLVM 21 toolchain. + option(JSR_BUN_JSC_LTO + "Link Bun's ThinLTO JavaScriptCore archives (requires clang, lld and llvm-ar/ranlib from LLVM 21)" OFF) + if(NOT CMAKE_CXX_COMPILER_ID STREQUAL "Clang") + message(FATAL_ERROR + "Bun's Linux JavaScriptCore archives are built against Clang/libstdc++; build with Clang") + endif() + if(JSR_BUN_JSC_LTO AND NOT CMAKE_CXX_COMPILER_VERSION MATCHES "^21\\.") + message(FATAL_ERROR + "JSR_BUN_JSC_LTO needs Clang 21 (Bun's bitcode is LLVM 21); found ${CMAKE_CXX_COMPILER_VERSION}") + 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(JSR_BUN_JSC_LTO) + if(NOT JSR_BUN_JSC_LLD_VERSION MATCHES "LLD 21\\.") + message(FATAL_ERROR + "JSR_BUN_JSC_LTO needs LLD 21 to LTO-link Bun's LLVM 21 bitcode; found '${JSR_BUN_JSC_LLD_VERSION}'") + endif() + # Static archives of bitcode need an LLVM-aware archiver for their symbol index; + # binutils ar (with or without a mismatched LLVMgold plugin) cannot read LLVM 21 + # bitcode and leaves the index empty, which the LTO link then fails on. + execute_process( + COMMAND "${CMAKE_AR}" --version + OUTPUT_VARIABLE JSR_BUN_JSC_AR_VERSION + ERROR_QUIET + OUTPUT_STRIP_TRAILING_WHITESPACE) + if(NOT JSR_BUN_JSC_AR_VERSION MATCHES "LLVM version 21\\.") + message(FATAL_ERROR + "JSR_BUN_JSC_LTO needs llvm-ar/llvm-ranlib from LLVM 21 as the archiver: configure with " + "-D CMAKE_AR=/usr/bin/llvm-ar-21 -D CMAKE_RANLIB=/usr/bin/llvm-ranlib-21 -D CMAKE_NM=/usr/bin/llvm-nm-21 " + "(CMAKE_AR is '${CMAKE_AR}': '${JSR_BUN_JSC_AR_VERSION}')") + endif() + 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. + if(JSR_BUN_JSC_LTO) + set(JSR_BUN_JSC_ASSET "bun-webkit-linux-amd64-lto.tar.gz") + set(JSR_BUN_JSC_SHA256 "a2d781473f54e674d4aff44528a0c6177d48d091f85a4d50726cb841eba9e490") + else() + set(JSR_BUN_JSC_ASSET "bun-webkit-linux-amd64.tar.gz") + set(JSR_BUN_JSC_SHA256 "dd3d3a304a1227fc8ffea071a2f2894ccb53fcc497a4d500e46b6d21554458c2") + endif() + elseif(JSR_BUN_JSC_PROCESSOR MATCHES "^(aarch64|arm64)$") + if(JSR_BUN_JSC_LTO) + set(JSR_BUN_JSC_ASSET "bun-webkit-linux-arm64-lto.tar.gz") + set(JSR_BUN_JSC_SHA256 "3f54821a771f2507586e05357f31d8e70f07181dc87f5e444d858a0bf0018471") + else() + set(JSR_BUN_JSC_ASSET "bun-webkit-linux-arm64.tar.gz") + set(JSR_BUN_JSC_SHA256 "3a5bb3a25ffd8bf8a45e961580cb158c17bcf03408c6c54ccfd5b12b98c6a5f9") + endif() + 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_BUN_JSC_ROOT) + set(BUN_JSC_DIR "${JSR_BUN_JSC_ROOT}") + else() + 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_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() + endif() - # 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") + 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") + + # Bun's JavaScriptCore builds (Linux and Android) allocate through Bun's fork of mimalloc (the + + # mi_theap_* thread-heap entry points exist only there) and the archives do not + + # bundle it, so the host has to provide it. Build the fork's static library with the + + # standard allocator untouched (MI_OVERRIDE=OFF): JavaScriptCore calls the mi_* API + + # explicitly, and nothing else in the process should change allocator. + + include(FetchContent) + + FetchContent_Declare(bun_mimalloc + + GIT_REPOSITORY https://github.com/oven-sh/mimalloc.git + + GIT_TAG 58460467413a27ca426951e9e06c5a222be9553c + + EXCLUDE_FROM_ALL) + + set(MI_OVERRIDE OFF CACHE BOOL "" FORCE) + + set(MI_BUILD_SHARED OFF CACHE BOOL "" FORCE) + + set(MI_BUILD_STATIC ON CACHE BOOL "" FORCE) + + set(MI_BUILD_OBJECT OFF CACHE BOOL "" FORCE) + + set(MI_BUILD_TESTS OFF CACHE BOOL "" FORCE) + + set(MI_INSTALL_TOPLEVEL OFF CACHE BOOL "" FORCE) + + # ThreadSanitizer instruments mimalloc through the global compile options, but its + # lock-free paths read a few fields racily on purpose; MI_TSAN switches those to + # atomics so an instrumented build only reports real races. + set(MI_TSAN ${ENABLE_THREAD_SANITIZER} CACHE BOOL "" FORCE) + + FetchContent_MakeAvailable(bun_mimalloc) + + set_property(TARGET mimalloc-static PROPERTY POSITION_INDEPENDENT_CODE ON) + + set_property(TARGET mimalloc-static PROPERTY FOLDER Dependencies) + + + 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) + target_compile_definitions(bun_jsc PRIVATE + JSR_BUN_JSC_WEBKIT_REVISION="${JSR_BUN_JSC_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(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" + mimalloc-static + "-Wl,--end-group" + log + ${CMAKE_DL_LIBS} + m) + else() + + set_property(TARGET bun_jsc PROPERTY INTERFACE_POSITION_INDEPENDENT_CODE ON) + if(JSR_BUN_JSC_LTO) + # Bun's bitcode archives are ThinLTO (module summaries, EnableSplitLTOUnit=0), so + # the bitcode this runtime contributes must be ThinLTO too: clang emits -flto-unit + # (splitting ON) for every -flto=full translation unit regardless of + # -fno-split-lto-unit, and lld rejects a link whose modules disagree + # ("inconsistent LTO Unit splitting"). No -fwhole-program-vtables for the same reason. + set(JSR_BUN_JSC_LTO_FLAG -flto=thin) + target_compile_options(bun_jsc PRIVATE + -O2 + ${JSR_BUN_JSC_LTO_FLAG} + -fno-sanitize=all) + target_link_options(bun_jsc INTERFACE + ${JSR_BUN_JSC_LTO_FLAG} + -O2 + -pie + "--ld-path=${JSR_BUN_JSC_LLD}" + "-Wl,--gc-sections") + else() + target_compile_options(bun_jsc PRIVATE + -O2 + -fno-sanitize=all) + target_link_options(bun_jsc INTERFACE + -O2 + -pie + "--ld-path=${JSR_BUN_JSC_LLD}" + "-Wl,--gc-sections") + endif() + 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" + mimalloc-static + "-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_DIR}/jni/${ANDROID_ABI}/libjsc.so") + 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() @@ -266,6 +620,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 64184b36..9c1584f6 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 defined(JSR_USE_BUN_JSC) +extern "C" void* JSCBunAcquireContextLock(JSGlobalContextRef context); +extern "C" void JSCBunReleaseContextLock(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 defined(JSR_USE_BUN_JSC) + class ContextLock final + { + public: + explicit ContextLock(Napi::Env env) + : m_opaqueLock{JSCBunAcquireContextLock(GetContext(env))} + { + } + + ~ContextLock() + { + JSCBunReleaseContextLock(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 5c8583bc..9e209a3d 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 @@ -1907,6 +1910,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 8d8dbd02..eb46735d 100644 --- a/Core/Node-API/Source/js_native_api_javascriptcore.h +++ b/Core/Node-API/Source/js_native_api_javascriptcore.h @@ -9,12 +9,55 @@ #include #include +#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 JSCBunContextLock final { + public: + explicit JSCBunContextLock(JSGlobalContextRef context) + : lock{JSCBunAcquireContextLock(context)} {} + + ~JSCBunContextLock() { + JSCBunReleaseContextLock(lock); + } + + JSCBunContextLock(const JSCBunContextLock&) = delete; + JSCBunContextLock& operator=(const JSCBunContextLock&) = delete; + + private: + void* lock{}; +}; + +class JSCBunAPILock final { + public: + explicit JSCBunAPILock(JSGlobalContextRef context, bool acquire = true) + : context{context}, locked{acquire && JSCBunLockContext(context)} {} + + ~JSCBunAPILock() { + if (locked) { + JSCBunUnlockContext(context); + } + } + + JSCBunAPILock(const JSCBunAPILock&) = delete; + JSCBunAPILock& operator=(const JSCBunAPILock&) = 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{}; @@ -32,6 +75,9 @@ struct napi_env__ { const std::thread::id thread_id{std::this_thread::get_id()}; napi_env__(JSGlobalContextRef context) : context{context} { +#if defined(JSR_USE_BUN_JSC) + JSCBunContextLock contextLock{context}; +#endif napi_envs[context] = this; JSGlobalContextRetain(context); init_symbol(constructor_info_symbol, "BabylonNative_ConstructorInfo"); @@ -41,12 +87,22 @@ struct napi_env__ { } ~napi_env__() { - 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) + { + // 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 napi_envs.erase(context); } @@ -74,13 +130,25 @@ 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 defined(JSR_USE_BUN_JSC) +#define CHECK_ENV(env) \ + do { \ + if ((env) == nullptr) { \ + return napi_invalid_arg; \ + } \ + } while (0); \ + /* 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) \ + 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_bun.cpp b/Core/Node-API/Source/jsc_bun.cpp new file mode 100644 index 00000000..b0c366d3 --- /dev/null +++ b/Core/Node-API/Source/jsc_bun.cpp @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#ifndef JSR_BUN_JSC_WEBKIT_REVISION +#error JSR_BUN_JSC_WEBKIT_REVISION must identify the linked Bun WebKit build +#endif + +#include +#include +#include +#include + +extern "C" void JSCBunInitialize() +{ + // 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* 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 + // 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 JSCBunReleaseContextLock(void* opaqueLock) +{ + delete static_cast(opaqueLock); +} + +extern "C" bool JSCBunLockContext(JSGlobalContextRef context) +{ + auto* vm = toJS(JSContextGetGroup(context)); + if (vm->currentThreadIsHoldingAPILock()) + { + return false; + } + + vm->apiLock().lock(); + return true; +} + +extern "C" void JSCBunUnlockContext(JSGlobalContextRef context) +{ + toJS(JSContextGetGroup(context))->apiLock().unlock(); +} + +extern "C" const char* JSCBunGetWebKitRevision() +{ + return JSR_BUN_JSC_WEBKIT_REVISION; +} diff --git a/Core/Node-API/Source/jsc_bun.exports b/Core/Node-API/Source/jsc_bun.exports new file mode 100644 index 00000000..15968d28 --- /dev/null +++ b/Core/Node-API/Source/jsc_bun.exports @@ -0,0 +1,8 @@ +{ + global: + JS*; + JSCBun*; + 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..3cd00d00 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_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 // bootstraps these host compilers automatically when cross-compiling, so no @@ -25,14 +30,16 @@ 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") } defaultConfig { applicationId "com.jsruntimehost.unittests" - minSdk 21 + minSdk appMinSdk targetSdk 33 versionCode 1 versionName "1.0"