news 2026/9/24 14:06:09

用 Zig 集成 PRQL 编译器:prqlc-c FFI 最小示例全解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
用 Zig 集成 PRQL 编译器:prqlc-c FFI 最小示例全解析
  • 后端

【免费下载链接】prql

PRQL is a modern language for transforming data — a simple, powerful, pipelined SQL replacement

项目地址:https://gitcode.com/gh_mirrors/pr/prql
点击查看免费下载

PRQL(Pipelined Relational Query Language)是一种面向数据转换的现代语言,目标是成为简单、强大、管道式的 SQL 替代品。本仓库中的prqlc-c是 PRQL 编译器prqlc的 C ABI 绑定,任何支持 FFI 的语言——包括 Zig——都可以直接链接调用。本文将基于仓库中 minimal-zig 示例 这一份官方最小示例,从零讲解如何在 Zig 项目中导入prqlc.h、链接libprqlc_c、调用compile把 PRQL 查询编译为 SQL,并深入剖析其背后的OptionsCompileResult等 C 结构体与 Rust 侧的内存管理约定。读完本文,你将掌握在 Zig 中嵌入 PRQL 编译能力的最小可运行方案,并能自行扩展处理错误信息与中间编译阶段。

示例总览:一条命令跑通

minimal-zig 示例的定位非常明确——它是 prqlc-c C 绑定 官方提供的三种示例之一(另外两个是 minimal-c 与 minimal-cpp),专门演示用 Zig 的@cImport机制直接消费prqlc.h头文件的用法。

该示例目录结构如下:

prqlc/bindings/prqlc-c/examples/minimal-zig/ ├── README.md # 官方说明:Run with `task zig` from the root of the repo ├── Taskfile.yaml # 定义 build-prql / build / run / test 等任务 ├── build.zig # Zig 构建脚本:配置 include 路径、链接 prqlc_c 系统库 └── src/ └── main.zig # 调用 prqlc-c API 的最小 Zig 程序 + 单元测试

官方 README 给出的运行方式是:在仓库根目录执行

task zig

根目录的 Taskfile.yaml 中,zig是一个 include 进来的任务别名,指向prqlc/bindings/prqlc-c/examples/minimal-zig目录,并以其为工作目录。因此执行task zig会进入该示例目录,随后触发示例自身Taskfile.yaml中定义的default任务——一条龙完成「构建 prqlc-c → 构建 Zig 可执行文件 → 运行 → 跑测试」四个步骤:

default: desc: "Build, run, test" cmds: - task: build-prql - task: build - task: run - task: test

运行成功后,程序会打印类似下面的输出:

Compiled with 0 errors Output: SELECT album_id, title FROM albums LIMIT 3

Output:后面的内容取决于是否开启format选项,未开启时是单行 SQL。)

三步构建流程:从 Rust 动态库到 Zig 可执行文件

第一步:构建 prqlc-c(task build-prql

prqlc-c本体是位于 prqlc/bindings/prqlc-c/src/lib.rs 的 Rust crate,通过#[no_mangle] extern "C"导出 C ABI 符号,并用 cbindgen 生成prqlc.h/prqlc.hpp头文件。示例中的build-prql任务负责完成构建与产物归位:

build-prql: desc: "Build prqlc-c" cmds: - cargo build --package prqlc-c --release - mkdir -p c/ - cp {{.project_root}}/prqlc/bindings/prqlc-c/prqlc.h c/ - cp {{.project_root}}/target/release/libprqlc_c.* c/

其中project_root定义为"../../../../..",从prqlc/bindings/prqlc-c/examples/minimal-zig/出发正好回到仓库根目录。这条任务做了三件事:

  1. cargo build --package prqlc-c --release:以 release 模式编译 prqlc-c crate;
  2. 把仓库内现成的 prqlc.h 头文件复制到示例的c/目录;
  3. target/release/下生成的动态库libprqlc_c.so(macOS 上为.dylib,Windows 上为.dll)复制到c/目录。

值得注意的是,cp .../libprqlc_c.* c/同时会把静态库libprqlc_c.a一起拷入(若静态特性开启),因此c/目录同时承担「头文件 + 库文件」的双重角色,build.zig中的 include 路径和 library 路径都指向它。

第二步:Zig 构建脚本链接 prqlc_c(task build

build.zig 使用 Zig 0.11+ 风格的 declarative build API 构建名为minimal-zig的可执行文件。与 prqlc-c 相关的关键配置有三处:

exe.root_module.addIncludePath(b.path("src")); // 让 main.zig 能 #include 到 ../c/prqlc.h exe.root_module.addLibraryPath(b.path("c")); // 指向包含 libprqlc_c.* 的目录 exe.root_module.linkSystemLibrary("prqlc_c", .{}); // 链接系统库 prqlc_c exe.installHeader(b.path("c/prqlc.h"), "prqlc.h"); // 把头文件装入安装产物

此外,createModule中设置了.link_libc = true,保证 Zig 侧链接 libc——这是使用 C ABI 头文件的必要前提。可执行文件同样以src/main.zig为根源文件构建单元测试目标(addTest),并对外暴露runtest两个 Zig build step。

第三步:运行与测试(task run/task test

run: deps: - task: build cmds: - ./zig-out/bin/minimal-zig test: desc: "Run tests" deps: - task: build cmds: - zig build test

run直接执行zig build安装产物zig-out/bin/minimal-zigtest则调用zig build test跑 main.zig 里的单元测试。需要提醒的是,由于动态库在运行时按路径搜索,若直接手动执行二进制遇到找不到libprqlc_c.so的问题,可显式设置LD_LIBRARY_PATH(macOS 用DYLD_LIBRARY_PATH)指向c/目录,或改用静态链接方案。

核心代码逐行拆解:main.zig

用 @cImport 引入 prqlc.h

src/main.zig 是整个示例的精华,第一段即演示 Zig 导入 C 头文件的标准姿势:

const std = @import("std"); const prql = @cImport({ @cInclude("../c/prqlc.h"); });

@cInclude的路径是相对于src/目录的,即src/../c/prqlc.h,正好落在第一步build-prql复制出来的头文件上。build.zigaddIncludePath(b.path("src"))保证了编译器在编译src/main.zig时能解析到这个相对 include。导入成功后,prql命名空间下即出现prql.Optionsprql.compileprql.result_destroy等 C API。

配置 Options 并指定编译目标

var target = "sql.mssql".*; // Setup PRQL compiler options const options = prql.Options{ .format = false, .signature_comment = false, .target = &target, };

这里的Options对应 prqlc.h 中 cbindgen 生成的 C 结构体,字段语义与 Rust 侧 src/lib.rs 中的 Options 一一对应:

字段类型默认值含义
formatbooltrue是否将生成的 SQL 交给格式化器,拆成多行并美化缩进与间距
targetchar *sql.any编译目标与方言,sql.any表示由查询头部的target参数决定方言
signature_commentbooltrue是否在生成的 SQL 末尾追加编译器签名注释

Zig 中var target = "sql.mssql".*;声明了一个可变的、以 NUL 结尾的 C 字符串数组(Zig 字符串字面量默认不可变且非 NUL 结尾,这里通过解引用.*拷贝成可写数组),再把&target赋给target字段——这正是 C 侧要求的char *类型。示例选取sql.mssql作为目标方言,意味着输出将是 SQL Server 风格的 SQL。

编译 PRQL 查询并处理结果

// Compile the PRQL query const prql_query = "from albums | select {album_id, title} | take 3"; const result = prql.compile(prql_query, &options); defer prql.result_destroy(result); std.debug.print("Compiled with {d} errors\n", .{result.messages_len}); std.debug.print("Output:\n\n{s}\n", .{result.output});

这一段对应 Rust 侧导出的 compile 函数:

pub unsafe extern "C" fn compile( prql_query: *const c_char, options: *const Options, ) -> CompileResult

它把 PRQL 源字符串编译为 SQL 字符串,内部等价于依次执行prql_to_plpl_to_rqrq_to_sql三个阶段(只是省略了阶段间的 JSON 序列化)。传入&options即传入非空指针;也可以传null表示使用全部默认选项(下面测试里就是这么做的)。

compile返回的CompileResult在 prqlc.h 中定义如下:

typedef struct CompileResult { const char *output; // 编译输出的 SQL 字符串(或阶段中间产物 JSON) const struct Message *messages; // 错误消息数组指针(无错误时为 NULL) size_t messages_len; // 错误消息条数 } CompileResult;

示例用result.messages_len判断是否成功(0 表示成功),用result.output取 SQL。同时注意defer prql.result_destroy(result);——Zig 的defer保证函数退出时一定释放CompileResult占用的内存。这是 prqlc-c 的硬性约定:Rust 侧 result_destroy 负责深度释放output字符串、messages数组以及每个Message内部堆分配的字段,调用方绝不能手动 free 其内部字段,且对同一个CompileResult只能调用一次result_destroy

内置单元测试

test "simple test" { const prql_query = "from albums | select {album_id, title} | take 3"; const result = prql.compile(prql_query, null); defer prql.result_destroy(result); try std.testing.expect(result.messages_len == 0); }

这个测试验证了两种用法要点:

  1. compile(prql_query, null)说明options参数可传NULL,此时全部选项走默认值(formatsignature_comment为 true,targetsql.any);
  2. result.messages_len == 0是判断编译成功与否的惯用方式——成功时messages为 null、messages_len为 0;失败时output为空串、messages_len为错误条数。

深入底层:compile 的编译流水线与错误处理

三段式编译管线

从 src/lib.rs 可以看到compile的实质是prql_to_plpl_to_rqrq_to_sql三个阶段的串联:

PRQL 源码 ──prql_to_pl──▶ PL AST(JSON) ──pl_to_rq──▶ RQ(JSON) ──rq_to_sql──▶ SQL
  • prql_to_pl:解析 PRQL 语法,生成 PL(Pipeline Language)语法树,序列化为 JSON;
  • pl_to_rq:解析变量引用、校验函数调用、确定 frame,把 PL 转换为 RQ(Relational Query);
  • rq_to_sql:将 RQ 翻译为指定方言的 SQL 字符串。

这三个阶段在 prqlc-c 中同样以独立 C 函数导出(prql_to_plpl_to_rqrq_to_sql),参数与返回值均为CompileResult。若你需要调试中间 AST,可以参考 minimal-c 示例 的用法:先调用prql_to_pl拿 PL JSON,再把res.output作为输入传给pl_to_rq,逐级打印中间结果。

Options 在 Rust 侧的转换

convert_options(src/lib.rs)把 C 结构体转为 Rust 侧prqlc::Options

  • targetNULL或空字符串时,回退为"sql.any",即由查询头部的 target 注释决定方言;
  • targetTarget::from_str解析,若给出非法方言名则产生编译错误;
  • 三个字段最终通过with_formatwith_targetwith_signature_comment应用到默认Options上。

这解释了为什么format/signature_comment在 Rust 侧默认true——C 结构体只是镜像,真正的默认值语义由prqlc::Options::default()定义,并在convert_options中按字段覆盖。

出错时如何取错误详情

compile失败时不会 panic,而是返回messages_len > 0CompileResult。每个Message结构体(prqlc.h)包含:

  • kind:消息类型,目前仅实现ErrorWarningLint为枚举占位);
  • code:机器可读的错误标识符;
  • reason:错误原因的纯文本;
  • hint:修复建议列表;
  • span:错误在源文件中的字符偏移区间(start/end);
  • display:带注释的代码片段,内含原因与提示,适合直接打印给用户;
  • location:错误的起止行号与列号(start_line/start_col/end_line/end_col)。

Zig 侧若要实现健壮的错误输出,可仿照 minimal-c 示例的print_result逻辑:遍历res.messages[0..res.messages_len],优先打印display(若有),否则回退到[code] reason或纯reason。注意hintcodedisplay是二级指针(const char *const *),取值前需判断非 NULL。

扩展思路:把示例改造成自己的 Zig 工程

基于这个最小示例,往自己的 Zig 项目集成 prqlc-c 只需对齐三处约定:

  1. 依赖产物:先构建 prqlc-c 并把 prqlc.h 与libprqlc_c.so/.dylib/.dll)放到同一目录,参考build-prql任务的复制逻辑;
  2. 构建脚本:在build.zig中为自己的可执行文件/库调用addIncludePathaddLibraryPathlinkSystemLibrary("prqlc_c", .{})并设置link_libc = true,可整体照搬示例的 build.zig;
  3. 调用约定:所有导出函数都接受 NUL 结尾的 C 字符串;每个返回CompileResult的调用都必须且只能配对一次result_destroyOptions可传NULL使用默认值。

至于链接时需要的系统依赖,prqlc-c README 给出了 CGO 场景的参考:-lprqlc_c -pthread -ldl -lm(macOS 还需-framework CoreFoundation)。Zig 通过linkSystemLibrary链接动态库时通常不需要手工追加这些依赖,但若选择静态链接libprqlc_c.a,可参考 minimal-c/Makefile 中的链接参数。

小结

minimal-zig 示例虽然只有数十行代码,却完整覆盖了在 Zig 中嵌入 PRQL 编译器的全部关键点:@cImport导入 cbindgen 生成的 prqlc.h、Options三字段(format/target/signature_comment)的配置方式、compile的调用与result_destroy的内存释放约定、基于messages_len的成败判断,以及prql_to_plpl_to_rqrq_to_sql的三段式编译流水线。以此为起点,你可以轻松把它扩展为支持方言切换、错误详情展示甚至分阶段调试的完整 Zig 集成方案。

  • 后端

【免费下载链接】prql

PRQL is a modern language for transforming data — a simple, powerful, pipelined SQL replacement

项目地址:https://gitcode.com/gh_mirrors/pr/prql
点击查看免费下载

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/24 14:05:20

Logistics | “Stock Days ” vs.“Inventory Coverage”

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/24 14:04:48

Python | 地址解析经纬度

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/24 14:04:12

【Dv3Admin】系统视图菜单按钮管理API文件解析

后台权限系统的发展趋势是细化到接口及操作按钮级别,以满足复杂业务下的安全与控制需求。菜单按钮权限管理模块基于 Django 与 DRF 实现,为后台平台提供了标准、细粒度的权限配置能力。 围绕 dvadmin/system/views/menu_button.py 源码,解析菜单按钮增删改查的实现方式,说…

作者头像 李华
网站建设 2026/9/24 14:04:10

Unity 引擎源码剖析:ICall 的 ABI 契约,为什么写错一个类型就会崩

开篇:一个查了三天的 bug QA 提的单子只有一行: 子弹偶尔穿过掩体,复现率约 5%,只在城区地图出现查射线检测的逻辑——没问题。 查碰撞体的配置——没问题。 查物理层级——也没问题。 最后在自己写的原生插件里,找到了这一行: extern "C" int IsBlocked(Vecto…

作者头像 李华
网站建设 2026/9/24 14:04:00

【Dv3Admin】应用URL路由配置文件解析

统一路由配置是 Django 项目结构的重要组成部分。通过集中管理接口、页面和文档入口,可以减少重复代码、提升维护效率,并支持模块化开发与动态扩展,满足中大型项目对灵活性和可控性的要求。 文章解析 application/urls.py 的关键实现,包括系统初始化、文档集成、认证接口注…

作者头像 李华
网站建设 2026/9/24 14:03:52

【Dv3Admin】解决vue页面拆分组件在视觉上出现顺序混乱

在前端开发中,经常会遇到布局中元素层级显示异常的问题。最近遇到了一个问题:在布局中,左侧是导航菜单,右侧包含模型菜单和具体模型组件。右侧的模型菜单总是优先显示在左侧导航菜单之上,导致页面层级混乱。 经过一些调试和研究,最终找到了解决方案。以下是问题的详细描…

作者头像 李华