# 4. 代码生成器
前面几个小节详细介绍了基于AWFlow的应用程序设计方法。总结起来,主要需要完成两项工作:一是根据需要实现一系列具体的节点类;一是构建好数据流图的描述。只要完成了这两项工作,其它代码基本就可以按照综合范例程序的编写方法,"依样画葫芦"即可。
在整个设计过程中,会涉及到很多结构性代码。比如节点类实现时,都是继承自某个基类,然后实现特定的抽象方法,而对于某个属性,可能会涉及到多个地方的编程:属性描述、set_prop()和get_prop()方法的实现、具体类中需包含该属性的定义。换句话说,一个属性关联的地方比较多。全靠人力来维护这些C代码显得较为繁琐。为此,AWFlow提供了一些代码生成工具,辅助用户设计,使用户只需要关注一个节点的核心业务逻辑,其它非核心代码交给工具来完成。
# 4.1 代码生成
# 4.1.1 节点描述文件
节点描述文件采用JSON格式,用于描述一个节点的相关信息,主要包括基本信息、属性描述信息和函数描述信息。有了这些信息,即可自动生成节点框架代码。
# 1. 基本信息
基本信息包括名称、节点类型、功能描述等,除此之外,还有一些辅助信息(可忽略),详见下表。
可选/必选 | 关键字 | 值 |
---|---|---|
必选项 | name | 节点的名称(由英文字母、数字和下划线组成的有效 C 语言ID) |
必选项 | category | 类别。取值为 pump、filter或sink,决定实际类继承自哪个基类 |
可选项 | desc | 节点的功能描述 |
可选项 | order | (辅助信息,可忽略) |
可选项 | version | 版本号。包括主版本、次版本和修正号,三者用英文的点(.)分隔 |
可选项 | icon | 图标文件(辅助信息,可忽略) |
可选项 | help | 帮助信息。一般取值为@help.html,表示从 help.html 中读取 |
可选项 | keywords | 关键字(辅助信息,可忽略) |
可选项 | author | 作者信息 |
除name和category外,其它都是可选信息,可忽略。name和category会决定生成的C代码。典型示例如下所示。
{
"name":"random_number",
"category":"pump",
"desc":"generate a random number",
"order" : 10,
"version":"0.1.0",
"icon" : "debug.png",
"help" : "@help.html",
"keywords":["random_number", "pump"],
"author": {
"name": "someone",
"email": "someone@zlg.cn",
"url": "http://www.zlg.com"
}
}
完整的节点描述文件还需要包含属性描述和函数描述。
其中,help.html是显示帮助信息的页面,一个简单的范例如下所示。
<p>生成一个指定范围的数据整数。
<p>输出:
<ol>
<li>值:AW_NODE_DATA_PROP_VALUE</li>
</ol>
其对应的页面详见下图。

这只是一个简单的示例,其说明了该节点的功能是生成一个指定范围的整数数据,值存储在名为AW_NODE_DATA_PROP_VALUE的属性中,AW_NODE_DATA_PROP_VALUE是一个宏,其定义为:
#define AW_NODE_DATA_PROP_VALUE "value"
# 2. 属性描述信息
属性描述信息通过JSON文件中一个关键字为"props"的键值对表示,属性描述中需要描述的内容基本与3.2.4节中介绍的属性描述(C语言版本)一致,包含类型、格式、标志、名称、显示名称、描述(属性的文字介绍)等信息,此外,针对特定类型的属性,可能还存在一些扩展成员(详见图3.2),比如int8_t类型的属性,还具有min、max、defvalue、step、unit等属性。
同样以random number节点为例,其定义了min和max两个属性,C语言的定义版本详见第三章3.4.2的第4小节(定义属性描述),对应的JSON文件描述如下所示。
{
"name":"random_number",
"category":"pump",
"desc":"generate a random number",
"order" : 10,
"version":"0.1.0",
"icon" : "debug.png",
"help" : "@help.html",
"keywords":["random_number", "pump"],
"author": {
"name": "someone",
"email": "someone@zlg.cn",
"url": "http://www.zlg.com"
},
"props": [
{
"name":"min",
"displayName":"Min Value",
"type":"int32_t",
"desc":"min value of the random number",
"min":0,
"max":1000,
"defvalue":0,
"persistent":true,
"configurable":true
},
{
"name":"max",
"displayName":"Max Value",
"type":"int32_t",
"desc":"max value of the random number",
"min":0,
"max":1000,
"defvalue":500,
"persistent":true,
"configurable":true
}
]
}
注意,部分数据的取值可能和C语言版本存在一定的差异(概念是完全相同的)。例如,对于类型,在C语言版本中使用了一系列枚举值表示(详见第三章 3.2.4第1小节的类型表格),但在JSON文件中,这些类型直接使用字符串表示,对应关系详见下表。
类型 | 简介 | 对应字符串 |
---|---|---|
VALUE_DESC_TYPE_INT8 | 有符号8位数 | "int8_t" |
VALUE_DESC_TYPE_UINT8 | 无符号8位数 | "uint8_t" |
VALUE_DESC_TYPE_INT16 | 有符号16位数 | "int16_t" |
VALUE_DESC_TYPE_UINT16 | 无符号16位数 | "uint16_t" |
VALUE_DESC_TYPE_INT32 | 有符号32位数 | "int32_t" |
VALUE_DESC_TYPE_UINT32 | 无符号32位数 | "uint32_t" |
VALUE_DESC_TYPE_INT64 | 有符号64位数 | "int64_t" |
VALUE_DESC_TYPE_UINT64 | 无符号64位数 | "uint64_t" |
VALUE_DESC_TYPE_FLOAT | 单精度浮点数 | "float" |
VALUE_DESC_TYPE_DOUBLE | 双精度浮点数 | "double" |
VALUE_DESC_TYPE_BOOL | 布尔类型 | "bool_t" |
VALUE_DESC_TYPE_STRING | 字符串类型 | "string" |
VALUE_DESC_TYPE_BINARY | 二进制数据 | "binary" |
VALUE_DESC_TYPE_INT_ENUMS | 整数枚举类型 | "int_enums" |
VALUE_DESC_TYPE_STRING_ENUMS | 字符串枚举类型 | "string_enums" |
特别地,对于属性标志,在C语言版本的定义中,标志是使用一系列枚举宏表示的,详见第三章3.2.4第3小节。但在JSON文件中,每个标志都改用对应的一个bool类型的键值对表示,值为true时,表示具有该标志,值为false时表示不具有该标志。
为了使JSON描述更加便捷,默认每个属性都具有标志:PROP_FLAGS_DEFAULT,其本质上是FLAG_OBJECT、FLAG_READABLE、FLAG_WRITBALE三个标志的集合,因而需要在JSON文件中描述的标志仅有两个:PROP_DESC_FLAG_CONFIGURABLE和PROP_DESC_FLAG_PERSISTENT,它们对应的键名如下表所示。
格式 | 简介 | 在JSON文件中的名称 |
---|---|---|
PROP_DESC_FLAG_CONFIGURABLE | 可配置 | "configurable" |
PROP_DESC_FLAG_PERSISTENT | 需要持久化(掉电不丢失) | "persistent" |
当省略某标志时,表示不具有该标志,与将该标志对应值设置为false的效果是相同的。
若某一属性的类型为"int_enums"(整数枚举类型),则其对应的值应该为一个字符串数组(每个字符串使用冒号连接一个整数序号和字符串内容),在JSON中的描述形式如下:
"enums":["0:red", "1:green", "2:blue"]
列表末尾无需再增加NULL作为结束标记。同理,若某一属性的类型为" string_enums"(字符串枚举类型),则其对应的值应该为一个字符串数组,在JSON中的描述形式如下:
"enums":["red", "green", "blue"]
# 3. 函数描述信息
函数描述信息通过JSON文件中一个关键字为"functions"的键值对表示,函数描述中需要描述的内容基本与3.2.5节中介绍的函数描述(C语言版本)一致,包含操作名、操作描述、具体执行的函数、执行函数的参数描述、返回值描述等。定义范例如下所示。
"functions": [
{
"name":"foo",
"desc":"foo function",
"ret": {
"type":"float",
"desc":"foo float",
"min":0,
"max":128
},
"args":[
{
"name":"arg1",
"type":"string",
"desc":"foo string",
"min":0,
"max":32,
"defvalue":"string demo"
},
{
"name":"arg2",
"type":"string_enums",
"desc":"foo string_enums",
"defvalue":"red",
"enums":["red", "green", "blue"]
}
]
}
]
"functions"的值是一个数组,每个数组成员都是一个独立的函数描述。上述程序中只描述了一个名为foo的函数。
"ret"对应的值表示返回值描述,表示了返回值的具体情况,其相关成员与属性描述是基本相同的,唯一的区别是返回值描述中无需再使用"configurable"或"persistent"控制对应的标志,这些标志对返回值描述是多余的。程序中描述了该操作的返回值为float类型,且值在0 ~ 128 之间。
"args"对应的值是一个数组,每个数组成员都描述了单个参数,单个参数的相关成员与属性描述是基本相同的,唯一不同的是flags标志的设置,对于参数描述来讲,目前只有一个标志可用:ARG_DESC_FLAGS_REQUIRED。在json文件中描述时,可以使用名为"required"的布尔值控制,例如:
"required":true
注意,在JSON文件中,并没有描述该函数对应的具体操作(exec),在自动生成对应的C程序时(下一小节会介绍具体如何生成),会针对exec生成一个空函数,这个空函数还需要用户根据实际情况完成,空函数示意如下所示。
static ret_t aw_node_pump_random_number_foo(void* obj, value_t* ret, tk_object_t* args) {
/*TODO*/
return RET_NOT_IMPL;
}
# 4.1.2 生成节点实现文件
使用AWFlow提供的工具(JS脚本:gen_node.js),可以很容易基于节点描述文件(JSON)生成相应的C程序,进而将程序添加到实际的应用程序中。
# 1. 安装node.js
由于工具为JS脚本,为了运行JS,需要安装一个运行环境。这里选择Node.js,Node.js 是一个基于 Chrome V8 引擎的 JavaScript 运行环境。可以在官网 (opens new window)直接下载安装包进行安装。
通常情况下,进入首页就会依照当前所使用的系统展示安装包的下载界面,详见下图。

左侧的LTS是长期支持版本,一般建议直接安装LTS版本。安装过程非常简单,全部按照默认设置安装即可。安装完成后,在控制台输入node --version 命令,如果能看到版本号的输出,则表明安装成功:
node --version
v10.16.3
具体版本号以实际安装的版本为准。有关Node.js的更多用法,可以阅读node.js官网提供的丰富文档 (opens new window)。
# 2. 生成实现文件
继续以random number节点为例,假定其对应的节点描述文件内容如4.1节第2小节的JSON文件描述所示,且存于名为pump_random_number.json的文件中,则具体的生成命令为:
node gen_node.js pump_random_number.json
命令的形式如上所示,如果gen_node.js和pump_random_number.json与运行命令的位置不同(不处于同一目录),则还需要在文件名前加上合适的路径。
生成的代码位于命令执行目录下新增的文件夹中,文件夹的名称为"*_gen_c",具体前缀与json文件的名称保持一致。对于本例,生成的代码即位于pump_random_number_gen_c文件夹中。
生成的文件包括.h和.c文件,文件名称由json文件中的name和category两个基础信息决定,命名规则为:aw_node_{category}_{name},对于本例,生成的两个文件即为:aw_node_pump_random_number.c和aw_node_pump_random_number.h。它们的内容分别如下所示。
#ifndef AW_NODE_PUMP_RANDOM_NUMBER_H
#define AW_NODE_PUMP_RANDOM_NUMBER_H
#include "aw_flow/base/aw_node_pump.h"
BEGIN_C_DECLS
struct _aw_node_pump_random_number_t;
typedef struct _aw_node_pump_random_number_t aw_node_pump_random_number_t;
struct _aw_node_pump_random_number_t {
aw_node_pump_t aw_node_pump;
int32_t min;
int32_t max;
tk_object_t* data;
};
aw_node_t* aw_node_pump_random_number_create(void);
#define AW_NODE_PUMP_RANDOM_NUMBER_PROP_MIN "min"
#define AW_NODE_PUMP_RANDOM_NUMBER_PROP_MAX "max"
#define AW_NODE_PUMP_RANDOM_NUMBER(node) ((aw_node_pump_random_number_t*)(node))
END_C_DECLS
#endif
#include "tkc/mem.h"
#include "tkc/utils.h"
#include "tkc/object_default.h"
#include "aw_node_pump_random_number.h"
static ret_t aw_node_pump_random_number_set_prop(tk_object_t* obj, const char* name, const value_t* v)
{
aw_node_pump_random_number_t* pump_random_number = AW_NODE_PUMP_RANDOM_NUMBER(obj);
return_value_if_fail(aw_node_check_prop_value(AW_NODE(obj), name, v) == RET_OK, RET_BAD_PARAMS);
if (tk_str_eq(AW_NODE_PUMP_RANDOM_NUMBER_PROP_MIN, name)) {
pump_random_number->min = value_int32(v);
return RET_OK;
} else if (tk_str_eq(AW_NODE_PUMP_RANDOM_NUMBER_PROP_MAX, name)) {
pump_random_number->max = value_int32(v);
return RET_OK;
}
return aw_node_set_prop_default(AW_NODE(obj), name, v);
}
static ret_t aw_node_pump_random_number_get_prop(tk_object_t* obj, const char* name, value_t* v)
{
aw_node_pump_random_number_t* pump_random_number = AW_NODE_PUMP_RANDOM_NUMBER(obj);
if (tk_str_eq(AW_NODE_PUMP_RANDOM_NUMBER_PROP_MIN, name)) {
value_set_int32(v, pump_random_number->min);
return RET_OK;
} else if (tk_str_eq(AW_NODE_PUMP_RANDOM_NUMBER_PROP_MAX, name)) {
value_set_int32(v, pump_random_number->max);
return RET_OK;
}
return aw_node_get_prop_default(AW_NODE(obj), name, v);
}
static const prop_desc_int32_t s_prop_min_desc = {
.value_desc = {
.type = VALUE_DESC_TYPE_INT32,
.name = AW_NODE_PUMP_RANDOM_NUMBER_PROP_MIN,
.display_name = "Min Value",
.desc = "min value of the random number",
.flags = PROP_FLAGS_DEFAULT | PROP_DESC_FLAG_PERSISTENT | PROP_DESC_FLAG_CONFIGURABLE,
.format = 0
},
.defvalue = 0,
.min = 0,
.max = 1000
};
static const prop_desc_int32_t s_prop_max_desc = {
.value_desc = {
.type = VALUE_DESC_TYPE_INT32,
.name = AW_NODE_PUMP_RANDOM_NUMBER_PROP_MAX,
.display_name = "Max Value",
.desc = "max value of the random number",
.flags = PROP_FLAGS_DEFAULT | PROP_DESC_FLAG_PERSISTENT | PROP_DESC_FLAG_CONFIGURABLE,
.format = 0
},
.defvalue = 500,
.min = 0,
.max = 1000
};
static const prop_desc_t* s_prop_descs[] = {
(const prop_desc_t*)(&s_prop_min_desc),
(const prop_desc_t*)(&s_prop_max_desc),
NULL
};
static ret_t aw_node_pump_random_number_on_destroy(tk_object_t* obj)
{
aw_node_pump_random_number_t* pump_random_number = AW_NODE_PUMP_RANDOM_NUMBER(obj);
aw_node_pump_deinit(AW_NODE(obj));
TK_OBJECT_UNREF(pump_random_number->data);
return RET_OK;
}
static ret_t aw_node_pump_random_number_on_event(aw_node_t* node, event_t* event)
{
aw_node_pump_random_number_t* pump_random_number = AW_NODE_PUMP_RANDOM_NUMBER(node);
if(event->type == AW_NODE_EVENT_LOADED) {
/*TODO:*/
}
return RET_OK;
}
static const object_vtable_t s_pump_random_number_vtable = {
.type = "random_number",
.desc = "aw_node_pump_random_number",
.size = sizeof(aw_node_pump_random_number_t),
.get_prop = aw_node_pump_random_number_get_prop,
.set_prop = aw_node_pump_random_number_set_prop,
.on_destroy = aw_node_pump_random_number_on_destroy
};
static const func_desc_t* s_func_descs[] = {
NULL
};
static tk_object_t* aw_node_pump_random_number_input(aw_node_t* node)
{
aw_node_pump_random_number_t* pump_random_number = AW_NODE_PUMP_RANDOM_NUMBER(node);
tk_object_t* data = pump_random_number->data;
/*TODO*/
TK_OBJECT_REF(data);
return data;
}
#include "tkc/object_default.h"
aw_node_t* aw_node_pump_random_number_create(void)
{
aw_node_t* node = NULL;
tk_object_t* obj = object_create(&s_pump_random_number_vtable);
aw_node_pump_random_number_t* pump_random_number = AW_NODE_PUMP_RANDOM_NUMBER(obj);
return_value_if_fail(pump_random_number != NULL, NULL);
node = AW_NODE(obj);
aw_node_pump_init(node, aw_node_pump_random_number_input);
node->prop_descs = s_prop_descs;
node->func_descs = s_func_descs;
node->on_event = aw_node_pump_random_number_on_event;
pump_random_number->min = s_prop_min_desc.defvalue;
pump_random_number->max = s_prop_max_desc.defvalue;
pump_random_number->data = object_default_create();
return node;
}
为了避免展示的代码过于繁琐,上面的都仅展示了一些核心代码。实际上,自动生成的代码还包含了比较全面的注释,读者可以自行尝试自动生成代码,查看代码实际生成效果。
.h文件中主要完成了节点类的定义,将其与第三章3.4.2第1小节所示的_aw_node_pump_random_number_t类定义对比可以发现,它们是完全一致的。生成的C代码基本搭建好了整个结构(特别是属性相关的代码),一些方法的实现保留了空函数(需要由用户添加代码的部分使用的TODO标识),主要存在于两个位置:
(1)on_event()方法实现
上述代码中的 aw_node_pump_random_number_on_event() 函数,事件处理方法实现。
(2)input()方法实现
上述代码中的 aw_node_pump_random_number_input() 函数,数据输入方法实现。
这些函数的实际功能与具体应用相关,工具无法自动生成,还需要由用户实现,实现方法与之前介绍的random number类是完全相同的。
按照同样的方法,log和add节点类对应的代码也可以通过工具自动生成。
# 4.2 本章小结
本章重点介绍了AWFlow的基本结构,以及使用AWFlow时会涉及到的相关概念,使读者能够掌握AWFlow的基本用法。
本章实现的一个具体示例(random_number、add、log)较为简单,但其却涵盖了AWFlow的各个方面,一些复杂的应用无非是具有更多的节点。基于AWFlow,可以实现非常复杂的应用,且因为基于AWFlow的应用程序主要由"节点"构成,使整个应用程序的开发自然而然的"规范"为了"组件化开发":每个节点就是一个可以重用的组件!
ZLG基于AWFlow设计研发了诸多数据流相关的产品,比较典型的就是系列协议转换模块:CANFD-Net(CANFD转以太网)、CANBlue(CAN转蓝牙)、CANFDWiFi(CANFD转WiFi)等。