Windows平台开发

windows的消息队列与消息循环

从最初开始学写Windows应用程序以来,都免不了和Windows消息打交道,但是事实上很长时间都没能把Windows的消息机制彻底弄清楚。本文记叙了我对Windows消息机制以及线程与消息关系的理解,因为水平所限,不免会有些错漏,希望对此有了解的同学指正。
1、Windows的消息队列与消息循环
所有创建了窗口的Windows程序,都需要运行一个消息循环,我们在无数的Windows编程书籍中都可以看到这样的经典代码:

1: while (GetMessage(&msg, hWnd, 0, 0) > 0)
2: {
3: TranslateMessage(&msg);
4: DispatchMessage(&msg);
5: }

这里的hWnd就是创建的窗口句柄,上述循环会不断的把该窗口(hWnd)相关的消息取出来,并分发到消息处理函数当中。
GetMessage函数是用来获取当前线程消息队列当中的消息的,其中的第二个参数如果传递一个窗口句柄,那么就会获取该窗口相关的消息,如果传NULL,那么会将线程消息队列中所有的消息都取出来。如果创建了多个窗口,而只对其中一个窗口句柄调用GetMessage形成消息循环,那么别的窗口都会毫无响应。
这里需要补充说明一个概念:消息队列是操作系统为每个需要处理消息的线程创建的,任何线程只要调用过与消息有关的函数(如,GetMessage,PeekMessage),操作系统就会为该线程创建消息队列。可能有人会问,那没有窗口的线程,操作系统也有必要为其创建消息队列么?这是有可能的,因为我们可以通过诸如PostThreadMessage的Api向别的线程或者本线程发送消息,如果目标线程没有消息队列,会导致这个函数返回失败。
做一个验证上述想法实验:
我们撰写类似于如下代码(所有的示例代码我都已经在Windows7系统上编写程序做过实验,这里忽略与要说明内容无关的细节并不影响对原理的理解,就不再将完整代码附上,读者感兴趣的话可以自行实验)

1: HWND hWnd1 = CreateWindow(…);
2: HWND hWnd2 = CreateWindow(…);
3: while (GetMessage(&msg, hWnd1, 0, 0) > 0)
4: {
5: TranslateMessage(&msg);
6: [...]

Win32平台下编译SVN源码全过程

前段时间曾经总结过一些在win32平台下基于SVN开发的一些注意事项,主要是在利用svn官方发布的二进制库进行开发过程中使用的方法和一些值得注意的问题。
 
由于svn官方发布的win32平台下的二进制文件是基于vc6编译的,在使用vc2005进行开发时,会遇到因CRT冲突而引起的link错误。因此,如果是使用vc2005(我推测使用VC2003也会遇到同样的问题,尚未验证)附带的CRT库与svn官方发布的binary进行link,那么无论如何都会出现crash的问题。最为彻底的解决方案,还是自行编译svn源码。
 
在win32下编译svn源码说明:
 首先需要从官方下载一份SVN源码,版本可以根据需要选取,比如最新的Release 1.6.3(目前已经更新到1.6.6)可以在这个地址下载到:
http://subversion.tigris.org/downloads/subversion-1.6.3.zip
以及svn所需的依赖包:
http://subversion.tigris.org/downloads/subversion-deps-1.6.3.zip
如果需要支持ssl的话,还需要下载openssl(根据实际需要选择相应版本):
http://www.openssl.org/source/
如果需要BerkeleyDB的话,需要下载WindowsBDB(BDB是可选的,如果不使用BDB,则默认使用FSFS):
http://subversion.tigris.org/servlets/ProjectDocumentList?folderID=688&expandFolder=688&folderID=2627
此外还有Windows libintl:
http://subversion.tigris.org/servlets/ProjectDocumentList?folderID=2627&expandFolder=2627&folderID=8100
 
 
解压svn源码包,可以在subversion-1.6.3目录中找到这么一个官方发布的说明文件INSTALL。该文件详述了安装通过源码编译SVN所需依赖的工具及第三方库,并且给出了详细的步骤。
网上同样有一个INSTALL的说明,可以在这里访问到:
http://svn.collab.net/repos/svn/trunk/INSTALL
 
编译开源项目的话,其附带的INSTALL说明都是最重要也是最全面的参考。网上搜索的其他资料,也会有相应的参考价值,但无论如何,其信息的来源也是INSTALL。因此编译开源项目时,认真阅读INSTALL是最重要且效率最高的。
 
从Google上进行一下简单的搜索的话,可以找到一篇介绍svn源码编译的文章,在这里:http://rocksun.cn/?p=103
 
为了后续的步骤方便,我们需要先准备编译所必须的一些东西:
包括Perl:http://www.activestate.com/activeperl/
Python:http://www.python.org/download/
 
 
安装Python以及Perl,比如分别安装到D:\Python26以及D:\Perl。安装好之后,将python以及perl的bin目录设为系统目录并重新启动使之生效。
 
编译svn源码第一步,将下载的svn源码包解压到X:\SVN\svn—trunk下。X可以是任意一个盘符。在我的机器上,我使用了F盘,下文中皆以F盘举例。
 
在F:\SVN\svn-trunk中解压了刚才下载的svn源码包以及依赖之后,可以在目录
F:\SVN\src-trunk\subversion-1.6.3中看到以下文件及目录:

以及文件:

Svn的编译需要依赖libapr以及libapr-util,SQLite,zlib,libintl可选,libneon/libserf二则择一,openssl可选,BerkeleyDB可选,libsasl可选,对Python,Perl,Java,Ruby支持的模块可选,以及KDELibs,GNOME Keyring可选。
 
我们依次先编译依赖项:首先进入到subversion-1.6.3\apr目录中。可以看到存在apr.dsw以及apr.dsp文件,这是VC6的工程文件。我们如果想在2005下编译的话,需要将其转换成sln及vcproj文件,简单的用vc2005打开该文件并保存即可。该目录下还有Makefile.win文件,是win下的makefile,我们打开makefile.win文件查看一下说明:可以得知如果需要编译.sln文件的话,需要置USESLN=1。
 
在VC2005的命令行中输入nmake -f makefile.win buildall checkall USESLN=1便可以开始编译apr了。Checkall表示编译完成后会去运行所有的测试用例。
 
编译完成后,当前目录下会多出2个文件夹,分别是LibR – StaticRelease,Release – DllRelease。如果选择Debug编译,则会生成LibD – StaticDebug, Debug – DllDebug。
 
类似的,我们将apr-util以及apr-iconv也编译好。
 
编译zlib:
进入zlib目录后,使用以下命令编译zlib库
nmake -f win32/Makefile.msc
 
编译openssl:
将此前下载的openssl解压到F:\SVN\openssl
阅读其INSTALL文档(INSTALL.W32)
使用VC编译openssl首先需要运行configure:
perl Configure VC-WIN32
接着运行
ms\do_masm
这里的do_masm是一个bat脚本,该脚本会生成nt.mak以及ntdll.mak分别是Release版本的静态和动态的库的make文件。如果想生成debug版本的make文件,可以通过修改do_masm.bat中的调用mk1mf.pl脚本处的参数实现,具体参数可以参考mk1mf.pl文件自身的说明。
接下来,创建动态链接库版本的ssl库用nmake -f ms\ntdll.mak,以及静态版本使用:nmake -f ms\nt.mak
生成的结果文件位于out32dll文件夹,以及out32文件夹中。
 
编译neon
进入F:\SVN\src-trunk\subversion-1.6.3\neon目录
nmake –f neon.mak
默认生成的是release版的libneon.lib (debug版为libneonD.lib)
可以用nmake –f neon.mak DEBUG_BUILD=1生成debug版的lib。
 
回到F:\SVN\src-trunk\subversion-1.6.3
运行python gen-make.py –help可以了解如何使用gen-make.py生成我们所需的svn编译文件。
由于在此,我打算选用neon, libintl, openssl(本例中并不打算使用BDB,如果需要BDB则需要增加—with-berkeley-db=DIR参数)进行编译,目前需要关注的几个重要参数如下:
–with-apr=DIR
–with-apr-util=DIR
–with-apr-iconv=DIR
–with-neon=DIR
–with-libintl=DIR
–with-openssl=DIR
–with-zlib=DIR
–vsnet-version=VER
 
运行
F:\SVN\src-trunk\subversion-1.6.3>python gen-make.py -t vcproj
–with-apr=apr –with-apr-iconv=apr-iconv –with-apr-util=apr-util –with-libintl=svn-win32-libintl –with-openssl=..\..\openssl –with-zlib=zlib –vsnet-version=2005
 
(其中的libintl需要解压到当前文件夹中)
即可生成vc2005的sln(subversion_vcnet.sln)文件了。
 
打开vc2005,选择Debug编译选项,对项目ALL进行编译。如果一切顺利,则会生成一个F:\SVN\src-trunk\subversion-1.6.3\Debug目录。内容包括svn的所有的lib及可执行文件。

将svn\svn.exe以及所有目录下的.dll文件拷贝到一个新建的bin目录下。将openssl的dll,apr, apr-util, apr-iconv的dll拷贝到同样的bin目录下,如下图:

运行svn –version看看结果吧~
 
最后,当我们有了自行编译的svn可以做什么?你可以做任何你想做的事——比如自己基于svn的接口进行开发(可以参考开源项目rapidsvn以及TortoiseSVN的源码)

编写一个DLL时应当注意什么

库与代码重用
1、静态库vs动态库
静态库的优势和劣势
动态库的优势与问题
2、静态C/C++运行库 vs 动态C/C++运行库 & manifest
3、关于动态库接口设计
 
 
库与代码重用
 
对于像C,C++这样的语言来说,库是扩展语言功能的重要手段,对于项目来说,代码库是节约开发时间,缩减成本,划分项目模块以利于多人合作,并通过重用已有代码而避免重复劳动的必要工具。开发一个稍具规模的项目,总免不了设计和划分模块,如何设计和划分,采取什么方案解决问题,总要面临诸多取舍。
最近跟动态库/静态库/动态静态CRT/MFC打了诸多交道。本文是对这段时间遇到的问题与思考的总结。
 
1.       静态库 vs. 动态库
 
静态库本质上就是编译好的目标代码(使用vc的话,编译.cpp文件或者.c文件之后会得到目标文件.obj,编译结束后,link程序会按照指定的要求将目标文件链接成最终的输出文件——常见的有.lib/.dll/.exe)的一份合集。
相比于动态库,静态库没有入口函数,不能独立执行,不能动态链接,一旦被链接到目标文件中,静态库的代码段/数据段都会被合并到目标文件当中去。这是静态库与动态库的本质区别。
 
由于上述差别,那么我们就很容易理解下面的情况:如果你在静态库中定义一个静态或全局对象,最终该对象会被链接到目标文件的数据段中(目标文件可能是某个dll也可能是exe)。
 
静态库的优势在于,简单易用,你无需考虑内存的申请与释放问题,因为静态库的代码段作为目标模块的一部分(EXE或DLL),在其中所调用的new/delete在链接阶段会被重定位到该目标模块的CRT函数地址。对应的,如果你使用DLL,由于DLL中会自行初始化一份CRT堆,因此它的内存申请与释放是在有别于在EXE的CRT堆中进行的(两个不同的CRT堆),如果你从DLL中申请一块内存出来使用,最后也必须交还给DLL模块内部去释放。否则会产生Heap Collision,在Debug模式下,你会收到一个系统位于malloc中的断言,并了解到发生了什么。
再有,如果你的静态库A依赖于静态库B的头文件,而目标应用程序EXE依赖了静态库A,那么你在链接静态库A的时候,也必须链接静态库B。否则会有静态库A中的某些符号无法被找到,从而导致链接失败。而动态库A如果依赖于一个静态库B,那么你无需在依赖该动态库的EXE中再次包含该静态库,因为该动态库A已经将该静态库B链接到A模块当中了,在DLL A中已经有了一份,如果EXE模块没有直接依赖于静态库B的话,那么就无需在EXE中再次link该静态库了。
 
上述问题也通常是混用静态库和动态库时可能遇到的一个问题:如果有一个静态库A,被动态库B和EXE C都用到了,而EXE C又需要使用动态库B,那么这份A的代码和全局数据就在动态库B和EXE C中存在了两份。第一,这是一种对空间的浪费;其次,这可能会引发问题,比如,当我在EXE C中new一份A中的某对象X的指针,并将该指针传入到动态库B中使用,表面上看似乎没有问题,但是如果这个对象X引用了A库中的全局/静态数据,就可能会引发问题,因为此时动态库B中存在有一份A库中的全局/静态数据,而EXE C中有另一份,如果这个全局/静态数据被X使用的话,那么就会得到前后不一致的上下文环境。具体可能产生什么问题要看具体的逻辑是如何组织的。在这种情况下,最好能将静态库A重新组织成一个动态库,这样就不存在多份代码以及多套全局数据的问题了;如果不能这么做,你可以根据具体的应用程序组织的形式,采取DLL导出A库接口,EXE使用DLL导出接口去访问A库而不直接链接A库本身的方法来变通,但通常这不是根本的解决方案。
 
另一方面,在DLL中可以包含自己的资源文件,而LIB中不能。因为DLL是一份完整的PE文件,而LIB只是若干个OBJ打包。这并不是说,使用LIB的时候不能使用资源(RC),你可以将LIB中使用的RC文件,resource.h都include到目标应用程序当中,只要没有资源ID冲突就可以正常使用,就像是你在目标应用程序的资源当中定义了这些资源一样。而DLL中的资源则隶属于DLL自身,不会与EXE中的资源在ID号上产生冲突,只是在使用的时候还需要针对Resource做一些Handle上的设置(参考AfxSetResourceHandle),才能使得API访问DLL中的资源而非EXE中的。如果要编写一个带有资源的库模块,DLL确实是不二选择。
 
        
2.       静态&动态C/C++ runtime 以及 manifest
 
静态的C-runtime库叫做LIBC.lib,我们通常使用多线程的版本叫LIBCMT.lib,debug下的版本叫LIBCMTD.lib。而动态的C-runtime,在VC的环境下,我们使用的是MSVCRT.lib,以及debug下的MSVCRTD.lib(这两个lib实际是两个导出符号文件,实际的运行库位于msvcrt.dll/msvcrtd.dll中(VC6),如果是VC2005的版本则是msvcr80.dll/msvcr80d.dll)
C++的runtime库,静态版的是LIBCPMT.lib/LIBCPMTD.lib。动态版的是MSVCPRT.lib以及MSVCPRTD.lib。(类似的,实际的运行库位于<VC2005中>msvcp80.dll/msvcp80d.dll)
 
在第一点的对比中已经提到过,如果同时又DLL模块依赖一个静态库,而又有一个依赖该DLL模块的EXE还依赖这个静态库,那么最终的内存中就会存在两份该静态库的代码以及全局数据。这一点对于C/C++运行库也是一样的。所需要注意的是,只要是基于C语言以及C++语言的程序,几乎所有的都会用到C/C++运行库,因此如果在规划一个项目的时候,发现有多个dll模块,那么最好是使用动态版的运行库,以避免最终的内存中出现多份不必要的运行库代码。
 
再有一点是关于XP之后的系统中,以及在VC2005之后的编译环境中,微软添加的manifest机制。该机制是为了解决同名但版本错误的DLL加载问题而提出的。比如,我们手上有两份VC编译器,分别是不同的版本,其中携带的MSVCR80.dll也是不同的版本,但是都叫MSVCR80.dll,如果我们在应用程序安装时,简单粗暴的将系统目录下替换成我们应用程序所依赖的MSVCR80.dll,那么可能会造成以往的应用程序无法正确运行的问题,因为他们可能依赖的是其他版本的MSVCR80.dll。
解决这个问题的途径是将一个dll的运行环境,版本,以及校验值等信息编码到一个目录名当中,而在安装应用程序时,将该版本的dll放置到对应名字的目录下,这样尽管dll名称一致,我们也可以找到所需要正确版本的dll。但是应用程序怎么知道需要依赖哪个版本的dll呢?如果应用程序不知道,即使系统中有正确版本的dll,操作系统也不知道应该去加载哪一个dll。
因此manifest文件就把该模块所依赖的模块都标明了,通过名称,校验码,版本等等信息以确保所指明的模块不会有歧义(用记事本打开一个manifest文件,你就可以看到上述信息),当该应用程序EXE或者DLL被操作系统加载时,系统就会根据该信息去查找对应模块,首先会在C:\Windows\WinSxS目录下找(打开这个目录,你就能看到大量不同版本的CRT/CPRT/MFC/ATL等等运行库),如果没有找到那么也会应用程序当前目录下找。
 
这就是为什么使用VC2005编译一个dll也好,一个exe也好,总会多生成一个manifest的文件的原因。所以如果使用了动态版本的CRT运行库,或者其他任何DLL,那么最好就不要关闭manifest生成的编译选项。
 
3.       关于动态库接口设计
 
如果动态库可能经常会更新版本,提供新的功能接口,那么如果使得动态库的升级无需改动依赖于他的可执行模块就是一个值得思考的问题。
具体要达到上述目标,需要保证的有以下几点:
a.       动态库中的类的内存布局不暴露给外界,也就是说动态库提供给用户的.h中,不应当有类的成员信息。这是因为,如果类的内部数据修改了,增加了一个变量或者减少了,那么这个类所占用的内存大小也就修改了,倘若有exe使用上一版本的类定义,new了一个类对象,那么一旦发生类定义修改的情况,这部分代码就必须要重新编译链接了。比较标准的做法是dll只向外界提供接口定义,而不提供实现定义,即提供的是一个类,但是类中只包含纯虚函数,而没有数据<这种做法有点类似于COM的思路>;或者是只提供一系列普通函数以及一个实现类的指针,这种做法有一个好处,如果添加新的函数接口,也无需重新更新外部程序,但是如果使用虚函数接口则不然。
b.       动态库中的类应当不给外界提供构造析构的能力,同样是基于上述原因,如果外界可以new/delete该类,那么当类的定义修改时就会出现不得不重新编译使用该库的应用程序的情况。倘若只通过dll中定义好的借口进行交互,则不存在这个问题。比如外部不能直接new一个对象,而是必须从一个dll导出函数中获取一个对象的指针,外部同样不能delete这个对象,而是必须将之送入另一个导出函数交给dll去删除。这样只要dll保持着两个接口不变,更新dll时是无需更新依赖该dll的应用程序的。(一个比较好的做法是将dll提供外界的接口类的构造析构函数设置为private)
c.       Dll应当尽可能使用动态crt,如果用到了mfc应当尽可能使用动态mfc。这是为了避免dll与外部模块中存在多份重复代码的情况。同时如果有dll需要依赖的其他静态库,也应当尽可能使用该库的动态版本。
 
p.s. 如果想了解更多的关于静态库,动态库,C运行库方面的知识,可以参考《程序员的自我修养——链接装载与库》一书。
p.s.2 果然如pongba老大所言,将自己认为没有问题的知识写下来是一次很好的学习过程,写下本篇文章的过程中,我将原先认为明白的,但是实际并不见得就真的明白的问题,梳理清楚了很多。写的过程有助于把原先大脑中下意识存在的假设显现出来,在思维中再次加工,将原先不明确的概念搞清楚。

Win32平台下基于SVN开发的若干问题整理

 

Keywords: svn  svn_cmdline_init crash 1.5.6 win32

 SVN(SubVersion)是目前开源的版本管理工具中较为流行的,最新的release是1.6.1。但是遗憾的是基于SVN开发的资料在互联网上异常的少。

         最近一段时间,我在工作上需要编写一个使用SVN进行版本管理的编辑器。(需要对编辑器生成的文件做版本管理以利于多人合作开发)起初是通过调用svn自身携带的的客户端程序svn.exe以实现相关功能,然而由于实际中的若干设计问题,最终的速度很不理想。于是产生了借用svn自身提供的api接口实现所需功能的想法。
 
         在实际使用svn提供的api时遇到了一些问题,经过多番查找资料以及尝试最终得以解决。将问题整理在此,以方便日后与我遇到类似问题的朋友。
 
         Svn提供的第三方开发接口sdk可以在这里下载到。目前最新的release是1.6.1。我在项目中实际使用的版本是1.5.6。开发需要的几个压缩包分别如下:
Svn发布的2进制可运行文件,包括客户端与服务端,以及所需的dll:
http://subversion.tigris.org/files/documents/15/45230/svn-win32-1.5.6.zip
Svn开发所需的lib及头文件以及文档:
http://subversion.tigris.org/files/documents/15/45236/svn-win32-1.5.6_dev.zip
Svn的调试符号文件:
http://subversion.tigris.org/files/documents/15/45234/svn-win32-1.5.6_pdb.zip
 
此外,在编写基于SVN的程序时,在链接时还有一些必要的库需要添加,分别是:
Berkeley DB for Windows:
http://subversion.tigris.org/files/documents/15/32472/db-4.4.20-win32.zip
libintl binaries for Windows:
http://subversion.tigris.org/files/documents/15/20739/svn-win32-libintl.zip
 
有了这几个包,就可以使用SVN提供的api开发基于SVN的第三方软件了。
 
         基于SVN1.5及之后的版本开发的时候,会遇到一个CRT冲突的问题,该问题会导致在调用svn_cmdline_init时,如果在error stream参数中传入非NULL值,则会直接crash。一个针对该问题的描述可以参见这里。
         我下载了调试符号以及源代码之后,跟踪调试到svn_cmdline_init函数当中,发现crash发生在一个CRT函数(setvbuf)的调用当中。实际跟踪调试的过程中发现,虽然该函数是crt中的函数,然而vc调试器却无法基于本地的调试符号正确跟踪到其源文件当中。
又该问题在1.4及之前的版本不存在。究其原因,我猜测可能是由于自1.5版本之后,SVN提供的库是基于动态链接的CRT库的。而svn本身的lib在编译时链接的crt与客户的VC6的CRT版本不符导致的。(1.4版本之前则是静态链接的CRT)
对该问题的描述还可以参见这里:
 
The problem is stderr — it is a FILE* which is a CRT type which means 
you have to be using the same CRT as the dll’s were built with. 
Unfortunately, there are a few API’s where raw CRT types [...]