跳到主要内容

架构

本页沿着一个加了属性的函数,从源码一路看到它被注册进 DuckDB。

架构图与更细的讲解

Zread 用生成的架构图梳理了这个仓库 —— 模块分层、注册流程等等, 内容非常详细丰富。本页不够用的时候,从那里入手。

组成​

Crate职责
duckfn-macro过程宏。读取属性、校验签名,生成包装代码与注册项。
duckfn运行时:适配器 trait、值类型、inventory 注册表、入口点胶水。
quack-rsDuckDB C API 绑定:各类 builder、LogicalType、DataChunk、VectorReader/VectorWriter、SqlMacro、ExtensionError。
libduckdb-sysDuckDB 的 C 头文件,以 loadable-extension feature 编译。

duckfn 的模块都是 pub(crate),公共接口就是 src/lib.rs 里那些 pub use 重导出 —— 这也是为什么 duckfn::DuckOptionResult 存在,而 duckfn::ExtensionError 不存在。

1. 宏展开​

属性宏走 common_build:保留原函数,并追加一个与函数同名的模块:

#item_fn // 原函数,原样保留
#vis mod #name { // 可见性继承自函数
use super::*;
#[derive(duckfn::DuckStruct, Debug, Clone, Default)]
#[duck(#attr)] // 属性参数原样转写
pub struct DuckArgsImpl { /* 每个参数一个字段 */ }
// 各宏特有的 item:适配器实现与 builder
}

DuckArgsImpl 是参数列表与数据流向之间的桥梁:#[derive(DuckStruct)] 让它变成 DuckColumns 类型, 于是同一个结构体既用来从 DataChunk 读参数,也用来把结构体参数写回去。适配器实现随后调用原函数。 各宏生成的 item 清单见属性参考。

2. 注册​

宏把注册项提交到 inventory 注册表,由链接期收集:

pub type DuckRegisterFn = fn(connection: &Connection) -> DuckResult<()>;

pub struct DuckFunctionItem {
pub register_fn: DuckRegisterFn,
}

inventory::collect!(DuckFunctionItem);

三张注册表对应三种注册策略:

注册表收集来源由谁注册
DuckFunctionItem大多数宏,以及每个 #[duck_custom_register] 函数register_all_duckfn
DuckAggregateOverloadItem聚合函数上的 overloads_nameregister_all_aggregate_overload
DuckScalarOverloadItem标量函数上的 overloads_nameregister_all_scalar_overload

register_all_duckfn 按上表顺序依次处理;重载那两遍用 itertools::into_grouping_map_by 按名字分组, 使共享同一个 overloads_name 的签名最终落进同一个函数集。注册表里的 name 字段用 &'static str 而不是 String,因为 inventory::submit! 展开成的是 static 初始化表达式,其中无法构造 String。

3. 入口点​

duckfn_entrypoint!("my_ext");

展开为

quack_rs::entry_point_v2!(my_ext_init_c_api, duckfn::register_all_duckfn);

所以导出符号是 {name}_init_c_api,函数体就是用 DuckDB 交来的 Connection 调用 register_all_duckfn。 名称在编译期被校验:非空,且只含小写 ASCII 字母、数字与下划线。

4. 分发​

开启 loadable-extension 后,DuckDB 的 API 函数不会被链接,而是经一张 AtomicPtr 表解析; DuckDB 加载扩展、调用入口点时把这张表填好:

这就是不需要编译 DuckDB 的原因,也是扩展必须与编译时所用 DuckDB 版本绑定的原因。

5. 适配器​

每种注册方式都有一个适配器 trait,负责把 Rust 值接到 DuckDB 基于 vector 的回调上。

标量函数​

先把整个输入 chunk 读成一批行(Vec<Option<Args>>,None 表示该行整体为 NULL),把这批行交给 apply_batch,再批量写出结果。apply_batch 的默认实现遍历这批行、逐行调用 apply_with_null, 因此逐行语义不变:任一非 Option 参数为 NULL 时 apply_with_null 返回 Ok(None),这就是整行短路的 来源。批量实现改为覆盖 apply_batch(batch = true 生成的就是它):一次拿到整批行、返回 Vec<Option<Output>>,其中的 Ok(None) 表示整批 NULL,返回长度必须与批大小一致。

null_handling() 默认返回 DefaultNullHandling,special_null_handling = true 时被覆盖为 SpecialNullHandling。类似地,volatile() 默认返回 false,volatile = true 时被覆盖为 true, 注册期随之调用 duckdb_scalar_function_set_volatile(DuckDB 1.5+)。varargs_element_type() 默认返回 None;varargs = true 时返回签名最后一个 Vec<T> 的元素类型,注册期调用 duckdb_scalar_function_set_varargs(DuckDB 1.5+),回调则改走 apply_varargs 而不是 apply (可变参数没有稳定的行结构,因此从不成批物化)。

聚合函数​

DuckDB 的六个回调都实现在状态类型上:

回调行为
c_state_size / c_state_init分配并初始化状态(Default)。
c_update读一行并调用 handle_row_with_null;默认跳过 NULL 行。
c_combine把部分状态合并进另一个状态,供并行聚合使用。
c_finalize对每个状态调用 result() 并写 vector;offset 非 0 会被拒绝。
c_state_destroy释放状态。

聚合函数与函数集都用 RAII 守卫包裹(AggregateFunctionGuard、AggregateFunctionSetGuard), 即使注册中途失败也能释放 DuckDB 对象。duckfn::DuckfnAggregateFunctionSetBuilder 的存在是因为 quack-rs 的函数集 builder 只能为整个函数集设一个返回类型,而 duckfn 的每个重载都有自己的 Output。

表函数​

with_state 是 bind 阶段:读取参数、通过 config_result_columns 决定输出列,并返回行迭代器。 scan 每个 chunk 从该迭代器拉一次数据。两者都包在 catch_unwind 里,因此任一处 panic 都会变成查询错误。 迭代器类型是 DuckFullIterator<T> = Box<dyn Iterator<Item = DuckOptionResult<T>> + Send>。

COPY 函数​

COPY ... TO 与 COPY ... FROM 都建立在运行时动态列(src/dynamic)之上,而不是 DuckValueType —— 因为 COPY 函数的列要到 bind 阶段才知道。

COPY ... TO 由四个回调驱动:bind 把输出列逐列反推成 DuckResultSchema(每列一次 DuckTypeDesc::from_logical_type)并读出 COPY 选项,两者存成 bind data;global_init 调用 DuckCopyToWriter::open(path, schema, options) 并把 writer 存成 global state;sink 通过 DuckDynamicRow::read_batch 把每个数据块读成 Vec<DuckDynamicRow> 后调用被标注的函数; finalize 调用 DuckCopyToWriter::finish。

COPY ... FROM 完全不是 COPY 的回调:它是一个普通的表函数,由它的 scan 产出行,再用 duckdb_copy_function_set_copy_from_function 接到格式上。quack-rs 0.16 没有 copy_from 辅助、也不 外泄表函数的原始句柄,所以适配层直接用 libduckdb_sys 建这个表函数,同时复用 quack-rs 的 FfiBindData / FfiInitData 完成 bind → init → scan 的状态传递。bind 解析 Args、用 duckdb_table_function_bind_get_result_column_* 读目标表的 schema(COPY FROM 的 reader 不声明结果 列);scan 调用被标注的取批函数并用 DuckDynamicRow::write_batch 写出;reader 的 finish 由 init data 的析构回调触发 —— 表函数没有 finalize 回调。

bind data 与 global state 都用 Box 承载、配上负责 drop 的析构回调交给 DuckDB,每个回调都包在 catch_unwind 里,错误经 set_error 上报。这套 API 来自 DuckDB 1.5.0+,因此这些模块、适配层与 quack-rs 的再导出都在 duckdb-1-5 feature 后面。

改这块代码时要记住两个所有权陷阱:duckdb_table_function_bind_get_result_column_name 返回的字符串、 以及 duckdb_copy_function_bind_get_options 返回的 value,都由 DuckDB 持有,不能释放(否则堆损坏 0xC0000374),适配层只借用它们。交给 duckdb_copy_function_set_copy_from_function 的 reader 表函数 句柄则刻意不销毁:DuckDB 是拷贝还是接管没有文档,double free 是致命的,而每次 LOAD 泄漏一个句柄 无害。

类型转换​

包装函数拿到 count、输入 vector 与输出 vector,逐行调用函数。出错时按转换路径处理: CastMode::Normal(CAST)让整条查询失败,CastMode::Try(TRY_CAST)记录行级错误并写入 NULL。

替换扫描​

scan_callback 收到未解析的表名。handle_info 调用用户的 handle_path,在 Some(table_fn) 时设置要转调的函数, 并把路径作为第一个 VARCHAR 参数传入。非 UTF-8 的名字会被跳过,Err 通过 duckdb_replacement_scan_set_error 上报。

SQL 宏​

最简单的适配器:返回 SqlMacro 就注册它,返回字符串就用 duckdb_query 执行 —— 这也是字符串里可以放多条语句的原因。

6. 值类型​

DuckValueType 是让一个 Rust 类型可以作为参数或结果的 trait:

方向方法
类型标识type_id()、logical_type()、from_null()
读取create_reader、read_valid、read_slot、read_by_duck_value*
写入write_batch、write_valid、write_null、write_finish

实现者只需重写 *_valid 那一半;read 与 write 不建议重写。DuckValueReader 与 DuckValueWriter 携带 vector 及其子 reader/writer,嵌套类型正是靠这一点递归进 LIST、MAP、ARRAY、STRUCT 的子节点。 #[derive(DuckStruct)] 还会为每个字段生成 assert_impl_duck_value_type::<T>() 调用, 因此不支持的字段类型是编译错误,而不是运行时的意外。

可空性是「类型」的属性,而不是容器的:Option<T> 实现了 DuckValueType,逻辑类型与 T 相同, 并把 from_null() 覆写成 Some(None),其余类型保持默认的 None。元素、map 键值、结构体字段 统一走 read_slot(),由它接上这个回退 —— 正因如此 Vec<T> / [T; N] / IndexMap<K, V> 各自只需一份实现就能同时服务可空与不可空的元素类型,#[derive(DuckStruct)] 也不必再按语法去 识别字段类型。

DuckValueReader 还带一个存活凭证(Arc<ChunkToken>,通过 alive_weak() 暴露成 Weak), DuckLazy<T> 正是靠它把读取推迟:拿到 reader 就等于拿到「这块向量此刻有效」的证明,于是延迟值能区分 「还在回调里」与「chunk 已经没了」,后者给出报错而不是解引用一块失效的向量。DuckLazySlot<T> 是这套 设计的使用端:它把延迟值解析一次、留在聚合状态里,combine 时搬运解析结果、result() 里取回 —— 凭证本身始终不出回调。

DuckStructTrait 是生成的结构体接口,三个 blanket impl 把它接入系统其余部分:DuckValueType(可作为值)、 DuckColumns(可作为表函数的输出行)、DuckBindArgs(可作为表函数的参数)。

去哪里看代码​

问题文件
属性接受哪些参数?该宏自己的文件,如 duckfn-macro/src/scalar_function.rs
宏生成了什么?同一个文件加上公共的 common.rs;derive 在 duck_struct_derive.rs / duck_enum_derive.rs
入口点怎么生成?duckfn-macro/src/entrypoint.rs
注册是怎么工作的?src/register.rs
回调是怎么实现的?src/functions/*_adapter.rs
类型是怎么转换的?src/value_types/*.rs

接下来​