引言
我们都知道,在C/C++程序中都有一个入口函数,即主函数main。而在WIN 32程序设计中,有入口函数WinMain,如果是平地建高楼,从头开始编写一个C/C++程序,都必须包含这两个入口函数中的其中一个。但是有时候,通过IDE提供的项目模板创建的项目,在自动生成的源文件(指实现文件.c文件和.cpp文件,以及头文件.h文件)中却找不到上述的入口函数,这让我们一时摸不着头脑。其实,入口函数被封装起来了,它通过调用源文件中的形式上的入口函数来完成工作。
介绍
本文提供了C/C++程序入口函数的封装方法,希望对您有所帮助。其中包含两种封装方式,一种是将入口函数封装到动态链接库中,另一种是将其封装到静态链接库中。
文中的代码均已在Visual Studio 2008上编译通过,如果您使用的IDE与本文不同,可根据实际情况进行相应调整。其中叙述的相关概念多来自网络,由于个人能力有限,不能保证完全准确无误,如果有不当之处请劳烦指出。
封装方法
为了叙述方便,下面将通过实例来进行讲解。
创建解决方案
首先我们在Visual Studio 2008中创建一个名为DLLEntry的WIN32项目(Project),该项目包含在名为EntryEncapsulation的解决方案中(在Visual C++ 6.0中称为工作空间,即Workspace)。为了便于区分,我们的解决方案不想与DLLEntry同名。DLLEntry项目用于将实际入口函数(main或WinMain)封装到DLL中。如果你是VS2008老手的话,应该知道怎么创建DDL项目和静态库项目,我只是个新手。接着,我们在EntryEncapsulation解决方案中再添加一个名为LIBEntry的静态库项目,这个项目用于将实际入口函数封装到LIB中。另外,为了在完成封装后可以检验一下我们的劳动成果,还需预先创建一个用于测试DLL和LIB的空项目NewEntry。顾名思义,我们将在这个项目里编写一个名为NewEntry的函数作为形式上的入口函数。由于我建的NewEntry项目是个WIN 32窗口程序项目,所以它实际入口函数为WinMain。如果您不喜欢的话,可以另外创建一个入口函数为main的WIN 32控制台应用程序项目。
好的,一切准备就绪,出发!期待激动人心的时刻到来吧!
现在先来搞定LIBEntry项目。
入口函数的静态库封装方法
首先在LIBEntry项目中添加源文件LIBEntry.cpp,并在其中编写实际入口函数WinMain。要编写的代码估计你已经很熟悉了,不过为了讲解方便,我得把它再简化些。LIBEntry.cpp中的代码如下:
#include "stdafx.h" // 标准头文件
< xmlnamespace prefix ="o" ns ="urn:schemas-microsoft-com:office:office" />
int NewEntry(void); // 声明形式入口函数NewEntry
// [--
// 在实际入口函数中调用形式入口函数,并把实际入口函数封装到LIB中
int WINAPI WinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine,
int iShowCmd)
{
// 提示先由实际入口函数进入
MessageBox(NULL, TEXT("我是实际入口函数,我封装在LIB里,")
TEXT("下面将调用形式入口函数!"),
TEXT("WinMain"), MB_OK);
// -- TODO:
// 可以通过GetCommandLine获取命令行参数,
// 并通过CommandLineToArgvW
// 将其转换成argv风格的字符串数组,
// 来调用int NewEntry(int argc, char * argv[]),
// 是不是很像int main(int argc, char * argv[])。
// -- ENDTOD
NewEntry(); // 调用形式入口函数,这个函数不在LIB里
return 0;
}
// WinMain
// --]
好了,在LIB中封装WinMain就这么简单。看到NewEntry了吧,我把它作为新的形式上的入口函数,只要在WinMain里调用它就可以了。封装之后,在外面就只能看到NewEntry。调用NewEntry前先做下声明,否则会提示找不到。LIBEntry项目中虽然声明了NewEntry函数,但却不存在它的函数实现,不过这照样能编译通过,因为LIB编译过程不检查函数是否实际存在,这样做是有好处的。在主程序链接时,LIB中的代码将被全部导入到EXE文件中。当程序运行时,不管是原LIB中的代码,还是链接前源文件中自带的代码,都会载入到同一个地址空间中。这样我们就可以假想,WinMain函数和NewEntry函数是被定义在同一个项目的不同源文件中的。入口函数的LIB封装方法就是根据这个原理实现的。事实上,检查函数存在性的过程被放在LIB链接阶段进行。如果在NewEntry项目中不存在NewEntry函数实现,那么项目就无法链接通过。
接下来我们来完成DLLEntry项目。
动态链接库封装方法
首先在DLLEntry项目的DLLEntry.cpp中编写C/C++实际入口函数,在这同样还是WinMain,你可以自己再添上main。DLLEntry.cpp中的代码如下:
#include "StdAfx.h" // 标准头文件
int (* NewEntry)(void); // 定义形式入口函数指针NewEntry
// [--
// 在实际入口函数中调用形式入口函数,并把实际入口函数封装到DLL中
int WINAPI WinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine,
int iShowCmd)
{
// 提示先由实际入口函数进入
MessageBox(NULL, TEXT("我是实际入口函数,我被封装在DLL里,")
TEXT("下面将调用形式入口函数!"),
TEXT("WinMain"), MB_OK);
// 获取形式入口函数指针
NewEntry = (int (*)())GetProcAddress(hInstance, "NewEntry");
if(!NewEntry)
{
// 提示获取形式入口函数地址出错
MessageBox(NULL, TEXT("找不到形式入口函数,即将退出程序!"),
TEXT("WinMain"), MB_OK);
ExitProcess(1); // 结束进程
}
else
{
// -- TODO:
// 可以通过GetCommandLine获取命令行参数,
// 并通过CommandLineToArgvW
// 将其转换成argv风格的字符串数组,
// 来调用int NewEntry(int argc, char * argv[]),
// 是不是很像int main(int argc, char * argv[])。
// -- ENDTOD
NewEntry(); // 调用形式入口函数,这个函数不在DLL中
}
return 0;
}
// WinMain
// --]
与LIB封装不同,不能在DLL中直接调用不存在的函数,只能通过GetProcAdress来获取其在地址空间中的指针,再通过指针调用。这是因为DLL编译时要检查函数存在性,但程序在运行之前,GetProcAdress无法判断指针的有效性。因此,为了防止NewEntry项目中不存在NewEntry函数实现而使程序在运行时出错,有必要在调用NewEntry之前判断其函数指针的有效性。
做了这些还不够,还有一些细节上的东西需要注意,否则会有苦头吃——调用的时候报错,告诉你没找到WinMain函数。如果你做过DLL封装的话,这对你应该不成问题。好了,先来分析一下吧。
DLL的链接方式有两种,分别是载入时动态链接(Load-Time Dynamic Linking)和运行时动态链接(Run-Time Dynamic Linking)。载入时动态链接是在链接时将函数所在DLL的导入库链接到可执行文件中。导入库向系统提供了载入DLL时所需的信息及用于定位DLL函数的地址符号。因此,采用载入时动态链接,在链接时能够检查到WinMain函数的所在。而运行时动态链接是在程序运行之后再载入DLL,并在需要的时候才调用DLL中的导出函数,只有调用的时候才能知道函数是否存在。由于WinMain函数是实际上的入口函数,程序最早从它开始执行,这在DLL载入之前,所以运行时动态链接无法检查它的存在性。
综上,NewEntry项目链接时只能采用载入时动态链接。
问题是,该怎么使导入库中含有WinMain函数的的地址符号。也许你会想到添加__declspec(dllexport)修饰,因为以前我们就是这么做的,但在这里这办法行不通。如果你尝试在WinMain前添加__declspec(dllexport)修饰的话,编译器会告诉你WinMain重定义,因为它跟WinBase.h里的声明有冲突。我想你不会去修改WinBase.h里面的声明吧,至少我不会。所以我们只能另辟蹊径,那就是使用老办法,创建模块定义文件。添加一个名为NewEntry.def的模块定义文件,内容如下:
LIBRARY "DLLEntry"
EXPORTS
WinMain
同时你得确保在“项目设置”的“链接器”中“模块定义文件”一项已经填写上了我们创建的NewEntry.def。如果没问题的话,编译时就可以在导入库DLLEntry.lib中生成WinMain的地址符号。
OK,激动人心的时刻快要到来了,赶快完成我们的NewEntry项目吧。
形式入口函数的使用
在NewEntry项目中添加一个名为NewEntry.cpp的源文件,源文件中的代码如下:
#include <windows.h>
int NewEntry(void)
{
MessageBox(NULL, TEXT("我是形式入口函数,实际入口函数调用了我!"),
TEXT("main"), MB_OK);
return 0;
}
看到了吧!除了提示消息框外,没法再简单了。然后我们依次使用LIBEntry.lib和DLLEntry.dll构建NewEntry项目。最方便的方法是设置NewEntry项目的“项目依赖项”,首先依赖LIBEntry项目,再对NewEntry项目进行构建,生成可执行文件NewEntry.exe,运行一下,弹出消息框,内容显示:
WinMain:“我是实际入口函数,我封装在LIB里,下面将调用形式入口函数!”
单击“确定”按钮,再次弹出一个消息框,显示:
NewEntry:“我是形式入口函数,实际入口函数调用了我!”
再次单击“确定”按钮,程序正常退出。
好的,LIB封装任务圆满完成。重新设置NewEntry项目的“项目依赖项”,这次依赖DLLEntry项目,再重新构建NewEntry项目,生成可执行文件NewEntry.exe,运行一下,弹出消息框,内容显示:
WinMain:“我是实际入口函数,我封装在DLL里,下面将调用形式入口函数!”
单击“确定”按钮,再次弹出一个消息框,显示:
NewEntry:“我是形式入口函数,实际入口函数调用了我!”
再次单击“确定”按钮,程序正常退出。
OK,大功告成。