张银峰的编程课堂

实现一个控制台项目生成器

使用Visual Studio向导创建的控制台应用程序,通常包含3个项目组织文件和1个代码文件。

  • .sln:解决方案文件
  • .vcxproj:项目文件
  • .vcxproj.filters:项目代码与资源管理文件
  • .c:我们创建的C文件

glimix.com

这些后缀各异的文件,事实上都是文本文件;而项目生成器的原理就是使用文件函数,生成同等内容的文件。这里我们创建了两个独立的项目Hello1与Hello2,并为它们各自添加Main.c文件,目的是为了找出项目之间的差异。

了解文件内容

我们以文本方式打开Hello1.sln文件,注意选中的这几个地方。

  • Line(06) 项目名称Hello1出现两次。
  • Line(06) 有两个不同的以'-'连接的字符串,称为GUID。
  • Line(29) 有第三个不同的GUID。
  • 黄色的GUID是相同的。

glimix.com

接下来我们使用文件比较工具,看看两个项目的.sln文件有什么不同。

glimix.com

可以看到,我们在第一张分析图中选中的部分,就是两个解决方案之间的差异。

生成文件

现在我们已经有把握通过程序生成.sln文件了:通过fputs/fprintf函数向文件写入格式化内容即可。

FILE *fp = fopen("my_project.sln", "w");
fputs("\n", fp);
fputs("Microsoft Visual Studio Solution File, Format Version 12.00\n", fp);
fputs("# Visual Studio Version 16\n", fp);
fputs("VisualStudioVersion = 16.0.31313.79\n", fp);
fputs("MinimumVisualStudioVersion = 10.0.40219.1\n", fp);

在继续之前,我们还需要实现一个冒牌的GUID生成函数。GUID串的取值范围是[0-9, A-F],加上连字符(-),总共36个字符。函数make_guid接收一个大小为37的字符数组,先将连字符位置填充上,然后使用rand()函数根据GUID字符表(table)的长度生成一个随机索引,再将此索引处的字符填充到结果数组中,便完成了GUID的生成。

/**
 * 生成冒牌的GUID (如:8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942)
 */
void make_guid(char guid[37])
{
    const char *table = "ABCDEF0123456789";
    int length = strlen(table);

    // 填充特殊字符
    guid[8]  = '-';
    guid[13] = '-';
    guid[18] = '-';
    guid[23] = '-';
    guid[36] = '\0';

    // 根据字符表填充其它字符
    for (int i = 0; i < 36; ++i)
    {
        if (guid[i] != '-')
            guid[i] = table[rand() % length];
    }
}

接下来是Line(6)的处理,变化部分是项目名称和两个GUID。

Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Hello1", "Hello1.vcxproj", "{5702FE21-3AD5-457A-B5B0-06379C4762ED}"

格式化函数 fprintf 在这里很合适,它根据可变部分,提供一个对应的格式化字符串,输出信息即可。

char guid1[37];
char guid2[37];

make_guid(guid1);
make_guid(guid2);

const char *project_name = "my_project";

fprintf(fp,
        "Project(\"{%s}\") = \"%s\", \"%s.vcxproj\", \"{%s}\"\n",
        guid1, project_name, project_name, guid2);

等等!在继续之前,我们看看还有没有其它的解决方法。受这里fprintf的启发,如果我们以这些文件为模板,将变化的行修改成格式化字符串,生成程序再处理这些特殊的行或许也是可行的。为此,我们建立一个模板项目console_template,并将它的.sln文件进行格式化修改。

ZYF

glimix.com

现在,以新的思路,我们已经可以生成.sln文件了。

/**
 * 生成 .sln 文件
 */
bool create_sln_file(const char *proj_name, const char *proj_guid)
{
    // 打开模板文件
    FILE *fin = fopen("./console_template/console_template.sln", "r");
    if (fin == NULL)
    {
        printf("template file not exist!");
        return false;
    }

    // 创建输出文件
    char file_name[256];
    sprintf(file_name, "%s/%s.sln", proj_name, proj_name);
    FILE *fout = fopen(file_name, "w");
    if (fout == NULL)
    {
        printf("create .sln file failed!\n");
        fclose(fin);
        return false;
    }

    // 生成解决方案文件内容
    char temp_guid[64];
    char line[256];
    int line_nums = 0;

    while (!feof(fin))
    {
        fgets(line, 256, fin);
        line_nums++;

        if (line_nums == 6)
        {
            make_guid(temp_guid);
            fprintf(fout, line, temp_guid, proj_name, proj_name, proj_guid);
        }
        else if (line_nums >= 16 && line_nums <= 23)
        {
            fprintf(fout, line, proj_guid);
        }
        else if (line_nums == 29)
        {
            make_guid(temp_guid);
            fprintf(fout, line, temp_guid);
        }
        else
        {
            fputs(line, fout);
        }
    }

    fclose(fin);
    fclose(fout);

    return true;
}

对于 .vcxproj ,通过比较文件可以看出仅有两处不一样。

  • Line(24) 使用了与.sln中同样的项目GUID
  • Line(25) 项目名称

glimix.com

我们以同样的手法格式化我们的模板文件 console_template.vcxproj

glimix.com

代码方面,则依葫芦画瓢。不同的是,我们连续处理了这两个变化的地方。

/**
 * 生成 .vcxproj 文件
 */
bool create_vcxproj_file(const char *proj_name, const char *proj_guid)
{
    FILE *fin = fopen("./console_template/console_template.vcxproj", "r");
    if (fin == NULL)
    {
        printf("template file not exist!");
        return false;
    }

    char file_name[256];
    sprintf(file_name, "%s/%s.vcxproj", proj_name, proj_name);
    FILE *fout = fopen(file_name, "w");
    if (fout == NULL)
    {
        printf("create .sln file failed!\n");
        fclose(fin);
        return false;
    }

    char line[256];
    int line_nums = 0;

    while (!feof(fin))
    {
        fgets(line, 256, fin);
        line_nums++;

        if (line_nums == 24)
        {
            fprintf(fout, line, proj_guid);

            fgets(line, 256, fin);
            line_nums++;
            fprintf(fout, line, proj_name);
        }
        else
        {
            fputs(line, fout);
        }
    }

    fclose(fin);
    fclose(fout);

    return true;
}

ZYF

最后就是main.c文件的生成了,这里我们没有采用模板的概念,而是直接将代码文件写入到文件中。

/**
 * 生成 main.c 文件
 */
bool create_source(const char *proj_name)
{
    char file_name[128];
    sprintf(file_name, "%s/main.c", proj_name);

    FILE *fout = fopen(file_name, "w");
    if (fout == NULL)
    {
        printf("create main.c file failed!\n");
        return false;
    }

    char *source =
        "#include <stdio.h>\n\n"
        "int main()\n"
        "{\n"
        "    printf( \"GLimix Tutorials\" );\n"
        "    return 0;\n"
        "}";

    fputs(source, fout);
    fclose(fout);

    return true;
}

最后,我们将生成器划分到maker.h/maker.c,暴露出接口 make_win32_console_app 即可。

#include "maker.h"

int main()
{
    return make_win32_console_app();
}

glimix.com