Skip to content

feat: build bdk-ffi for wasm32-unknown-unknown - #1118

Open
bennyhodl wants to merge 1 commit into
bitcoindevkit:masterfrom
bennyhodl:feat/wasm32-target
Open

bennyhodl wants to merge 1 commit into
bitcoindevkit:masterfrom
bennyhodl:feat/wasm32-target

Conversation

@bennyhodl

Copy link
Copy Markdown

Description

Make the crate compile for wasm32-unknown-unknown, so that a UniFFI binding generator such as uniffi-bindgen-react-native (wasm2 flavor) can produce browser and Node bindings from the same API the native bindings use. Native targets are unchanged.

How the wasm package gets built and published is still open. That discussion lives in this gist: https://gist.github.com/bennyhodl/e290b7de4ee8c746ee28f19695ab7bd2. Whatever is decided there, the cfg splits in this PR are required for any wasm build, because rusqlite, minreq, bdk_electrum and bdk_kyoto have no wasm32 support and cannot be replaced from a downstream package.

What changes:

  • Cargo.toml: rusqlite, the blocking Esplora client, bdk_electrum and bdk_kyoto move to a cfg(not(target_arch = "wasm32")) table. A wasm32 table adds the async Esplora client, getrandom/js, UniFFI's wasm-unstable-single-threaded feature, uniffi-runtime-wasm, and wasm-bindgen/js-sys/futures-channel for the sleeper. resolver = "2" stops features from unifying across target tables.
  • esplora.rs: on wasm32 EsploraClient wraps AsyncClient. Every method keeps its name, arguments and result but is async. The constructor returns a Result because building the reqwest client can fail. Retry backoff sleeps through the host's global setTimeout, so it works in browsers, workers and Node. The request/update helpers are shared by both implementations.
  • store.rs: Persister::custom is available on every target. The SQLite constructors and the pre-v1 migration helper are native only.
  • error.rs: EsploraError gains a Reqwest variant. It exists on every target so the generated API is identical, but native never produces it. Electrum, SQLite, migration and Kyoto conversions are native only.
  • lib.rs: the electrum and kyoto modules are not compiled on wasm32. An extern crate uniffi_runtime_wasm line keeps the runtime exports from being dropped by the linker.
  • CI: a new wasm job runs cargo check --locked and cargo clippy --locked for wasm32-unknown-unknown.

Notes to the reviewers

  • wasm-unstable-single-threaded is not a threading model choice. It only removes the Send + Sync bounds UniFFI requires on native, where foreign threads call in. wasm32-unknown-unknown has no threads. The Persistence trait keeps its Send + Sync bounds and compiles as is.
  • secp256k1-sys needs a C compiler with a wasm32 backend. Ubuntu's clang has one, so CI sets CC_wasm32_unknown_unknown=clang. Apple's clang does not, so on macOS point that variable at an LLVM clang (for example Homebrew's llvm).
  • Feature isolation was verified with cargo tree: native has no reqwest or wasm-bindgen, and wasm32 has no rusqlite or minreq.
  • The proxy argument on the wasm32 EsploraClient constructor is kept for API parity and does nothing on that target.
  • No CHANGELOG.md edit in this PR, following the release flow where entries come from the changelog: label.

Documentation

Changelog

changelog: added

Checklists

All Submissions:

  • I've signed all my commits
  • I followed the contribution guidelines
  • I ran cargo fmt and cargo clippy before committing
  • I've added exactly one changelog:* label (needs a maintainer: changelog: added)
  • I've linked the relevant upstream docs or specs above

New Features:

  • I've added tests for the new feature. The CI job checks and lints the wasm32 target. A runtime test needs a JavaScript host and lives with the binding generator, not in this crate. The native test suite is unchanged.
  • I've added docs for the new feature

Make the crate compile for the wasm32-unknown-unknown target so that
uniffi-bindgen-react-native's `wasm2` flavor can generate browser and
Node bindings from it, the same way it generates the React Native ones.

Native targets are unchanged. On wasm32:

- The blocking esplora, electrum, kyoto and rusqlite dependencies move
  to a `cfg(not(target_arch = "wasm32"))` table. The wasm32 table adds
  the async esplora client, `getrandom/js`, uniffi's single-threaded
  mode, `uniffi-runtime-wasm`, and the wasm-bindgen glue needed for a
  `setTimeout` based sleeper.
- `resolver = "2"`, because resolver 1 unifies features across target
  tables: it pulled rusqlite into the wasm build and the reqwest error
  variant into the native one.
- `EsploraClient` wraps `esplora_client::AsyncClient`. Every method
  keeps its name, arguments and result but is `async`. The constructor
  returns a `Result` because building the reqwest client can fail.
  Retry backoff sleeps through the host's global `setTimeout`, so it
  works in browsers, workers and Node.
- `Persister` offers only `custom`; the SQLite constructors and the
  pre-v1 migration helper are native only.
- `EsploraError` gains a `Reqwest` variant. It exists on every target
  so the generated API stays the same, but native never produces it.
- The electrum and kyoto modules are not compiled.

CI gets a job that runs `cargo check --locked` and clippy for the
wasm32 target. Ubuntu's clang carries the wasm32 backend that
secp256k1-sys needs; Apple's clang does not, so local builds on macOS
must point `CC_wasm32_unknown_unknown` at an LLVM clang.

@Ugarba202 Ugarba202 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a massive step forward for the JS/web ecosystem!
I pulled the branch locally and ran through some checks:

  • Dependency Isolation: Verified the cfg feature gating in Cargo.toml. The resolver = "2" addition does a great job at isolating the features.
  • Wasm Compilation: Ran cargo clippy --target wasm32-unknown-unknown and it compiled flawlessly without any feature leaks.
  • Native Integrity: Ran the standard cargo test suite on the Native targets just to be extra safe, and everything passed perfectly. No regressions for existing mobile/native users.
    The async translation for the Esplora client and the custom setTimeout sleeper wrapper for the retry backoffs look really solid. Exposing the Reqwest error variant globally to keep the UniFFI API shape identical across platforms is also a smart tradeoff.
    Everything looks great on my end. Thanks for putting this together!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

2 participants