实战案例12:代码生成器:根据模板生成代码、替换占位符、生成函数声明、生成注释文档

说实话,做C语言开发这么多年,我最烦的就是写重复代码。尤其是那些结构相似的函数声明、注释模板、还有各种配置代码。你想想看,一个项目里几十个模块,每个模块都要写类似的初始化函数、清理函数、注册函数……手写一遍,复制粘贴改改,不仅累,还容易出错。

所以,我决定用正则表达式写一个代码生成器。说白了,就是给一个模板,让程序自动把占位符替换成实际内容,生成完整的代码文件。今天我就把这个实战案例分享给你。

核心思路:模板 + 占位符 + 替换

代码生成器的核心逻辑其实很简单:

  1. 准备一个模板文件,里面用特殊的标记(占位符)表示需要替换的位置
  2. 读取模板,用正则表达式匹配这些占位符
  3. 根据实际数据替换占位符,生成最终代码

我在项目中遇到过好几次这样的情况:团队里有人手动复制代码,结果漏改了一个函数名,编译报错查了半天。从那以后,我就坚持用代码生成器来干这种脏活累活。

核心知识点:正则表达式中的捕获组 () 和反向引用,是实现占位符替换的关键。

模板设计:占位符的约定

我们先定义一套占位符规则。我个人习惯用 {{变量名}} 这种格式,清晰且不容易和C语言语法冲突。

占位符 含义 示例值
{{MODULE_NAME}} 模块名称 uart
{{MODULE_PREFIX}} 模块前缀(大写) UART
{{AUTHOR}} 作者 Zhang San
{{DATE}} 创建日期 2025-01-15
{{DESCRIPTION}} 模块描述 UART 驱动模块

嗯,这里要注意:占位符的命名最好用大写+下划线,这样在模板里一眼就能看出来哪些地方需要替换。

实战代码:模板文件示例

我们先写一个模板文件 module_template.h

/**
 * @file {{MODULE_NAME}}.h
 * @author {{AUTHOR}}
 * @date {{DATE}}
 * @brief {{DESCRIPTION}}
 *
 * 本文件包含 {{MODULE_NAME}} 模块的函数声明和宏定义。
 */

#ifndef __{{MODULE_PREFIX}}_H__
#define __{{MODULE_PREFIX}}_H__

#include <stdint.h>

/* {{MODULE_NAME}} 初始化函数 */
int {{MODULE_NAME}}_init(void);

/* {{MODULE_NAME}} 反初始化函数 */
void {{MODULE_NAME}}_deinit(void);

/* {{MODULE_NAME}} 配置函数 */
int {{MODULE_NAME}}_config(uint32_t param);

/* {{MODULE_NAME}} 状态获取函数 */
uint32_t {{MODULE_NAME}}_get_status(void);

#endif /* __{{MODULE_PREFIX}}_H__ */

你看,这个模板里到处都是 {{...}} 占位符。我们的任务就是写一个C程序,把这些占位符全部替换成实际的值。

核心实现:正则替换引擎

下面这段代码,是我在实际项目中提炼出来的。它用 <regex.h> 库实现了占位符的查找和替换。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <regex.h>

#define MAX_LINE 1024
#define MAX_RESULT 4096

/**
 * 替换字符串中所有匹配正则表达式的占位符
 * @param template 模板字符串
 * @param pattern 正则表达式模式
 * @param replacement 替换内容
 * @param result 输出结果缓冲区
 * @return 0成功,-1失败
 */
int replace_placeholder(const char *template, 
                        const char *pattern, 
                        const char *replacement, 
                        char *result) {
    regex_t regex;
    regmatch_t match;
    const char *cursor = template;
    int result_len = 0;
    
    // 编译正则表达式
    if (regcomp(&regex, pattern, REG_EXTENDED) != 0) {
        fprintf(stderr, "正则表达式编译失败: %s\n", pattern);
        return -1;
    }
    
    // 循环查找并替换
    while (regexec(&regex, cursor, 1, &match, 0) == 0) {
        // 复制匹配位置之前的内容
        int prefix_len = match.rm_so;
        if (result_len + prefix_len < MAX_RESULT) {
            strncpy(result + result_len, cursor, prefix_len);
            result_len += prefix_len;
        }
        
        // 复制替换内容
        int repl_len = strlen(replacement);
        if (result_len + repl_len < MAX_RESULT) {
            strcpy(result + result_len, replacement);
            result_len += repl_len;
        }
        
        // 移动光标到匹配位置之后
        cursor += match.rm_eo;
    }
    
    // 复制剩余内容
    strcpy(result + result_len, cursor);
    
    regfree(&regex);
    return 0;
}

避坑指南:我曾经在替换时忘记处理特殊字符,比如替换内容里包含 $\,结果正则引擎把它们当成了特殊标记。后来我加了一层转义处理,才彻底解决这个问题。

生成函数声明:批量处理

实际项目中,我们经常需要生成一组函数声明。比如一个模块有初始化、读、写、控制等操作。我们可以用正则表达式批量生成:

/**
 * 根据函数列表生成函数声明
 * @param module_name 模块名称
 * @param functions 函数名数组
 * @param func_count 函数数量
 * @param output 输出缓冲区
 */
void generate_function_declarations(const char *module_name,
                                    const char **functions,
                                    int func_count,
                                    char *output) {
    char line[MAX_LINE];
    int i;
    
    output[0] = '\0';
    
    for (i = 0; i < func_count; i++) {
        // 生成注释
        snprintf(line, sizeof(line), 
                 "/* %s_%s - %s 模块的 %s 操作 */\n",
                 module_name, functions[i],
                 module_name, functions[i]);
        strcat(output, line);
        
        // 生成函数声明
        snprintf(line, sizeof(line),
                 "int %s_%s(void);\n\n",
                 module_name, functions[i]);
        strcat(output, line);
    }
}

你想想看,如果模块有10个函数,手写要写多久?用这个生成器,一秒搞定。而且格式统一,不会出现缩进不一致、注释漏写的情况。

生成注释文档:Doxygen风格

我个人习惯用 Doxygen 风格的注释。正则表达式在这里也能派上用场:

/**
 * 生成 Doxygen 风格的函数注释
 * @param func_name 函数名
 * @param description 函数描述
 * @param params 参数列表(格式:"param1 desc1, param2 desc2")
 * @param return_desc 返回值描述
 * @param output 输出缓冲区
 */
void generate_doxygen_comment(const char *func_name,
                              const char *description,
                              const char *params,
                              const char *return_desc,
                              char *output) {
    char temp[MAX_RESULT];
    const char *pattern = "([a-zA-Z_][a-zA-Z0-9_]*) ([a-zA-Z_][a-zA-Z0-9_]*)";
    regex_t regex;
    regmatch_t match;
    const char *cursor = params;
    
    snprintf(output, MAX_RESULT,
             "/**\n"
             " * @brief %s\n"
             " *\n", description);
    
    // 解析参数列表,生成 @param 标签
    if (regcomp(&regex, pattern, REG_EXTENDED) == 0) {
        while (regexec(&regex, cursor, 1, &match, 0) == 0) {
            char param_name[64];
            int len = match.rm_eo - match.rm_so;
            strncpy(param_name, cursor + match.rm_so, len);
            param_name[len] = '\0';
            
            snprintf(temp, sizeof(temp),
                     " * @param %s 参数描述\n", param_name);
            strcat(output, temp);
            
            cursor += match.rm_eo;
            // 跳过逗号和空格
            while (*cursor == ',' || *cursor == ' ') cursor++;
        }
        regfree(&regex);
    }
    
    // 生成 @return 标签
    snprintf(temp, sizeof(temp),
             " * @return %s\n"
             " */\n",
             return_desc);
    strcat(output, temp);
}

注意:正则表达式解析参数时,要考虑到C语言参数可能包含指针类型(如 int *p)或 const 修饰符。我建议在模板中预先定义好参数格式,避免解析出错。

完整流程:从模板到最终代码

下面这张图展示了代码生成器的完整工作流程:

代码生成器工作流程 模板文件 module_template.h 读取 正则替换引擎 匹配 {{...}} 占位符 替换 生成代码文件 uart.h / uart.c 输入数据 模块名、作者、日期等 输出产物 函数声明、注释文档 批量处理 多个模块、多个函数 最终代码文件 可直接编译的 .h/.c 核心思想:一次编写模板,多次生成代码,减少重复劳动

实际应用:生成一个完整的模块

假设我们要生成一个 uart 模块。只需要提供以下数据:

// 输入数据
const char *module_name = "uart";
const char *module_prefix = "UART";
const char *author = "Zhang San";
const char *date = "2025-01-15";
const char *description = "UART 串口驱动模块";

// 函数列表
const char *functions[] = {"init", "send", "receive", "config", "deinit"};
int func_count = 5;

运行代码生成器后,会输出:

/**
 * @file uart.h
 * @author Zhang San
 * @date 2025-01-15
 * @brief UART 串口驱动模块
 *
 * 本文件包含 uart 模块的函数声明和宏定义。
 */

#ifndef __UART_H__
#define __UART_H__

#include <stdint.h>

/* uart_init - uart 模块的 init 操作 */
int uart_init(void);

/* uart_send - uart 模块的 send 操作 */
int uart_send(void);

/* uart_receive - uart 模块的 receive 操作 */
int uart_receive(void);

/* uart_config - uart 模块的 config 操作 */
int uart_config(void);

/* uart_deinit - uart 模块的 deinit 操作 */
int uart_deinit(void);

#endif /* __UART_H__ */

我的经验:代码生成器最值钱的地方不是省那几分钟打字时间,而是保证代码风格统一、减少人为错误。我曾经在一个项目里用生成器生成了200多个模块的代码,零错误。如果手写,不出10个模块就会有人忘记加头文件保护宏。

总结

正则表达式在代码生成中的应用,说白了就是「模板 + 替换」的套路。你只要掌握好正则的捕获组和替换语法,就能做出很实用的工具。

我个人建议:把常用的代码模板整理好,比如函数声明模板、注释模板、错误处理模板。然后用正则表达式写一个通用的生成器。以后每次新建模块,只需要提供几个参数,代码就自动生成了。

嗯,这个实战案例就讲到这里。记住,工具是为人服务的,别让重复劳动消耗你的创造力。


公众号:蓝海资料掘金营,微信 deep3321