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.index | 0 开头的循环索引号 | number |
loop.index1 | 1 开头的循环索引号 | 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
部分。