
本教程旨在详细阐述如何利用#%#$#%@%@%$#%$#%#%#$%@_23eeeb4347bdd26bfc++6b7ee9a3b755dd的lark库解析自定义消息定义文件,并通过lark的interpreter功能,结合python f-string模板,自动化生成c++语言的消息结构体代码。通过构建领域特定语言(dsl)的语法规则、实现语法解析器以及定义代码生成逻辑,开发者可以高效地从简洁的消息定义中生成复杂的c++样板代码,显著提升开发效率并减少手动编写重复代码的工作量。
在无线通信协议或嵌入式系统开发中,定义消息结构体往往涉及大量的重复性代码编写,例如为每个消息创建对应的C++结构体、定义成员变量、构造函数以及ID等。为了解决这一痛点,我们可以借鉴ROS/ROS2等框架的思路,通过定义一种简洁的消息描述语言(DSL),然后利用Lark这样的解析器生成器工具,自动将这些描述转换为目标语言(如C++)的代码。
1. 定义消息描述语言(DSL)
首先,我们需要设计一个简洁明了的消息定义文件格式。本例中,我们采用以下格式来定义一个消息:
name TWIST id 123 float variableone float variabletwo
这个示例定义了一个名为 TWIST 的消息,其ID为 123,包含两个浮点型成员变量 variableone 和 variabletwo。这种格式清晰地表达了消息的关键信息:消息名称、ID以及其成员的类型和名称。
2. 构建Lark语法
Lark是一个强大的Python解析器生成器,支持EBNF(扩展巴科斯范式)语法。我们将使用Lark来解析上述自定义消息定义文件。以下是用于解析的Lark语法定义:
立即学习“Python免费学习笔记(深入)”;
from lark import Lark message_grammar = """ start: message+ // 一个文件可以包含一个或多个消息定义 message: msgname msgid member+ // 一个消息定义包含名称、ID和至少一个成员 msgname: "name" MSG_NAME // 消息名称以"name"关键字开头 msgid: "id" MSG_ID // 消息ID以"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: 匹配字面量 "name" 后面跟着一个 MSG_NAME。
- msgid: 匹配字面量 "id" 后面跟着一个 MSG_ID。
- member: 匹配一个 DATATYPE 后面跟着一个 MEMBER_NAME。
- DATATYPE: 定义了消息成员支持的数据类型,目前包括 "float"、"int" 和 "bool"。
- MSG_NAME, MEMBER_NAME, MSG_ID: 分别使用Lark的 WORD 和 INT 规则来匹配消息名称、成员名称和消息ID。
- %import common (INT, WORD, WS): 导入Lark内置的常用终端符号,如整数 (INT)、单词 (WORD) 和空白符 (WS)。
- %ignore WS: 指示解析器在解析过程中忽略空白符。
3. C++代码模板
在生成C++代码之前,我们需要定义一个C++结构体的模板。Python的f-string非常适合用于这种简单的模板需求。
ctemplate = """
struct {name} {{
{name}(const Packet&);
static constexpr const int id={id};
{cmembers}
}};
"""这个模板定义了一个C++结构体的基本框架。其中,{name}、{id} 和 {cmembers} 是占位符,将在代码生成阶段被实际数据填充。{cmembers} 将用于插入所有成员变量的定义。注意,这里将 id 定义为 static constexpr const int,这是一种更现代和高效的C++常量定义方式。
4. 实现代码生成逻辑
Lark解析器会将输入文本转换为一个抽象语法树(AST)。为了从这个AST中提取信息并生成C++代码,我们可以使用Lark提供的 Interpreter 模式。Interpreter 允许我们遍历AST,并在访问每个节点时执行自定义逻辑。
from lark.visitors import Interpreter
class CGen(Interpreter):
def __init__(self):
super().__init__()
self.generated_sources = [] # 用于存储所有生成的消息代码
def start(self, tree):
# 遍历所有消息定义
self.visit_children(tree)
def message(self, tree):
# 初始化当前消息的数据结构
self.current_msg = { "members": {} }
# 访问子节点以填充 current_msg
self.visit_children(tree)
# 处理完一个消息后,生成其C++代码并添加到列表中
self.generated_sources.append(CGen._process_message(self.current_msg))
@staticmethod
def _process_message(msg_data):
# 拼接成员变量的C++定义
members_str = ""
for _name, _type in msg_data["members"].items():
if members_str:
members_str += "\n " # 换行并缩进
members_str += f"{_type} {_name};"
msg_data["cmembers"] = members_str # 将拼接好的成员字符串添加到数据中
# 使用模板和收集到的数据生成C++代码
return ctemplate.format(**msg_data)
def msgname(self, tree):
# 提取消息名称
self.current_msg["name"] = tree.children[0].value
def msgid(self, tree):
# 提取消息ID,并转换为整数
self.current_msg["id"] = int(tree.children[0].value)
def member(self, tree):
# 提取成员的数据类型和名称
member_type = None
member_name = None
for child in tree.children:
if child.type == 'DATATYPE':
member_type = child.value
if child.type == 'MEMBER_NAME':
member_name = child.value
# 将成员添加到当前消息的成员字典中
if member_name and member_type:
self.current_msg["members"][member_name] = member_type
CGen 类说明:
- __init__: 初始化 generated_sources 列表,用于存储所有生成的消息代码。
- start(self, tree): Interpreter 会从 start 规则对应的节点开始遍历。这里我们只是简单地继续访问其子节点。
- message(self, tree): 当 Interpreter 访问到 message 规则对应的节点时,会调用此方法。我们在这里初始化一个 self.current_msg 字典来收集当前消息的所有信息,然后访问其子节点(msgname, msgid, member),最后调用 _process_message 静态方法生成C++代码。
- _process_message(msg_data): 这是一个静态方法,负责将收集到的消息数据 (msg_data) 格式化为C++代码。它会遍历 members 字典,构建 members_str,然后使用 ctemplate.format(**msg_data) 填充模板。
- msgname(self, tree): 当访问到 msgname 节点时,提取其子节点(即消息名称的词法单元)的值,并存储到 self.current_msg["name"] 中。
- msgid(self, tree): 类似地,提取消息ID并转换为整数,存储到 self.current_msg["id"] 中。
- member(self, tree): 遍历 member 节点的子节点,分别提取数据类型 (DATATYPE) 和成员名称 (MEMBER_NAME),然后将它们作为键值对存储到 self.current_msg["members"] 字典中。
5. 整合与执行
现在,我们将所有部分整合起来,演示如何解析一个消息定义并生成对应的C++代码。
# 示例消息定义文件内容
example_msg_content = """
name TWIST
id 123
float variableone
float variabletwo
name STATUS
id 456
bool is_active
int error_code
"""
# 使用Lark解析器解析消息定义内容
parse_tree = parser.parse(example_msg_content)
# 实例化CGen解释器并访问解析树
cgen = CGen()
cgen.visit(parse_tree)
# 打印所有生成的C++代码
for source_code in cgen.generated_sources:
print(source_code)
输出结果:
struct TWIST {
TWIST(const Packet&);
static constexpr const int id=123;
float variableone;
float variabletwo;
};
struct STATUS {
STATUS(const Packet&);
static constexpr const int id=456;
bool is_active;
int error_code;
};注意事项与总结
- 语法设计的重要性:清晰、无歧义的语法是成功解析的基础。本例中的语法通过 message 规则强制了 name、id 和 member 的顺序,确保了结构的正确性。
- Interpreter 与 Visitor 的选择:Lark提供了 Visitor 和 Interpreter 两种遍历AST的方式。Visitor 适用于简单的树遍历和数据收集,它会自动遍历所有子节点。而 Interpreter 提供了更精细的控制,允许你在访问子节点之前或之后执行逻辑,这对于需要按特定顺序处理节点或进行代码生成等复杂任务时非常有用。本例中,Interpreter 让我们能在处理完一个完整的 message 节点后立即生成其C++代码。
- 错误处理:本教程侧重于代码生成逻辑,但实际应用中应考虑输入文件不符合语法规则时的错误处理机制。Lark在解析失败时会抛出异常,可以捕获这些异常并提供友好的错误信息。
- 扩展性:如果需要支持更多的数据类型、消息特性(如嵌套消息、数组等),只需修改Lark语法规则和 CGen 解释器中的相应逻辑即可。
- 模板引擎:对于更复杂的代码生成场景,可以考虑使用更专业的模板引擎,如Jinja2,它们提供更丰富的模板逻辑控制(循环、条件判断等)。
通过Lark和Python的结合,我们成功地构建了一个自动化代码生成工具,能够将自定义的简洁消息定义转换为结构化的C++代码。这种方法极大地减少了样板代码的编写,提高了开发效率,并使得消息定义的管理更加集中和标准化。这不仅适用于C++,同样可以推广到其他需要从DSL生成代码的场景。










