现在是这样,有头上有个代码量还算行的小项目,一直用开源工具链gcc/g++和make管理,但最近发现make总是出一些奇怪的错误,要么打印错位,要么我粗心大意搞错文件名之类的,给自己带来了不少麻烦.
因此,既然安装了Visual Studio 2022RC,肯定要发挥用处对不对,在上述要求下,只得在项目里想办法插入vs的管理,但同时保存原有管理方案,以便换机没有vs的环境仍然能简便的安装MinGW-w64来编译项目.
学习过python3.10.2源码后,我们知道有一个PCBuild
文件夹,里面储存.sln(解决方案文件,也就是项目的根)和.vcxproj文件(所谓的"项目",其实是相对于"解决方案"的一个个组件罢了)
那我们也可以在原有项目下添加一个VSSolution
目录,分别存储vs管理整个项目的所需文件,并且添加到版本控制(具体添加那些目录和文件后文详细说明),然后把原有文件添加到proj里面,这样就能然vs用MSVC的编译器了.而且免去很多维护麻烦,比如最近强迫症重命名一个文件,把xxx/A.c
改成xxx/B.c
,忘记修改Makefile,半天说"no rule to make xxx.o,needed by …"之类的.
因此,如果用vs管理,不仅享受更好的代码优化和警告控制安全性,而且更方便维护项目进展,可谓一举N得
下面步入正题.我的项目地址在这里 (或许你们访问的时候已经修改好了呢)
├─.vscode
├─bin
├─build
├─builtin
├─documents
│ ├─bte
│ ├─rte
│ └─wrn
├─dogc
│ ├─common
│ ├─errors
│ └─ir
├─dogt
├─include
│ ├─dogc
│ │ ├─common
│ │ ├─errors
│ │ └─ir
│ ├─dogt
│ ├─parser
│ └─pcd
├─parser
├─pcd
├─runtime
├─test
│ ├─control
│ ├─output
│ │ ├─control
│ │ └─syntax
│ └─syntax
└─tools
这是结构,tree
命令.我的命名规则是,项目根目录每个文件夹存源码(.c/.cpp),include/子目录对应文件夹,比如根目录是dogc
实现编译器功能,那么include/下就有dogc
目录存储dogc模块的相关.h包含文件.(没用到内联文件哈,项目比较简单)
(个人建议这步操作前存个commit,以免出现意外,并推送到远程服务器)
然后在根目录.
下建立VSSolution/
目录.在 在VS里操作之前,我们先改掉.gitignore
配置,免得版本控制意外加载一些临时文件和不需要的大量文件.
在原有基础上添加如下几行
x86/
x64/
.vs/
*.vcxproj.user
解释下,x86/
和x64/
是屏蔽掉输出的中间文件和最终二进制文件,.vs/
是个隐藏文件夹,在VSSolution
下面,是vs运行的一些必要数据,不需要存版本库.*.vcxproj.user
看这个.user后缀,就能知道是跟本机用户相关的,换台设备就不能使唤了,所以不需要.
然后,在vs里新建项目,名字dog-toolchain
(p.s.选择项目类型的时候选空项目就行了,我们自己添加现有文件)
注意取消勾选"Place solution and project in the same directory".点击Create
.
这时文件系统的项目根目录.
下有子目录dog-toolchain
,手动把里面的文件全都移动到VSSolution/
下,这一步请关闭vs,不然文件占用我可不管.
移动后,删掉dog-toolchain
目录.因为它失去利用价值.
这时文件系统如下:
不要打开.sln,先把这个目录里面的dog-toolchain
删掉,待会重建,然后打开.sln:
说白了那个"项目"被我们删掉了,无法加载.没关系啊!去掉它重来.
remove掉这个项目,点确定
然后add一个新"项目",这里由于我需要编译成dll,所以选DLL项目,(前文说过,实际上是一个组件,对应到toolchain里面就是一个个.exe工具,叫做"项目.vcxproj",整个工具链叫做"解决方案(.sln)")
加载进去后,手动删掉里面的几个文件:(选中,摁del,对话框里选"remove"而不是delete,但其实都一样.实际项目中一般选remove,因为delete就把硬盘上也删掉了)
然后添加我们的文件~(这是源码,也就是.c文件)
正确操作是,选中要添加的文件,(不要选择文件夹,你没法添加的).这里因为项目具体的原因,只选这几个(Dogt.c
后面用到,作为"更新exe架构"的例子)
添加成功.
同理,把include下的相关.h文件添加进去,你会发现不带include/前缀:
随便打开一个文件,代码量不多,但…报错99+.红条占满右侧!壮观!
仔细一看,多半是名称未定义导致的,那肯定是include出毛病了呗!
在项目dogt-dll
上右键(注意不是解决方案,是dogt-dll项目);点最后一个,“属性(Properties)”,沿着路径找到这一步:
(依次点击C/C++
->Additional Include Directories
)
添加相对路径的include path:../../include/
可见错误都没啦~
首先,发现有个"您是没添加’pch’文件吗?",那么禁用掉预编译头文件选项不就完了:
项目属性里,一步步点,如下:
然后应用,按F5,发现好多警告像下面这样:
C4819 The file contains a character that cannot be represented
in the current code page (936).
Save the file in Unicode format to prevent data loss
翻译过来就是,文件包含一个不能正常时候用的字符在当前代码页下(936),请使用UTF-8保存以免数据丢失(人工翻译的,不一定好听)
解决请见我的另一篇博客:解决C4819文件编码警告
搞定,警告少多了:(虽然还是很多)
可以看见好多警告都是"指针可能为0",那解决方案自然是,在指针初始化之后就判空呗…定义一个宏,便于定位调试信息:
#define CHECK_POINTER(p) if (!(p)) { \
printf("%s:%d: Pointer is NULL.", __FILE__, __LINE__); \
abort();}
写在include/debug.h
里面,大家都可以引用.
然后在每个需要判空的头文件里加上对debug头的包含,并在源码具体位置调用,就行啦(就像这样)
ok,只剩安全性错误(毕竟这源码在gcc下能过.那么说明只会引发环境不同导致的错误而不是代码的错误;这也能说明在CL编译器自身没问题的情况下,我们的程序肯定能跑起来)
这个错误是说什么呢?sprintf不安全,请用sprintf_s代替.
为什么会这样?sprintf的标准格式是sprintf(char *dst, const char *fmt, ...)
,想没想过,如果fmt导致的结果字符串太长,假设dst分配了100字节,但凡结果是"100个x+两个y"就有102个字节,算上结尾空字符103,内存:你礼貌吗
sprintf_s则多出来一个参数(在dst前面)size_t count
,控制最大字符数,免得超出dst的界限.(当然如果你能认为保证不越界当然很好,但这样确实更安全,你能保证人不出错率跟电脑比?)
把所有sprintf相关的都改一下:
(这里只需要跟malloc的一致就行啦)
其实,sprintf_s的第二个参数很麻烦,但是我们可以把fmt的长度.加上格式参数里字符串的长度和,加上预计的%d,%f之类长度,编译器会常量折叠,不必担心效率.
还有fopen和strcpy,同理全部换掉.
当然,如果嫌每次写这么多判空很麻烦,那么定义一个宏,以后直接展开:
// 检测指针非空
#define CHECK_POINTER(p) if (!(p)) { \
printf("%s:%d: Pointer is NULL.\n", __FILE__, __LINE__); \
abort();}
// 输出log(编译器bug)
#ifdef __cplusplus
#define LOG(con) cout << __FILE__ << ":" << __LINE__ << con << endl;
#endif
#define OPENFILE(fs, fn, md) if (fopen_s(&(fs), (fn), (md))) { \
printf("%s:%d: File open failed.(%s with mode)\n", fn, md); \
abort(); \
}
由于项目自身原因,我们首先按照同样的方法把parser模块和pcd模块一起迁移过来.这个解析器模块注意,你得手动生成需要的.c和.h,然后添加进项目,这要求语法分析器一次到位(实际上这个项目早就做到了)
这里已经解决完没有任何依赖的libdparser
和libpcd
dll项目(附:写到这里时,dogt的动态库已经被我改成libdogt,所以这段文字以上的截图就不改了)
借此机会讲解dll项目的构建哈
我们打开"属性"->“C/C++”->“预处理”->“预处理宏”,会发现它帮我们定义了一个XXX_EXPORTS
宏,这个XXX就是项目名去掉特殊字符,全部大写
这个宏应该只在dll项目里会被定义,且只在编译时会被定义,也就是说外部程序编译的时候跟这个宏没关系.利用它,我们可以泛华DLL导入导出接口的修饰,控制接口函数到底是导出:暴露给外部使用 还是导入:查找符号并且为当前程序所用
具体代码段如下,在有接口函数的声明的头文件里定理DLLAPI宏:
#ifdef LIBPCD_EXPORTS
#define DLLAPI __declspec(dllexport)
#else
#define DLLAPI __declspec(dllimport)
#endif
翻译过来就是,如果定义了导出宏,就把API修饰宏设置成导出,否则导入.
然后在函数的声明(注意实现不需要加)式前加上DLLAPI前缀:
注意了,这里建议只添加需要导出的函数,内部处理逻辑就省去了,第一可以免得导入库太大,第二,安全性你懂的.
好了说到正题,libdogt会调用pcd和dparser里的接口,因此我们需要把这两个dll跟libdogt.dll关联起来.
libdogt
->References
右键,像下面这样,点第一个,“添加引用”
把两个要依赖的dll项目勾上,ok
这时候"引用"选择器下应该有两项:
然后检查下,也就是项目是否正常依赖另一个项目.在libdogt
右键,找到一个Build Dependencies
,中文应该是"构建依赖"之类的.
会有这个窗口,发现复选框都勾上了,就是对的,这意味着一旦被依赖的项目更改,这个项目会发现并且重构被依赖者,而且这个项目在构建的时候,会先尝试构建被依赖项目,除非它们是最新的.
可见,重编译(Rebuild)libdogt,出现如图说明三个项目都重构建了:
下面总结下把一个Makefile管理的项目迁移到vs管理模式下的步骤:
1.新建一个子目录,保存vs项目管理文件
2.修改版本控制的忽略选项,让它略过vs自动生成的临时文件
3.配置项目,把路径调整到合适的地方
4.把原有源码添加进.vcxproj使vs能管理
5.根据报错和警告调整代码,让它又安全又高效,满足新的环境所需
6.若有dll,编译时根据EXPORTS符号确认导入导出状态;引用是直接在"引用"里加同一工程下的项目,他会自己找.dll和.lib
续集:把项目切换成release编译以及发布注意事项(DLL项目)