miniblink49 内置 Google Test 的 Pump 元编程工具手册:从 .pump 源码生成 C++ 模板代码
【免费下载链接】miniblink49a lighter, faster browser kernel of blink to integrate HTML UI in your app. 一个小巧、轻量的浏览器内核,用来取代wke和libcef项目地址: https://gitcode.com/GitHub_Trending/mi/miniblink49
导读
Pump 是 Google Test 团队开发的一个轻量级 C++ 元编程(meta-programming)工具,它通过一种内嵌在 C++ 代码中的小型领域专用语言(DSL),把"仅参数个数不同、其余几乎重复"的模板、宏和类一次性生成出来,免去大量机械且易错的复制粘贴工作。在 miniblink49 仓库随 V8 7.5 一起内置的 Google Test 中,Pump 正是Values()、Combine()、tuple等 API 得以支持"1 到 50 个参数"的幕后功臣。读完本文,你将掌握 Pump 的完整语法、运行方式、底层实现原理,以及如何在 v8_7_5/testing/gtest 中基于真实.pump文件完成代码再生成。
问题背景:模板库的"参数个数诅咒"
模板库和宏库经常需要定义大量仅在参数个数上不同的类、函数或宏。例如 Google Test 的Values(v1, v2, ..., vN)参数生成器,需要为 1 到 50 个参数各写一份重载;tuple需要为 0 到 10 个字段各写一份特化。这是海量重复、机械且极易出错的工作。
可变参数模板(variadic templates)和可变参数宏(variadic macros)虽然能缓解这个问题,但编写该文档时它们都还未进入 C++ 标准、也未被编译器广泛支持,移植性差,能力也仍然有限(详情见 V1_7_PumpManual.md)。于是,这类库的作者通常会写脚本去生成实现。但脚本往往难以反映生成代码的结构,可读性差、难编辑:生成代码里一个很小的改动,可能要求脚本做大量不直观、不平凡的修改,实验迭代时尤其痛苦。
我们的解决方案:Pump——为元编程而生
Pump(Pump is Useful for Meta Programming / Pretty Useful for Meta Programming / Practical Utility for Meta Programming,三种解释随你喜好)是一个简单的 C++ 元编程工具。程序员编写一个foo.pump文件,里面同时包含 C++ 代码和操控这些 C++ 代码的元代码(meta code)。元代码支持:
- 对某个区间做迭代(含嵌套迭代);
- 局部元变量定义;
- 简单算术;
- 条件表达式。
你可以把它看作一个小型领域专用语言。元语言被刻意设计得非侵入式(例如不会干扰 Emacs 的 C++ 模式)且简洁,使 Pump 代码直观、易于维护。
设计亮点
- 实现只有单个 Python 脚本,超强可移植:无需构建、无需安装,跨平台直接运行;
- 尽量遵循 Google 代码风格规范:自动在合适位置折断超长行(生成代码很容易超长),控制在 80 列以内,并正确缩进续行;
- 格式人类可读,比 XML 更简洁;
- 与 Emacs 的 C++ 模式配合良好。
快速上手:运行 pump.py
Pump 的实现位于 scripts/pump.py。该文件自带完整的使用说明(见 pump.py#L32-L63):
USAGE: pump.py SOURCE_FILE EXAMPLES: pump.py foo.cc.pump Converts foo.cc.pump to foo.cc.即:pump.py <源码文件>,输出文件为去掉.pump后缀的同名文件。当输入文件名不以.pump结尾时,结果打印到标准输出(output_file_path == '-',见 pump.py#L838-L843)。需要说明的是,脚本使用 Python 2 语法(如print语句、file()内建函数),在当前仓库环境中运行前请确认所用解释器版本。
当输出是文件时,pump.py 会在生成文件头部写入一段"生成告警",例如仓库中真实的 gtest-tuple.h 开头就是:
// This file was GENERATED by command: // pump.py gtest-tuple.h.pump // DO NOT EDIT BY HAND!!!这一约定在 README.md 中也有明确说明:正常情况下无需担心重新生成源码文件,除非你需要修改它们;此时应修改对应的.pump文件,再运行 pump.py 脚本重新生成。
元语言速览:从示例读懂 Pump
Pump 的元关键字以$开头,[[与]]是元代码块定界符,$$开启一条元注释(到行尾结束)。
下面这段完整示例(节选自 V1_7_PumpManual.md)同时演示了元变量、区间、循环和条件:
$var n = 3 $$ Defines a meta variable n. $range i 0..n $$ Declares the range of meta iterator i (inclusive). $for i [[ $$ Meta loop. // Foo$i does blah for $i-ary predicates. $range j 1..i template <size_t N $for j [[, typename A$j]]> class Foo$i { $if i == 0 [[ blah a; ]] $elif i <= 2 [[ blah b; ]] $else [[ blah c; ]] }; ]]经 Pump 编译器翻译后得到:
// Foo0 does blah for 0-ary predicates. template <size_t N> class Foo0 { blah a; }; // Foo1 does blah for 1-ary predicates. template <size_t N, typename A1> class Foo1 { blah b; }; // Foo2 does blah for 2-ary predicates. template <size_t N, typename A1, typename A2> class Foo2 { blah b; }; // Foo3 does blah for 3-ary predicates. template <size_t N, typename A1, typename A2, typename A3> class Foo3 { blah c; };注意$if/$elif/$else的分支选择:Foo0落入i == 0分支生成blah a;,Foo1、Foo2落入i <= 2分支生成blah b;,Foo3落入$else分支生成blah c;。
再看迭代分隔符的用法(节选自 V1_7_PumpManual.md):
$range i 1..n Func($for i + [[a$i]]); $$ The text between i and [[ is the separator between iterations.$for i与[[之间的+就是各次迭代之间的分隔符。根据n的值会生成:
Func(); // If n is 0. Func(a1); // If n is 1. Func(a1 + a2); // If n is 2. Func(a1 + a2 + a3); // If n is 3. // And so on...元编程构造总览
Pump 支持的完整元编程构造如下表(完整继承自 V1_7_PumpManual.md#L120-L132):
| 构造 | 说明 |
|---|---|
$var id = exp | 定义具名常量值。$id在当前元词法块(meta lexical block)结束前一直有效。 |
$range id exp..exp | 设置迭代变量的区间,该变量可在之后的多个循环中复用。 |
$for id sep [[ code ]] | 迭代。id的区间必须已事先定义。$id在code中有效。 |
$($) | 生成一个单独的$字符。 |
$id | 具名常量或迭代变量的值。 |
$(exp) | 表达式的值。 |
$if exp [[ code ]] else_branch | 条件分支。 |
[[ code ]] | 元词法块。 |
cpp_code | 原样透传的 C++ 代码。 |
$$ comment | 元注释。 |
换行规则说明:为了给用户在排版 Pump 源码时留出自由度,Pump 会忽略紧跟$for foo之后、或紧邻[[/]]的换行符。没有这条规则,你往往会被迫写出超长行才能得到期望的输出。因此,若你确实希望这些位置出现换行,有时需要额外插入一个换行符。
Pump 文法
Pump 的完整文法如下(完整继承自 V1_7_PumpManual.md#L143-L160):
code ::= atomic_code* atomic_code ::= $var id = exp | $var id = [[ code ]] | $range id exp..exp | $for id sep [[ code ]] | $($) | $id | $(exp) | $if exp [[ code ]] else_branch | [[ code ]] | cpp_code sep ::= cpp_code | empty_string else_branch ::= $else [[ code ]] | $elif exp [[ code ]] else_branch | empty_string exp ::= simple_expression_in_Python_syntaxexp即"Python 语法中的简单表达式"——这一点在 pump.py#L62 的文档字符串中有同样的声明,也决定了 Pump 元表达式直接复用 Python 的算术与比较语义。
源码级原理:pump.py 是如何把元代码变成 C++ 的
scripts/pump.py 的实现虽然只有几百行,却走了一条完整"词法 → 语法 → 求值 → 美化"的编译流水线。
词法阶段:TOKEN_TABLE
词法器通过正则表达式表(pump.py#L72-L84)识别所有元关键字:
TOKEN_TABLE = [ (re.compile(r'\$var\s+'), '$var'), (re.compile(r'\$elif\s+'), '$elif'), (re.compile(r'\$else\s+'), '$else'), (re.compile(r'\$for\s+'), '$for'), (re.compile(r'\$if\s+'), '$if'), (re.compile(r'\$range\s+'), '$range'), (re.compile(r'\$[_A-Za-z]\w*'), '$id'), (re.compile(r'\$\(\$\)'), '$($)'), (re.compile(r'\$'), '$'), (re.compile(r'\[\[\n?'), '[['), (re.compile(r'\]\]\n?'), ']]'), ]注意两个细节:[[/]]的正则都允许后随一个可选的\n,这正是上一节"换行规则"在实现层的体现;\$\$元注释并不在 token 表里,而是在 StripMetaComments 中于解析前被整体剥离(先删掉整行只有注释的行,再删除内容行行尾的注释)。
语法阶段:从 Token 到 AST
Tokenize(pump.py#L382-L387)把源码流式切成 token,ParseToAST(pump.py#L577-L581)再按文法递归下降,构造出由CodeNode、VarNode、RangeNode、ForNode、IfNode、RawCodeNode、LiteralDollarNode、ExpNode组成的 AST(节点定义见 pump.py#L390-L441)。其中ParseExpNode会把表达式里的每个标识符\w+改写为self.GetValue("\1")(pump.py#L470-L472),从而把元变量解析连接到求值环境。
求值阶段:Env 环境与递归展开
Env类(pump.py#L584-L638)维护元变量栈与区间栈,GetValue在未定义元变量时报错退出。RunAtomicCode(pump.py#L656-L699)按节点类型递归执行:
VarNode:先求值子代码块,再把结果PushVariable入栈;RangeNode:把表达式求值为整型后PushRange;ForNode:for i in range(lower, upper + 1)闭区间迭代,每次把i压入变量栈后递归展开循环体,非末次迭代时追加分隔符sep(pump.py#L668-L680);IfNode:eval条件表达式,为真展开 then 分支,否则展开else_branch;ExpNode:把求值结果字符串化输出;LiteralDollarNode:原样输出$。
由于变量/区间入栈都是"入栈到头部 + 递归时 Clone 环境"(见Env.Clone),嵌套作用域天然隔离,内层循环结束后外层变量不受影响。
美化阶段:BeautifyCode 与 80 列折行
展开后的原始文本再经BeautifyCode(pump.py#L814-L820)逐行处理。WrapLongLine(pump.py#L790-L811)按行内容分派:
- 普通代码行超 80 列时,优先在
,或;后折行,续行缩进 4 格(WrapCode,pump.py#L741-L768); - 单行注释超长时按单词折行并保持
//前缀对齐(WrapComment,pump.py#L717-L738); - 预处理器指令用行尾反斜杠续行(
WrapPreprocessorDirective,pump.py#L771-L772); - 头文件守卫、
#include、IWYU pragma 等按风格规范特例放行不折行。
整条流水线汇聚在 ConvertFromPumpSource:先剥注释、再解析 AST、然后求值展开、最后美化输出。
仓库中的真实案例:Google Test 的 4 个 .pump 文件
当前仓库的 Google Test 共有 4 个.pump源文件,它们在 Makefile.am 中被明确登记为分发源:
- include/gtest/gtest-param-test.h.pump
- include/gtest/internal/gtest-param-util-generated.h.pump
- include/gtest/internal/gtest-tuple.h.pump
- include/gtest/internal/gtest-type-util.h.pump
案例一:gtest-param-test.h.pump —— 参数生成器重载的批量生产
gtest-param-test.h.pump 开头用两个元变量声明容量上限(第 2-3 行):
$var n = 50 $$ Maximum length of Values arguments we want to support. $var maxtuple = 10 $$ Maximum number of Combine arguments we want to support.Values()的 1..50 参数重载由双重循环生成(第 346-355 行):
$range i 1..n $for i [[ $range j 1..i template <$for j, [[typename T$j]]> internal::ValueArray$i<$for j, [[T$j]]> Values($for j, [[T$j v$j]]) { return internal::ValueArray$i<$for j, [[T$j]]>($for j, [[v$j]]); } ]]这里$for j, [[...]]中的,就是迭代分隔符,最终展开出Values(T1 v1)、Values(T1 v1, T2 v2)……直到Values(T1 v1, ..., T50 v50)的 50 个重载。同理,Combine()的 2..10 个生成器重载由$range i 2..maxtuple驱动生成(第 430-441 行)。这正是"改一处元代码即可整体调整重载数量"的典型收益:想把上限从 50 提到 60,只改$var n = 50一行。
案例二:gtest-tuple.h.pump —— 宏与模板的联合展开
gtest-tuple.h.pump 用$var n = 10声明 tuple 字段上限,并同时声明了三条区间(第 64-66 行):
$range i 0..n-1 $range j 0..n $range k 1..n随后用$for k循环生成GTEST_0_TUPLE_~GTEST_10_TUPLE_等宏定义(第 70-84 行),并用$for i [[typename T$i = void]]一次性生成tuple主模板的 0..9 个类型参数(第 92-93 行)。这里还能看到两个有意思的细节:
GTEST_$(n)_TYPENAMES_(U)这种写法,是"元变量后紧跟字母/数字时用[[]]分隔"技巧的变体($()显式取表达式值);- 生成出的 gtest-tuple.h 头部明确写着由
pump.py gtest-tuple.h.pump生成、禁止手改。
再生成工作流:改 .pump 而非改 .h
按照 README.md 与 DevGuide.md 的约定,修改这类生成代码的正确流程是:
- 编辑对应的
.pump源文件(例如调整$var n = 50的参数上限,或修改循环体模板); - 在 scripts 目录下运行
pump.py <文件名>.pump,重新生成同名.h文件; - 检查生成文件头部的
// This file was GENERATED by command:注释,确认生成命令正确,然后照常提交。
使用技巧
以下技巧同样来自 V1_7_PumpManual.md#L174-L178,在实际编写.pump文件时非常实用:
- 变量与字母数字粘连时用
[[]]分隔:如果元变量后面紧跟字母或数字,可以用[[]]插入一个空字符串来隔开。例如Foo$j[[]]Helper在j为 1 时生成Foo1Helper。否则$jHelper会被词法器按\$\w+规则整体吞掉(见 TOKEN_TABLE 中的$id规则),从而解析失败。 - 用
[[]]+ 换行自由断行:为了避免 Pump 源码出现超长行,可以在任意位置插入[[]]后换行。由于紧邻[[/]]的换行符会被忽略,生成代码中不会出现这个换行,因此不会污染输出格式。
小结
Pump 用"元代码 + C++ 代码"混排的方式,把"参数个数 N 变化"这一类 C++ 模板/宏库的经典痛点,收敛成一套极其轻量的可移植解决方案:单文件 Python 实现、无构建无安装、语法直观、输出自动对齐 80 列风格规范。在 miniblink49 内置的 Google Test 里,Values()的 50 档重载、Combine()的 10 档笛卡尔积、tuple的 10 字段支持,全部来自 scripts/pump.py 与仓库内 4 个.pump源文件(Makefile.am 中均有登记)。若你在其他项目里也遇到"仅参数个数不同"的重复代码,Pump 这份实践是一份非常值得参考的模板:先写脚本生成,再让脚本服务于易维护的声明式元语言。
【免费下载链接】miniblink49a lighter, faster browser kernel of blink to integrate HTML UI in your app. 一个小巧、轻量的浏览器内核,用来取代wke和libcef项目地址: https://gitcode.com/GitHub_Trending/mi/miniblink49
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考