03 - inja 模板引擎

尝试从零实现(抄)一个反射系统,并应用于我酝酿中的游戏引擎项目里!本节将实现反射系统的第三部分:代码生成。首先简要介绍 inja 代码模板生成引擎,然后尝试利用获取到的 Schema 信息自动生成上一节文章中写的反射代码。

Inja 简介

Inja 是为现代 C++ 服务的模板引擎,它可以按模板生成变量、循环、条件等代码,十分强大。例如如下代码将存储在 Json 中的数据交给模板渲染,并返回渲染结果:

   
inja::json data; data["name"] = "world"; // 返回: "Hello world!" inja::render("Hello {{ name }}!", data);

环境配置

Inja 将数据存储在 Json 中,因此需要依赖 Json 库 nlohman/json.hpp (>= v3.8.0)。此外 Inja 也是一个单头文件库,因此在这里下载好,把这两个头文件库放到 include 文件夹下即可。

接下来介绍它的常用操作。

渲染模板

最基本的模板渲染就是直接将数据渲染到模板,以 std::string 或流的形式返回:

   
json data; data["name"] = "world"; // Returns std::string "Hello world!" render("Hello {{ name }}!", data); // Writes "Hello world!" to stream render_to(std::cout, "Hello {{ name }}!", data);

进阶方法是让环境 inja::Environment 来进行模板渲染,它就像一个状态机,设置好状态后便能按状态渲染:

   
Environment env; // 直接渲染字符串模板 std::string result = env.render("Hello {{ name }}!", data); // "Hello world!" // 先文件读取模板, 然后进行渲染 Template temp = env.parse_template("./templates/greeting.txt"); std::string result = env.render(temp, data); // "Hello world!" data["name"] = "Inja"; std::string result = env.render(temp, data); // "Hello Inja!" // 模板和数据都可先通过文件读取, 然后渲染 result = env.render_file("./templates/greeting.txt", data); result = env.render_file_with_json_file("./templates/greeting.txt", "./data.json"); // 渲染的结果也能存储到本地磁盘中 env.write(temp, data, "./result.txt"); env.write_with_json_file("./templates/greeting.txt", "./data.json", "./result.txt");

环境状态的常用设置方法如下:

   
// 默认设置 Environment env_default; // 存放模板文件的全局路径, 生成的文件也会在这存储 Environment env_1 {"../path/templates/"}; // 作为输入的模板文件路径, 作为输出的生成文件路径 Environment env_2 {"../path/templates/", "../path/results/"}; // 设置各种模板参数的起始符和终结符 (这里是默认的) env.set_expression("{{", "}}"); // 表达式 env.set_comment("{#", "#}"); // 注释 env.set_statement("{%", "%}"); // 各种语句, 详见下文 env.set_line_statement("##"); // 行语句的开头

基本语法

接下来看看 Inja 模板引擎的基本语法,方便我们进行后续代码渲染操作。

变量

变量在 {{ ... }} 中被渲染:

   
json data; data["neighbour"] = "Peter"; data["guests"] = {"Jeff", "Tom", "Patrick"}; data["time"]["start"] = 16; data["time"]["end"] = 22; // Indexing in array render("{{ guests.1 }}", data); // "Tom" // Objects render("{{ time.start }} to {{ time.end + 1 }}pm", data); // "16 to 23pm"

如果未找到变量,则直接打印有效的 JSON,否则将引发 inja::RenderError 异常。

表达式

表达式可以在 {% ... %} 中被包含,也能写在以##开头的整行中。常用到的表达式有循环,条件和包含。所有表达式均可嵌套。

循环语句

循环的基本写法如下:

   
// Combining loops and line statements render(R"(Guest List: ## for guest in guests {{ loop.index1 }}: {{ guest }} ## endfor )", data) /* Guest List: 1: Jeff 2: Tom 3: Patrick */

其中,被定义的特殊变量及释义如下:

特殊变量释义值类型
loop.index0 开头的循环索引号 number
loop.index11 开头的循环索引号 number
loop.is_first 是否是第一个元素 boolean
loop.is_last 是否是最后一个元素 boolean

对于嵌套循环,可以通过形如 loop.parent.index 的形式去访问外层的循环变量。

除了遍历数组,还能遍历对象:

// Time:  end : 22  start : 16
Time: {% for key, value in time %} {{ key }} : {{ value }} {% endfor %}

条件语句

条件语句则支持传统的 if - else if - else 结构:

   
// 和变量的比较 render("{% if time.hour >= 20 %}Serve{% else if time.hour >= 18 %}Make{% endif %} dinner.", data); // Serve dinner. // 列表中的变量 render("{% if neighbour in guests %}Turn up the music!{% endif %}", data); // Turn up the music! // 逻辑运算 and render("{% if guest_count < (3+2) and all_tired %}Sleepy...{% else %}Keep going...{% endif %}", data); // Sleepy... // 逻辑运算 not render("{% if not guest_count %}The End{% endif %}", data); // The End

包含语句

可以包含来自内存或文件系统中的其他模板:

   
// 要想包含来自内存的模板, 得先从环境中转换 inja::Template content_template = env.parse("Hello {{ neighbour }}!"); env.include_template("content", content_template); env.render("Content: {% include \"content\" %}", data); // "Content: Hello Peter!" // 根据当前文件位置的相对位置包含其他来自文件的模板 render("{% include \"footer.html\" %}", data);

如果文档没有被包含成功(在文件系统中找不到),就会触发包含回调:

   
// 当前路径和要包含的模板名作为参数 env.set_include_callback([&env](const std::string& path, const std::string& template_name) { return env.parse("Hello {{ neighbour }} from " + template_name); });

赋值语句

在模板中也能通过 set 语句为自定义变量赋值:

render("{% set new_hour=23 %}{{ new_hour }}pm", data); // "23pm"
render("{% set time.start=18 %}{{ time.start }}pm", data); // using json pointers

函数

Inja 还实现了一些内置函数:

   
// 给字符串使用的大小写转换函数 render("Hello {{ upper(neighbour) }}!", data); // "Hello PETER!" render("Hello {{ lower(neighbour) }}!", data); // "Hello peter!" // 给循环使用的range函数 render("{% for i in range(4) %}{{ loop.index1 }}{% endfor %}", data); // "1234" render("{% for i in range(3) %}{{ at(guests, i) }} {% endfor %}", data); // "Jeff Tom Patrick " // 获取列表的长度 render("I count {{ length(guests) }} guests.", data); // "I count 3 guests." // 获取列表第一个/最后一个元素 render("{{ first(guests) }} was first.", data); // "Jeff was first." render("{{ last(guests) }} was last.", data); // "Patir was last." // 排序列表 render("{{ sort([3,2,1]) }}", data); // "[1,2,3]" render("{{ sort(guests) }}", data); // "[\"Jeff\", \"Patrick\", \"Tom\"]" // 为列表间元素添加分隔符 render("{{ join([1,2,3], \" + \") }}", data); // "1 + 2 + 3" render("{{ join(guests, \", \") }}", data); // "Jeff, Patrick, Tom" // 给元素按位数四舍五入 render("{{ round(3.1415, 0) }}", data); // 3 render("{{ round(3.1415, 3) }}", data); // 3.142 // 检查该元素是奇数/偶数/被x整除 render("{{ odd(42) }}", data); // false render("{{ even(42) }}", data); // true render("{{ divisibleBy(42, 7) }}", data); // true // 获取列表最大/最小元素 render("{{ max([1, 2, 3]) }}", data); // 3 render("{{ min([-2.4, -1.2, 4.5]) }}", data); // -2.4 // 转换字符串为数值 render("{{ int(\"2\") == 2 }}", data); // true render("{{ float(\"1.8\") > 2 }}", data); // false // 如果该变量未定义, 返回设定好的默认值 render("Hello {{ default(neighbour, \"my friend\") }}!", data); // "Hello Peter!" render("Hello {{ default(colleague, \"my friend\") }}!", data); // "Hello my friend!" // 动态访问对象的值 render("{{ at(time, \"start\") }} to {{ time.end }}", data); // "16 to 22" // 检查对象中是否有所给key render("{{ exists(\"guests\") }}", data); // "true" render("{{ exists(\"city\") }}", data); // "false" render("{{ existsIn(time, \"start\") }}", data); // "true" render("{{ existsIn(time, neighbour) }}", data); // "false" // 检查所给key是否为某种类型 render("{{ isString(neighbour) }}", data); // "true" render("{{ isArray(guests) }}", data); // "true" // Implemented type checks: isArray, isBoolean, isFloat, isInteger, isNumber, isObject, isString,

注释

注释可被 `` 包围,被它包围的内容不会输出。

其他特性

没有涉及到的特性如下:

  • 通过自定义回调来实现自定义函数的功能。
  • 模板之间也存在继承关系,可以继承一个模板覆写某些内容。
  • 可以通过设置环境来管理空格。

详见参考资料 1。

实践

接下来开始利用 EnTT::meta 库封装自己的反射库,详见我的毕业设计Tools/MetaParser 部分。

参考资料

  1. Inja: Main Page (pantor.github.io)