<article class=“article fmt article-content”><blockquote><strong>导读</strong>:咱们生存在一个多样的世界:丰盛多样的操作系统、丰盛多样的编程语言、丰盛多样的技术栈,如此丰盛多样的技术栈为软件提供商带来了的挑战:如何疾速笼罩这些零碎/技术栈以满足不同背景的用户的需要?本文基于网易云信的落地场景,具体介绍了基于 clang 的源到源转译工具。</blockquote><p>文|开开 网易云信资深 C++ 开发工程师</p><h1><strong>01 前言</strong></h1><p>咱们生存在一个多样的世界: 丰盛多样的操作系统、丰盛多样的编程语言、丰盛多样的技术栈,上面是对<strong>前端</strong>一个粗略地统计:</p><p><span class=“img-wrap”></span></p><p> </p><p>如此丰盛多样的技术栈为<strong>软件提供商</strong>带来了的挑战:如何疾速笼罩这些零碎/技术栈以满足不同背景的用户的需要?</p><p>以网易云信 IM 为例,它的研发流程大抵如下:</p><p>(https://p3-juejin.byteimg.com…)</p><p> </p><p>随着业务倒退,网易云信 IM 的 API 越来越多(有几百个),为适配其余平台,工程师须要投入大量的工夫来编写 language binding,这部分工作繁杂、耗时、且重复性极大;在维护阶段,对 C++ 接口的批改都须要同步到各个 language binding 中,稍有脱漏则会导致问题。为进步生产力和研发效率,将工程师从反复且沉重的"体力活"中解放出来让其更专一于重要性能的研发, <strong>网易云信的大前端团队研发了基于 clang 的源到源转译工具 NeCodeGen</strong>,本文将对 NeCodeGen 进行介绍,以期为面临雷同问题的工程师们提供解决问题的办法与思路。</p><h1><strong>02 为什么要重造轮子?</strong></h1><p>网易云信团队对 language binding 有很多灵便的自定义需要: </p><ol><li><strong>从实现层面:</strong> 须要可能自定义命名格调、办法实现细节、业务逻辑的封装等;</li><li><strong>从接口易用性、敌对性的角度:</strong> 作为软件提供商,须要保障 API 简略易用,且合乎语言的最佳实际;</li></ol><p>调研了以后比拟风行的同类工具后,发现它们在自定义代码生成上的反对不够,用户很难对生成的代码进行管制,无奈满足下面提及的需要。<strong>为此云信团队联合本身需要研发了 NeCodeGen,通过代码模板给予使用者对生成的代码齐全的管制,使之成为一个通用的、灵便的工具。</strong></p><p>以后开源世界中存在很多十分优良的自动化生成 language binding 工具,比方弱小的 SWIG、dart ffigen 等,<strong>NeCodeGen 的次要指标是满足灵便的自定义需要</strong>,可能作为现有工具集的一个补充。在云信团队中,经常将它和其余代码生成工具联合应用来晋升研发效率,上面是云信的一个利用场景:</p><p><span class=“img-wrap”></span><br/> </p><p>因为 dart ffigen 只反对 C 接口,因而首先应用 NeCodeGen 开发生成 C API 和对应的 C implementation 的应用程序,而后应用 dart ffigen 由 C API 来生成的 dart binding,因为 dart ffigen 生成的 dart binding 大量应用 dart ffi 中的类型,它无奈满足易用性、敌对性需求(上图中将称为low level dart binding)。还须要基于它进一步进行封装,云信再次应用 NeCodeGen 生成更加敌对易用的 high level dart binding,在实现上依赖 low level dart binding。</p><h1><strong>03 NeCodeGen 简介</strong></h1><p>NeCodeGen 是一个代码生成框架,它以 Python package 的形式公布,工程师能够基于它来开发本人的利用,<strong>它的目标是简化具备雷同需要的用户的开发成本,提供解决这类问题的最佳工程实际,</strong> 具备如下个性:</p><ol><li>应用灵便: 内置模板引擎 jinja,让工程师应用 jinja 模板语言来灵便的形容代码模板;</li><li>反对从 C++ 同时生成多种目标语言程序,便于工程师同时治理多种目标语言程序,这一点和 SWIG 相似;</li><li>提供最佳工程实际;</li><li>充分利用 Python 的语法糖;</li></ol><p>在实现上 NeCodeGen 应用 Python3 作为开发语言,应用 Libclang 作为 compiler front end,应用 jinja 作为模板引擎,它借鉴了:</p><ol><li>在 Python 中十分风行的 web 框架 Flask;</li><li>clang 的 LibASTMatchers 和 LibTooling;</li><li>SWIG;</li></ol><p>下文将对 NeCodeGen 的各个局部进行更加具体的介绍。</p><h1><strong>04 clang 的简介</strong></h1><p>clang 是 LLVM project 的 C 系语言 compiler front end,它反对的语言包含: C、C++、Objective C/C++ 等。clang 采纳的是“Library Based Architecture"”(基于 library 的架构),这意味着它的各个功能模块会以独立的库的形式实现,工程师能够间接应用这些性能,并且 clang 的 AST 可能残缺的反映 source code 的信息。clang 的这些个性帮忙了工程师基于它来开发一些工具,典型的例子就是 clang-format。<strong>网易云信的工程师在调研后抉择应用 clang 来作为 NeCodeGen 的 compiler front end。</strong></p><h1><strong>05 工欲善其事,必先利其器: 学习 clang AST</strong></h1><p>咱们先做一些筹备工作: 学习 clang AST,这是应用它来实现源到源转译工具的前提,如果读者曾经把握了 clang AST,能够跳过本段。clang AST 比拟庞杂,从根本上来说这是源于 C++ 语言的复杂性,本节应用 Libclang 的 Python binding 率领读者以实际摸索的形式学习 clang AST。</p><p>读者首先须要装置 Libclang 的 Python binding,命令如下:</p><pre><code>pip install libclang</code></pre><p>为便于演示,不将 C++ code 保留到文件中,而是通过字符串的形式传入到 Libclang 中进行编译,残缺程序如下:</p><pre><code>import clang.cindexcode = “”"#include <string>/// test functionint fooFunc(){ return 1;}/// test classclass FooClass{ int m1 = 0; std::string m2 = “hello”; int fooMethod(){ return 1; }};int main(){ fooFunc(); FooStruct foo1; FooClass foo2; }""" # C++源代码index = clang.cindex.Index.create() # 创立编译器对象translation_unit = index.parse(path=‘test.cpp’, unsaved_files=[(’test.cpp’, code)], args=[’-std=c++11’]) #</code></pre><p>index.parse 函数编译 C++ code,参数 args 示意编译参数。</p><h2><strong>Translation unit</strong></h2><p>index.parse 函数的返回值类型为 clang.cindex.TranslationUnit(转换单元),咱们能够应用 Python 的 type 函数进行验证: </p><pre><code> type(translation_unit) Out[6]: clang.cindex.TranslationUnit</code></pre><h2><strong>查看 include</strong></h2><pre><code>for i in translation_unit.get_includes(): print(i.include.name)</code></pre><p>通过调用 <strong>get_includes()</strong> 能够查看 translation unit 所蕴含的所有的头文件。如果读者理论进行执行的话,会发现它理论蕴含的头文件不止 <string>,这是因为头文件 <string> 会蕴含其余头文件,而这些头文件还会包好其余的头文件,compiler 须要一一蕴含。</p><h2><strong>get_chidren</strong></h2><p>clang.cindex.TranslationUnit 的 cursor 属性示意它的 AST,咱们来验证一下它的类型:</p><p></p><pre><code> type(translation_unit.cursor) Out[9]: clang.cindex.Cursor</code></pre><p>从输入能够看出,它的类型是 <strong>clang.cindex.Cursor</strong>;它的成员办法 <strong>get_children()</strong> 能够返回它的间接子节点:</p><p></p><pre><code>for child in translation_unit.cursor.get_children(): print(f’{child.location}, {child.kind}, {child.spelling}’)</code></pre><p>输入摘要如下:</p><p></p><pre><code>……<SourceLocation file ‘D:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Tools\MSVC\14.29.30133\include\string’, line 24, column 1>, CursorKind.NAMESPACE, std<SourceLocation file ’test.cpp’, line 4, column 5>, CursorKind.FUNCTION_DECL, fooFunc<SourceLocation file ’test.cpp’, line 8, column 7>, CursorKind.CLASS_DECL, FooClass</code></pre><p>"……“示意省略了局部输入内容;仔细观察最初四行,它们是文件 <strong>test.cpp</strong> 中的内容,可能和源代码正确地匹配,这也验证了后面提及的:“clang AST 可能残缺的反映 source code 的信息”。</p><p>DECL 是“declaration”的缩写,示意“申明”。</p><h2><strong>walk_preorder</strong></h2><p>clang.cindex.Cursor 的 walk_preorder 办法对 AST 进行先序遍历:</p><p></p><pre><code>children = list(translation_unit.cursor.get_children()) foo_class_node = children[-2] # 选取 class FooClass 的节点树for child in foo_class_node.walk_preorder(): # 先序遍历 print(f’{child.location}, {child.kind}, {child.spelling}’)</code></pre><p>上述对 class FooClass 对应的 AST 进行先序遍历,输入如下:</p><p></p><pre><code><SourceLocation file ’test.cpp’, line 8, column 7>, CursorKind.CLASS_DECL, FooClass<SourceLocation file ’test.cpp’, line 9, column 9>, CursorKind.FIELD_DECL, m1<SourceLocation file ’test.cpp’, line 9, column 14>, CursorKind.INTEGER_LITERAL, <SourceLocation file ’test.cpp’, line 10, column 17>, CursorKind.FIELD_DECL, m2<SourceLocation file ’test.cpp’, line 10, column 5>, CursorKind.NAMESPACE_REF, std<SourceLocation file ’test.cpp’, line 10, column 10>, CursorKind.TYPE_REF, std::string<SourceLocation file ’test.cpp’, line 11, column 9>, CursorKind.CXX_METHOD, fooMethod<SourceLocation file ’test.cpp’, line 11, column 20>, CursorKind.COMPOUND_STMT, <SourceLocation file ’test.cpp’, line 12, column 9>, CursorKind.RETURN_STMT, <SourceLocation file ’test.cpp’, line 12, column 16>, CursorKind.INTEGER_LITERAL,</code></pre><p>请读者自行将上述输入和源代码进行比照。</p><h2><strong>AST node: clang.cindex.Cursor</strong></h2><p>对于 clang.cindex.Cursor,上面是它十分重要的成员:</p><ol><li><strong>kind,</strong> 类型是 clang.cindex.CursorKind;</li><li><strong>type,</strong> 类型是 clang.cindex.Type,通过它能够取得类型信息;</li><li><strong>spelling,</strong> 它示意节点的名称。</li></ol><h1><strong>05 jinja 模板引擎简介</strong></h1><p>因为前面的例子中会应用 jinja,故先对它进行简略介绍。读者不须要有学习新事物的惶恐,因为 jinja 非常简单易学,模板并不是什么新概念,相熟模板元编程的读者对于模板应该不会生疏,并且 jinja 的模板语言和 Python 基本相同,因而并不会引入太多新的概念,一些 jinja 中的概念其实齐全能够应用咱们熟知的概念来进行类比。</p><p>上面是一个简略的 jinja 模板和渲染模板的程序:</p><pre><code>from typing import Listfrom jinja2 import Environment, BaseLoaderjinja_env = Environment(loader=BaseLoader)view_template = jinja_env.from_string( ‘I am {{m.name}}, I am familiar with {%- for itor in m.languages %} {{itor}}, {%- endfor %}’) # jinja模板class ProgrammerModel: "”" model """ def init(self): self.name = ’’ # 姓名 self.languages: List[str] = [] # 把握的语言def controller(): xiao_ming = ProgrammerModel() xiao_ming.name = ‘Xiao Ming’ xiao_ming.languages.append(‘Python’) xiao_ming.languages.append(‘Cpp’) xiao_ming.languages.append(‘C’) print(view_template.render(m=xiao_ming)) if name == ‘main’: controller()</code></pre><p>下面程序定义了一个简略的软件工程师自我介绍的模板 view_template,而后对其进行渲染,从而失去残缺的内容,运行程序,它的输入如下:</p><pre><code>I am Xiao Ming, I am familiar with Python, Cpp, C,</code></pre><h2><strong>jinja template variable 其实就是 “模板参数”</strong></h2><p>认真比照 view_template 和最终的输入,能够发现其中应用 {{ }} 括起来的局部会被替换,它就是 jinja template variable,即“模板参数”,它的语法为: {{template variable}}。</p><h2><strong>MVC 设计模式</strong></h2><p>在下面的程序中,其实咱们应用了 MVC 设计模式:</p><p> </p><p><span class=“img-wrap”></span><span class=“img-wrap”></span></p><p>在前面的程序中,还会持续应用这种设计模式,NeCodeGen 十分举荐工程师应用这种设计模式来构建利用,在前面有专门的章节对 MVC 设计模式进行介绍。</p><h2><strong>jinja render 其实很像是“替换”</strong></h2><p>view_template.render(m=xiao_ming) 即是对模板进行渲染,这个过程能够简略的了解为“替换”,即应用变量 xiao_ming 对模板参数 m 进行替换,如果应用函数参数来进行类比的话,变量 xiao_ming 是实参。</p><h1><strong>06 Abstraction and code template</strong></h1><p>当程序中呈现反复代码的时候,咱们最先想到的是<strong>泛型编程、元编程、注解</strong>等编程技巧,它们可能帮忙工程师简化代码,但不同的 programming language 的形象能力不同,并且对于一些编程工作上述编程技巧也杯水车薪。这些都导致了工程师不可避免地去反复写雷同模式的代码,这种问题在实现 language binding 中尤其突出。</p><p>对于这类问题,NeCodeGen 给出的解法是:</p><ol><li>对于反复的代码,工程师须要形象出它们的通用模式(代码模板),而后应用 template language 来形容代码模板,在 NeCodeGen 中,应用的 template language 是 jinja;</li><li>NeCodeGen 会编译源程序文件并生成 AST,工程师须要从 AST 中提取必要的数据,而后执行转换(参见前面的“代码转换”章节),而后将转换后的数据作为代码模板中模板参数的实参实现了代码模板的渲染,从而失去了指标代码。</li></ol><p>上面就联合简略的例子来对对上述解法进行更加具体的阐明,在这个例子中,工程师须要将 C++ 中的 struct 在 TypeScript 中进行等价的定义,为清晰起见,上面以表格的模式展现了一个具体的例子: </p><table><thead><tr><th> </th><th> </th></tr></thead><tbody><tr><td>C++</td><td>TypeScrip</td></tr><tr><td> </td><td><span class=“img-wrap”></span> </td></tr></tbody></table><p>当初咱们须要思考如何让程序自动化地帮咱们实现这个工作。显然通过 clang,咱们能够拿到 struct NIM_AuthInfo 的 AST,咱们还须要思考如下问题:</p><p><strong>Q1:C++ 类型和 TypeScript 类型的对应关系?</strong></p><p>A: std::string -> string,int -> integer</p><p><strong>Q2:C++ 中 struct 在 TypeScript 中如何进行命名?</strong></p><p>A:为简略起见,咱们让 TypeScript 中的名称和 C++ 的保持一致。</p><p><strong>Q3:TypeScript 中应用什么语法来形容相似于 C++struct?</strong></p><p>A: 应用的 TypeScript interface 来进行形容,咱们能够应用 jinja 写出通用的代码模板来进行形容。</p><p>上面咱们给出具体的实现。依照后面的 MVC 章节提出的思维,咱们能够首先建设 struct 的数据建模:</p><pre><code>class StructModel: def init(self): self.src_name = ’’ # 源语言中的名称 self.des_name = ’’ # 目标语言的名称 self.fields: List[StructFieldModel] = [] # 构造体的字段 class StructFieldModel: def init(self): self.src_name = ’’ # 源语言中的名称 self.des_name = ’’ # 目标语言的名称 self.to_type_name = ’’ # 目标语言的类型名称</code></pre><p>而后咱们写出 TypeScript 的代码模板,这个代码模板是基于 StructModel 来写的: </p><p></p><pre><code>export interface {{m.des_name}} for itor in m.fields %}{{itor.des_name}} : {{itor.to_type_name}} ,{% endfor %}}</code></pre><p>接下来的工作就是从 C++ struct AST 中提取要害数据并进行必要的转换:</p><p></p><pre><code>def controller(struct_node: clang.cindex.Cursor, model: StructModel) -> str: model.src_name = model.des_name = struct_node.spelling # 提取struct的name for field_node in struct_node.get_children(): field_model = StructFieldModel() field_model.src_name = field_model.des_name = field_node.spelling # 提取字段的name field_model.to_type_name = map_type(field_node.type.spelling) # 执行类型映射 model.fields.append(field_model) return view_template.render(m=model) # 渲染模板,失去TypeScript代码</code></pre><h2><strong>残缺程序</strong></h2><p>残缺程序能够通过如下链接取得: </p><p>https://github.com/dengking/c…</p><h1><strong>07 从源语言到目标语言的转译</strong></h1><p>将由源语言编写的程序转译为目标语言的程序时,次要波及如下三个方面的转换:</p><h2><strong>类型转换 type mapping</strong></h2><p>从源语言中的类型到目标语言中的类型的转换。在 NeCodeGen 中对 C++ 语言的内置类型和 C++ 规范库类型进行了枚举并给出了预约义,对于这部分类型的转换,应用 hash map 建设映射关系;对于用户自定义类型,NeCodeGen 无奈给出预约义,则须要由工程师自行定义。</p><h2><strong>命名转换 name mapping</strong></h2><p>不同语言的命名标准不同,因而工程师须要思考命名转换。如果源程序遵循对立的命名标准,那么应用正则表达式可能不便地进行命名的转换,这样可能保障生成的程序的严格的遵循用户设置的命名标准,这也体现了自动化代码生成工具的劣势:程序对命名标准的恪守比工程师更加严格。</p><h2><strong>语法转换 syntax mapping</strong></h2><p>在网易云信的 NeCodeGen 中,<strong>语法转换次要是通过代码模板实现的,</strong> 工程师须要依照目标语言的语法来编写代码模板,而后通过渲染即可失去合乎目标语言语法的程序。</p><h1><strong>08 NeCodeGen 的 Design pattern</strong></h1><p>至此,读者曾经对云信 NeCodeGen 有了一些根本意识,本节次要介绍云信 NeCodeGen 举荐的一些设计模式,在云信 NeCodeGen 的实现中,提供了反对这些 design pattern 的根底性能。这些设计模式是通过工程实际后总结得出的,可能帮忙工程师开发出更易保护的利用,因为 C++ 语言的复杂性,其 AST 的解决也会比较复杂,适合的设计模式就尤为重要,这对于大型项目而言,具备重要意义。</p><h2><strong>Matcher</strong></h2><p>在编写源到源转译工具时,<strong>罕用的模式是匹配感兴趣的节点,而后对匹配的节点执行对应的解决,</strong> 比方名称转换、类型转换。Matcher pattern 就是为这种典型的需要而创立的:框架遍历 AST,并执行用户注册的 match funcion(匹配函数),一旦匹配胜利,则执行 match funcion 对应的 callback。这种模式是 clang 社区为开发 clang tool 而总结进去的,并提供了反对库 LibASTMatchers,对于此,读者能够浏览如下文章: </p><ul><li>https://clang.llvm.org/docs/L…</li><li>https://clang.llvm.org/docs/L…</li></ul><p>云信 NeCodeGen 借鉴了这种模式,并联合 Python 语言的个性、本身的需要进行了本地化实现,它使用了 Python 的 decorator 语法糖,通用的写法如下:</p><p></p><pre><code>@frontend_action.connect(match_func)def callback(): pass</code></pre><p>上述写法的含意是: 通知frontend_action连贯(connect) match funcionmatch_func 和 callback callback;frontend_action 在遍历 AST 时,会将节点作为入参,顺次执行所有向它注册的 match func,如果 match func 返回 True,则示意匹配胜利,框架就会执行 callback 函数来对匹配胜利的节点进行解决,否则 pass。</p><p>通过实际来看,这种模式可能利用的构造更加清晰、代码复用水平更高。</p><p>目前 clang 官网并没有提供 LibASTMatchers 的 Python binding,为便于用户应用,云信 NeCodeGen 提供对罕用节点进行匹配的 match funcion。</p><h2><strong>MVC</strong></h2><p>MVC 模式读者应该不会生疏,它是前端开发中常常应用的一种模式,在本文后面的“jinja模板引擎简介”章节中曾经对其进行了简略介绍,云信 NeCodeGen 中 MVC 能够演绎为:</p><p><span class=“img-wrap”></span>![]</p><p> </p><p>理论应用中,<strong>举荐工程师应用自顶向下的思路:</strong> 定义 model,确定 model 的成员,基于 model 来编写代码模板,而后再编写提取、转换函数来获取数据来对 model 进行初始化,最初应用 model 来渲染模板。</p><p>从实际来看,MVC 可能使代码构造清晰,维护性更强;对于须要从一种源语言生成多种目标语言程序的我的项目,MVC 可能保障 Model 在目标语言中保持一致,这在肯定水平上可能提醒代码复用。</p><h2><strong>总结</strong></h2><p>Matcher pattern 是 NeCodeGen 框架应用的模式,MVC pattern 则是举荐利用开发者应用的模式;Matcher pattern 的 callback 对应了 MVC pattern 的 controller,即工程师在 callback 中实现 controller 的性能。</p><h1><strong>09 How NeCodeGen run</strong></h1><p>通过后面的介绍,咱们曾经对 NeCodeGen 的运行流程有了大抵的意识,上面是以流程图的模式对 NeCodeGen 的运行流程进行总结 <strong>:</strong></p><p><span class=“img-wrap”></span></p><p> </p><h1><strong>10 利用价值</strong></h1><p>代码生成工具的次要目标是晋升生产力,对于大型项目而言,它的作用更加显著。在网易云信的工程实际中,工程师会综合使用多种代码生成工具,充分发挥工具的威力,并将工具退出 CICD,这样的做法极大地晋升了研发效率;对生产力的进步还体现在重构和保护上,在批改源语言程序后,运行工具即可失去更新后的目标语言程序,这可能防止因为源语言程序和目标语言程序的不统一而导致的谬误;重构工作也将变得简略,对目标语言程序的重构将简化为批改代码模板,从新运行工具后,即可实现所有的重构。代码生成工具劣势还体现在对代码标准的恪守上,通过将命名标准、代码标准等编码在工具中,可能保障生成的程序对代码标准百分之百的恪守。</p><p>NeCodeGen 除了能够利用于 language binding 的生成上,还能够利用于其余畛域,比方实现相似于 QT 的 Meta-Object System,它也能够作为一个 stub code generator。</p><h2><strong>术语</strong></h2><p><span class=“img-wrap”></span></p><h2><strong>参考内容</strong></h2><p>https://en.wikipedia.org/wiki…</p><h3><strong>作者介绍</strong></h3><p>开开,网易云信资深 C++ 开发工程师,负责云信根底技术研发,具备丰盛的研发教训,相熟编程语言实践。</p></article>
...