feat(bb.js): generated bb.js-api package with an in-process wasm transport - #25433
Open
charlielye wants to merge 25 commits into
Open
feat(bb.js): generated bb.js-api package with an in-process wasm transport#25433charlielye wants to merge 25 commits into
charlielye wants to merge 25 commits into
Conversation
…sport ipc-runtime gains a generic wasm FFI backend (@aztec-foundation/ipc-runtime/wasm) for any wasi reactor implementing the ipc-codegen FFI contract; ipc-codegen emits the FFI entry itself for --server --ffi (C++ and Rust) and a wasm transport for generated TS packages; bb gets a Warmup command and the generated bb_ffi.cpp replaces the hand-written entry; bb.js consumes the generated @aztec-foundation/bb.js-api package and drops its own wasm plumbing. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0136VgtizjT6krfyRB6TeqKG
The package shell imports its bindings from ./generated, so a --package invocation only ever had one valid --out. Default it to <package>/src/generated, reject a conflicting value, and drop the redundant line from every bootstrap. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0136VgtizjT6krfyRB6TeqKG
bb_ipc_ffi_entry/_alloc/_free instead of one fixed name, so libraries of several services can be statically linked into one binary. The Rust and Zig client ffi_backend files are generated (link_name) instead of copied templates, the TS package passes the names to the wasm host, and the host discovers the single *_ipc_ffi_entry export when not told. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0136VgtizjT6krfyRB6TeqKG
…ps its bundled bb The client package now owns how the service is reached. <Svc>Service.create picks the spawned process when the binary resolves and the wasm module otherwise; `backend:` forces one (no fallback, and more wasm threads than the host can give is an error rather than a silent downgrade) or takes a backend object of the consumer's own; createBackend/createBackendSync expose the same policy to facades. One entry per host (node, browser, react-native), each with its own types, offers only the backends that exist there; the react-native entry has none built in and takes a registered or passed backend. A spawned process receives `threads` as HARDWARE_CONCURRENCY and RAYON_NUM_THREADS. bb.js maps its BackendType options onto that policy, resolves bb through bb.js-api's per-platform packages (the binary's one published home) and keeps bundling only the LMDB NAPI module. --client is dropped from the package bootstraps, which imply it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0136VgtizjT6krfyRB6TeqKG
…age has A package without the wasm transport was told it had a browser backend and got a dangling clause in the synchronous-form sentence. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0136VgtizjT6krfyRB6TeqKG
cross_copy_bb_js now stages bb.js-api's binaries too, so it installs and builds in barretenberg/ts like bb-avm-sim-cross-copy does; the two had no ordering between them. bb.js's build also ran the API package's generate-and-compile twice. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0136VgtizjT6krfyRB6TeqKG
A worker runs a thread to completion inside wasi_thread_start, so round-robin over a fixed pool of threads-1 workers only worked because bb spawns exactly that many: any module spawning more would have queued two threads on one worker, and the second would never run. Create a worker when the module asks for a thread and drop it when the thread exits, so the workers follow the module's own pool and a module that never spawns costs nothing. The thread worker now serializes init against start rather than relying on the parent to await readiness first. Adds engine tests over a hand-built module: on-demand creation, the single- threaded refusal, following the module's shared-memory declaration, and narrowing an over-large memory maximum. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0136VgtizjT6krfyRB6TeqKG
…call Each call allocated a request buffer and a slot pair in the module, freed both, and built three JS views over module memory. Hold one buffer that grows to fit and cache the views, rebuilding them only when a call grows memory. The cheapest call bb offers (blake2s of one byte) goes 1.455 -> 1.254 us; a call doing real work is unchanged, since the saving is fixed per call. The response still comes back in a buffer the module allocated, as the FFI contract says: measured, that malloc is a small part of the fixed cost, and taking it out would change the contract for every language. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0136VgtizjT6krfyRB6TeqKG
… override The node and browser entries were near-identical: fourteen re-exports and three factory functions each, differing only in the platform they bind and how they spawn a worker. They now share entry.ts and are twenty lines apiece. The allocator pair was both discovered from the entry's own name and passable as an option, threaded through five files to reach the instance. Only the discovery is kept; `entry` stays, since a binary linking two services is the reason its name is service-prefixed at all. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0136VgtizjT6krfyRB6TeqKG
The platform-specific files were distinguished by a suffix and interleaved with the shared ones. Each host now has a directory holding the same four file names, so the split is visible in the tree rather than read off filenames. Each host also gets its own export subpath with its own types, so code already committed to one host imports it by name. That removes the shim by which the node worker handle also accepted a web Worker, which existed only because the `types` condition resolved to node for everyone. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0136VgtizjT6krfyRB6TeqKG
The choice was already the caller's — a worker by default, the calling thread with `worker: false`, and always the calling thread for the synchronous backend — but nothing said so. Name it on the option and give the generated README a section, for packages that have a wasm backend. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0136VgtizjT6krfyRB6TeqKG
A module either imports its memory or defines and exports one. bb imports, so the host always used the memory it had made itself; a Rust cdylib exports its own, and the host would have read and written a memory the module never touches. Adopt the exported one after instantiation, and refuse to thread such a module, since every instance would have a memory to itself. Found by giving the echo example a wasm reactor: the Rust crate already emits the FFI entry, so a cdylib on wasm32-wasip1 is the whole build. The TS package now offers uds, shm and wasm from that one crate, and its test drives the reactor worker-hosted, on the calling thread, and synchronously. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0136VgtizjT6krfyRB6TeqKG
The package resolved a default thread count from the host's parallelism without the cap the engine then applies, so on a large machine it reported 160 threads for a run that used 32. Export the cap from the runtime and use it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0136VgtizjT6krfyRB6TeqKG
Only a real application/wasm response reaches WebAssembly.compileStreaming as a network response, which is the precondition for a browser caching the compiled code between visits; a precompressed module has to be inflated into a synthetic response first, and forfeits that. npm tarballs are gzipped either way, so this costs nothing on install, and on a host that compresses the wire size is the same or better. It does assume the host compresses: most CDNs compress application/wasm, nginx does not unless it is added to gzip_types. wasmPath (or BB_WASM_PATH) takes a gzipped copy for hosts that cannot, so the choice stays the consumer's. With base64 embedding already gone, nothing now inlines a module into a bundle: both are assets, and only the one chosen is fetched. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0136VgtizjT6krfyRB6TeqKG
…ntime Two generated packages differed by one or two lines per file across six files: binary resolution, process spawning, thread resolution, module selection and the backend-selection policy were the same code with different literals, regenerated per package. A fix to any of them would have had to reach every package by regeneration; today's two bugs were both in the runtime, where one fix covers everyone. ipc-runtime gains findServiceBinary, spawnServiceBackend(Sync), runServiceBinary, serviceProcessEnv and pickServiceBackend over a ServiceBinary description, and resolveWasmThreads/chooseWasmModule in the wasm entry. The package now supplies what only it can: that description, the module and worker URLs, which are relative to its own files and must be literals a bundler can see, and the typed service classes. platform.ts 68 -> 29, process.ts 100 -> 37, bin.ts 19 -> 5, and the selection policy loses the `!true` an interpolated flag had left in it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0136VgtizjT6krfyRB6TeqKG
…ging out Two more things were generated that had no reason to be. The React Native backend registry is a global-symbol map keyed by service name, identical in every package: it is now ipc-runtime's, reachable through a host-neutral ./registry subpath so a React Native entry does not pull in the node transports or the wasm engine to find it. And prepare_arch_packages.sh was 63 lines of shell per package that differed only in two names, both of which it can read from the package's own package.json; it is now an ipc-runtime bin. react-native.ts 84 -> 54, and scripts/prepare_arch_packages.sh is gone. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0136VgtizjT6krfyRB6TeqKG
A schema-driven IPC generator had a --curve-constants flag that read a JSON of BN254, Grumpkin and secp moduli and generators and emitted them as TypeScript. That is one service's domain knowledge sitting in a tool that otherwise knows nothing about any service. The flag, its JSON and the generated module are gone. bb.js now carries them as source, written as hex so they read as the constants they are, with a test asserting they match `bb msgpack curve_constants` — which computes the same values from the curve definitions bb is compiled against. The old JSON was hand-maintained with nothing checking it; it was correct, but by luck rather than by construction. Also makes the arch-package staging script Node rather than shell: yarn shims a bin through node, so a .sh bin failed with ERR_UNKNOWN_FILE_EXTENSION and the staging step silently did nothing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0136VgtizjT6krfyRB6TeqKG
Generation only ever wrote files, so a change to what it emits left the old output behind: dropping --curve-constants left curve_constants.ts in every checkout's src/generated, still compiled and still exported. CI runners reuse working directories, so this is not only a local annoyance. Each run now records what it produced and deletes anything the previous run produced that this one did not. Generated packages also compile from a clean dest, since tsc would otherwise keep the output of a source that is gone. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0136VgtizjT6krfyRB6TeqKG
IMsgpackBackend, IMsgpackBackendSync and IMsgpackBackendAsync were a second set of names for ipc-runtime's IpcClientAsync and IpcClientSync, which is what the backends have been for a while; they were never exported, so the three internal users now name the runtime types directly. asyncMap and writeBenchmark have no callers anywhere and are not part of the package's exports, so nothing could reach them. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0136VgtizjT6krfyRB6TeqKG
The comment said bb monitors parent death and so must be unref'd. bb does not: ipc-runtime's C++ installs the watch (prctl on Linux, a kqueue NOTE_EXIT watcher on macOS) and the generated serve() calls it, so this is true of every service spawned this way, not of bb. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0136VgtizjT6krfyRB6TeqKG
unref was meant to say "you never have to destroy this for node to exit". Both transports already implemented that honestly — the uds client refs the socket while a call is outstanding and unrefs when the last one lands, and the shm client acquires a thread-safe-function reference for the same span. The child process handle did not: it was unref'd at spawn, before anything was connected, so during a slow start nothing held the loop and node could exit mid-startup, silently. The child is now unref'd once the backend is connected, after which the transports' own refcounting covers every call. That also collapses unrefStdio into unref. It existed because unref'ing the child leaves its stdout/stderr pipes holding the loop, so the guarantee did not hold whenever a logger was attached. The pipes are now unref'd at the same moment, and the one cost — a trailing log line lost if the process exits mid-write — is stated on the remaining option. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0136VgtizjT6krfyRB6TeqKG
Four folders held one file each, and the one named after the API held only tests. Now: retry moves next to its single caller in crs, proof and the test Timer become plain modules, the Chonk test joins the rest of the barretenberg tests, and bb_backends loses the stutter to become backends. The platform-split directories stay directories — the browser build finds them by rewriting "/node/" in import paths. Two dead things go with it: index.html loaded a simple_test.js that has never existed in this tree, alongside the simple_test script pointing at a missing src/examples, and bbapi/exception_handling.test.ts asserted the same srsInitSrs failure the wasm backend test already covers, so its synchronous half moves there and the file goes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0136VgtizjT6krfyRB6TeqKG
Nothing has ever been tagged // POSTPROCESS ESM ONLY, so the pass that stripped those lines only ever read files. What remains is the one real problem — a single import.meta.url, which is a syntax error in CommonJS — and a note saying what may not be written in code this build compiles, since blanking it makes the expression evaluate rather than fail. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0136VgtizjT6krfyRB6TeqKG
The captureStackTrace call removed exactly one frame — its own constructor — and the comment claimed it maintained a stack trace it was not responsible for; super() captures that either way. Nothing has ever caught the type in this repository, so what the class earns is its name: a log line that says the failure came from bb rather than from bb.js. Also records an asymmetry a caller would otherwise meet by surprise. Only the native backends raise it. bb's wasm build compiles with BB_NO_EXCEPTIONS, so a failing command aborts into the host's throw hook and arrives as a plain Error with the same message. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0136VgtizjT6krfyRB6TeqKG
Every generated TS API took an optional `createError` so a consumer could name the error a failed command raises. Only bb.js ever passed one, to get BBApiException, and the option was threaded through six option interfaces and four constructors in every generated package, with 'createError' listed in each Omit<> alongside it. Generate the class instead: api_types.ts declares one named for the service's error response (BbErrorResponse -> BbError), and the async and sync APIs throw it. Every package now has a typed error with no configuration, and the factory is gone. bb.js re-exports it, aliased to BBApiException so existing callers keep compiling. The new test pins the contract, which is per-failure-site rather than per-backend: a handler that reports with BBAPI_ERROR returns an error frame and raises BbError on every backend, while one that reports with throw_or_abort raises BbError on native but a plain Error on wasm, where BB_NO_EXCEPTIONS compiles the dispatcher's catch away and the throw reaches the host hook instead. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0136VgtizjT6krfyRB6TeqKG
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #25362 (
cl/ipc-bb-bbjs-migrate).bb.js reached bb's wasm through ~1,400 lines of hand-written plumbing (comlink workers, WASI stubs, a heap allocator, pako inflation, base64-embedded modules) next to a generated client. This PR moves "how a client reaches a service module" into ipc-codegen/ipc-runtime, generates the bb API as its own package, and makes bb.js a facade over it.
ipc-runtime:
@aztec-foundation/ipc-runtime/wasmA generic backend for any wasi reactor implementing the FFI contract (
<svc>_ipc_ffi_entry+<svc>_ipc_ffi_alloc/_free, found by suffix when not named):WasmFfiBackend(async; main instance in a worker, wasi threads on further workers) andWasmFfiBackendSync, implementing the runtime'sIpcClientAsync/IpcClientSyncso the generatedAsyncApi/SyncApisit on them unchanged.WebAssembly.compileStreamingloading with gzip support (browsers that cache compiled wasm start warm on a repeat visit); memory created to the module's declared shape (a threads build gets a shared memory even for one thread; maximum probed down, iOS-sized in browsers); a small WASI shim (clock, random, stdout/stderr to the logger, environ).hostImportsfor modules whose platform layer imports functions beyond WASI (bb:logstr,throw_or_abort_impl,env_hardware_concurrency).new Worker(new URL(..., import.meta.url), { type: 'module' }), the expression bundlers need to see to bundle a worker's dependency graph.ipc-codegen
--server --ffinow emits the FFI entry itself: C++<svc>_ffi.{hpp,cpp}(the service defines one hook,ipc_ffi_dispatcher()), Rust<svc>_ffi.rswith anexport_<svc>_ffi!(path, Handler)macro over aHandler + Defaulttype. The exported symbols carry the service name (bb_ipc_ffi_entry) so libraries of several services can be statically linked into one binary; the Rust/Zig clientffi_backendfiles are generated to match instead of copied templates. The echo example exercises all three languages: a C++ffi_testbinary, a Rust in-process round trip (FfiBackend→ own entry), and the Zigffi_check.--packagegains thewasmtransport (--package-transports uds,shm,wasm,--package-wasm-module,--package-wasm-threads-module,--package-wasm-host-imports): the package shipswasm/, abrowserentry, its own worker scripts,createWasmBackend(Sync)and<Svc>Service.wasm().main/typesfields added for CommonJS consumers' resolution.<Svc>Service.create(options)(and<Svc>ServiceSync.create) picks the spawned process when the binary resolves, else the wasm module;backend: 'process' | 'wasm' | <object>forces one with no fallback (asking for more wasm threads than the host can give is an error, not a downgrade);createBackend/createBackendSyncexpose the same policy to facades. A spawned process receivesthreadsasHARDWARE_CONCURRENCYandRAYON_NUM_THREADS. One entry per host with its own types: node (everything),browser(wasm only),react-native(no built-in backend: a native package callsregisterBackend, or the app passes one).--outand--clientare implied by--package(<package>/src/generated; the package is the client package); the bootstraps drop both lines.SCHEMA_SPEC.md("FFI entry"); flags in the README.bb
Warmupcommand: runs the prover's hot loops once (a 2^14 Pippenger MSM over2^i·Gpoints, batch inversion, Poseidon2) so a tiering wasm engine has optimized them before real work. Exposed in bb.js asBarretenberg.new({ warmup: true }).bb_ffi.cppprovidesbb_ipc_ffi_entry/_alloc/_free;c_bind.cppshrinks to the dispatcher hook. barretenberg-rs regenerates against the new name (its FFI tests pass againstlibbb-external.a). The generated source is listed explicitly in CMake because the module's configure-time glob cannot see a build-time output on a clean tree.bb.js
barretenberg/ts/bb.js-api(@aztec-foundation/bb.js-api), generated bybarretenberg/ts/bootstrap.shlike bb-avm-sim/cdb (build_bb_js_apistages the two wasm builds and the native bb). bb.js depends on it for types,AsyncApi/SyncApi, curve constants and the wasm backends.BackendTypeoptions onto the package'screateBackend/createBackendSync(bb's thread defaults, shm ring sizes and process lifetime settings live in that mapping); its own uds/shm/wasm backend files are gone. bb resolves through bb.js-api's per-platform packages (findBbBinaryis re-exported from there); bb.js no longer bundlesbuild/<platform>/bb, only the LMDB NAPI module (findNapiBinary). The facades are unchanged.src/barretenberg_wasm/**,bb_backends/{wasm,node/native_socket,node/native_shm,node/native_shm_async}.ts,scripts/generate.sh,scripts/copy_wasm.sh, the comlink dependency and the base64 wasm embedding in the browser postprocess.Behaviour to note
new URL(..., import.meta.url)(streaming compile + browser code cache). This needs the same bundler support the worker scripts already required (Vite:optimizeDeps.excludefor bb.js and bb.js-api).wasmPath/BB_WASM_PATHstill override the module.@aztec-foundation/bb(bb-cli) is under separate review.Testing
yarn test(23 tests, incl. new module-source tests).ffi_test, Rustcargo test --features ffi, Zigffi_check, ts_package over uds and shm (incl. thecreate()default-policy check);schema_visitortests. bb-avm-sim (process-only) and wsdb regenerate and build.bbapi_tests --gtest_filter='CBind.*'(incl. newWarmupSucceeds); both wasm builds export the three prefixed FFI symbols; barretenberg-rscargo test --release --features ffi(72 tests) againstlibbb-external.a.dest: existing suites against native bb (uds + shm via the NAPI addon) and with the binary hidden (wasm fallback); newbb_backends/wasm.test.tscoveringWasmandWasmWorkerat 4 threads,warmup: true, the sync backend, and error propagation.yarn formattingclean.🤖 Generated with Claude Code
https://claude.ai/code/session_0136VgtizjT6krfyRB6TeqKG