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..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 b1334c22..59521578 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..e0629f0f 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,285 @@ 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 "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() + + 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() + 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_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") + + 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" + "-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_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 +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 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 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..f44fcc93 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 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{}; @@ -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 defined(JSR_USE_BUN_JSC) + JSCBunContextLock contextLock{context}; +#endif napi_envs[context] = this; JSGlobalContextRetain(context); init_symbol(constructor_info_symbol, "BabylonNative_ConstructorInfo"); @@ -32,12 +78,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); } @@ -65,13 +121,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"