miniblink49 内置 Google Test 的 Pump 元编程工具手册:从 .pump 源码生成 C++ 模板代码
2026/9/17 17:29:20 网站建设 项目流程

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;Foo1Foo2落入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的区间必须已事先定义。$idcode中有效。
$($)生成一个单独的$字符。
$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_syntax

exp即"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)再按文法递归下降,构造出由CodeNodeVarNodeRangeNodeForNodeIfNodeRawCodeNodeLiteralDollarNodeExpNode组成的 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
  • ForNodefor i in range(lower, upper + 1)闭区间迭代,每次把i压入变量栈后递归展开循环体,非末次迭代时追加分隔符sep(pump.py#L668-L680);
  • IfNodeeval条件表达式,为真展开 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 的约定,修改这类生成代码的正确流程是:

  1. 编辑对应的.pump源文件(例如调整$var n = 50的参数上限,或修改循环体模板);
  2. 在 scripts 目录下运行pump.py <文件名>.pump,重新生成同名.h文件;
  3. 检查生成文件头部的// This file was GENERATED by command:注释,确认生成命令正确,然后照常提交。

使用技巧

以下技巧同样来自 V1_7_PumpManual.md#L174-L178,在实际编写.pump文件时非常实用:

  • 变量与字母数字粘连时用[[]]分隔:如果元变量后面紧跟字母或数字,可以用[[]]插入一个空字符串来隔开。例如Foo$j[[]]Helperj为 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),仅供参考

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

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

立即咨询