15、状态机代码生成:基于表格的代码生成、模板引擎使用、自动化脚本
代码生成这件事,说白了就是「把设计稿变成能跑的代码」。我早期做嵌入式项目时,状态机都是手写的——switch-case 一把梭,改一个状态就要翻半天代码。后来项目规模上来了,状态数超过 20 个,手写真的扛不住。于是我开始琢磨:能不能用表格定义状态机,然后自动生成代码?
这一章,我就把这几年的实践经验拆开来讲。你会看到三种主流方式:基于表格的代码生成、模板引擎的使用、以及自动化脚本的整合。嗯,咱们一个一个来。
15.1 基于表格的代码生成
表格是最直观的状态机描述方式。你想想看,状态、事件、动作、下一个状态——这些信息天然适合放在表格里。我习惯用 CSV 或 Excel 来维护这个表格,然后写个脚本把它转成 C 代码。
先看一个典型的状态转移表:
| 当前状态 | 事件 | 动作 | 下一个状态 |
|---|---|---|---|
| IDLE | EVENT_START | start_motor() | RUNNING |
| RUNNING | EVENT_STOP | stop_motor() | IDLE |
| RUNNING | EVENT_ERROR | log_error() | ERROR |
| ERROR | EVENT_RESET | clear_error() | IDLE |
这个表格,就是状态机的「源代码」。我写了一个 Python 脚本,读取这个 CSV,然后生成对应的 C 代码。核心逻辑是这样的:
# 简化版代码生成脚本
import csv
def generate_state_machine(csv_file):
with open(csv_file, 'r') as f:
reader = csv.DictReader(f)
transitions = list(reader)
# 生成状态枚举
states = set()
for t in transitions:
states.add(t['当前状态'])
states.add(t['下一个状态'])
print('typedef enum {')
for s in sorted(states):
print(f' STATE_{s},')
print('} state_t;')
# 生成事件枚举
events = set()
for t in transitions:
events.add(t['事件'])
print('\ntypedef enum {')
for e in sorted(events):
print(f' {e},')
print('} event_t;')
# 生成状态转移表
print('\nstate_t state_table[][MAX_EVENTS] = {')
for s in sorted(states):
print(f' [STATE_{s}] = {{')
for e in sorted(events):
next_state = 'STATE_' + s # 默认保持当前状态
for t in transitions:
if t['当前状态'] == s and t['事件'] == e:
next_state = 'STATE_' + t['下一个状态']
break
print(f' [{e}] = {next_state},')
print(' },')
print('};')
这段脚本跑完之后,会生成一个完整的 C 头文件。你只需要在代码里包含它,然后调用状态机引擎就行了。我在一个工业控制项目里用过这个方案,状态表有 40 多行,生成代码只用了 0.1 秒——比手写快太多了。
核心思路:表格是「数据」,代码生成器是「引擎」。数据与逻辑分离,改状态不用改代码,改表格就行。
15.2 模板引擎的使用
直接 print 代码的方式,说实话有点「糙」。代码结构稍微复杂一点,print 就变得难以维护。这时候就该模板引擎上场了。
我个人比较喜欢 Jinja2,它是 Python 生态里最成熟的模板引擎之一。你可以把 C 代码的骨架写成模板,然后把状态机数据填进去。这样代码生成器和模板是分离的,改模板比改 print 语句舒服多了。
看一个 Jinja2 模板的例子:
// state_machine_template.h.j2
#ifndef STATE_MACHINE_H
#define STATE_MACHINE_H
typedef enum {
{% for state in states %}
STATE_{{ state.name }}{% if not loop.last %},{% endif %}
{% endfor %}
} state_t;
typedef enum {
{% for event in events %}
{{ event.name }}{% if not loop.last %},{% endif %}
{% endfor %}
} event_t;
extern state_t current_state;
state_t state_transition(state_t current, event_t event);
#endif
对应的 Python 代码就简洁多了:
from jinja2 import Environment, FileSystemLoader
import csv
def generate_with_template(csv_file, template_file, output_file):
# 读取 CSV 数据
with open(csv_file, 'r') as f:
reader = csv.DictReader(f)
transitions = list(reader)
# 提取状态和事件
states = [{'name': s} for s in sorted(set(
[t['当前状态'] for t in transitions] +
[t['下一个状态'] for t in transitions]
))]
events = [{'name': e} for e in sorted(set(
[t['事件'] for t in transitions]
))]
# 渲染模板
env = Environment(loader=FileSystemLoader('.'))
template = env.get_template(template_file)
output = template.render(states=states, events=events)
with open(output_file, 'w') as f:
f.write(output)
这样做的好处很明显:模板文件就是 C 代码的样子,你一眼就能看出生成结果长什么样。我曾经在一个团队里推广这个方案,新同事上手特别快——他们不需要懂 Python,只需要会改模板和 CSV 就行。
小技巧:模板里可以加注释,标注「这段是自动生成的,不要手动修改」。我吃过这个亏——有人改了生成的文件,下次重新生成时改动就丢了。
15.3 自动化脚本
有了表格和模板,下一步就是把整个流程串起来。我习惯写一个 Makefile 或者 shell 脚本,一键完成「读取表格 → 生成代码 → 编译验证」的全流程。
一个典型的自动化脚本长这样:
#!/bin/bash
# generate_state_machine.sh
CSV_FILE="state_machine.csv"
TEMPLATE_DIR="./templates"
OUTPUT_DIR="./generated"
echo "=== 状态机代码生成器 ==="
# 1. 检查输入文件
if [ ! -f "$CSV_FILE" ]; then
echo "错误: 找不到 $CSV_FILE"
exit 1
fi
# 2. 创建输出目录
mkdir -p $OUTPUT_DIR
# 3. 生成头文件
echo "生成状态机头文件..."
python3 generate.py \
--csv $CSV_FILE \
--template $TEMPLATE_DIR/header.h.j2 \
--output $OUTPUT_DIR/state_machine.h
# 4. 生成实现文件
echo "生成状态机实现文件..."
python3 generate.py \
--csv $CSV_FILE \
--template $TEMPLATE_DIR/impl.c.j2 \
--output $OUTPUT_DIR/state_machine.c
# 5. 编译验证
echo "编译验证..."
gcc -Wall -Wextra -o test_state_machine \
$OUTPUT_DIR/state_machine.c \
main.c \
-I$OUTPUT_DIR
if [ $? -eq 0 ]; then
echo "编译成功!"
else
echo "编译失败,请检查模板或表格数据。"
exit 1
fi
echo "=== 完成 ==="
这个脚本我用了好几年,每次改完状态表,跑一遍脚本就能拿到可编译的代码。如果编译失败,脚本会直接报错,不会让你带着错误的代码去调试。
注意:自动化脚本一定要做「输入校验」。我曾经遇到过 CSV 里多了一个空行,导致生成的状态枚举少了一个逗号,编译报错找了半天。后来我在脚本里加了数据校验步骤,提前检查表格格式。
15.4 整体流程与架构
把上面三部分串起来,整个代码生成的架构就清晰了。我画了一张图,帮你理解数据是怎么流动的:
从这张图你能看到,整个流程是闭环的。数据从 CSV 流入,经过模板渲染,生成 C 代码,然后编译验证。如果编译失败,就回去改表格或模板,再跑一遍。这个循环跑顺了,状态机的维护成本会大幅降低。
15.5 避坑指南
最后分享几个我踩过的坑:
- 表格格式要严格:我曾经在 CSV 里用了中文逗号,结果脚本解析出错。建议在脚本里加一个格式校验函数,提前发现这类问题。
- 模板不要写死路径:用相对路径 + 配置文件的方式,方便不同项目复用同一套模板。
- 生成的文件要加版本号:我习惯在生成的文件头部加一行注释,记录生成时间和表格的 MD5 值。这样出了问题能快速定位是哪个版本生成的。
- 不要手动修改生成的文件:这是铁律。如果非要改,就去改模板或表格,然后重新生成。手动修改的结果就是「下次生成时你的改动就丢了」。
好了,这一章的内容就到这里。代码生成这件事,说白了就是「把重复劳动交给机器」。你花一天时间搭好生成框架,后面每次改状态表只需要几秒钟——这笔账怎么算都划算。
公众号:蓝海资料掘金营,微信deep3321