能力所限,本讨论仅局限于MSVC的cl编译器和MinGW的gcc编译器。
- 第一部分:不涉及Qt(理清链接子系统和入口函数)
- 第二部分:Qt的链接子系统和入口函数(与第一部分完全对应上)
-
第三部分:QtTest模块出现控制台的原因与方案
- 第四部分:Graeme Gill 给出的很有意思的代码。
再探 链接子系统
在 浅谈Console与Windows子系统 一文中我们简单讨论了一个Windows系统下的 Console 和 Windows 两个链接子系统,但是描述可能有些乱。这儿换种方式整理一下:
-
console 子系统 ==> 拥有黑色CMD窗口
-
windows 子系统 ==> 没有黑色CMD窗口
考虑一个简单的程序代码
代码中定义两个入口函数:main和WinMain(不要觉得两个同时出现很奇怪),下面测试时
- 源码3种情况 :只有main、只有WinMain、二者同时存在
- 链接子系统3中情况 :不指定子系统、指定windows子系统、指定console子系统
- 编译器2种 :msvc的cl、mingw的gcc
#include <windows.h> int main() { MessageBoxW (NULL, L"Hello World from main!", L"hello", MB_OK | MB_ICONINFORMATION); return 0; } int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInst, LPTSTR lpCmdLine, int nShowCmd) { MessageBoxW (NULL, L"Hello World from WinMain!", L"hello", MB_OK | MB_ICONINFORMATION); return 0; }
不指定链接子系统
如果我们分别用MSVC的编译器cl 和 MinGW的编译器gcc (在不指定链接子系统的情况下)分别编译
cl /EHsc hello.c user32.lib gcc hello.c -o hello
会有什么效果呢:
源码入口函数 | 编译器 | 默认链接子系统 | 默认入口函数 |
只有main | gcc | console | |
cl | console | ||
只有WinMain | gcc | console | |
cl | windows | ||
WinMain、main并存 | gcc | console | main,(无法选择WinMain入口,除非你去掉main入口) |
cl | console | main,,但可以通过 |
可以看到:
- 只有一种情况下不是控制台程序(即不弹黑色的cmd窗口)
- MSVC下可以选择根据需要选择入口函数
- MinGW下只要main存在,永远不会使用WinMain
在Qt中,如果是控制台程序(CONFIG+=console),程序只有一个入口,也就是你写的main函数;如果是GUI程序,则处于双入口并存的局面(第二部分对此有详细解释)。
注意Qt的处理方式:在MinGW下,双入口时它将main改成了qMain,如果你在用MinGW,如果你有兴趣可以自己做如下实验:
- 不要启用CONFIG+=console,不用使用QtTest 模块
- 将你的main函数改成qMain
- 然后和平时一样编译,运行
指定windows子系统
更进一步:如果我们制定链接子系统呢,比如,指定windows子系统(注意此处的选项,我们在Qt部分的CONFIG+=windows对应的文件中会再次看到它)
cl /EHsc hello.c /link /subsystem:windows user32.lib gcc hello.c -o hello -Wl,-subsystem,windows
源码入口函数 | 编译器 | 需要指定入口函数 |
只有main | gcc | 不需要 |
cl | 必须指定 /entry:mainCRTStartup | |
只有WinMain | gcc | 不需要 |
cl | 不需要 | |
WinMain、main并存 | gcc | 不需要(始终是main入口) |
cl | 默认是WinMain,可以通过 |
指定console子系统
为了完整起见,看一下指定console子系统的情况(注意此处的选项,我们在Qt部分的CONFIG+=console对应的文件中会再次看到它)
cl /EHsc hello.c /link /subsystem:console user32.lib gcc hello.c -o hello -Wl,-subsystem,console
结果:
源码入口函数 | 编译器 | 需要指定入口函数 |
只有main | gcc | 不需要 |
cl | 不需要 | |
只有WinMain | gcc | 不需要 |
cl | 必须指定 /entry:WinMainCRTStartup | |
WinMain、main并存 | gcc | 不需要(始终是main入口) |
cl | 默认是main,可以通过 |
入口函数
看前面的3个表,入口函数应该会让你眼花缭乱,但,其实,很简单...
MinGW
-
对于MinGW来说,入口函数和链接子系统无关。无论指定什么子系统,它都会寻找main这个入口,如果main找不到,才会去找WinMain入口。
- 这意味着什么呢?
-
意味着如果代码中同时存在main和WinMain,你无法使用WinMain入口!!(注意Qt中的处理方法)
-
- 具体一点:
-
如果 main 函数不存在,libmaingw32.a将被链接进来,该库里面提供了一个main函数(该函数将调用用户的WinMain函数)
- 可以注意和Qt提供 qtmain 这个库进行对比哈
-
MSVC
对 MSVC 系列的编译器,指定链接子系统比如 /subsystem:console,链接器就会寻找main函数,并选择mainCRTStartup函数;对windows子系统,情况类似。
当我们程序的入口函数是 WinMain 时,如果指定 console 子系统,链接器将报错,这时我们可以指定入口点启动函数 /entry:WinMainCRTStartup 来解决这种问题。
Qt指定链接子系统
Qt默认是设置了windows子系统(因为Qt是界面库,它默认设置这个很容易理解哈),因为不用手动输入CONFIG+=windows,我们应该更熟悉下面这个语句:
CONFIG += console
console.prf
看过qmake的manual,我们可以知道,CONFIG 中指定的东西一般要对应于 features 文件(即 console.prf 或 windows.prf 文件)
这两个文件在 $$QTDIR/mkspecs/features/win32 目录下,其内容会被包含进我们的*.pro文件。
我们打开 console.prf 文件看看:
CONFIG -= windows contains(TEMPLATE, ".*app") : QMAKE_LFLAGS += $$QMAKE_LFLAGS_CONSOLE
呵呵,很容易理解对吧,就是设置一个链接选项。
根据我们所用编译器(比如mingw-g++)的不同,去看看相应的qmake.conf文件($$QTDIR/mkspecs/win32-g++/qmake.conf):
QMAKE_LFLAGS_CONSOLE = -Wl,-subsystem,console
通过第一部分的学习,这么简单的东西,现在不需要解释了吧。我们接下来重点看一下windows.prf文件
windows.prf
这个东西就复杂多了。我们的关注点:
1. 指定了链接选项(和前面console一样,此处略)
2. 定义了一个宏 QT_NEEDS_QMAIN (该宏存在是,我们的main函数其实被替换成了qMain)
3. 链接了一个新的库 qtmain.lib (libqtmain.a)
CONFIG -= console contains(TEMPLATE, ".*app"){ QMAKE_LFLAGS += $$QMAKE_LFLAGS_WINDOWS win32-g++:DEFINES += QT_NEEDS_QMAIN win32-borland:DEFINES += QT_NEEDS_QMAIN qt:for(entryLib, $$list($$unique(QMAKE_LIBS_QT_ENTRY))) { isEqual(entryLib, -lqtmain): { CONFIG(debug, debug|release): QMAKE_LIBS += $${entryLib}$${QT_LIBINFIX}d else: QMAKE_LIBS += $${entryLib}$${QT_LIBINFIX} } else { QMAKE_LIBS += $${entryLib} } } }
QMAKE_LIBS_QT_ENTRY
这个文件有些复杂,里面有个QMAKE_LIBS_QT_ENTRY,它涉及另外一个问题,就是我们在Qt在Windows下的入口函数 一文中提到的:
-
我们在Qt程序中只写main函数,从来不写WinMain函数
- Qt 的lib目录下有 qtmain.lib 和 qtmaind.lib(或者 libqtmain.a和 libqtmaind.a) 这样库
-
该库提供了WinMain 入口,并调用我们写的main函数
-
- 为了证实我的说法,我们此处可以查看其源码:%QTDIR%/src/winmain/qtmain_win.cpp
/* WinMain() - Initializes Windows and calls user's startup function main(). NOTE: WinMain() won't be called if the application was linked as a "console" application. */ extern "C" int APIENTRY WinMain(HINSTANCE instance, HINSTANCE prevInstance, LPSTR, int cmdShow) { ... int result = main(argc, argv.data()); ... }
-
对应上了没,当我们指定windows子系统时,MSVC不是需要WinMain入口么,qtmain就提供了这个入口,该入口进而调用了我们自己写的main函数!
QT_NEEDS_QMAIN
注意,注意 ,发现问题没?我们一开始提到了,当WinMain入口和main入口同时出现时,采用MSVC编译器时,我们可以根据链接子系统选择使用哪一个入口。
可是,我们同时说了,当采用MinGW时,两个入口同时出现时WinMain入口永远不会被使用。这可怎么办?
- 这样一来,qtmain 这个库对与 MinGW 来说就没有任何作用了。确实如此
- 但是,Qt官方还是让他起作用了,这就是,对于MinGW,当使用windows子系统时定义 QT_NEEDS_QMAIN 宏的原因
还是一切用代码说话:无论打开 qwindowdefs.h 还是 qtmain_win.cpp 这个文件,我们都能看到这样的代码
#if defined(QT_NEEDS_QMAIN) int qMain(int, char **); #define main qMain #endif
- 哈哈哈,好玩吧,当该宏出现时,我们的最最常见的main函数,其实被宏替代成了 qMain 了。
-
MinGW 你不是牛么?你不是在main和WinMain同时出现时不使用WinMain么,我惹不起,我把main改成qMain
QtTest模块
这是涉及控制台的有一个地方。非常诡异哈,一旦启用了该模块,就会出现控制台,还很难去掉。在Qt程序弹出CMD窗口 一文中我们讨论了这个问题,并给出一个能工作但很不优雅的方案。这儿我试图告诉大家问题原因及根本解决方法。
启用QtTest 有两种方法:
CONFIG += qtestlib
或
QT += testlib
前者
前者直接配置CONFIG,我们直接去看qtestlib.prf文件(你知道的,在$$QTDIR/mkspecs/features/目录下)就行了:
CONFIG += console qtAddLibrary(QtTest)
文件内容很短,只有两行:
- 第一行:强制设置了console链接子系统
- 第二行:设置头文件路径和库文件(这是必须的哈)
注意:如果你不想要控制台,去掉这儿的第一行就可以啦。
后者
QT += testlib 是 CONFIG += QT 的细化,我们需要查看 qt.prf 文件(文件长,摘取片段):
for(QTLIB, $$list($$lower($$unique(QT)))) { DEFINES *= $$upper(QT_$${QTLIB}_LIB) isEqual(QTLIB, testlib):qlib = QtTest isEqual(QTLIB, testlib):CONFIG += console qtAddLibrary($$qlib)
除了多定义了一个QT_TESTLIB_LIB宏外,和完全前者一样。使用该方式是,如果不想要控制台,直接注释掉console这行即可。
注意:此处也解释了 qmake之CONFIG与QT 一文中的问题。
from邮件列表
我们知道:
- 非windows平台下,没有链接子系统的问题。一个程序,在控制台中启动时,就是一个控制台程序,程序可以直接输出数据到控制台。在窗口系统双击启动时,则不出现控制台。
- 在windows下,则区分这两个东西,所以我们同样需要对两个debug和release分别设置。
废话写了这么多了,看点有意思的,前些天,Qt-interest 邮件列表中,Graeme Gill 在对控制台是否出现的控制上,有个新的想法:让windows下的程序和unix下有同样的行为。
只需要在main函数开始加入如下代码(需要头文件windows.h):
#ifdef Q_WS_WIN { BOOL (WINAPI *AttachConsole)(DWORD dwProcessId); *(FARPROC *)&AttachConsole = GetProcAddress(LoadLibraryA("kernel32.dll"), "AttachConsole"); if (AttachConsole != NULL && AttachConsole(((DWORD)-1))) { if (_fileno(stdout) == -1) freopen("CONOUT$","wb",stdout); if (_fileno(stderr) == -1) freopen("CONOUT$","wb",stderr); if (_fileno(stdin) == -1) freopen("CONIN$","rb",stdin); } } #endif // Q_WS_WIN
很有意思哈,只要检测到在console下运行,则链接上标准输入、标准输出、标准出错。
参考
-
http://hi.baidu.com/cyclone/blog/item/96624a90fb4ca081a977a4db.html
-
http://hi.baidu.com/cyclone/blog/item/31748a82fcc1d39df703a655.html
-
http://hi.baidu.com/cyclone/blog/item/730334a8820911bcca130c23.html
-
http://hi.baidu.com/cyclone/blog/item/d299af511bed309f8c5430c3.html
-
http://lists.trolltech.com/qt-interest/2007-06/thread00740-0.html
-