ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

RethinkDB 的 ADT 扩展 Protobuf 预处理器(scripts/adtproto)原理与实战

RethinkDB 的 ADT 扩展 Protobuf 预处理器(scripts/adtproto)原理与实战 数据库文档数据库分布式数据库【免费下载链接】rethinkdbThe open-source database for the realtime web.项目地址https://gitcode.com/gh_mirrors/re/rethinkdb点击查看免费下载导读本文围绕 scripts/adtproto/README.md 讲解 RethinkDB 仓库中一个特殊的内置工具ADT 扩展的 protobuf 预处理器。它接收一种近似 protobuf 语法的输入同时产出标准 protobuf 语法文件与用于结构良构性well-formedness校验的 C 代码为基于 protobuf 的消息定义引入代数数据类型ADT/变体/带标签联合tagged union表达能力。读完本文你将掌握这套自定义语法的书写规范、三种 branch 分支形式的语义、data构造到 protobuf 消息与 C 检查函数的完整转换规则以及如何通过 Makefile 构建并运行该工具生成.proto与.cc文件。一、工具定位解决什么问题的预处理器从源码目录结构scripts/adtproto/可以推断adtproto 是 RethinkDB 早期内部自用的开发期代码生成工具而非运行时组件——仓库检索显示adtproto字样仅出现在其自身的 Makefile 中src源码目录中没有well_formed校验函数的直接引用说明它属于一次性/半离线的辅助脚本用于在工程实践中以更接近领域建模的语言书写复杂消息结构再降级为标准 protobuf。它的核心设计如下原文直述输入是近似 protobuf 的语法带有少量扩展和少量缺失部分输出是 (a) protobuf 语法(b) 检查结构良构性的 C 代码。换言之这是一个语法翻译器 约束检查器生成器的二合一工具由以下三个模块协作完成模块职责Parse.hs用 Parsec 将.adt源文件解析为中间语法树ADTProto.hs定义语法树的全部数据类型Program、DeclADT、DeclBranch、DeclField、DeclEnum等Compile.hs两路生成protoProgram输出 protobuf 文本checkProgram输出 C 良构性检查代码二、语法扩展一默认 required 与字段号自动分配原文档明确指出adtproto 对标准 protobuf 做了两处省事设计原文措辞为These are arguably misfeatures即这些可以说是设计上的争议点1. 字段默认是required无需显式标注。因此标准 protobuf 的写法required int foo 1; optional string bar 2;在 adtproto 中可以简写为int foo; optional string bar;2. 字段号tag完全按声明顺序自动生成无需手写 N。这一行为在 Compile.hs 的protoDeclField中有明确的实现证据生成器以State Int维护一个从1开始的计数器每输出一个字段取当前值并自增protoDeclField (Field name typ mod) do field_id - get put (field_id 1) return $ hsep [protoModifier mod, protoType typ, text name, equals, text (show field_id) semi]对应的修饰符翻译也清晰可见Required - required、Optional - optional、Repeated - repeated。需要注意的是字段号分配是扁平的、按消息体内声明顺序递增的因此中间插入一个嵌套消息定义不会影响后续普通字段的编号连续性嵌套消息内部会重新从1开始计数。三、语法扩展二data构造与三种 branch 分支形式adtproto 最有价值的扩展是新增了data构造用于定义代数数据类型 / 变体 / 带标签联合。data体内可以声明若干branch分支每个分支可以携带关联数据关联数据有三种形态消息体分支分支携带一个内联消息体类型别名分支分支等价于另一个已存在的类型用 Type表示空分支分支不携带任何关联数据纯标签。原文档给出的完整示例data Example { branch Branch1 { // A message body goes here. int foo; string blah; repeated Example children; }; // this branch is just an int, no message type branch Branch2 int; // this branch has no associated data branch Branch3; };这里同时展示了三种形式的混用以及data的自引用能力repeated Example children允许消息体引用外层data自身从而构造递归树结构。在 Parse.hs 中三种形式分别对应parseDeclBranch的三个分支choice [ (:[]) . BranchMessage id $ parseMessageBody -- branch Foo { ... } , equals branchIsType id * semi -- branch Foo Type , branchesEmpty id * semi ] -- branch Foo, Bar, Baz;值得注意的细节是branchesEmpty的实现map BranchEmpty . (id:) $ many (comma ident)它允许用逗号一次性声明多个空分支例如branch GetAttr, HasAttr, PickAttrs;会被展开为三个独立的空分支这一能力在 example.adt 的Builtin中得到了实战运用。四、生成结果详解protobuf 输出对上面的data Exampleadtproto 生成如下 protobuf 代码原文档原文message Example { enum ExampleType { BRANCH1 0; BRANCH2 1; BRANCH3 2; }; required ExampleType type 1; message Branch1 { required int foo 1; required string blah 2; repeated Example children 3; }; optional Branch1 branch1 2; optional int branch2 3; };这一转换在 Compile.hs 的transADT与transBranch中实现规则清晰可查枚举标签每个branch名称经toTagNamemap toUpper全大写转换为枚举标签名放入名为{Name}Type的枚举编号从0开始protoEnumDecl的计数器初始值为0。判别字段首先生成required {Name}Type type 1作为当前实际激活的分支判别符。消息体分支BranchMessage生成一个同名嵌套message再生成一个optional {FieldName} {fieldName} N字段。注意 Compile.hs 中有一条重要的注释Required on a branch gets turned into Optional b/c it really means the field is required IF the branch tag indicates this branch——即分支字段在消息层面必须用optional因为必填语义是条件性的只有当下判别符指向该分支时才必须出现否则每个消息实例都会强制携带所有分支字段。类型别名分支BranchIsType直接生成一个字段类型即所引用的类型字段名由分支名转换而来若在分支上显式写了repeated修饰符则生成repeated字段见 example.adt 中branch Array repeated Term;的用法。空分支BranchEmpty不生成任何字段只贡献一个枚举标签。字段命名转换分支名到字段名通过toFieldName完成——splitOn isUpper按大写字母拆分再intercalate _将驼峰式FooBarBaz转为蛇形foo_bar_baz例如Branch1→branch1Json_Null→json_null。五、生成结果详解C 良构性检查代码data的第二个产物是一段 C 函数用于在读取消息后校验判别符与携带字段是否一致。原文档展示的Example检查代码为bool well_formed(const Example self) { if (self.type Example::BRANCH1 !self.has_branch1()) return false; if (self.has_branch1() self.type ! Example::BRANCH1) return false; if (self.type Example::BRANCH2 !self.has_branch2()) return false; if (self.has_branch2() self.type ! Example::BRANCH2) return false; return true; }这套检查由 Compile.hs 的branchCheck函数按固定模板生成其逻辑本质是两条双向蕴含约束标签 ⇒ 字段tagImpliesField若判别符等于某分支标签、但对应字段缺失!self.has_xxx()则非法。这一条只为Required修饰的分支生成——注意BranchMessage分支的原始修饰符是Required见checkDeclBranch中branchCheck i Required所以消息体分支会生成该检查。字段 ⇒ 标签fieldImpliesTag若对应字段存在、但判别符不等于该分支标签则非法。此条对所有携带字段的分支含类型别名分支都会生成。对repeated分支字段hasField会改用self.xxx_size() 0作为字段是否被使用的判定case Repeated of ... - self. field _size() 0这与 protobuf 生成的访问器 API 完全吻合。外层函数骨架由wellformed生成函数名固定为bool well_formed(const {ScopeName} self)其中ScopeName通过getScope将当前类型名以_连接支持嵌套作用域如Foo_Bar。六、完整实战示例example.adt 逐段剖析仓库自带的 example.adt 是一份远超 README 示例的完整规格展示了该语法在生产型建模中的全部能力。以下逐段说明其技术要点1. 简单的二值类型与自引用树data Bool { branch True, False; }; data Tree { branch Node { repeated Tree children; }; branch Leaf string; };branch True, False;演示逗号批量声明空分支Tree是典型递归 ADTNode携带repeated Tree children可空数组天然支持空树Leaf直接以string为载荷。2. 跨消息引用与内联消息分支message VarTermTuple { string var; Term term; }; data Term { branch Var string; branch Number int32; branch String string; branch Json_Null; branch Let { repeated VarTermTuple binds; Term expr; }; branch If_ { Term test; Term true_branch; Term false_branch; }; branch Array repeated Term; branch Call { Builtin builtin; repeated Term args; }; };message VarTermTuple在data Term之前声明、并被Term的Let分支引用——说明先声明后引用是允许且必要的语法分析器是顺序遍历前向引用无法解析branch Var string;与branch String string;说明同一个底层类型可以被多个分支复用靠判别符区分branch If_的名称刻意写成If_避免与 C 关键字if冲突——分支名最终会进入 C 枚举和字段名因此必须避开 C/protobuf 关键字同理Json_Null是为了转成枚举JSON_NULLbranch Array repeated Term;演示BranchIsType携带Repeated修饰符的完整写法BranchIsType的另一个价值是递归共用Array直接复用Term本身使 ADT 能表达任意深度的嵌套。3. 分支间的字段复用与 check 自定义校验data Builtin { branch Not; branch Add, Subtract, Multiply, Divide; branch Limit int32; branch Filter { Predicate predicate; }; branch Map { Mapping mapping; }; branch Reduce { Reduction reduction; }; branch OrderBy { OrderDirection order_direction; Mapping mapping; }; // We can declare types inline and use them for branches enum Comparison { EQ; NE; LT; LE; GT; GE; }; branch Comparison Comparison; // we can optimize encoding of these branches by reusing the same field for // all of them. branch GetAttr, HasAttr, PickAttrs; repeated string attrs; // -- namely, this field check { if (self.attrs_size() 0) { if (self.type ! Builtin::GETATTR, HASATTR, PICKATTRS) return false; } }; // etc, etc... };这一段浓缩了 adtproto 最工程化的三个特性在data内联声明类型enum Comparison { ... };直接写在data Builtin体内parseADTDecl的(:[]) . ADTDeclType $ parseDeclType分支支持随后branch Comparison Comparison;把它复用为分支载荷类型Filter、Map、Reduce分支则复用此前声明的Predicate、Mapping、Reduction消息多分支共享同一字段branch GetAttr, HasAttr, PickAttrs;生成三个空分支紧接着的repeated string attrs;是data体的普通字段声明ADTDeclField由 Compile.hs 的transDecl原样保留为消息字段。这样三个相关分支可以共用一个载荷字段显著压缩编码体积——这正是注释所说 optimize encoding ... by reusing the same field 的意图check块注入任意 C 校验check { ... };中的代码由parseCXXBlock原样捕获并在生成well_formed函数时逐行内联Compile.hs 的checkADTDecl分支return [text s]。上面的例子即对共享字段场景施加额外约束只要attrs非空判别符必须是三个相关分支之一。这是生成器模板无法覆盖的业务规则属于留给使用者的自定义钩子。4. 其它顶层声明enum OrderDirection { ASCENDING; DESCENDING; }; message Mapping { string arg; Term body; }; message Predicate { string arg; Term body; }; message Reduction { Term base; string var1; string var2; Term body; };enum与message顶层构造parseDeclType的另外两个分支可以出现在data之外作为被data分支引用的辅助类型。枚举标签同样自动编号从0开始。七、从源码看完整处理管线工具入口在 Main.hs整个管线只有数十行值得完整引述其行为main :: IO () main do [filename] - getArgs input - readFile filename prog - case runP parseProgram () filename input of Right result - return result Left err - do hPrint stderr err exitFailure writeFile (filename .proto) $ show $ protoProgram prog writeFile (filename .cc) $ show $ checkProgram prog由此可以得到明确的命令行契约用法adtproto 输入文件仅接受单个文件参数输出在输入文件同目录生成两个文件——输入文件名.protoprotobuf 定义与输入文件名.ccC 检查代码错误处理解析失败时把 Parsec 错误打印到stderr并以非零状态退出exitFailure便于纳入 Makefile 依赖链输出格式两个生成文件都直接以show输出 PrettyPrint 的Doc即纯文本。语法层的约束同样可以在 Parse.hs 中找到采用javaStyle词法P.LanguageDef因此支持//行注释且caseSensitive Truedata、branch等关键字大小写敏感保留字集合为data message enum branch required optional repeated check这些词不能用作标识符顶层程序由若干data/message/enum声明组成many parseDeclType * eof字段修饰符required/optional/repeated均可省略省略时默认Requiredoption Required parseModifier与 README 的说明一致。八、构建、运行与 Makefile 集成Makefile 提供了开箱即用的构建与验证流程GHCFLAGS: -fwarn-incomplete-patterns -Werror adtproto: FORCE ghc $(GHCFLAGS) --make -o $ Main example.adt.proto: example.adt adtproto ./adtproto $make adtproto用 GHC 编译Main依赖Parse、ADTProto、Compile并开启-fwarn-incomplete-patterns-Werror即任何未覆盖的模式匹配都会被视为编译错误——这是对语法树模式匹配完整性的强约束make example.adt.proto自动先构建adtproto再对 example.adt 运行预处理器生成example.adt.proto以及伴随的example.adt.cc可作为一次性的冒烟验证make clean清理*.o、*.hi、可执行文件与两个生成文件。因此把某个自定义.adt文件接入工程时只需在 Makefile 中添加形如file.proto: file.adt adtproto的规则即可获得 protobuf 定义与校验代码的自动再生成。注意该工具需要 Haskell 环境GHC 与 Parsec 库才能构建运行属于开发期工具链的一部分。九、已知缺陷与边界原文 Bugs 及源码 FIXME原文档在末尾的# Bugs中明确列出Doesnt recursively check for well-formedness yet.即生成的well_formed函数只校验当前层级判别符 ↔ 字段的一致性不会递归深入嵌套的data分支调用内层消息的well_formed若需全树校验必须在读取端手动对嵌套结构递归调用对应检查函数。除此之外源码中还散布着数处 FIXME/注释属于使用该工具前应知悉的边界check块中的 C 是括号匹配式解析Parse.hs 的注释embedding C is a hack: we just check that the number of braces match up它只统计花括号配对不识别 C 字符串字面量与注释内部的花括号因此check块内若出现含{/}的字符串或注释可能造成解析错乱splitOn的惰性隐患Compile.hs 的FIXME: splitOn is probably too lazy; may require O(n^2) memory?字段名转换的字符串拆分实现可能有二次方级别的内存开销对极长的类型名需留意默认required 自动编号被 README 作者自评为arguably misfeatures意味着添加新字段时不得在旧字段之间插入否则全部后续字段号平移破坏线上已序列化数据的兼容性——自动编号只适合消息结构从未对外发布的内部场景。十、总结adtproto 用不到两百行 Haskell 实现了一个小而精的领域专用预处理器以data/branch表达代数数据类型自动分配字段号与枚举号双向生成 protobuf 消息与 C 良构性检查并通过check块保留对生成校验函数的自定义扩展能力。理解它的转换规则标签枚举、判别字段、分支载荷的optional化、驼峰转蛇形命名既能帮助你读懂 RethinkDB 这类大型 C 数据库项目中由生成代码支撑的复杂消息建模思路也能为你在其他语言生态中实现类似的 ADT→protobuf 桥接提供可直接对照的参考实现。若要在实际工程中复用请牢记三点字段顺序一经发布不可调整、check块内避免花括号歧义、嵌套良构性需自行递归校验。赞分享数据库文档数据库分布式数据库【免费下载链接】rethinkdbThe open-source database for the realtime web.项目地址https://gitcode.com/gh_mirrors/re/rethinkdb点击查看免费下载相关推荐Retrofit Protobuf 转换器实战指南converter-protobuf 的接入、原理与进阶用法Retrofit Protobuf 转换器实战指南converter protobuf 的接入、原理与进阶用法 Retrofit 官方为 Protocol B网络API设计yaitoo/xun拦截器设计请求预处理与后处理的扩展点yaitoo/xun拦截器设计请求预处理与后处理的扩展点 痛点传统中间件的局限性 在Web开发中你是否遇到过这样的困境当需要处理特殊请求头、实现自定义重后端Web框架xmake-io/xmake多仓库项目构建submodule与依赖管理xmake io/xmake多仓库项目构建submodule与依赖管理 痛点直击多仓库项目的构建困境 你是否还在为多仓库项目的依赖管理而头疼子模块版本不一序列化上一篇E-Hentai图库一键打包浏览器自动化下载解决方案下一篇E-Hentai图库批量下载终极指南免费快速打包ZIP文件创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表