一、引言
在昨天探索和学习 libcurl 库的时候,在 libcurl 库的源代码中的 FAQ 文档中有这么一句话我非常上心:
As a general rule, building a DLL with static CRT linkage is highly discouraged, and intermixing CRTs in the same app is something to avoid at any cost.
简单翻译下:
有个通用的规则,那就是强烈建议不要使用静态的 CRT 链接库去构建一个 DLL 动态链接库,同样的,在同一个程序中混合使用 CRT 的各个版本也是一定要禁止的。
这就引申出来了一个问题:
为什么构建一个动态链接库尽量不要使用静态的 CRT 库呢?
为什么混合使用 CRT 库的各个版本是禁止的呢?
libcurl 的 FAQ 文档中给出了微软官方的一些文档供我们查看,但是这些文档已经有些过时了,这里我找到了两个比较新的文档(都有官方中文翻译):
CRT 库功能
跨 DLL 边界传递 CRT 对象时可能的错误
这篇博客就是参考了上述两篇文章,并且身体力行在 Visual Studio 2017 中上手实践操作了第二篇文章中的实例代码,在实践中去寻找上述两个问题的原因。
这里,我也建议大家去认真看一看上述的两篇文章,对于 Windows 程序员是非常有帮助的。
ps:本篇博客的实验代码上传到了 GitHub,可以在这里进行查看:
wangying2016/CRT_Test
二、示例一:跨 DLL 边界传递文件句柄
话不多说,我们直接上手写代码,首先照着引言中的第二篇文章中的第一个案例进行编写实践。
编写代码
1. 新建一个 CRT_Test1 解决方案,在里面创建两个 Visual C++ 的空项目,其中一个项目是 test1Dll,另一个项目是 test1Main
2. 在 test1Dll 项目中添加源代码文件 test1Dll.cpp 文件,编写代码如下:
// test1Dll.cpp
// compile with: cl /EHsc /W4 /MD /LD test1Dll.cpp
#include <stdio.h>
__declspec(dllexport) void writeFile(FILE *stream)
{
char s[] = "this is a string\n";
fprintf( stream, "%s", s );
fclose( stream );
}
3. 在 test1Main 项目中添加源代码文件 test1Main.cpp 文件,编写代码如下:
// test1Main.cpp
// compile with: cl /EHsc /W4 /MD test1Main.cpp test1Dll.lib
#include <stdio.h>
#include <process.h>
#include <stdlib.h>
void writeFile(FILE *stream);
int main(void)
{
FILE *stream;
errno_t err = fopen_s(&stream, "fprintf.out", "w");
writeFile(stream);
system("type fprintf.out");
system("pause");
return 0;
}
可见这里,我们在 mian 函数中将一个名为 stream 的文件句柄传递到了 test1Dll 中的 writeFile 函数中去,在这个函数中向 stream 指代的文件中输入了文本信息,最后再返回到 main 函数中,在控制台窗口打印出文件的文本信息。
到这步,当前的解决方案目录如下:
测试代码
现在让我们来测试代码,根据文章中的指示,首先让我们配置 test1Dll 和 test1Main 两个项目都是动态生成,也就是都是 /MD 生成。
1. /MD 生成 DLL 和 .exe 文件
配置 test1Dll 项目的项目属性页:
常规 -> 配置类型:设置为动态库(.dll)
C/C++ -> 代码生成 -> 运行库:设置为多线程调试 DLL(/MDd)
右键项目 -> 生成,可以看到生成成功即可。
配置 test1Main 项目的项目属性页:
VC++ 目录 -> 包含目录:添加 $(SolutionDir)$(Configuration);
定位到当前运行目录的 test1Dll.dll 动态库文件
VC++目录 -> 库目录:添加 $(SolutionDir)$(Configuration);
定位到当前运行目录的 test1Dll.lib 库文件
C/C++ -> 代码生成 -> 运行库:设置为多线程调试 DLL(/MDd)
链接器 -> 附件依赖项:添加 test1Dll.lib
右键项目 -> 生成,可以看到生成成功即可。
此时,我们就可以运行下 test1Main 项目了:
可见,结果是正常的。
2. 使用 /MT 重新生成两个项目
我们上一步中使用的都是 /MD 的方式生成的项目,我们这一次使用 /MT 试一下看看。
大体的配置都不需要改动,只有两个地方需要改一下:
配置 test1Dll 项目的项目属性页:
C/C++ -> 代码生成 -> 运行库:设置为多线程调试 (/MTd)
配置 test1Main 项目的项目属性页:
C/C++ -> 代码生成 -> 运行库:设置为多线程调试 (/MTd)
然后我们重新生成两个项目再运行下 test1Main 项目:
可以看到,运行报错了!
这是为什么呢?
思考总结
根据引言中文章的仔细阅读,我发现:
在第一步中,我们使用 /MD 生成的 DLL 和 .exe 文件,是可以共享单个 CRT 副本的;而在第二步中,我们使用 /MT 重新生成的版本,是让 test1Dll 和 test1Main 两个项目使用了其各自的单独的 CRT 副本,所有运行生成的 test1Main.exe 将会导致访问冲突。因为 test1Main.exe 中的文件句柄,在 test1Dll.dll 中根本访问不到,这才出了错。
也就是文章中所说的:
CRT 库的每个副本都具有单独且完全不同的状态,且按应用或 DLL 保存于线程本地存储中。 因此,CRT 对象(例如,文件句柄、环境变量和区域设置)仅对在其中分配或设置这些对象的 CRT 的应用或 DLL 的副本有效。 当 DLL 及其应用客户端使用不同的 CRT 库副本时,你无法跨 DLL 边界传递这些 CRT 对象,也无法期望在另一侧正确地选取它们。
所以,第一步中的 /MD 生成,两个项目使用了同一个 CRT 副本,该程序没有问题;但在第二步中,使用了 /MT 生成,两个项目分别使用了各自的 CRT 副本,就会出现问题了。
三、示例二:跨 DLL 边界传递环境变量
我们还可以再看一个例子,相关的项目建立配置步骤都是跟上一个例子一样的,这里我就简单粘贴代码和展示结果了:
1. test2Dll.cpp
// test2Dll.cpp
// compile with: cl /EHsc /W4 /MT /LD test2Dll.cpp
#include <stdio.h>
#include <stdlib.h>
__declspec(dllexport) void readEnv()
{
char *libvar;
size_t libvarsize;
/* Get the value of the MYLIB environment variable. */
_dupenv_s( &libvar, &libvarsize, "MYLIB" );
if( libvar != NULL )
printf( "New MYLIB variable is: %s\n", libvar);
else
printf( "MYLIB has not been set.\n");
free( libvar );
}
2. test2Main.cpp
// test2Main.cpp
// compile with: cl /EHsc /W4 /MT test2Main.cpp test2dll.lib
#include <stdlib.h>
#include <stdio.h>
void readEnv();
int main(void)
{
_putenv("MYLIB=c:\\mylib;c:\\yourlib");
readEnv();
system("pause");
}
3. /MD 下的结果
4. /MT 下的结果
可见,从 main 函数中传递到 DLL 中的 readEnv 函数中的环境变量信息,只有在 /MD 的情况下被读了出来,而 /MT 失败了。这是因为 /MD 下 DLL 和 .exe 使用的是同一个 CRT 副本环境,而 /MT 却不是。
四、总结
引言中提到的两篇文章写得都非常好,对于 Windows 程序员来说真的是大有裨益。
我也是因为 libcurl 库的安装文档中稍有提到才开始留意这个问题,那么现在我们就能回答引言中的两个问题了:
为什么构建一个动态链接库尽量不要使用静态的 CRT 库呢?
这是因为,静态链接的 CRT 拥有的是自己的 CRT 副本环境,一旦动态链接库中想要传递诸如文件句柄或者环境变量到静态链接的 CRT 函数中去,都是不可能成功的。而动态的 CRT 库就不会出现这个问题。因为动态的 CRT 库拥有同一份相同的 CRT 副本环境。
为什么混合使用 CRT 库的各个版本是禁止的呢?
这是因为,不同的 CRT 库的副本,拥有的是不同的环境,有各自的代码逻辑、堆栈、环境等等,在同一个程序中出现,是绝对禁止的,尽管可能会不出问题,但也绝不是一个好的设计。
至此,关于 CRT 跨 DLL 边界传递 CRT 对象时可能出现的问题的探讨就告一段落了。有关这些问题还有待在实际项目中加以体会。
To be Stronger:)