我们将在本文中介绍一种基于Python模板引擎的自动代码生成方法
在做C代码项目的时候,我们期望做到代码的高复用,高复用意味着代码的高配置性,即通过简单的配置修改达到复用代码的目的。如果代码高复用,支持灵活的配置,那么完全可以在上边做一个更简单的配置工具,用来修改代码配置,这么做相对于提供可以配置的.c/.h源代码有一些好处:
- 配置转换为容易理解的GUI描述,配置人员不需要深入理解C代码即可以实现配置
- 如果你只是想封装一个库给你的客户,你可以同时提供这样一个建议工具,即可以保护你的核心代码,也可以让客户容易上手
然而,能够实现基于模板的自动代码的前提是,你的原始C代码要足够灵活,剩下需要做的就是根据用户的输入信息,调整某些可以修改的参数或者调用,而这些配置可以存储在一些标准的数据存储格式中(如,xml,json,甚至于数据库等等)最后解析配置数据,生成配置相关的.c和.h文件。
在这一篇文章中我们将通过一个示例,来用一种简化的方式讲解这种代码生成的概念,希望读者能管中窥豹,获得一些灵感,并在工作中能够应用起来。
1. 目标分析
我们将以汽车上广泛使用到的UDS协议中常用到的0x31服务作为自动代码的实现目标,该服务用于请求ECU执行特定的函数(服务代码),而具体函数(服务)的内容则是由不同的OEM自己定义的,这是一个很典型的可以用自动代码生成来处理的场景,一般来说0x31的相关的c代码会长下边这个样子:
1
2
3
4
5
6
7
8
9
|
/*!
* UDS sub-service list of $31
*/
const UDS_Routine_Ctrl_T UDS_RountineControl_Services[] =
{
{0xFF00, (UDS_Routine_Ctrl_Func_T)Erase_Flash_Start,(UDS_Routine_Ctrl_Func_T)NULL, (UDS_Routine_Ctrl_Func_T)NULL, 0x01},
{0xDF00, (UDS_Routine_Ctrl_Func_T)Check_CRC_Start, (UDS_Routine_Ctrl_Func_T)NULL, (UDS_Routine_Ctrl_Func_T)NULL, 0x00},
{0xFF01 , (UDS_Routine_Ctrl_Func_T)Check_Dependencies_Start,(UDS_Routine_Ctrl_Func_T)NULL, (UDS_Routine_Ctrl_Func_T)NULL, 0x01}
};
|
通常来讲在协议栈核心代码中,会查询类似这样表格中的元素,表格每一个元素代表一个自服务的相关配置,核心代码会根据收到的子服务ID(类似于0xFF00,0xDF00这些)然后在这张表格中找到对应的配置,从而根据这些配置去执行响应的动作。
因此针对不同的OEM需求,协议栈针对0x31服务的核心处理算法是一致的,区别仅仅是子服务的内容,而子服务的内容配置,抽象出来就是:
- ID - 子服务的ID
- startFunc - 启动函数,用来启动服务
- stopFunc - 停止函数,用来停止服务
- resultFunc - 获取结果函数
- access_level - 访问等级
然后,自动代码生成的关键就是通过一个程序,能够读取配置数据,数据中包括上边这些信息,然后生成必要的.c 和 .h代码,而在这些.c或者.h中,有很多内容是不变的,例如UDS_RountineControl_Services这个数组的名称和类型,等等,因此你可以这么想象这个过程,我们会创建一些模板,模板中含有这些不变的部分,而可变的部分则留空(类似于填空题),而留空部分则根据配置数据,动态的填入,最终填完所有的空就形成了完整的C代码内容,将这些内容保存为.c或者.h文件就完成了代码生成的过程。
2. 趁手的工具
根据上文的分析,自动代码生成工具有几个组成部分:
- 配置数据录入:就是人机界面,可以使用PyQt做桌面版(可以参考本站的PyQt系列教程),也可以做成网页应用,这不是本篇文章的重点,因此,我们本篇文章的代码不会实现这部分
- 配置数据存储:json,xml,数据库,或者更简单一点使用变量存放在内存中(如果不需要保存到电脑上供分享或者下次使用),本文使用json来作为演示
- 将配置转换为代码:将填空题填完的过程,我们使用Python的第三方模板库实现,有很多可选的比如Mako, Jinja等,这是本篇文章要讲述的重点部分,我们将使用Jinja2模板引擎
- 代码模板:留有空位的填空题,也是C代码中不变的部分
Python中的模板引擎主要是配合web框架实现动态生成html,本质上来说html只是给浏览器用来做解析的文本文件,而同样的C源代码本质上来说是给编译器用来做编译的文本文件,因此使用web模板引擎来动态生成C源代码是没有任何问题的。
安装,并导入库
1
2
3
|
import json
from jinja2 import Environment, FileSystemLoader, select_autoescape</pre>
|
3. 准备填空题 - 创建模板
首先我们要准备好填空题,所谓填空题就是代码模板文件,模板中还有固定内容和可变内容,固定内容为C源代码中不需要动态生成的部分,而可变内容则是依赖于用户给定的配置数据来生成的部分。
针对固定内容,很简单的,在模板文件中就是普通的符合C语言语法的文本。
针对可变内容,在模板文件中则是安装模板引擎语法放置的占位符,这些内容将由模板引擎结合配置数据(也就是这些空位的答案)进行渲染生成。
根据我们在第一章节的分析,我们期望针对一个自服务,配置5个配置项,而用户可以添加任意个自服务,同时针对自服务的服务函数,他们的类型都是一样的(返回值和参数),因此我们还可以生成响应的服务函数框架。
实现一系列优雅的模板,需要掌握模板引擎的语法,Jinja的语法可以在这个链接中找到:https://jinja.palletsprojects.com/en/2.11.x/templates/
我们来创建一个名为srv31_pbconfig.ct,文件的后缀并不是很重要,这里选取了ct,含义似c-template,然后添加我们的模板代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
|
#include "uds.h"
{% for item in services %}
{% if item.start_fun != "NULL" %}
static uint8_t {{ item.start_fun }}(void);
{% endif %}
{% if item.stop_fun != "NULL" %}
static uint8_t {{ item.stop_fun }}(void);
{% endif %}
{% if item.result_fun != "NULL" %}
static uint8_t {{ item.result_fun }}(void);
{% endif %}
{% endfor %}
const UDS_Routine_Ctrl_T UDS_RountineControl_Services[] =
{
{% for item in services %}
{ {{ item.id }}, (UDS_Routine_Ctrl_Func_T){{ item.start_fun }}, (UDS_Routine_Ctrl_Func_T){{ item.stop_fun }}, (UDS_Routine_Ctrl_Func_T){{ item.result_fun }}, {{ item.access_level }}},
{% endfor %}
};
/*!
* The size of the rountine control table, it will be updated automatically, no need to change this.
*/
const uint16_t UDS_RountineControl_Services_Size = sizeof(UDS_RountineControl_Services)/sizeof(UDS_Routine_Ctrl_T);
{% for item in services %}
{% if item.start_fun != "NULL" %}
static uint8_t {{ item.start_fun }}(void)
{
uint8_t status;
/*!!!!! User need to complete this function */
return status;
}
{% endif %}
{% if item.stop_fun != "NULL" %}
static uint8_t {{ item.stop_fun }}(void)
{
uint8_t status;
/*!!!!! User need to complete this function */
return status;
}
{% endif %}
{% if item.result_fun != "NULL" %}
static uint8_t {{ item.result_fun }}(void)
{
uint8_t status;
/*!!!!! User need to complete this function */
return status;
}
{% endif %}
{% endfor %}
|
其中:使用{% %}括起来的代码就是Jinja2的模板语法,也就是我们一直提到的可变内容,引擎会在渲染的时候解析这些语法,动态的填入内容。
- 模板中的3到13行,用于生成函数申明
- 模板中的17到19行,用于生成子服务数组
- 模板中的27到57行,用于生成函数体定义
- 模板中的其他部分为固定内容
模板语法是跟python语法基本一致,模板中仅仅用到了一个数据结构,那就是services变量,它是一个python list,里边包含了所有子服务数据。将会由配置数据提供。
4. 准备答案 - 创建配置数据
接下来为了能够正确的将填空题的空白填满,我们要提供正确的数据,我们将使用json文件存储这些数据,在本文中,我们将手工编写json文件,而在实际应用中,这个文件通常通过GUI的方式,由用户通过界面录入。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
{
"service_31": [
{
"id": "0xFF00",
"start_fun": "Erase_Flash_Start",
"stop_fun": "NULL",
"result_fun": "NULL",
"access_level": "0x01"
},
{
"id": "0xDF00",
"start_fun": "Check_CRC_Start",
"stop_fun": "NULL",
"result_fun": "Get_CRC_Result",
"access_level": "0x00"
},
{
"id": "0xFF01",
"start_fun": "Check_Dependencies_Start",
"stop_fun": "Check_Dependencies_Stop",
"result_fun": "NULL",
"access_level": "0x01"
}
]
}
|
在这个配置数据文件中,我们提供了3个0x31服务的配置,并指定了对应的ID,start,stop,result函数,以及access_level。
5. 完成填空,交卷 - 渲染
最后,我们将数据和模板组合在一起进行渲染,将答案填到填空题的空白中。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
env = Environment(
loader=FileSystemLoader('.'),
trim_blocks=True
)
srv31_template = env.get_template('srv31_pbconfig.ct')
with open('auto_coding_config.json', 'r') as f:
config=json.load(f)
srv31_source_code = srv31_template.render(services=config['service_31'])
with open('srv31_pbconfig.c', 'w') as f:
f.write(srv31_source_code)
|
首先我们创建了Jinja环境,并指定环境的loader是一个以当前目录创建的FileSystemLoader,也就是说环境会在当前目录中寻找模板文件,而我们创建的模板文件名为srv31_pbconfig.ct, 因此在下边一行代码,可以使用env的get_template方法将名为srv31_pbconfig.ct的模板加载进来,如果你有多个模板,因为前边已经通过loader告知环境,这个目录下存放所有模板,所以通过模板文件名就可以取回模板,并创建模板对象。
然后我们打开数据配置文件,将配置读入config变量。
第三步,我们使用模板对象的render方法,将配置数据渲染进去,也就是我们比喻为填答案的过程,渲染的结果将通过这个方法返回
最后,将渲染结果保存到目标文件中,就是我们的自动生成的C代码。因为代码略长,我们就不贴在这篇文章里边了,如果需要,大家可以参考github仓库:
https://github.com/pythonlibrary/auto-coding-demo
6. 总结
本文以最简单的例子,介绍了一种使用模板引擎来自动生成C代码的方法,虽然例子很简单,但是方法可以扩展到非常复杂的应用,在做类似应用的过程中,其实是一个Python代码(或者代码生成器)和C原始代码(模板)的一个平衡,哪一部分放在哪个代码里边实现是需要仔细斟酌考量的,希望这篇文章能够给你一些灵感。
7. 小抄 - 参考文档