实现一个控制台项目生成器
使用Visual Studio向导创建的控制台应用程序,通常包含3个项目组织文件和1个代码文件。
- .sln:解决方案文件
- .vcxproj:项目文件
- .vcxproj.filters:项目代码与资源管理文件
- .c:我们创建的C文件

这些后缀各异的文件,事实上都是文本文件;而项目生成器的原理就是使用文件函数,生成同等内容的文件。这里我们创建了两个独立的项目Hello1与Hello2,并为它们各自添加Main.c文件,目的是为了找出项目之间的差异。
了解文件内容
我们以文本方式打开Hello1.sln文件,注意选中的这几个地方。
- Line(06) 项目名称Hello1出现两次。
- Line(06) 有两个不同的以'-'连接的字符串,称为GUID。
- Line(29) 有第三个不同的GUID。
- 黄色的GUID是相同的。

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

可以看到,我们在第一张分析图中选中的部分,就是两个解决方案之间的差异。
生成文件
现在我们已经有把握通过程序生成.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文件进行格式化修改。

现在,以新的思路,我们已经可以生成.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) 项目名称

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

代码方面,则依葫芦画瓢。不同的是,我们连续处理了这两个变化的地方。
/**
* 生成 .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;
}
最后就是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();
}
