quack-rs
Write DuckDB extensions in pure Rust, with no C or C++ glue code
Highlights
- Wraps the DuckDB C Extension API (stable since v1.1) so you never touch C++
- Encapsulates the documented FFI pitfalls as compile-time guarantees
- FfiState<T> wrapper eliminates double-free and pointer lifecycle bugs
- Single macro call replaces the unsafe entry-point boilerplate
- Supports every extension type (scalar, aggregate, table, cast, copy, replacement scan, SQL macro)
- generate_scaffold() emits all files required for community submission
- InMemoryDb harness for unit-testing aggregates without full DuckDB builds
Background
DuckDB is the analytical database I reach for first: in-process,
columnar, single-binary, and fast enough to treat as a
first-class runtime rather than an external service. Its
extension system is what makes it a platform. The extension
system is what lets you ship behavioral.duckdb_extension and
have an analyst three time zones away install it with one SQL
statement.
The catch is that DuckDB’s own documentation acknowledges writing a Rust-based extension requires writing glue code in C++. That glue is where memory-safety bugs, NULL-handling mistakes, and validity-bitmap errors hide. For a project that cares about Rust’s guarantees, and especially for anything that will run unattended in production, the C++ boundary is an unacceptable place for the code to sit.
quack-rs exists to move that boundary.
What the SDK does
The SDK wraps the DuckDB C Extension API (stable since v1.1)
and hoists the entire surface into safe Rust. The documented
footguns in the upstream C API (boolean reads that need to
normalize the u8 != 0 convention, NULL writes that have to be
paired with validity-bitmap updates, string encoders that have
to respect a specific length rule, scan-state lifetimes that
absolutely cannot outlive their cursor) are encapsulated as
compile-time guarantees.
The FfiState<T> wrapper is a good example of the design
style. DuckDB’s C API lets you attach per-function state via a
raw pointer, which is where double-frees and use-after-free
bugs live. The wrapper makes the pointer’s lifecycle the type
system’s problem, and the resulting code reads like normal
Rust. Complex types (STRUCT, LIST, MAP, ARRAY) are navigable
through strongly-typed accessors, and panics are caught at the
FFI boundary so a buggy extension cannot poison the DuckDB
process.
What the SDK covers
Every extension type the C API supports (scalar functions,
aggregate functions, table functions, cast functions, copy
functions, replacement scans, and SQL macros) is exposed
through the same idiomatic API. A single macro call replaces
the unsafe entry-point boilerplate. generate_scaffold() emits
the files required for a community extension submission, so
going from “idea” to “PR open against duckdb/community” is a
matter of minutes rather than half a day of file plumbing.
For testing, InMemoryDb lets you unit-test aggregate
functions without building a full DuckDB runtime into the test
harness. Tests run fast enough to belong in the tight inner
development loop.
Design philosophy
The goal is not to ship a complete Rust reimplementation of
DuckDB. The goal is to make the extension boundary invisible.
If using quack-rs feels like writing normal Rust, and the
extension it produces is indistinguishable at runtime from one
written in C++, the SDK has earned its keep.
What it enables downstream
quack-rs is the foundation that
duckdb-behavioral and
duck_net were built on. Without a safe
Rust SDK, neither of those would have been realistic to ship to
the quality bar I wanted. With one, they became a matter of
application-level design choices rather than weeks of C API
archaeology.