65.9K
CodeProject 正在变化。 阅读更多。
Home

EGen – 一个可伸缩的代码生成和维护框架

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.18/5 (9投票s)

2008 年 8 月 11 日

CPOL

12分钟阅读

viewsIcon

43475

downloadIcon

501

关于 EGen 的一篇文章——一个适用于 C/C++/C#/Java 的可扩展代码生成和维护框架,用 Ruby 实现。

引言

本文讨论了 EGen——一个用 Ruby 实现的代码生成框架,它试图超越经典的模板变量替换。它允许您创建独立的模板或将标记的模板块嵌入到 C/C++/C#/Java 代码中。这些模板包含用于简单生成的 Ruby 代码,但也能导入和处理项目中其他文件的指定部分,否则这些部分需要手动同步和维护。它通过将元数据保留在一个地方(一个轻量级的文本数据库)并让您使用它来生成项目的各种代码部分,从而应用了“不要重复自己”(DRY)原则。将元数据数据库和模板保留为文本,可以轻松地将它们与项目中的其他文件一起添加到存储库中,并可以与它们一起恢复或分支。

该框架的主要重点是主动代码生成。模板成为宿主项目的一部分,并允许您在元数据更改时自动刷新代码并维护代码段之间的依赖关系。发生此类更改时,传统的向导只能重新创建骨架代码,您需要手动将所有自定义修改从旧代码迁移到新代码。为了完整起见,包含了一些被动代码生成功能,尽管随着大多数现代 IDE 都包含大量向导,这一点现在已不那么重要。

文章附带的演示展示了它在 C++ 项目中使用 Visual Studio 2005 的情况。然而,该框架仅依赖于 Ruby 和两个 Ruby gem;因此,它可以与其他使用斜星 (/* */) 注释的开发环境和/或语言一起使用。包含的插件特定于 Visual Studio 2005。

背景

在研究了各种提出的代码生成解决方案并结合我的经验后,我决定创建一个能够

  • 易于使用,理想情况下无需学习曲线
  • 具有最少的依赖项
  • 能够将模板块嵌入到普通代码文件中,并且还拥有用于生成整个文件的独立模板
  • 分离元数据和逻辑(生成脚本)
  • 提供安全简单的方式来管理元数据
  • 提供版本化元数据的方式
  • 被解释执行,以便更改能够立即生效

为了避免重复造轮子,我希望尽可能多地使用现成的组件,经过仔细权衡,我选择了 Ruby 作为我的模板和框架实现的语言,erb 作为模板处理器,以及 KirbyBase 来存储和操作元数据。使用功能齐全的解释语言有优势,因为它带有大量的库和工具,可以访问数据库、网络、任意文件,并允许您执行任意计算然后基于这些计算生成代码。领域特定语言很难与之匹敌。

工作原理

该框架实现为一系列 Ruby 模块和脚本。其中一些是开箱即用的,另一些是在初始化项目进行代码生成时自动创建的。框架提供了一个 Visual Studio 2005 插件,以便直接从 IDE 使用脚本。您可以将独立模板 (*.template) 添加到项目中,并将其处理为代码文件,或者您可以将模板块嵌入到源代码中。嵌入式模板块对编译器来说显示为斜星注释,并且会被编译器忽略。只有生成的部分才会被编译和使用。此外,您还可以通过嵌入式模板在源文件中声明(例如接口定义)块,用标记分隔,然后导入并在其他文件中使用它们。目标文件如果与生成代码完全相同,则不会被覆盖,以避免混淆代码版本控制系统。

安装

首先,如果您还没有在工作站上安装 Ruby,请在此处下载 Ruby One-Click 安装程序并安装它。之后,从本文顶部下载 egen_install.zip 存档,将其解压缩到您选择的文件夹,然后双击 egen_install.bat。这将打开一个命令窗口并继续安装两个必需的 Ruby gem(Facets 和 KirbyBase);然后,它将启动 EGen 插件 的 MSI 安装程序。插件安装程序会将框架脚本放在 c:\egen,并将以下四个命令添加到 Visual Studio 2005 的 Tools 菜单中

  • EGen - 处理当前文件
  • EGen - 处理所有模板
  • EGen - 扫描模板
  • EGen - 准备项目以进行代码生成

它还设置了两个环境变量:RUBYLIB=c:\egenRUBYPATH=c:\egen

用法

将要使用此代码生成框架的每个项目都需要进行初始化。您可以从 IDE 中选择 Tools 菜单下的EGen - Prepare project for generation 来完成此操作。它将使用当前项目的路径和名称进行初始化。确保已选择所需的项目,以便 IDE 能够将正确的信息传递给脚本。

准备步骤会将以下内容添加到您的项目文件夹中

  • 文件夹 %project_root%gen 用于存放独立模板处理的结果。
  • 文件夹 %project_root%meta 用于存放独立模板 (*.template 文件)。
  • 文件夹 %project_root%meta/data 用于存放项目的数据库 (KirbyBase)。
  • 文件夹 %project_root%rscript 用于存放所有项目特定的自定义脚本。
  • 文件 %project_root%rscript/%proj_name%_gen.rbw 包含一个为项目动态创建的自定义生成器类(您可以在此处添加其他项目无法访问的特定方法)。
  • 文件 %project_root%rscript/egen_all.rbw 包含一个用于重新生成项目中每个合适文件(独立模板和嵌入式模板)的脚本。
  • 文件 %project_root%rscript/egen_xone.rbw 包含一个用于从 IDE 手动重新生成单个文件的脚本。

项目准备好后,您可以将独立模板(参见演示项目中的 DEMO_Enums.template)添加到 meta 文件夹中,然后通过运行 Tools 菜单中的EGen - Process current file(即编辑器中打开的文件)或EGen - Process all templates来处理它们。要使扫描脚本识别新的模板添加,必须在独立模板中包含以下形式的目的地指令

ERB_DESTINATION = "relative_destination_file_path_here"

此路径必须相对于 rscript 文件夹。

将模板块添加到代码中甚至更简单。下面是一个用预定义整数序列填充向量的示例

/* BGN::ERB_DEFINITION
  std::vector< int > my_vector;
% [1,3,4,67,43,88,95,65,84,68].each do |val|
  my_vector.push_back(<%= val %>);
% end
END::ERB_DEFINITION*/
  std::vector< int > my_vector;
  my_vector.push_back(1);
  my_vector.push_back(3);
  my_vector.push_back(4);
  my_vector.push_back(67);
  my_vector.push_back(43);
  my_vector.push_back(88);
  my_vector.push_back(95);
  my_vector.push_back(65);
  my_vector.push_back(84);
  my_vector.push_back(68);
/*END::ERB_EXPANSION*/

一个cos()查找表(此处为简洁起见只显示部分)

/*BGN::ERB_DEFINITION
 std::vector< double > m_COSLookup;
% (0..360).each do |a|
  <%= "_CB_ #{a}_CE_ m_COSLookup.push_back(#{("%0.10f" 
                   % Math::cos(a.to_f/180*Math::PI))})" %>;
% end
END::ERB_DEFINITION*/
 std::vector< double > m_COSLookup;
  /* 0*/ m_COSLookup.push_back(1.0000000000);
  /* 1*/ m_COSLookup.push_back(0.9998476952);
  /* 2*/ m_COSLookup.push_back(0.9993908270);
  ...
  /* 359*/ m_COSLookup.push_back(0.9998476952);
  /* 360*/ m_COSLookup.push_back(1.0000000000);
/*END::ERB_EXPANSION*/

以及一个填充文件类型映射

/*BGN::ERB_DEFINITION
% %W{cs vb vj h cpp}.each do |type| 
%  aft_txt = "boost::shared_ptr< CAppFileType >(
     new CAppFileType(\"*.#{type}\", icon#{type.upcase}, #{type}_ID)"
   aft_ptr = <%= aft_txt %>;
   m_AppFileTypes[<%= "#{type}_ID" %>] = aft_ptr;
% end
END::ERB_DEFINITION*/
   aft_ptr = boost::shared_ptr< CAppFileType >(
      new CAppFileType("*.cs", iconCS, cs_ID);
   m_AppFileTypes[cs_ID] = aft_ptr;
   aft_ptr = boost::shared_ptr< CAppFileType >(
      new CAppFileType("*.vb", iconVB, vb_ID);
   m_AppFileTypes[vb_ID] = aft_ptr;
   aft_ptr = boost::shared_ptr< CAppFileType >(
      new CAppFileType("*.vj", iconVJ, vj_ID);
   m_AppFileTypes[vj_ID] = aft_ptr;
   aft_ptr = boost::shared_ptr< CAppFileType >(
      new CAppFileType("*.h", iconH, h_ID);
   m_AppFileTypes[h_ID] = aft_ptr;
   aft_ptr = boost::shared_ptr< CAppFileType >(
      new CAppFileType("*.cpp", iconCPP, cpp_ID);
   m_AppFileTypes[cpp_ID] = aft_ptr;
/*END::ERB_EXPANSION*/

最初插入五个键值对的映射可能看起来有些费力,但当您后来决定添加以下内容时:cc cxx c hpp hh hxx js cd resx res css htm html xml xsl xslt xsd,添加它们比复制、粘贴和编辑 34 行代码要简单快捷得多。更好的是,在实际应用程序中,扩展名列表可以放在元数据表中,多个模板将负责定义类型 ID、图标 ID 以及使用正确实例填充类似上面的映射。一旦模板就位,只需要修改表,模板就会处理其余的事情。不要忘记在每次更改模板定义后重新生成文件。可以通过将 egen_scan.rbwegen_all.rbw 脚本添加到项目的 Pre-Build Event 阶段来自动化此任务。请查阅 erbKirbyBase 文档以获取有关编写模板和操作数据库表的更多信息。

试用

该框架附带了一些示例来说明其工作原理。它们都包含在附加的演示应用程序的源代码中,并利用了包含的脚本。

A. 为 C++ 应用程序生成智能枚举

该框架的一个简单应用是,它接收一个包含您元数据的表,并生成一个格式精美的头文件和实现文件,其中声明了您的枚举以及一些在应用程序开发过程中可能很有用的辅助函数。最重要的是,您会在头文件的开头获得一个全面的摘要,以节省其他程序员挖掘可用信息的工作。每次您需要添加一个新enum类别或删除一个现有类别时,您只需要编辑 enum 表,相关的声明和函数就会通过处理模板来创建或删除。

有关详细信息,请参阅以下演示文件:DemoEnums.templateDemoEnums.hDemoEnums.cpp 以及 gen_demo\meta\data 下的表 enums.tbl

B. 文件内模板扩展

此功能利用了框架的能力,可以扩展包含代码的常规文件部分,用于各种目的,例如定义类的初始状态、填充集合、初始化查找表、编写测试或轻松地将 SQL 插入编译代码中。

必须使用以下标记来分隔定义部分和扩展部分

/*BGN::ERB_DEFINITION
   the definition to be expanded comes here …
END::ERB_DEFINITION*/
   the result of processing the above definition
   will be written in this section,
   the previous filling of this section 
   is unconditionally overwritten,
   therefore any manual edits will be lost …
/*END::ERB_EXPANSION*/

要处理此类嵌入式模板,请使用 Visual Studio 工具菜单中的EGen - Process current fileEGen - Process all templates命令。

有关示例模板,请参阅以下演示文件:DEMO_Enums.cppFruit.hVegetable.h

C. 同步一个类声明与其实现的接口

这只是嵌入式模板扩展的一个特定用例,其中扩展文本取自另一个头文件,并在用于填充扩展部分之前进行处理。

接口声明应被以下通用形式的标记包围

/*BGN::MARKER_WORD*/
   here goes the interface code…
/*END::MARKER_WORD*/

EGen 提供了一个 get_interface 函数,该函数从给定文件中返回所需的接口块。该函数声明如下

get_interface(file_path, block_marker=”INTERFACE”, remove_zero=true)

通常,文件路径必须相对于 rscript 目录,并且 block_marker 必须标识文件中所需的精确块。最后一个参数 remove_zero 会触发自动删除 C++ 虚函数末尾的“ = 0”。

有关详细信息,请参阅以下演示文件:IEdible.hFruit.hVegetable.h

D. 生成 C++ 类(头文件和实现文件)以及接口(仅头文件)

这是框架最简单的用法;它有助于加快在应用程序代码中创建新实体的过程,并使遵循内部指南和标准变得轻而易举。与专注于主动代码生成的其他框架不同,这些是被动生成器,旨在减少输入。在命令行中运行脚本 egen_mk_class.rbw,并提供命名空间和类名,它将根据给定的模板为该类生成头文件和实现文件。接口声明(在 C++ 中它们只是只有虚拟方法但没有属性的结构)可以通过运行 egen_mk _interface.rbw 脚本类似地生成。要运行脚本,请在提示符处键入以下命令之一

ruby –S egen_mk_class.rbw –f target_folder –n 
     namespace_label class_name_1 .. [class_name_n]

ruby –S egen_mk_interface.rbw –f target_folder –n 
     namespace_label interface_name_1 .. [interface_name_n]

其中

  • target_folder – 指向您希望创建实体的位置;可以是相对路径或绝对路径。
  • namespace_label – 要在类或接口声明中使用的命名空间。
  • class_name / interface_name – 添加任意数量的类名或接口名,它们将被创建在目标文件夹中并且在指定的命名空间内;如果类名以‘C’开头,例如 CSomething,那么‘C’将被从文件名中删除,从而创建文件:Something.hSomething.cpp;类本身将按预期命名。

随意从演示应用程序复制模板,并进行调整以适应您的内部标准。

在命令行中,请确保您位于项目根目录下的 rscript 文件夹中,这样脚本才能找到模板。有关详细信息,请参阅以下演示文件:ipp.templatehpp.templatecpp.template 以及脚本 egen_mk_interface.rbwegen_mk_class.rbwegen_class_maker.rbw

E. 复制版权声明和其他重复文本

这是嵌入式模板扩展的另一个用例,它从一个源文件提取文本并将其插入到其扩展区域。这里使用的 EGen 函数是 get_block,声明如下

get_block(file_path, block_name, block_prefix = 'BGN::', block_postfix = 'END::')

file_pathblock_name 符合 get_interface 的要求,而 block_prefixblock_postfix 为了保持一致性,最好保持原样。与 get_interface 不同,此函数在不更改或处理源标记之间的内容的情况下检索其内容。

有关详细信息,请参阅以下演示文件的顶部:IEdible.hFruit.hFruit.cppVegetable.h

结论

本文为该框架提供的示例只是展示了其可能用途的冰山一角。由于其开放的性质,该框架可以扩展和改进以适应任何开发人员或团队的需求,并可扩展以适应任何规模的项目。

请注意,我实现中的一些内容特定于其在生成 C/C++ 代码中的使用。例如注释括号,当您转向为非 C 派生语言生成代码时可能无法正常工作。只需进行简单的更改注释标记即可适应您首选的语言。

另外,尽管框架代码及其所有先决条件都是平台无关的,但我在此文章中发布的实现仅在 Windows 上开发和测试。然而,没有理由阻止它在支持 Ruby 的任何其他系统上运行。可能需要进行一些小的调整来适应操作系统之间的差异。

到目前为止,该框架在许多编程任务中都表现出色,我希望许多人都能在工具箱中找到它的一席之地。我期待听到您的意见和建议。

未来计划包括

  • 添加 C# 演示;
  • 添加 SQL Server 演示;
  • 创建用于可视化操作元数据表 (KirbyBase) 的 GUI;
  • 为类和接口骨架生成器脚本创建 GUI;
  • 实现一种灵活的方式来支持其他不支持 C 类注释括号的语言,并对其进行测试/调整以在其他平台和 IDE 上运行。

致谢

非常感谢以下使我能够构建 EGen 的工具的作者

  • Ruby 语言(Yukihiro Matsumoto 和 Ruby 团队)。
  • Ruby Facets 库(Thomas Sawyer 和所有 Facets 贡献者)。
  • KirbyBase(Jamey Cribbs)。
  • The Pragmatic Programmers(Andrew Hunt 和 David Thomas),感谢他们关于 Ruby 和通用编程的书籍。

历史

  • 2008 年 8 月 11 日 – 首次发布。
  • 2008 年 8 月 14 日
    • 改进了与 EGen - Process current file 关联的脚本,使其能够从 IDE 处理 *.template 文件和嵌入式模板;
    • 现在可以像其他命令一样从 Tools 菜单运行准备步骤,方法是选择 EGen - Prepare
    • 精简了文章。
  • 2008 年 8 月 24 日
    • 推出了EGen - Visual Studio 2005 插件的第一个版本,以简化日常使用和安装。
    • 添加了一个简单的安装程序,消除了以前繁琐的手动步骤。
    • 编辑了文章以反映新的更改。
© . All rights reserved.