PRQL 官方 minimal-cpp 示例解析:在 C++ 中通过 prqlc-c FFI 编译 PRQL 查询
2026/9/24 18:11:12 网站建设 项目流程
  • 后端

【免费下载链接】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 的简单、强大的流水线式替代品。本指南以仓库中 minimal-cpp 示例 为核心,完整讲解如何在 C++ 程序中通过prqlc-c这一 C/C++ FFI 绑定,把 PRQL 查询编译为 SQL。读完本文,你将掌握从环境准备、Makefile 链接配置到compile/result_destroy等关键 API 调用的完整实战链路,并理解其底层编译流水线与内存管理约定。

一、示例概述:一条命令跑通 C++ 调 PRQL

minimal-cpp 示例 是 PRQL 仓库中面向 C++ 开发者的最小可用示例。它本身只有三部分内容:一个 C++ 源文件 main.cpp、一份 Makefile 以及一段极其精简的使用说明:

A minimal example for using prqlc-c with `gcc` and `make`. ## How to run make run

也就是说,官方给出的运行方式只有一行命令make run。其背后的完整工作流是:Makefile 先通过 Cargo 构建 Rust 侧静态库libprqlc_c.a,再用g++编译链接示例程序并直接执行。下面各节将逐层拆解这条命令背后发生的每一件事。

二、main.cpp 代码逐行解读

完整源码位于 main.cpp,全文仅 24 行,核心逻辑如下:

#include <cstring> #include <iostream> #include "prqlc.hpp" using namespace prqlc; void print_result(CompileResult res) { if (strcmp(res.output, "") == 0) { std::cout << "Output: <empty>\n\n"; } else { std::cout << "Output:\n\n" << res.output; } } int main() { const auto prql_query = "from albums | select {album_id, title} | take 3"; CompileResult res = compile(prql_query, nullptr); print_result(res); result_destroy(res); return 0; }

可以提炼出调用 FFI 的四个标准步骤:

  1. 引入头文件:通过#include "prqlc.hpp"引入由 cbindgen 自动生成的 C++ 头文件(using namespace prqlc;使其所有类型可直接使用)。
  2. 构造 PRQL 查询字符串:示例使用"from albums | select {album_id, title} | take 3",即从albums表选取album_idtitle两列并取前 3 行——一个典型的 PRQL 流水线。
  3. 调用compileCompileResult res = compile(prql_query, nullptr);。第二个参数传nullptr表示使用编译器的默认选项。
  4. 释放结果result_destroy(res);归还 FFI 侧分配的堆内存。

输出判断用strcmp(res.output, "") == 0来识别"空输出"(编译失败时output为空字符串,详细错误在messages中)。注意最小示例只打印输出、不打印错误消息,更完整的错误处理写法见后文第六节。

三、Makefile 深度拆解:链接静态库的完整参数

Makefile 是这个示例中信息量最大的部分,它展示了"C++ + prqlc-c"的标准构建与链接方式:

PRQL_PROJECT=../../../../.. run: build ./main.out build-prql: cargo build --package prqlc-c --release UNAME_S := $(shell uname -s) LD_FLAGS = -L${PRQL_PROJECT}/target/release \ ${PRQL_PROJECT}/target/release/libprqlc_c.a ifeq ($(UNAME_S),Darwin) LD_FLAGS := $(LD_FLAGS) -framework CoreFoundation endif build: main.cpp build-prql g++ main.cpp -o main.out \ -I${PRQL_PROJECT}/prqlc/bindings/prqlc-c \ $(LD_FLAGS) valgrind: build valgrind ./main.out

逐项说明:

  • PRQL_PROJECT=../../../../..:从prqlc/bindings/prqlc-c/examples/minimal-cpp/向上回溯 5 级到仓库根目录,作为定位 Rust 构建产物和头文件的基准路径。
  • cargo build --package prqlc-c --release:以 release 模式构建prqlc-ccrate。根据 prqlc-c/Cargo.toml 中的crate-type = ["staticlib", "cdylib"],一次构建会同时产出静态库libprqlc_c.a与动态库libprqlc_c.so(macOS 上为.dylib),产物位于仓库根目录的target/release/下。
  • -L${PRQL_PROJECT}/target/release:把静态库所在目录加入链接器搜索路径。
  • ${PRQL_PROJECT}/target/release/libprqlc_c.a:直接以完整路径显式链接静态库,绕开-l的命名查找规则。
  • -I${PRQL_PROJECT}/prqlc/bindings/prqlc-c:头文件搜索路径,使#include "prqlc.hpp"能够命中 prqlc.hpp。
  • macOS 特殊处理ifeq ($(UNAME_S),Darwin)时追加-framework CoreFoundation。这是因为 prqlc 依赖 Rust 的core-foundation系系统库,macOS 上链接静态库必须显式带上该 framework。
  • valgrind目标:在构建完成后用 Valgrind 运行./main.out,用于检测内存泄漏——这与下文第五节的内存管理要求直接呼应。

在 Linux 上,完整的等价命令可还原为(假定仓库根目录为$PRQL_PROJECT):

cargo build --package prqlc-c --release g++ main.cpp -o main.out \ -I${PRQL_PROJECT}/prqlc/bindings/prqlc-c \ -L${PRQL_PROJECT}/target/release \ ${PRQL_PROJECT}/target/release/libprqlc_c.a ./main.out

四、FFI 核心 API:五个导出函数与三个数据结构

prqlc-c的全部 FFI 表面由 src/lib.rs 定义,并通过 cbindgen 同步生成 C/C++ 两个头文件 prqlc.h 与 prqlc.hpp。C++ 侧可见的 API 全部位于namespace prqlc中,共有 5 个导出函数:

函数签名(C++ 侧)作用
compileCompileResult compile(const char *prql_query, const Options *options)一步完成 PRQL → SQL 的完整编译,options 可传nullptr
prql_to_plCompileResult prql_to_pl(const char *prql_query)PRQL 源码 → PL AST(JSON 序列化输出)
pl_to_rqCompileResult pl_to_rq(const char *pl_json)PL JSON → RQ AST(JSON 序列化输出)
rq_to_sqlCompileResult rq_to_sql(const char *rq_json, const Options *options)RQ JSON → SQL 字符串
result_destroyvoid result_destroy(CompileResult res)释放CompileResult占用的全部堆内存

从 lib.rs 中compile的实现 可以看到,它本质上是后三个阶段的无 JSON 中转封装:

let result = options .and_then(|opts| { Ok(prql_query.as_str()) .and_then(prqlc::prql_to_pl) .and_then(prqlc::pl_to_rq) .and_then(|rq| prqlc::rq_to_sql(rq, &opts.unwrap_or_default())) }) .map_err(|e| e.composed(&prql_query.into()));

即一条调用链:prql_to_pl → pl_to_rq → rq_to_sql,对应 PRQL 编译器经典的 PL(流水线 AST)→ RQ(关系代数 AST)→ SQL 三阶段设计。

4.1 Options:编译选项结构体

Options是唯一需要 C++ 侧手动填充的结构体,其三个字段及默认值(prqlc.hpp 中的文档注释)为:

struct Options { bool format; // 是否对生成的 SQL 做美化格式化(多行、缩进、间距),默认 true char *target; // 目标 SQL 方言,默认 "sql.any"(由查询头决定方言) bool signature_comment; // 是否在生成的 SQL 尾部附加编译器签名注释,默认 true };

对照 lib.rs 中convert_options的实现可以发现两个细节:

  • targetnullptr或空字符串时会被归一化为"sql.any"
  • Options各字段会透传给prqlc::Options,因此传入nullptr与传入"全默认字段"的Options行为等价

target可取如sql.mssqlsql.duckdbsql.postgres等方言标识,可用于跨数据库方言的编译输出。

4.2 CompileResult:编译结果结构体

struct CompileResult { const char *output; // 编译输出:成功时为 SQL(或 PL/RQ 的 JSON),失败时为空字符串 const Message *messages; // 错误/警告消息数组,无消息时为 nullptr size_t messages_len; // 消息条数 };

编译成功时output持有结果、messages_len为 0;编译失败时output为空、messages指向Message数组(见 result_into_c_str 实现)。Message结构体还包含机器可读错误码code、纯文本reason、修复建议hint、带上下文的display、字符偏移span以及行列位置location,可用于构造友好的错误报告。

五、内存管理约定:为什么必须调用 result_destroy

这是使用 prqlc-c 最容易踩坑的地方,官方在头文件与源码中反复强调。所有返回CompileResult的函数(compileprql_to_plpl_to_rqrq_to_sql)都会在 Rust 侧堆上分配内存(字符串、消息数组、Options 等),因此:

  • 每个CompileResult必须且只能调用一次result_destroy,且不得手动free其任何字段(见 prqlc.hpp 的 Safety 说明);
  • 输入字符串要求是0 结尾的 C 字符串,Rust 侧通过CStr::from_ptr读取(lib.rs);
  • 从 result_destroy 的实现 可见,它会递归释放每条消息的codereasonhintspandisplaylocation,再释放消息数组和output字符串。因此print_result必须在result_destroy之前完成对res.output的读取——这正是最小示例的调用顺序。

示例 Makefile 中内置的valgrind目标(valgrind ./main.out)即用于验证这类内存生命周期是否正确。在编写自己的代码时,建议像 C++ 的 RAII 习惯那样,把result_destroy放入析构/延迟释放逻辑(例如 Zig 示例中的defer prql.result_destroy(result),见 minimal-zig 的 main.zig),确保所有返回路径都会释放。

六、从最小示例到生产用法:错误处理与自定义选项

minimal-cpp 只演示了最简路径,同目录的 minimal-c/main.c(C 语言版)则补齐了另外三个关键用法,C++ 侧可完全照搬同样的模式:

1. 遍历并打印错误消息

for (size_t i = 0; i < res.messages_len; i++) { Message const *e = &res.messages[i]; if (e->display != NULL) { printf("%s", *e->display); } else if (e->code != NULL) { printf("[%s] Error: %s\n", *e->code, e->reason); } else { printf("Error: %s", e->reason); } }

e->codee->display是"指向指针的指针"(const char *const *),指向 C 字符串指针数组,因此需要解引用一层再按字符串打印。

2. 自定义 Options(以 SQL Server 方言为例)

Options opts; opts.format = false; // 关闭格式化 opts.signature_comment = false; // 关闭签名注释 opts.target = "sql.mssql"; // 输出 SQL Server 方言 res = compile(prql_query, &opts);

注意 C++ 侧Options.target类型为char *,传字符串字面量时需要自行保证其生命周期覆盖compile调用期间。

3. 使用分阶段 API 查看中间 AST

res = prql_to_pl(prql_query); // 得到 PL JSON res2 = pl_to_rq(res.output); // 将 PL JSON 转 RQ JSON rq_to_sql(res2.output, NULL); // 最终转 SQL

result_destroy(res)再复用,可避免提前释放被下游消费的output缓冲区。这套分阶段接口非常适合调试编译管线或在 JSON 层做自定义处理。

七、prqlc.hpp 从哪来:cbindgen 与头文件再生成

prqlc.hpp 与 prqlc.h 均以/* This file is autogenerated. Do not modify this file manually. */开头,由 cbindgen 从 src/lib.rs 自动生成。若需在修改 FFI 后重新生成头文件,官方 README(prqlc-c/README.md)与根目录 Taskfile.yaml 给出了标准做法:

task build-prqlc-c-header

对应 Taskfile 中的实际命令是:

cbindgen --crate prqlc-c --output prqlc.h cbindgen --crate prqlc-c --lang C++ --output prqlc.hpp

日常开发中无需手动生成,直接使用仓库内已有的头文件即可。

八、迁移到其他构建系统与语言

prqlc-c不只服务于 C/C++。由于它编译为标准的 C ABI 静态/动态库,任何支持 FFI 的语言都可复用同一套 API。prqlc-c/README.md 给出了在其他构建系统中链接静态库的通用配方:

CGO_LDFLAGS="-L/path/to/target/release -lprqlc_c -pthread -ldl -lm" go build

即链接时除-lprqlc_c外还需带上其系统依赖-pthread -ldl -lm(macOS 上再加-framework CoreFoundation)。仓库内还提供了另外两个对照实现:

  • minimal-c:功能最全的 C 示例(错误处理、自定义 Options、分阶段 API 全覆盖);
  • minimal-zig:通过 Zig 的@cImport直接引用prqlc.h

如果你的应用需要嵌入 PRQL 编译能力,推荐以 minimal-cpp 为起点跑通"构建 → 链接 → 调用 → 释放"全流程,再逐步引入第六节中的错误处理与自定义方言选项。

九、小结:掌握的最小可用链路

回顾整条链路:make run一次执行背后是Cargo 构建静态库 → g++ 编译链接 → 运行可执行文件三个步骤;程序内部则是compile一行调用 → 三阶段编译流水线 →print_result读取输出 →result_destroy释放内存。理解这四点,即可把 PRQL 的编译能力稳定嵌入到任意 C++ 项目中,并在此基础上平滑迁移到错误处理、自定义方言乃至分阶段 AST 调试等进阶用法。

  • 后端

【免费下载链接】prql

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

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

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询