Beancount C++ 版本:依赖项

Martin Blais,2020 年 6 月

Beancount 将被重写为 C++,以下是我在长期维护中已测试并愿意支持的依赖项集合:

基础环境

  • Bazel 构建系统 (https://github.com/bazelbuild/bazel):Google 的构建系统是我所知最稳定的构建方式,远优于 SCons,也远胜于 CMake。它允许你通过显式引用其他仓库及特定提交版本的 Git 仓库(包括未发布的版本)来锁定特定的依赖项,其有时令人烦恼的约束反而带来了其他构建系统无法实现的完全隔离的可复现构建。这最大限度地减少了意外情况,并有望减少平台相关的可移植性问题。同时,它也最小化了我们对系统预装软件包的依赖(例如,它会自行下载并编译自己的 Bison)。它运行迅速,能精确计算出需要重新构建的最小测试集和目标,并且高度可配置。

选择 Bazel 的缺点与其他 Google 开源项目相同:其原始版本是内部使用的,因此存在许多奇怪的特有行为(例如 //external、@bazel_tools 仓库等),其中许多在公司外部文档中记录不佳,且存在大量未解决的工单。然而,在现阶段,我已经成功创建了一个包含本节所述大部分依赖项的可用构建

  • C++14 配合 GCCClang/LLVM:两种编译器都将获得支持。Clang 提供了更优秀的前端和标准库实现,但构建速度稍慢;GCC 在实际环境中更为常见,但其错误信息……嗯,我想我们都已习惯了。需要注意的是,尽管要求使用 C++14,我将避免使用语言中的 exotic 特性(包括类)。关于 Windows 支持可能存在疑问。

  • Abseil-Cpp 基础库 (https://github.com/abseil/abseil-cpp):该基础函数库源自 Google 自身庞大的代码库,经过了无与伦比的实战检验——Google 产品正是运行于此。它提供了极其稳定的 API(鉴于大量代码依赖它,其接口几乎不可能发生重大变化),并与 stdc++ 完美互补,其现有接口边界注定保持高度稳定。它比 Boost 更简单、更稳定,且不提供大量我们根本不需要的库(此外,我非常欣赏Titus的 C++ 设计理念)。它弥补了 C++ 中缺失的许多 Python 原生提供的基础字符串操作功能(例如 absl::StrCat)。

  • Google Test (https://github.com/google/googletest):这是我已熟悉的广泛使用的 C++ 测试框架,支持匹配器和模拟功能。

数据表示

  • Protocol Buffers (https://github.com/protocolbuffers/protobuf):在此 C++ 重写中,我将保持函数式风格,并需要替代 Python 的 nametuple 来表示指令。这意味着需要创建大量简单的裸结构数据,这些数据必须在测试中动态生成(已有良好的文本格式解析器),并序列化到磁盘,因为核心与查询语言之间的边界将成为一个 protobuf 消息文件。Protobuf 提供了良好的分层数据结构,支持重复字段,并被多种语言支持(这为使用如 Go 等语言编写插件打开了可能性),同时也可以为其提供 Python 绑定。
    它还将成为 Beancount 核心与查询语言输入之间的接口。我们将使用 proto3,版本 >= 3.12,以支持可选字段(空值)。

  • Riegeli (https://github.com/google/riegeli):一种高效且压缩的二进制格式,用于将 protobuf 消息序列存储到文件中。我认为 Beancount 核心将输出此格式;它体积紧凑、读取速度快。这也是另一个 Google 项目,理应获得比当前更多的关注,它同时支持 C++、Python 和 protobuf。

  • mpdecimal (https://www.bytereef.org/mpdecimal/)这是 Python 实现 Decimal 数字时所使用的同一 C 级库。使用此库将便于在 C++ 核心与 Python 运行时之间进行操作。我需要在 C++ 内存中表示十进制数,功能尽量精简、范围合理(大数类通常超出我们的需求)。我们对十进制数的范围需求并不大……主要是基本算术运算 + 量化。
    其他可用库包括:GMPdecNumber 此线程中有一些相关信息:(https://stackoverflow.com/questions/14096026/c-decimal-data-types)。对于磁盘表示,我需要为这些数据定义一个 protobuf 消息结构,我考虑定义一个联合体,包含字符串(便于阅读但需频繁进行字符串到十进制的转换)和更高效的指数+尾数十进制等价形式。

解析器

  • RE/flex 词法分析器 (https://github.com/Genivia/RE-flex):这个现代的基于正则表达式的扫描器生成器原生支持 Unicode,速度极快且文档完善。它为老旧的 GNU flex 提供了一个绝佳的替代方案,后者在支持非 ASCII 字符(如账户名称)时存在困难。我在其他项目中已成功使用过它。许多用户希望使用母语作为账户名称;这将使为整个文件提供 UTF-8 解析器变得轻而易举。

  • GNU Bison (https://git.savannah.gnu.org/git/bison.git):我们将继续使用 GNU Bison,但改用其支持的 C++ 完整模式。我仍在犹豫是否继续使用这个解析器生成器,因为它显得有些过时,但其稳定性很高,我尚无法充分证明升级到 ANTLR 所需的额外工作是合理的。
    我们需要采取一些技巧,以支持为 v2 生成 C 代码、为下一版本生成 C++ 代码的同一语法;解析器代码可通过一个函数分发表来实现,该表在 v2 中是静态 C 函数,在 C++ 版本中则是类方法。部分生成参数(% 指令)会有所不同(参见 此处 的示例)。

  • 国际组件用于 Unicode(ICU) (https://github.com/unicode-org/icu.git):这是 Unicode 支持的标准库。我们的 C++ 代码将不使用 std::wstring/std::wchar,而是使用普通的 std::string,并在必要时调用此库的函数。

Python

  • Python3 (https://www.python.org/):没什么可说的。我会继续使用最新版本。Python 是一种极其强大的扩展语言,我没有任何更换计划。

  • pybind11 (https://github.com/pybind/pybind11):我希望提供一个与 Beancount 当前 API 几乎完全相同、甚至更优(即更简洁)的 Python API。我提出的一项要求是,能够以极低开销将一组 protobuf 对象(指令)传递给 Python 回调函数,避免在 C++ 和 Python 之间进行复制(序列化和反序列化)——特别是针对插件。我曾调研过多种 C++ 与 Python 互操作的库:Cython、CLIF、SWIG 等,但序列化始终是个难题(参见 此部分解决方案)。目前最具活力的库是 pybind11,这是一个纯头文件库,作为 Boost::Python 的演进版本,它对生成的 API 提供了最大程度的控制。它还能与使用 fast_cpp_protos 构建的协议缓冲区目标良好协作:仅传递指针,因此插件传递完整的指令列表成为可能。此外,我曾于 20 年前使用过 Boost::Python,对它相当熟悉,两者实际上非常相似(但 pybind11 摒弃了 Boost 的其他部分)。

  • 类型注解PyType(或MyPy?):我已遵循自定义的 PyLint 配置来规范 Python 代码,但代码库尚未采用日益普及的类型注解。在将保留的重写代码子集中,我希望所有函数都添加类型注解,并用更自由的文档形式替代有时冗余的 Args/Returns 文档字符串(类型信息可能足以取代 Args/Returns 块的正式格式)。我需要观察这会对自动生成的文档产生何种影响。
    一个重要改进是,我不仅希望添加类型注解,还希望在构建过程中自动运行其中一种类型检查器。我已熟悉 Google 的 pytype,但 MyPy 也可能是一个不错的替代方案。无论如何,实现这一目标的唯一障碍是编写 Bazel 规则,使其在整个代码库中作为 py_library() 和 py_binary() 规则的一部分自动调用这些工具。我还将尝试让 pylint 以相同方式运行(作为构建的一部分),并通过自定义标志在开发时禁用它,而不是维持一个独立的 lint 目标。

  • Subpar (https://github.com/google/subpar):目前我尚不清楚如何为 Bazel 构建生成兼容 pip 的 setup.py,但肯定能找到一种方法,利用 Bazel 构建的二进制文件生成适用于 PyPI 的 wheel 包。对于打包包含 Python 及其扩展的独立二进制文件,"subpar" Bazel 规则本应能处理此需求。然而,目前它尚不支持 C 扩展