使用Lark解析自定义消息定义并生成C++结构体

使用Lark解析自定义消息定义并生成C++结构体

本文详细阐述了如何利用pythonlark库解析自定义消息定义文件,并自动化生成相应的c++结构体代码。通过定义Lark语法、构建C++代码模板,并实现一个自定义的Lark解析树解释器,我们可以高效地将简洁的消息定义转换为结构清晰、可维护的C++代码,从而显著减少手动编写大量重复性代码的负担,提升开发效率和代码一致性。

在无线通信协议或嵌入式系统开发中,定义和维护消息结构常常涉及大量的重复性C++代码编写,例如为每个消息创建结构体、定义成员变量以及构造函数等。这种手动操作不仅效率低下,还容易引入错误。本文旨在提供一个基于Lark解析库的解决方案,通过定义一种简洁的消息描述语言,并自动生成相应的C++结构体代码,从而实现消息定义的自动化管理。

1. 定义消息协议语法

首先,我们需要一种简洁的文本格式来描述我们的消息。例如,一个消息定义文件 example.msg 可能如下所示:

name TWIST id 123 Float variableone float variabletwo

这个文件定义了一个名为 TWIST、ID 为 123 的消息,它包含两个 float 类型的成员变量 variableone 和 variabletwo。

立即学习C++免费学习笔记(深入)”;

为了解析这种格式,我们使用Lark定义一个语法。Lark语法是一种EBNF(扩展巴科斯范式)风格的描述语言,用于定义文本的结构。

from lark import Lark  # 定义Lark语法 message_grammar = """ start: message+ // 一个文件可以包含一个或多个消息定义 message: msgname msgid member+ // 每个消息包含名称、ID和至少一个成员 msgname: "name" MSG_NAME msgid: "id" MSG_ID member: DATATYPE MEMBER_NAME  DATATYPE: "float"|"int"|"bool" // 支持的数据类型 MSG_NAME: word // 消息名称通常是单词 MEMBER_NAME: WORD // 成员名称通常是单词 MSG_ID: INT // 消息ID是整数  %import common (INT, WORD, WS) // 导入Lark内置的常用规则,如整数、单词和空白符 %ignore WS // 忽略空白符 """  # 初始化Lark解析器 parser = Lark(message_grammar)

在这个语法中:

  • start 规则表示整个输入可以由一个或多个 message 定义组成。
  • message 规则定义了单个消息的结构,包括 msgname、msgid 和一个或多个 member。
  • msgname、msgid 和 member 定义了如何识别消息的名称、ID和成员。
  • DATATYPE 定义了我们支持的基本数据类型(float、int、bool)。
  • MSG_NAME、MEMBER_NAME 和 MSG_ID 使用了Lark的 common 模块中的 WORD 和 INT 规则,方便地匹配单词和整数。
  • %ignore WS 指示Lark在解析过程中忽略所有空白字符。

2. 构建C++结构体模板

接下来,我们需要一个C++代码模板来生成最终的结构体。Python的f-String功能非常适合构建这种模板,因为它允许我们直接在字符串中嵌入变量和表达式。

# C++结构体代码模板 ctemplate = """ struct {name} {{     {name}(const Packet&); // 构造函数,假设从Packet解析      Static constexpr const int id={id}; // 消息ID,使用静态常量表达式      {cmembers} // 成员变量列表 }}; """

在这个模板中:

使用Lark解析自定义消息定义并生成C++结构体

通义视频

通义万相AI视频生成工具

使用Lark解析自定义消息定义并生成C++结构体70

查看详情 使用Lark解析自定义消息定义并生成C++结构体

  • {name} 将被替换为消息的名称。
  • {id} 将被替换为消息的ID。
  • {cmembers} 将被替换为所有成员变量的C++定义(例如 float variableone;)。

我们使用 static constexpr const int id={id}; 来定义消息ID,这是一种C++中定义编译时常量的推荐方式,比简单的 int id={id}; 更具优势,因为它保证了ID在编译时就确定,并且不可修改。

3. 实现Lark解析树解释器

Lark解析器会将输入文本转换为一个解析树(Parse Tree)。为了从这个解析树中提取信息并生成C++代码,我们需要实现一个Lark的 Interpreter。Interpreter 允许我们遍历解析树,并在访问每个节点时执行自定义逻辑。与 Visitor 不同,Interpreter 提供了对遍历顺序的完全控制,这对于需要在进入子节点前或离开子节点后执行操作的场景(例如,打开和关闭代码块)非常有用。

from lark.visitors import Interpreter  class CGen(Interpreter):     def __init__(self):         super().__init__()         self.source = "" # 存储生成的C++代码      def start(self, tree):         # 遍历所有消息定义         self.visit_children(tree)      def message(self, tree):         # 为每个消息初始化数据结构         self.msg = { "members": {} }         self.visit_children(tree) # 访问子节点以填充msg字典         self.source += CGen._process_message(self.msg) # 处理并生成C++代码         self.source += "n" # 每个消息结构体后添加换行      @staticmethod     def _process_message(msg):         """根据解析到的消息数据,使用模板生成C++代码"""         members_str = ""         for _name, _type in msg["members"].items():             if members_str:                 members_str += "n    " # 成员之间换行并缩进             members_str += f"{_type} {_name};"         msg["cmembers"] = members_str # 将生成的成员字符串添加到msg字典中          return ctemplate.format(**msg) # 使用模板格式化输出      def msgname(self, tree):         """处理消息名称节点"""         self.msg["name"] = tree.children[0].value      def msgid(self, tree):         """处理消息ID节点"""         self.msg["id"] = int(tree.children[0].value)      def member(self, tree):         """处理成员变量节点"""         member_type = ""         member_name = ""         for child in tree.children:             if child.type == 'DATATYPE':                 member_type = child.value             if child.type == 'MEMBER_NAME':                 member_name = child.value         self.msg["members"][member_name] = member_type # 存储成员名称和类型

CGen 解释器的工作原理:

  • __init__:初始化一个 source 属性来存储最终生成的C++代码。
  • start(self, tree):这是根节点的处理方法。它简单地调用 self.visit_children(tree) 来遍历所有消息定义。
  • message(self, tree):当Lark遇到一个 message 节点时,会调用此方法。
    • 它初始化一个 self.msg 字典来收集当前消息的所有信息。
    • self.visit_children(tree) 会递归调用 msgname、msgid 和 member 方法来填充 self.msg 字典。
    • 子节点访问完成后,_process_message 静态方法被调用,它使用之前定义的C++模板和收集到的数据来生成C++代码,并将其追加到 self.source 中。
  • _process_message(msg):这是一个辅助静态方法,负责将收集到的消息数据格式化成C++成员变量字符串,并最终使用 ctemplate 生成完整的C++结构体。
  • msgname(self, tree):从 msgname 节点中提取消息名称。tree.children[0].value 获取了 MSG_NAME 终端节点的值。
  • msgid(self, tree):从 msgid 节点中提取消息ID,并转换为整数。
  • member(self, tree):从 member 节点中提取成员的数据类型和名称,并存储到 self.msg[“members”] 字典中。

4. 整合与代码生成

现在,我们将所有部分整合起来,解析一个示例消息定义文件并生成C++代码。

# 示例消息定义内容 example_msg_content = """ name TWIST id 123 float variableone float variabletwo  name ODOMETRY id 456 int x_pos int y_pos bool is_valid """  # 使用Lark解析消息内容 parse_tree = parser.parse(example_msg_content)  # 实例化CGen解释器并访问解析树 cgen = CGen() cgen.visit(parse_tree)  # 打印生成的C++代码 print(cgen.source)

运行上述代码,将得到以下C++输出:

struct TWIST {     TWIST(const Packet&);      static constexpr const int id=123;      float variableone;     float variabletwo; };  struct ODOMETRY {     ODOMETRY(const Packet&);      static constexpr const int id=456;      int x_pos;     int y_pos;     bool is_valid; };

5. 注意事项与扩展

  • 错误处理: 当前的实现没有包含错误处理。在生产环境中,您可能需要捕获Lark的 LarkError 或在解释器中添加验证逻辑。
  • 复杂数据类型: 如果需要支持更复杂的数据类型(如嵌套消息、数组、字符串),Lark语法和 CGen 解释器都需要相应地扩展。例如,嵌套消息可以引入一个新的语法规则,并在 Interpreter 中递归处理。
  • 文件操作: 在实际应用中,您会从文件读取消息定义,并将生成的C++代码写入 .hpp 或 .h 文件。
  • 代码风格: C++模板可以根据您的项目代码规范进行调整,例如缩进、命名约定等。
  • Lark特性: Lark还提供了其他高级特性,如 transformer,它可以在解析后转换解析树,这在某些场景下可能比 Interpreter 更简洁。然而,对于需要严格控制代码生成顺序和上下文的场景,Interpreter 往往是更灵活的选择。

通过这种方式,我们成功地将自定义的消息定义语言与C++代码生成过程解耦,极大地提高了消息定义的灵活性和开发效率。这种基于Lark的自动化方法为管理复杂的通信协议消息提供了强大而专业的解决方案。

上一篇
下一篇
text=ZqhQzanResources