corvid-dart
corvid-dart is the Dart
binding: it links the engine’s published FFI artifacts (the platform
cdylib and corvid.h) through dart:ffi, with the raw layer
generated by ffigen from the release’s own header (committed and
drift-gated in CI), and carries an idiomatic Dart API on top.
Deliberately the corvid-c/corvid-go pattern (a fetched, checksummed
shared library), not the node/python one (Rust-source builds):
./fetch.sh (macOS/Linux) or ./fetch.ps1 (Windows) downloads and
sha256-verifies the pinned release archive, dart test does the rest —
no Rust toolchain, no vendored binaries. Dart VM on desktop (macOS,
Linux, Windows x64; macOS + Linux arm64), CI-verified on all three
operating systems; Flutter plugin packaging is a documented follow-up
with a trigger (see the repo’s docs/PLAN.md platform story).
When to choose this binding: your project is Dart — a VM app, a
CLI, a server — and you want corvid embedded as a plain pub dependency:
documents are Map<String, Object?>, vectors are Float32List, keys
are Uint8List, and every failure is a CorvidException.
The idiom mapping
Section titled “The idiom mapping”| C ABI | corvid-dart |
|---|---|
opaque handles (corvid_db*, …) | Db / Collection / Query classes with explicit idempotent close() and a NativeFinalizer backstop — close deliberately, the finalizer only keeps a leak from pinning engine memory |
CORVID_ERR + thread-local last error | CorvidException carrying the ABI code + message, read off the thread-local slot immediately after the failing call |
| frozen enums | Metric, Quant, FieldType, … re-declared idiomatic Dart enums (exact ABI values) |
| consumed-by-call args (pred trees, builders) | Predicate/Query consumed by their terminal (filter/and/run/aggregates); the finalizer is detached on consumption, close() frees an abandoned builder |
borrowed views (_ref buffers, row docs, callback keys) | copies at the boundary — every key/doc/vector is copied into Dart-owned memory inside the wrapper call before the next next() or free; nothing borrowed is retained |
corvid_update_fn / corvid_scan_fn | ordinary Dart closures over NativeCallable.isolateLocal trampolines; a thrown exception is captured (never unwinding through native frames), the scan/update is aborted at the ABI level, and the original error + stack trace is rethrown at the call site |
| strings / bytes / vectors | String (UTF-8 at the boundary), Uint8List, Float32List — NaN/±inf/-0.0 cross bit-exact |
| documents | Map<String, Object?> / List<Object?> / null / bool / int (int64) / double |
The raw ffigen layer stays importable as package:corvid_dart/src/...
(internal); the public package:corvid_dart/corvid.dart leaks no
dart:ffi type in any signature (the ABI’s ruling 3).
Install
Section titled “Install”From the pinned release artifacts:
./fetch.sh # fetch + sha256-verify corvid v0.3.1 into deps/currentdart pub getdart test # the golden suite (267 lines) + supplemental contractsdart run examples/quickstart.dartRequirements: the Dart SDK ^3.10 floor; CI runs the stable channel
and 3.10.0 across linux/macos/windows (the toolchain policy — modern
minimums, latest + previous in CI). Until the pub.dev publish is
announced, consume from git (publish_to: none today):
dependencies: corvid_dart: git: https://github.com/corvid-db/corvid-dart.gitDocuments, maps, and phrases
Section titled “Documents, maps, and phrases”Engine v0.3.0’s ABI additions are first-class here:
- Complete map decode — decoding a document enumerates every map’s
keys through
corvid_value_map_keys(the §4.4 string cursor), so whatever wrote the data, the Dart side sees the whole document. Themapkeys_test.dartcontract pins the across-a-reopen shape. Collection.phraseSearch(field, phrase, k)— the DIRECT positional search: consecutive, in-order analyzed tokens, stop words collapsing out of adjacency ("leaps over a sleeping"matching"jumps over a sleeping dog"-style text), rows carrying the BM25 phrase score (the phrase scale, not the builder’s fused RRF scale);k == 0answers empty — inert, never an error. Thetext_searchexample demonstrates all of it, CJK bigram phrases included.
The examples
Section titled “The examples”Six runnable programs under the repo’s examples/ directory
(dart run examples/<name>.dart), executed on every CI leg with
deterministic output: quickstart, hybrid (the flagship below),
vector_index (in-memory / on-disk / binary-quantized HNSW vs the
exact scan, plus a close/reopen), text_search (BM25 incl. CJK
bigram segmentation, plus the v0.3.0 direct phraseSearch), graph
(neighbors/traverse + delete cascade), and geo (radius / bbox /
nearest with haversine kilometres). The quickstart and hybrid sources
are embedded below — imported from the repo so they cannot drift from
what CI executes (scripts/sync-binding-examples.sh; the drift gate
reddens docs CI if they diverge).
Quickstart
Section titled “Quickstart”void main() { final db = Db.openMemory(); final docs = db.collection('docs');
docs.insert(kb('p1'), { 'title': 'rust embedded database', 'kind': 'doc', 'v': Float32List.fromList([1.0, 0.0]), }); docs.insert(kb('p2'), { 'title': 'python web frameworks', 'kind': 'doc', 'v': Float32List.fromList([0.0, 1.0]), }); docs.insert(kb('p3'), { 'title': 'rust again database', 'kind': 'doc', 'v': Float32List.fromList([0.9, 0.1]), });
// kNN: the 3 nearest documents to (1, 0) under cosine. Row.doc is // materialized only under select — retrieval rows carry keys and // scores, so select the field the printout needs. final rows = docs .query() .vector('v', Float32List.fromList([1.0, 0.0]), 3, Metric.cosine) .select(['title']) .run(); var rank = 0; for (final r in rows) { print('${++rank}. ${String.fromCharCodes(r.key)} ' 'score=${r.score.toStringAsFixed(6)} ${r.doc}'); }
docs.close(); db.close();}Hybrid retrieval
Section titled “Hybrid retrieval”void main() { final db = Db.openMemory(); final docs = db.collection('docs');
docs.insert(kb('s1'), { 'kind': 'doc', 'body': 'rust embedded database', 'v': Float32List.fromList([1.0, 0.0]), }); docs.insert(kb('s2'), { 'kind': 'doc', 'body': 'python web frameworks', 'v': Float32List.fromList([0.0, 1.0]), }); docs.insert(kb('s3'), { 'kind': 'doc', 'body': 'rust again database', 'v': Float32List.fromList([0.9, 0.1]), }); docs.insert(kb('m1'), {'kind': 'meta'}); // filtered out below
// The flagship query: filter + vector + text, RRF + MMR + limit. final rows = docs .query() .filter(field('kind').eq('doc')) .vector('v', Float32List.fromList([1.0, 0.0]), 2, Metric.cosine) .text('body', 'rust database', 2) .fuseRRF(60) .rerankMMR(1.0) .limit(2) .select(['body']) .run(); var rank = 0; for (final r in rows) { print('${++rank}. ${String.fromCharCodes(r.key)} ' 'score=${r.score.toStringAsFixed(6)} ${r.doc}'); }
docs.close(); db.close();}The correctness floor
Section titled “The correctness floor”dart test replays the engine’s entire golden fixture suite — 267
executable lines across 8 files, including the v0.3.0 VMAP_KEYS
(map-key iteration) and PHRASE/PHRASE_K0 (direct positional
search) lines — against the downloaded cdylib, through the binding
(test/golden_test.dart): every counted line must dispatch, the first
failure names file:line + OP + expected-vs-got, and one
SMOKE <file> lines=<n> executed=<n> line per fixture proves the
dispatch count. Supplemental contracts pin the §1.6 callback
exception-surfacing (callback_test.dart — a thrown error surfaces
verbatim at the call site and the engine stays usable), the complete
across-a-reopen map decode (mapkeys_test.dart), and the frozen
0..19 error table (errcodes_test.dart). The fixtures are vendored in
the repo and byte-compared against the release’s copies at fetch time,
so a bad artifact is a loud fetch failure, never a silent skip.
On top sits docs/SURFACE.tsv — every construct of the engine’s public
surface (327 rows at this pin) resolved to the Dart API exposing it
plus the golden line that proves it, or N/A with the ABI’s §9
reason, gated in CI (scripts/surface-gate.sh). A fourth CI job
regenerates the ffigen layer (lib/src/bindings.dart) from the
freshly fetched header and byte-diffs the commit, so an engine pin
bump can never change the ABI surface silently.
Next: corvid-js.