From b44f153b5220ce77322e3fee2797746b4f6010fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hl=C3=B6=C3=B0ver=20Sigur=C3=B0sson?= Date: Sun, 18 Oct 2026 12:22:42 +0300 Subject: [PATCH 1/2] fix(wasm): build npm plugins for the shared-memory runtime --- 7.x/cmake/WasmPlugins.cmake | 11 +++++++++- package-lock.json | 20 +++++++++--------- package.json | 7 +++---- wasm/README.md | 23 ++++++++++++--------- wasm/default.nix | 39 ++++++++++++++++++++++++++--------- wasm/libunwind/CMakeLists.txt | 2 +- wasm/tests/runtime.test.js | 12 ++++++++++- 7 files changed, 77 insertions(+), 37 deletions(-) diff --git a/7.x/cmake/WasmPlugins.cmake b/7.x/cmake/WasmPlugins.cmake index 541f6c4..2c75753 100644 --- a/7.x/cmake/WasmPlugins.cmake +++ b/7.x/cmake/WasmPlugins.cmake @@ -3,6 +3,9 @@ set(CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY) set(USE_COMPILER_OPTIMIZATIONS OFF CACHE BOOL "" FORCE) set(REQUIRE_PTHREADS OFF CACHE BOOL "" FORCE) set(USE_DOUBLE ON CACHE BOOL "" FORCE) +if(NOT WASM_MAX_MEMORY) + message(FATAL_ERROR "WASM_MAX_MEMORY must match the shared-memory Csound engine") +endif() execute_process(COMMAND ${CMAKE_C_COMPILER} --print-libgcc-file-name OUTPUT_VARIABLE WASM_BUILTINS OUTPUT_STRIP_TRAILING_WHITESPACE COMMAND_ERROR_IS_FATAL ANY) @@ -13,7 +16,8 @@ function(configure_wasm_plugin target) target_compile_definitions(${target} PRIVATE USE_DOUBLE=1 _WASI_EMULATED_SIGNAL _WASI_EMULATED_MMAN) target_compile_options(${target} PRIVATE - -fPIC "SHELL:-mllvm -wasm-enable-sjlj" + -fPIC -pthread "SHELL:-mllvm -wasm-enable-sjlj" + "SHELL:-mllvm -wasm-use-legacy-eh=false" "$<$:-std=gnu11>" "$<$:-nostdinc++>" "$<$:-isystem${WASM_CXX_INCLUDE_DIR}>") @@ -29,7 +33,12 @@ function(configure_wasm_plugin target) "LINKER:--no-entry" "LINKER:--import-table" "LINKER:--import-memory" + "LINKER:--shared-memory" + "LINKER:--max-memory=${WASM_MAX_MEMORY}" "LINKER:--allow-undefined" + "LINKER:--export=__wasm_init_tls" + "LINKER:--export=__tls_size" + "LINKER:--export=__tls_align" "LINKER:--export=__wasm_call_ctors" "LINKER:--export-if-defined=__wasm_apply_data_relocs" "LINKER:--export-if-defined=csound_opcode_init" diff --git a/package-lock.json b/package-lock.json index 1f9e10f..0948b95 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,16 +1,16 @@ { "name": "@csound/plugins", - "version": "7.0.0-beta.0", + "version": "7.0.0-beta.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@csound/plugins", - "version": "7.0.0-beta.0", + "version": "7.0.0-beta.1", "license": "LGPL-2.1-or-later", "devDependencies": { - "@csound/browser": "7.0.0-beta39", - "@csound/wasm-bin": "7.0.0-beta29" + "@csound/browser": "7.0.0-beta41", + "@csound/wasm-bin": "7.0.0-beta31" }, "engines": { "node": ">=22.13.0" @@ -27,9 +27,9 @@ } }, "node_modules/@csound/browser": { - "version": "7.0.0-beta39", - "resolved": "https://registry.npmjs.org/@csound/browser/-/browser-7.0.0-beta39.tgz", - "integrity": "sha512-/POVGpLfRrQ3kVjCPBp5bTnfYgMUQRCUHtrAilwSjrlRMQbQvQw3qjtcNbwTAxdY6uhppbpy7GbUAU2kl+OHug==", + "version": "7.0.0-beta41", + "resolved": "https://registry.npmjs.org/@csound/browser/-/browser-7.0.0-beta41.tgz", + "integrity": "sha512-FKZ9v3HGtqksZkM+Oc7C1mUfvCk0y+PTL/iXYiCGDYj6gHVjoms34z9wc/BqYGDM+gzuAnimV65SlSb1mEqrZg==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -49,9 +49,9 @@ } }, "node_modules/@csound/wasm-bin": { - "version": "7.0.0-beta29", - "resolved": "https://registry.npmjs.org/@csound/wasm-bin/-/wasm-bin-7.0.0-beta29.tgz", - "integrity": "sha512-OfrrLaS+h6Br7S9XaJWK9tBxj1JgPRxEySYx2XyZjgAnt+UIn9Dm8H2qBSJUJgBWIT2KBmFCyw4fcdcwtGGyOg==", + "version": "7.0.0-beta31", + "resolved": "https://registry.npmjs.org/@csound/wasm-bin/-/wasm-bin-7.0.0-beta31.tgz", + "integrity": "sha512-CaFhe9LZ/KwPQKTnVc8L5EJXaTgGCX/1/xepLlRETIv5kdbSEnhyWcrGxltxg2EPhgDK8gmvtN85cTCq21FxrQ==", "dev": true, "license": "LGPL-2.1" }, diff --git a/package.json b/package.json index 5242717..2df63e8 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,6 @@ { "name": "@csound/plugins", - "version": "7.0.0-beta.0", - "private": true, + "version": "7.0.0-beta.1", "description": "Csound 7 opcode plugins for WebAssembly", "type": "module", "license": "LGPL-2.1-or-later", @@ -41,8 +40,8 @@ "prepack": "npm test" }, "devDependencies": { - "@csound/browser": "7.0.0-beta39", - "@csound/wasm-bin": "7.0.0-beta29" + "@csound/browser": "7.0.0-beta41", + "@csound/wasm-bin": "7.0.0-beta31" }, "keywords": [ "csound", diff --git a/wasm/README.md b/wasm/README.md index 5a361cf..a2039f2 100644 --- a/wasm/README.md +++ b/wasm/README.md @@ -8,9 +8,11 @@ Csound 6 is not supported. ## Runtime dependency -Builds use the plugin SDK from `@csound/wasm-bin`, and tests use the published -`@csound/browser` runtime. [package.json](../package.json) and +Builds use the shared-memory plugin SDK and toolchain from `@csound/wasm-bin`. +Tests use the published `@csound/browser` runtime. [package.json](../package.json) and [package-lock.json](../package-lock.json) record the versions used together. +Rebuild after updating these dependencies. Plugins built for the old, +non-shared engine cannot load in the shared-memory runtime. ## Use @@ -62,8 +64,10 @@ npm pack ``` Nix pins the toolchain and builds the existing `7.x` CMake targets with Ninja. -Dependencies use static PIC builds. The C runtime imports the engine's allocator; -it does not link its own heap. STK uses a separate libc++ build with C++ exceptions +Dependencies use threaded static PIC builds. Each plugin exports its TLS size, +alignment, and initializer so the engine can set up thread-local storage. +The C runtime imports the engine's allocator; it does not link its own heap. +STK uses a separate libc++ build with C++ exceptions and a static WASM unwinder. Other C++ plugins keep exceptions disabled. CMake's `STK_EMBED_RAWWAVES` option embeds the samples and defaults to on for WASM. Native builds keep using the installed raw-wave directory by default. @@ -93,10 +97,9 @@ uses the `csound` Cachix cache and uploads builds when `CACHIX_AUTH_TOKEN` is available. Fork PRs read from the cache without the secret. Native builds use GitHub Actions caches. -## First npm release +## npm releases -Run the build, test, and pack commands against the published browser package -with `CSOUND_BROWSER_URL` unset. When ready to publish, remove `private` from -`package.json`, set the package version with `npm version`, and run -`npm publish --tag beta --access public` from a clean build. No workflow publishes -this package yet. +Set the next package version with `npm version`, then run the build, test, and +pack commands against the published browser package with `CSOUND_BROWSER_URL` +unset. Publish with `npm publish --tag beta --access public` from that build. +No workflow publishes this package yet. diff --git a/wasm/default.nix b/wasm/default.nix index 9b7353a..fa6ac34 100644 --- a/wasm/default.nix +++ b/wasm/default.nix @@ -3,7 +3,16 @@ , csoundSdkArchive ? null }: let - wasi = pkgs.pkgsCross.wasi32.clangStdenv; + lock = builtins.fromJSON (builtins.readFile ../package-lock.json); + wasmBin = lock.packages."node_modules/@csound/wasm-bin"; + wasmPackage = pkgs.runCommand "csound-wasm-bin-${wasmBin.version}" {} '' + mkdir -p $out + tar -xzf ${pkgs.fetchurl { url = wasmBin.resolved; hash = wasmBin.integrity; }} --strip-components=1 -C $out + ''; + # Use the same threaded WASI toolchain and memory limit as the engine. + toolchain = import "${wasmPackage}/src/wasi-realtime-toolchain.nix" { inherit pkgs; }; + maxMemory = builtins.fromJSON (builtins.readFile "${wasmPackage}/src/shared-memory-max.txt"); + wasi = toolchain.stdenv; # Nix's WASI wrapper otherwise forces -fno-exceptions after CMake's flags. # Let each target select its exception mode instead. wasiCxx = pkgs.overrideCC wasi (wasi.cc.override (old: { @@ -11,15 +20,17 @@ let cc-cflags = builtins.filter (flag: flag != "-fno-exceptions") old.nixSupport.cc-cflags; }; })); - libc = pkgs.pkgsCross.wasi32.wasilibc.overrideAttrs (old: { - NIX_CFLAGS_COMPILE = (old.NIX_CFLAGS_COMPILE or []) ++ [ "-fPIC" ]; + libc = toolchain.libc.overrideAttrs (old: { + NIX_CFLAGS_COMPILE = (old.NIX_CFLAGS_COMPILE or []) ++ [ + "-fPIC" "-mllvm" "-wasm-enable-sjlj" "-mllvm" "-wasm-use-legacy-eh=false" + ]; # Use the engine's allocator. wasi-libc also skips its fixed symbol list # for this mode; PIC adds imports absent from that non-PIC list. makeFlags = (old.makeFlags or []) ++ [ "MALLOC_IMPL=none" ]; preFixup = ""; dontStrip = true; }); - libcxx = pkgs.pkgsCross.wasi32.llvmPackages.libcxx.overrideAttrs (old: { + libcxx = toolchain.libcxx.overrideAttrs (old: { cmakeFlags = old.cmakeFlags ++ [ "-DCMAKE_POSITION_INDEPENDENT_CODE=ON" "-DLIBCXX_ENABLE_FILESYSTEM=ON" @@ -43,11 +54,9 @@ let "-DLIBCXXABI_ENABLE_EXCEPTIONS=ON" ]; preConfigure = (old.preConfigure or "") + '' - cmakeFlagsArray+=("-DCMAKE_CXX_FLAGS=-fexceptions -fwasm-exceptions -I${libunwindSource}/libunwind/include") + cmakeFlagsArray+=("-DCMAKE_CXX_FLAGS=-fexceptions -fwasm-exceptions -mllvm -wasm-use-legacy-eh=false -I${libunwindSource}/libunwind/include") ''; }); - lock = builtins.fromJSON (builtins.readFile ../package-lock.json); - wasmBin = lock.packages."node_modules/@csound/wasm-bin"; sdkArchive = if csoundSdkArchive == null then null else builtins.path { path = csoundSdkArchive; name = "csound-plugin-sdk.tar.gz"; @@ -55,8 +64,7 @@ let sdk = pkgs.runCommand "csound-wasm-plugin-sdk-${wasmBin.version}" {} '' mkdir -p $out ${if csoundSdkArchive == null then '' - tar -xzf ${pkgs.fetchurl { url = wasmBin.resolved; hash = wasmBin.integrity; }} - tar -xzf package/lib/csound-plugin-sdk.tar.gz --strip-components=1 -C $out + tar -xzf ${wasmPackage}/lib/csound-plugin-sdk.tar.gz --strip-components=1 -C $out '' else '' tar -xzf ${sdkArchive} --strip-components=1 -C $out ''} @@ -66,6 +74,16 @@ let inherit (pkgs.zlib) version src; nativeBuildInputs = [ pkgs.cmake pkgs.ninja ]; cmakeFlags = [ "-DCMAKE_POSITION_INDEPENDENT_CODE=ON" "-DCMAKE_INSTALL_LIBDIR=lib" "-DZLIB_BUILD_EXAMPLES=OFF" ]; + # zlib 1.3.1 has no CMake switch to omit its shared library. Build only + # the static target so it does not link the toolchain's non-PIC libc. + ninjaFlags = [ "zlibstatic" ]; + installPhase = '' + runHook preInstall + install -Dm644 libzlibstatic.a $out/lib/libzlibstatic.a + install -Dm644 ../zlib.h $out/include/zlib.h + install -Dm644 zconf.h $out/include/zconf.h + runHook postInstall + ''; dontStrip = true; }; libpng = wasi.mkDerivation { @@ -80,7 +98,7 @@ let "-DPNG_SHARED=OFF" "-DPNG_STATIC=ON" "-DPNG_TESTS=OFF" "-DPNG_TOOLS=OFF" "-DZLIB_LIBRARY=${zlib}/lib/libzlibstatic.a" ]; - NIX_CFLAGS_COMPILE = [ "-mllvm" "-wasm-enable-sjlj" ]; + NIX_CFLAGS_COMPILE = [ "-mllvm" "-wasm-enable-sjlj" "-mllvm" "-wasm-use-legacy-eh=false" ]; dontStrip = true; }; in wasiCxx.mkDerivation { @@ -94,6 +112,7 @@ in wasiCxx.mkDerivation { "-DCMAKE_POLICY_VERSION_MINIMUM=3.5" "-DCMAKE_BUILD_TYPE=Release" "-DCSOUND_INCLUDE_DIR=${sdk}/include/csound" + "-DWASM_MAX_MEMORY=${toString maxMemory}" "-DWASM_LIBC=${libc}/lib/libc.a" "-DWASM_SETJMP=${libc}/lib/libsetjmp.a" "-DWASM_LIBCXX=${libcxx}/lib/libc++.a" diff --git a/wasm/libunwind/CMakeLists.txt b/wasm/libunwind/CMakeLists.txt index 5e79ec6..c722bd2 100644 --- a/wasm/libunwind/CMakeLists.txt +++ b/wasm/libunwind/CMakeLists.txt @@ -6,7 +6,7 @@ project(CsoundWasmUnwind C) add_library(unwind STATIC "${LIBUNWIND_SOURCE_DIR}/src/Unwind-wasm.c") set_target_properties(unwind PROPERTIES POSITION_INDEPENDENT_CODE ON) target_compile_features(unwind PRIVATE c_std_11) -target_compile_options(unwind PRIVATE -fwasm-exceptions) +target_compile_options(unwind PRIVATE -fwasm-exceptions "SHELL:-mllvm -wasm-use-legacy-eh=false") target_compile_definitions(unwind PRIVATE _LIBUNWIND_HIDE_SYMBOLS) target_include_directories(unwind PRIVATE "${LIBUNWIND_SOURCE_DIR}/include") install(TARGETS unwind ARCHIVE DESTINATION lib) diff --git a/wasm/tests/runtime.test.js b/wasm/tests/runtime.test.js index b7ca0f6..3b69d97 100644 --- a/wasm/tests/runtime.test.js +++ b/wasm/tests/runtime.test.js @@ -70,7 +70,7 @@ async function run(body, check, withPlugins = binaries, prepare = () => {}) { } } -test("all plugins are WASI side modules", async () => { +test("all plugins are WASI side modules with the shared-memory TLS exports", async () => { for (const bytes of binaries) { const module = await WebAssembly.compile(bytes); const imports = WebAssembly.Module.imports(module); @@ -79,6 +79,16 @@ test("all plugins are WASI side modules", async () => { assert.ok(imports.some(({ kind, name }) => kind === "memory" && name === "memory")); assert.ok(imports.some(({ kind }) => kind === "table")); assert.ok(exports.some(({ name }) => name === "__wasm_call_ctors")); + for (const [name, kind] of [ + ["__wasm_init_tls", "function"], + ["__tls_size", "global"], + ["__tls_align", "global"], + ]) { + assert.ok( + exports.some((entry) => entry.name === name && entry.kind === kind), + `${name} must be exported as a ${kind}`, + ); + } assert.ok(exports.some(({ name }) => ["csoundModuleInit", "csound_opcode_init"].includes(name))); assert.ok(!exports.some(({ name }) => name === "_start")); } From 4387a9be3dd84a2f6e2a4e45c3c70a49775f66a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hl=C3=B6=C3=B0ver=20Sigur=C3=B0sson?= Date: Sun, 18 Oct 2026 12:37:57 +0300 Subject: [PATCH 2/2] fix(wasm): pass exception mode to C++ plugin links --- 7.x/cmake/WasmPlugins.cmake | 1 + 1 file changed, 1 insertion(+) diff --git a/7.x/cmake/WasmPlugins.cmake b/7.x/cmake/WasmPlugins.cmake index 2c75753..1d183cf 100644 --- a/7.x/cmake/WasmPlugins.cmake +++ b/7.x/cmake/WasmPlugins.cmake @@ -23,6 +23,7 @@ function(configure_wasm_plugin target) "$<$:-isystem${WASM_CXX_INCLUDE_DIR}>") if(WASM_CXX_EXCEPTIONS) target_compile_options(${target} PRIVATE "$<$:-fwasm-exceptions>") + target_link_options(${target} PRIVATE "$<$:-fwasm-exceptions>") else() target_compile_options(${target} PRIVATE "$<$:-fno-exceptions>") endif()