cratestack_cbor 0.8.0
cratestack_cbor: ^0.8.0 copied to clipboard
Native CBOR codec for CrateStack Dart/Flutter: flutter_rust_bridge (native, vendored) and wasm-bindgen (web, vendored), mirroring @cratestack/cbor conditional exports for JS.
cratestack_cbor #
Native CBOR codec for CrateStack Dart/Flutter clients (cratestack#563). One
uniform API, two backends selected automatically per platform — mirrors
@cratestack/cbor's conditional-export
umbrella shape for JavaScript (@cratestack/cbor-node / @cratestack/cbor-web
behind one import path).
import 'package:cratestack_cbor/cratestack_cbor.dart';
final codec = await createCborCodec();
final bytes = codec.encodeJson('{"hello":"world"}');
final json = codec.decodeJson(bytes);
Backends #
| Platform | Backend | Artifact |
|---|---|---|
Native (dart.library.io) |
flutter_rust_bridge =2.12.0 over crates/cratestack-client-flutter's cbor module |
A vendored prebuilt native library — blobs/linux-x64/libcratestack_client_flutter.so (Linux) or blobs/android/<abi>/libcratestack_client_flutter.so (Android: arm64-v8a, x86_64, armeabi-v7a) in this release. No Rust toolchain, no network fetch, at consumer build time. |
Web (dart.library.js_interop) |
The existing cratestack-cbor-wasm wasm-bindgen artifact (already shipped to npm as @cratestack/cbor-web) |
A vendored wasm-pack --target web build — lib/src/web/wasm-pkg/ — loaded at runtime via dart:js_interop. No new codec binding; this reuses the exact same Rust wasm-bindgen crate the JS package already binds. |
Linux and Android share the same flutter_rust_bridge Dart glue (lib/src/native/rust/, platform-independent — frb codegen introspects Rust source, not a target triple) but vendor genuinely different resolution mechanisms: Linux resolves the vendored library by an executable-relative path, Android resolves it by bare SONAME from the app's own native library directory (populated by android/build.gradle's jniLibs.srcDirs) — see lib/src/native/native_cbor_codec.dart.
Both backends round-trip byte-identical CBOR to cratestack-codec-cbor's
CborCodec for the same input — see test/shared_fixtures.dart, which both
test/native_cbor_codec_test.dart and test/web_cbor_codec_test.dart assert
against, using the same hex fixtures cratestack-cbor-napi,
cratestack-cbor-wasm, and crates/cratestack-client-flutter's own test
suites assert (three independent bindings agreeing on the same wire bytes).
Why a JSON-text boundary, not a native Dart value type #
flutter_rust_bridge has no dynamic "any JSON value" wire type the way napi's
serde-json feature or wasm-bindgen's JsValue do — every bridged Rust
function signature is a concrete, statically-known type its codegen can walk
ahead of time. So encodeJson/decodeJson cross the native FFI boundary as
String (JSON text), and the web backend normalizes to the exact same
boundary (via the browser's own JSON.parse/JSON.stringify) so callers
never branch on platform — one uniform CratestackCborCodec interface, see
lib/src/cbor_codec.dart.
Scope of this release — read before depending on this in production #
This is a partial platform matrix (cratestack#563), not the full package:
- Native platform matrix: Linux x86_64 and Android (arm64-v8a, x86_64,
armeabi-v7a) only.
resolveVendoredLibraryPath()throws a clearUnsupportedErroron every other platform (macOS, Windows, iOS, Linux arm64) rather than silently failing. The remaining matrix is deliberate follow-up work, not an oversight. - Not published to pub.dev yet. The publish workflow (GitHub Actions
OIDC, verified publisher
cratestack.dev) and version-locking to the workspace version both exist — seedocs/tooling/dart-publishing.md— but pub.dev cannot automate a brand new package's first publish (only subsequent versions). Until a maintainer performs that one-time manualdart pub publishand enables Automated publishing on pub.dev's Admin tab, this package stays unpublished andpublish-pubdev-cborinrelease-cli.ymlfails on every tag push by design, rather than silently skipping. - The Dart generator does not use this package yet.
crates/cratestack-client-dart/templates/pubspec.yaml.j2still emitscbor: ^6.5.1— flipping that seam before this package is published would breakdart pub getfor every generated client (pub.dev returns 404 for an unpublished package name).
Flutter app integration — proven, not just dart test #
dart test/dart test -p chrome prove the codec works as a Dart library.
They do not prove it works inside a real Flutter app: a compiled app has no
package source tree for Isolate.resolvePackageUri to find, a release web
bundle has no dev server to serve the packages/... URL convention from, and
Android has no dart test story at all. This package closes that gap for
Linux desktop, Android, and web:
- Linux desktop:
linux/CMakeLists.txtmakes this package a Flutter FFI plugin (pubspec.yaml'sflutter: plugin: platforms: linux: ffiPlugin: true) that hands the vendoredblobs/linux-x64/libcratestack_client_flutter.soto Flutter's own<plugin>_bundled_librariesmechanism — the same oneflutter create -t plugin_ffiscaffolds, not thecargokitpattern most flutter_rust_bridge plugins use (that builds Rust from source at consumer build time; the maintainer decision on cratestack#563 rejected imposing a Rust toolchain on every consuming Flutter developer). A realflutter build linuxcopies the library into the built bundle'slib/, next to the app executable;lib/src/native/native_cbor_codec.dartresolves it there first (falling back to the dev-modeIsolate.resolvePackageUripath only fordart test/dart run). Deliberately noflutter: sdk: flutterdependency on this package — verified empirically that Flutter's plugin tooling honors theflutter:pubspec key without it, so plaindart pub get/dart test(no Flutter SDK at all) keep working exactly as before. - Android:
android/build.gradlemakes this package an Android FFI plugin (pubspec.yaml'sflutter: plugin: platforms: android: ffiPlugin: true) that packages the vendored per-ABI libraries (blobs/android/<abi>/libcratestack_client_flutter.so) into the APK via the standardjniLibs.srcDirssource-set mechanism — again, no cargokit/CMake/NDK invocation at consumer build time, same constraint as Linux. This is a genuinely different resolution mechanism from Linux, not a relocated copy of it: Android's dynamic linker finds a bundled library by bare SONAME from the app's own native library directory, sonative_cbor_codec.dart's Android branch computes no path at all — it just openslibcratestack_client_flutter.sodirectly. Seedocs/tooling/cratestack-cbor-development.md's gotcha 6 for why an APK that compiles does not by itself prove the library shipped. - Web:
pubspec.yaml'sflutter: assets:vendors the.js/.wasmpair as real Flutter assets, so a releaseflutter build webcopies them intobuild/web/assets/packages/cratestack_cbor/....web_cbor_codec.darttries the dev-serverpackages/...URL first (unchanged, still whatdart test -p chrome/flutter run -d chromeuse), then falls back to the releaseassets/packages/.../lib/...URL — the two conventions coexist, neither subsumes the other.
dart-packages/cratestack_cbor/example/ is a minimal Flutter app proving all
three, with real builds: see that directory's README, just cbor-example-verify (Linux+web, wired into CI as cratestack-cbor-example),
and just cbor-example-verify-android (Android APK build + per-ABI presence
proof, wired into CI as cratestack-cbor-android). Linux and web build in
release mode, actually run the Linux binary headless, and actually serve
and load the web release bundle in a real headless Chrome — not flutter run's dev server, which resolves assets differently and would prove nothing
about a release deploy. Android additionally has a local/manual
companion, just cbor-example-verify-android-emulator, that installs the
built APK on a real Android emulator and asserts the app actually round-trips
CBOR at runtime — deliberately not wired into CI, since booting an emulator
on a hosted runner is substantially heavier and flakier than everything else
this package's CI already does; see that recipe's own comment in the
justfile for the full reasoning.
iOS, macOS, Windows, and Linux arm64 remain out of scope for this slice (deliberately — see the platform matrix note above).
Regenerating the vendored artifacts #
Working on this package rather than using it? Read docs/tooling/cratestack-cbor-development.md first. It covers the toolchain pins, the first-run steps, and four failure modes that each look like success — a missing
.pubignoresilently publishing a package without its binaries, a web test that passes against the native backend, a bridged function that is 2x slower than pure Dart because it is async, and rustfmt reformatting generated glue.
All three artifacts are build outputs from crates in this repo and are regenerated, not hand-written. From the repository root:
just cbor-vendor-native # flutter_rust_bridge glue + blobs/linux-x64/*.so
just cbor-vendor-web # wasm-pack --target web build -> lib/src/web/wasm-pkg/
just cbor-vendor-android # cargo ndk cross-compile -> blobs/android/<abi>/*.so (reuses cbor-vendor-native's glue — run that first)
See the justfile for what each does.
None of the vendored output is git-tracked — not the Dart glue at
lib/src/native/rust/, the native libraries at blobs/linux-x64/ and
blobs/android/<abi>/, nor the wasm build at lib/src/web/wasm-pkg/. This matches CLAUDE.md's "don't
commit generated build output", cratestack#563's own "frb glue is generated
in CI, not committed" decision (which gitignores the byte-identical
frb_generated.* files in crates/cratestack-client-flutter), and the two
sibling npm packages: @cratestack/cbor-node's compiled .node addon and
@cratestack/cbor-web's wasm-pkg/ are both gitignored, with the release
workflow building them immediately before publishing.
There is a real trap here, and .pubignore is what disarms it. Per
pub.dev's publishing docs, pub
uses git ls-files to decide what to publish when the package directory
sits inside a Git working tree — so files excluded by .gitignore are
silently omitted from the published tarball, with no files-field
equivalent to opt them back in and no warning. Gitignoring these
directories naively would ship a package missing the very artifacts this
ticket exists to vendor.
.pubignore resolves it: when present in a directory, pub consults it
instead of .gitignore there. This package's .pubignore is
intentionally empty of exclusions, so the gitignored build outputs are
still packed. Verified, not assumed — dart pub publish --dry-run with
all four vendored artifact directories gitignored and untracked still lists
them all:
├── blobs
│ ├── android
│ │ ├── arm64-v8a
│ │ │ └── libcratestack_client_flutter.so (904 KB)
│ │ ├── armeabi-v7a
│ │ │ └── libcratestack_client_flutter.so (492 KB)
│ │ └── x86_64
│ │ └── libcratestack_client_flutter.so (946 KB)
│ └── linux-x64
│ └── libcratestack_client_flutter.so (903 KB)
...
│ ├── cratestack_cbor_wasm.js (21 KB)
│ └── cratestack_cbor_wasm_bg.wasm (121 KB)
Total compressed archive size is ~1 MB with all four native libraries plus
the wasm pair vendored — still a small fraction of pub.dev's 100 MB gzip
recommendation (see cratestack#563's thread for the original size-budget
analysis) — and git ls-files reports zero of them tracked. So the
published package vendors its binaries exactly as decided, while the
repository stays free of build output.
Regenerate them with the just recipes above; treat them as build outputs
to keep in sync with the source crates, never as source to hand-edit.
.pubignore must stay tracked — without it in a fresh
checkout, publishing silently reverts to dropping these files.
Verifying both backends #
just cbor-verify-package # both backends, native (dart test) + web (dart test -p chrome)
or directly, from this directory:
dart test # native backend only (@TestOn('vm'))
dart test -p chrome # web backend only (@TestOn('browser'))
@TestOn('vm') / @TestOn('browser') are load-bearing, not decorative — see
test/web_cbor_codec_test.dart's doc comment for why: the conditional export
in lib/cratestack_cbor.dart is resolved by the compile target, not by
which test file imported it, so an unguarded dart test would otherwise
silently compile the "web" test file against the native backend on the VM
and report false-positive passes without ever touching dart:js_interop.
Why dart-packages/, not packages/ #
The npm umbrella this mirrors lives at packages/cratestack-cbor
(@cratestack/cbor) — but pnpm-workspace.yaml globs packages/*, and
every existing entry under packages/ is a pnpm workspace member (has a
package.json). This package has none (it's pure Dart/pub, not npm), so
placing it at packages/cratestack_cbor would put an unrecognized directory
inside pnpm's workspace glob. It also can't reuse the exact name
packages/cratestack-cbor (already the npm umbrella) even if collisions
were fine, since Dart requires a snake_case package name
(cratestack_cbor) while this repo's packages/ entries are kebab-case.
dart-packages/ is a new top-level directory, parallel to crates/
(Rust), packages/ (npm), and examples/ — reserved for Dart/pub packages,
not swept into any existing tool's workspace glob.
License #
MIT