链接器如何决定是调用 WinMain 还是 wWinMain?
如果你没有为 Visual C++ 链接器指定 /ENTRY
选项,它会尝试猜测。文档中详细说明了这一点,但我将在这里重述,因为这将引出一个故障排除会话。
首先,如果你指定了 /DLL
标志,那么默认的入口点是 _DllMainCRTStartup
。这是简单的情况。
如果你没有指定 /DLL
标志,那么链接器会查看你的 /SUBSYSTEM
标志。如果你请求了 /SUBSYSTEM:CONSOLE
,那么它会寻找 wmain
和 main
。如果你请求了 /SUBSYSTEM:WINDOWS
,那么它会寻找 wWinMain
和 WinMain
。如果你没有指定子系统,那么它会寻找所有四个符号,并且它找到的第一个符号决定了隐含的 /ENTRY
是什么:
如果链接器找到 | 那么入口点是 |
---|---|
wmain | wmainCRTStartup |
main | mainCRTStartup |
wWinMain | wWinMainCRTStartup |
WinMain | WinMainCRTStartup |
这些神奇符号的搜索是在模块中的符号经过外部引用解析后进行的(根据链接的经典模型)。如果没有匹配,那么它使用 ANSI 入口点并希望一切顺利。
好的,现在你可以尝试解决这位客户的问题:
我们有几个遵循相同总体结构的程序。我们的想法是将
wWinMain
函数放在一个通用的静态库中,wWinMain
函数将使用全局变量(每个程序中定义不同)来管理程序特定的业务逻辑。但当我们这样做时,我们得到了一个链接器错误,说 “无法解析的外部符号 WinMain 由 invoke_main 引用。” 如果我们使用WinMain
,它就可以工作,但我们想要支持 Unicode。
将 wWinMain
函数移到库中意味着链接器在查找入口点时看不到它。因此它假设是 ANSI,然后在找不到 WinMain
时陷入困境。
解决这个问题的一种方法是要求库的客户定义自己的 wWinMain
,但它只是将调用转发到你的库。
int WINAPI wWinMain(HINSTANCE hinst,
HINSTANCE hinstPrev,
PWSTR pszCmdLine,
int nCmdShow)
{
return Contoso::wWinMain(hinst, hinstPrev,
pszCmdLine, nCmdShow);
}
现在 wWinMain
函数在项目对象文件中定义,链接器会看到它并将其作为入口点。
拥有一个单独的函数,让客户将他们的 wWinMain
转发过来,也意味着程序可以使用多个库。
int WINAPI wWinMain(HINSTANCE hinst,
HINSTANCE hinstPrev,
PWSTR pszCmdLine,
int nCmdShow)
{
if (GetConfiguration("useContoso")) {
return Contoso::wWinMain(hinst, hinstPrev,
pszCmdLine, nCmdShow);
} else {
return Fabrikam::wWinMain(hinst, hinstPrev,
pszCmdLine, nCmdShow);
}
}
如果你不关心与其他库和平共处,另一个解决方案是有一个函数,每个人都必须调用,但它不做什么,②并将该函数放在与你的 wWinMain
相同的 .obj
文件中。链接的经典模型将尝试解析该函数,并且它会从库中拉入 .obj
文件,而该 .obj
文件会顺便带上 wWinMain
。
另一个可能性是,既然链接器会使用 WinMain
作为 /SUBSYSTEM:WINDOWS
模块的名称(如果找不到 wWinMain
),那么你只需在你的库中放入 WinMain
。你的 WinMain
可以忽略它的 ANSI 命令行并调用 GetCommandLineW()
来获取 Unicode 命令行。
但如果你不在乎与其他库和平共处,可能最简单的解决方案是添加一个
#pragma comment(linker, "/include:wWinMain")
这会强制链接器解析 wWinMain
,它会在你的库中找到它并将其对象文件添加到你的项目中,到那时,“我应该使用什么入口点?”将看到 wWinMain
存在并使用它。
¹ 当然,这是在去除死代码之前完成的:没有入口点,你拥有的只是死代码!
² 基本上,你正在使用 InitCommonControls 的技巧。