【工具】分享一个代码生成器编写思路!


大家好,我是杂烩君。

在之前转载的文章:嵌入式中,我们如何面对单调重复的任务?中,李先静前辈提到一点:让电脑去做单调重复的工作。

这点让我很受启发,在工作中需要这类重复性的工作时,我也会编写代码生成器来帮我处理。最近,又完成了一个代码生成器的开发,一键生成大部分原本需要靠体力输出的相似代码,极大地提高了开发效率。

业内知名的代码生成器有很多,如:STM32CubeMX生成STM32基础库代码、project_generator生成器生成基础工程、protoc生成protobuf协数据格式代码等。

project_generator相关文章:嵌入式项目生成器,了解一下!

protoc工具会解析xxx.proto文件并生成对应的xxx.pb-.c及xxx.pb-c.h:

protobuf相关:一种更轻量的数据格式——protobuf干货 | protobuf-c之嵌入式平台使用

AI时代,各种AI辅助编程工具也很多,可以用来快速生成一些工具代码。比如:

  • MarsCode:豆包旗下的智能编程助手,提供智能代码补全等核心能力。支持主流编程语言及IDE,能在编码过程中提供单行或整个函数的建议,同时支持代码解释、单测生成、问题修复、技术问答等辅助功能。
  • GitHub Copilot:一款广为人知的代码编程辅助AI工具,由GitHub和OpenAI合作开发。基于OpenAI Codex模型,能够在各种编程环境中提供代码建议,支持多种编程语言。通过学习大量代码库,帮助开发者提高编码效率和质量。
  • CodeGeeX:CodeGeeX是一款基于智谱AI大模型GLM的强大智能编程助手,提供代码生成/完成、注释生成、代码翻译和基于人工智能的聊天等功能。支持多种编程语言,如Python、C++、Java、JavaScript等,并且适配多种主流IDE。

但是,在我们实际项目中,有些相似性比较高、重复性比较高、更新迭代过程需要频繁新增的一些需要复制、粘贴、然后再进行修改的代码。

以上这些代码生成的手段对我们的帮助可能比较有限。这时候我们可以自己根据自己的项目代码,定制化地写个代码自动生成的小软件/小脚本。

写代码生成工具之前,需要自己评估一下,这个事情是否值得做。因为如果你花了很大力气写了一个辅助工具,结果使用的频次很少,也不是很有必要。或者在某个文件生成几行代码,这手动适配也花不了多少时间,收益不是很大。

我觉得,需要频繁地修改到若干个文件的多处代码,这才比较有必要去写个代码生成工具。比如协议数据新增方面,就是个更新迭代可能会频繁修改的东西吧?

只要一条数据通了,后续新增是不是都会复制粘贴然后修改?

而且有些项目代码分层会比较细,数据传输链路可能会经过多层代码的多个文件,都需要做对应的修改。这手动适配起来费时费力,还容易搞错,复制粘贴,结果某一处忘记修改了没检查到,跑不通又得来回调试。

废话不多说,如果大家也遇到这种情况,那就自己写个代码生成工具吧,然后之后就可以快乐的摸鱼了。

思路很简单,在哪个位置,生成什么代码。我们可以根据需要自己评估是否有必要开发带图形界面的代码生成器,如果是自己使用,那就无所谓,怎么省时怎么来。

下面看看使用shell脚本来生成的例子。

例子

首先我们需要思考两个点:

“生成怎么样的代码?”

脚本处理的优势就是处理一些重复性的、相似性的事情。所以要想脚本生成代码,就得在写代码时有意识地构造脚本能找到一些共性的代码。

”在哪个位置插入代码?“

这个条件从原有的代码里可能不太好确定,为了脚本能简单点,这个条件我们可以自己构造:以注释的形式在想要插入代码的地方做个标记

脚本去匹配代码里的这些标记,然后插入想要插入的代码即可。

废话不多说,直接看例子:

假如我们有这么一份代码:读取命令行的测试指令并执行对应的测试代码。

test.c:

// 公众号:嵌入式大杂烩
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>

#define int8   char
#define uint8  unsigned char
#define uint16 unsigned short int

typedef enum cmd_id_e
{
    CMD_ID_TEST_RAND,
    CMD_ID_TEST_MAX,
}cmd_id_t;

#define TO_CMD_ID_STR(cmd_id) #cmd_id

typedef int (*cmd_func_t)(void);  

typedef struct cmd_s
{
    cmd_id_t cmd_id;
    cmd_func_t cmd_func;
}cmd_t;


static int rand_test_func(void)
{
	printf("--------------------%s start!--------------------\n",__FUNCTION__);
	int a = 0;
	srand((unsigned)time(NULL));
	a = rand() % 10;		
	printf("%d\n", a);
	printf("--------------------%s end!--------------------\n",__FUNCTION__);
	printf("\n");
}

static cmd_t s_cmd_table[CMD_ID_TEST_MAX] = 
{  
    {CMD_ID_TEST_RAND, rand_test_func},  
};  

static void execute_cmd(cmd_id_t cmd_id) 
{  
    if (cmd_id > CMD_ID_TEST_MAX)
    {
        printf("[%s]Invalid param\n", __FUNCTION__);  
        return; 
    }

    for (int i = 0; i < CMD_ID_TEST_MAX; i++) 
    {  
        if (s_cmd_table[i].cmd_id == cmd_id) 
        {  
            s_cmd_table[i].cmd_func();  
            return;  
        }  
    }  
} 

cmd_id_t menu_select(void)
{
	cmd_id_t cmd_id = CMD_ID_TEST_MAX;
	char str[64] = {0};
	
    printf("=============================================================================\n");
	printf("[%.3d] Test: %s\n", CMD_ID_TEST_RAND, TO_CMD_ID_STR(CMD_ID_TEST_RAND));
    printf("=============================================================================\n");

	do
	{
		printf("Enter your choice: ");
		scanf("%s", str);
		cmd_id = atoi(str);
	}while(cmd_id < 0 || cmd_id > CMD_ID_TEST_MAX);
	
	return cmd_id;
}

int main(void)
{
	cmd_id_t cmd_id = CMD_ID_TEST_MAX;
	
	while(1)  
	{
		cmd_id = menu_select();
        execute_cmd(cmd_id);
        usleep(1000 * 1000);
	}
	
	return 0;
}

这个测试代码,目前只有一个测试函数,之后每次新增测试函数,需要新增如下代码:

  • 命令id枚举,新增枚举值。
  • 新增测试函数函数体。
  • 补充命令表。
  • 补充菜单函数。

按照上面说的思路,我们在这四个地方以注释的形式插入代码插入点标签:

// code_insertion_point__cmd_id
// code_insertion_point__cmd_func
// code_insertion_point__cmd_table
// code_insertion_point__cmd_description

插入的代码?

提取公共部分,补充差异。这个例子中,为了脚本能简单点,我们可以依赖于CMD_ID_TEST_RAND,因为这里的信息可以区分不同的命令,比如新的命令就是CMD_ID_XXX格式的。其它几个插入点的代码都可依赖这个生成。假如我们的脚本名称为code_generator.sh,我们期望最终的使用方式如:

./code_generator.sh CMD_ID_XXX

shell基本知识:Shell编程必备简明基础知识!

编写shell脚本:code_generator.sh

#!/bin/bash

code_generate__cmd_id()
{
    APPEND_STR=$1
    DST_FILE_PATH=$2
    sed -i "/\/\/ code_insertion_point__cmd_id/i \\$APPEND_STR" $DST_FILE_PATH
}

code_generate__cmd_func()
{
    APPEND_STR=$1
    DST_FILE_PATH=$2
    sed -i "/\/\/ code_insertion_point__cmd_func/i \\$APPEND_STR" $DST_FILE_PATH
}

code_generate__cmd_table()
{
    APPEND_STR=$1
    DST_FILE_PATH=$2
    sed -i "/\/\/ code_insertion_point__cmd_table/i \\$APPEND_STR" $DST_FILE_PATH
}

code_generate__cmd_description()
{
    APPEND_STR=$1
    DST_FILE_PATH=$2
    sed -i "/\/\/ code_insertion_point__cmd_description/i \\$APPEND_STR" $DST_FILE_PATH
}

code_generate()
{
    CMD_ID_STR=$1

    DST_FILE_PATH="./test.c"
    TAB_SPACE="    "

    # code_insertion_point__cmd_id
    echo "CMD_ID_STR = ${CMD_ID_STR}"
    CMD_ID_APPEND_STR="${TAB_SPACE}${CMD_ID_STR},"
    echo "CMD_ID_APPEND_STR = ${CMD_ID_APPEND_STR}"

    code_generate__cmd_id "${CMD_ID_APPEND_STR}" "${DST_FILE_PATH}"

    # code_insertion_point__cmd_table
    CMD_FLAG="${CMD_ID_STR##*_}" 
    echo "CMD_FLAG = ${CMD_FLAG}"
    CMD_FUNC_STR="${CMD_FLAG,,}_test_func"
    echo "CMD_FUNC_STR = ${CMD_FUNC_STR}"

    CMD_TABLE_APPEND_STR="${TAB_SPACE}{${CMD_ID_STR}, ${CMD_FUNC_STR}},"
    code_generate__cmd_table "${CMD_TABLE_APPEND_STR}" "${DST_FILE_PATH}"

    # code_insertion_point__cmd_description
    CMD_DSCRIPTION_APPEND_STR="${TAB_SPACE}printf(\"[%.3d] Test: %s\\\n\", ${CMD_ID_STR}, TO_CMD_ID_STR(${CMD_ID_STR}));"
    echo "CMD_DSCRIPTION_APPEND_STR = ${CMD_DSCRIPTION_APPEND_STR}"
    code_generate__cmd_description "${CMD_DSCRIPTION_APPEND_STR}" "${DST_FILE_PATH}"

    # code_insertion_point__cmd_func
    CMD_FUNC_STR_PART1="static int ${CMD_FUNC_STR}(void)\n{\n"
    CMD_FUNC_STR_PART2="${TAB_SPACE}printf(\"--------------------%s start!--------------------\\\n\",__FUNCTION__);\n"
    CMD_FUNC_STR_PART3="${TAB_SPACE}\/\/ todo: add your code\n"
    CMD_FUNC_STR_PART4="${TAB_SPACE}printf(\"--------------------%s end!--------------------\\\n\",__FUNCTION__);\n"
    CMD_FUNC_STR_PART5="${TAB_SPACE}printf(\"\\\n\");\n}"
    CMD_FUNC_APPEND_STR=${CMD_FUNC_STR_PART1}${CMD_FUNC_STR_PART2}${CMD_FUNC_STR_PART3}${CMD_FUNC_STR_PART4}${CMD_FUNC_STR_PART5}
    echo "CMD_FUNC_APPEND_STR = ${CMD_FUNC_APPEND_STR}"
    code_generate__cmd_func "${CMD_FUNC_APPEND_STR}" "${DST_FILE_PATH}"
}

code_generate $1

测试:

生成的代码:

一些代码细节,代码生成脚本不能事先知道,生成时可以统一备注:

// todo: add your code

方便后续填充代码。

编译、执行:

可见,我们写的代码生成脚本帮我们生成的代码,执行也是符合预期的。

以上只是提供了一个简单的代码生成脚本的思路及很理想情况下的demo,实现方式可能有多种,大家可以自己思考挖掘。

并且实际项目中的情况会复杂得多。

特别是处理代码格式,如果需要脚本做,脚本就会比较复杂了,比如:

  • 代码中间的不定空格数?

  • 代码增加注释?

  • ……

需要下很大的功夫。有时候需要做一些取舍,比如,假如上面的命令表代码为:

此时中间的空格长度根据命令的长度不一而是一个变化的值,脚本要去处理这个事情就需要做不少逻辑,这会增加一些脚本的开发时间。

最后,再说一下:

写代码生成工具之前,需要自己评估一下,这个事情是否值得做。因为如果你花了很大力气写了一个辅助工具,结果使用的频次很少,也不是很有必要。或者在某个文件生成几行代码,这手动适配也花不了多少时间,收益不是很大。

本文的c_test及脚本,可在【嵌入式大杂烩】公众号回复关键词: 代码生成脚本 ,即可获取。



文章作者: 杂烩君
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 杂烩君 !
  目录