← back to projects

quack-rs

Write DuckDB extensions in pure Rust, with no C or C++ glue code

lang Rust license MIT view on GitHub →

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.