# 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>

其对应的页面详见下图。

图4.1 节点帮助信息所示页面
图4.1 节点帮助信息所示页面

这只是一个简单的示例,其说明了该节点的功能是生成一个指定范围的整数数据,值存储在名为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)直接下载安装包进行安装。

通常情况下,进入首页就会依照当前所使用的系统展示安装包的下载界面,详见下图。

图4.2 node.js首页
图4.2 node.js首页

左侧的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)等。