荔园在线

荔园之美,在春之萌芽,在夏之绽放,在秋之收获,在冬之沉淀

[回到开始] [上一篇][下一篇]


发信人: jek (好好学习天天向上), 信区: Program
标  题: 创世纪的 C++builder(4)
发信站: BBS 荔园晨风站 (Sun Mar 12 02:37:20 2000), 转信

第十二章 动态链结函式库(DLL-Dynamic Linked Library)



前言



本章要介绍的是动态链结函式库(Dynamic Linked

Library,简称DLL)的撰写、使用及相关主题。动态链结函式库是Windows

程式设计的一门重要领域,不信的话,你可以看看在Windows系统目录下那

些数量庞大的

.DLL档案,它的重要性及使用频率由此可见一般。



基本上,如果略去VCL软体元件不谈的话,在C++Builder中撰写及使用DLL

的方法是和传统Windows

SDK是一致的,然而如此一来C++Builder也就失去了它傲人的优势了。因此

在本章中我会为你介绍如何撰写使用VCL元件的

DLL,同时也针对各种不同程式发展平台如Visual C++, VB之间的DLL使用

上应注意的事项,做一个全面的探讨。



以C++Builder撰写动态链结函式库 (DLL)



图一 以C++Builder撰写的About Dialog



图一所展示的就是我所要撰写的一个以VCL元件组合而成的About Dialog,

如何?看起来是不是颇具商业软体架势呢?



C++Builder由於其先天上的优势,因此在视觉化的程式设计领域游刃有馀。

然而在现实的工作环境中,也许在你手中的专案并非使用C++Builder来撰

写,而是以其他程式工具如Visual

C++,VB或是Borland

C++完成的,如果要全部改写原来的程式,不仅旷日废时,而且可能老板也

不允许,那麽该怎麽办呢?对了,就是利用撰写DLL的途径来达到程式共享

的目的,为了要让传统的Windows

SDK程式设计人员也可以享受此一优势,因此你可以将部份视觉程式设计部

份以DLL完成,然後提供外部函式供他人呼叫,如此你就可以兼顾两者,『执

其两端,用於其中』,而顺利地解决问题了。





好了!废话不多说了,现在开始进入正题吧!



建立DLL专案



建立DLL专案的方式和一般应用程式大致相同。同样地你可以由【File/New】

来建立一个新的专案,然後选择DLL类型的专案。

如图二所示:







图二 选择DLL专案类型



建选择完专案类型之後,它就自动为你产生了相关档案。和应用程式不同的

是,它只产生了一个Project档,而不包含表格档,而该档案只是一个包含

DLL进入点程式的可钊氲慕樯堋?



再来我们来看函式本身,这个函式很简单,只是利用new动态产生一个表格,

然後利用ShowModal来显示该表格,ShowModal会一直等到使用者按Click

之後才关掉表格,此时我们再以delete指令来释放占用的记忆体。





void _export _stdcall ShowImage(void)

{

Form1 = new TForm1(NULL);

Form1->ShowModal();

delete Form1;

}



在完成以上程式之後,你就可以编译程式。此时C++Builder会产生一个DLL

档,以本程式而言,它会产生一个DLLSAMP.DLL档案,而这个就是供外部呼

叫的动态链结函式库。





在C++Builder中使用DLL



再来我要告诉你如何使用动态链结函式库。我们以前面所产生的DLL为例。

使用DLL有两种方式,分别为明确呼叫及不明确呼叫。



我先说明不明确呼叫的使用方式。不明确呼叫指的是,在程式中并没有一行

程式是用来载入DLL,而是利用链结一个记载输入函式的函式库档案(LIB),

来进行链结,如此系统会自动将该DLL载入,同时在使用完毕後将其释放,

不必由使用者(也就是呼叫它的函式)来进行载入及释放的动作。





首先必须产生一个LIB档,你可以利用C++Builder程式目录内的IMPLIB.EXE

来产生该档案,切忌勿使用Visual C++

的IMPLIB.EXE,因为Microsoft所使用的格式是COFF格式的LIB档,而

Borland所使用的格式是OMF格式的LIB档。(同样地,若是你的LIB档是要

给Visual

C++ 链结用的,那就要使用它所附的IMPLIB.EXE,在使用时不可不察)。因

此我们可用以下指令产生DLLSAMP.LIB档。



IMPLIB DLLSAMP.LIB DLLSAMP.DLL



如此你就可以得到供程式链结用DLLSAMP.LIB档了。



接着我们来撰写使用该DLL的范例程式。这个程式相当简单,我只在表格中

放置一个Button,然後撰写该Button的OnClick事件处理函式,使其呼叫

ShowImage函式即可。





有一点要注意的是,你必须将先前产生的DLLSAMP.LIB加入此专案中,利用

【Project/Add to Project】选择LIB型态档案,即可将其加入。



最後我们就可以链结程式,以下为其执行结果。



图四 执行结果。



动态链结函式库彻底研究



在前面的范例中,我们已经示范了一个基础dll的撰写方式,然而那只能说

是少部份的Know-How而已,接下来我想针对DLL做一个彻底的探讨,企图

使您对它有一个全面的认知,同时也希望在Know-How之外,可以告诉你一

些关於DLL的Know-Why。





DLL的生与死



DLL顾名思义,是一个可以动态链结的函式库。这其中包含两个意义。第一,

它是动态链结的,也就是说它必须具有『招之即来,挥之即去』的基本特性,

它只有在被需要的时候才会被载入系统中,而在不被需要时,即自系统中释

放。第二,它是一个函式库,因此它的行为模式和一般的函式库没什麽不同,

当它载入时,它就视同其他一般的函式般。





『招之即来,挥之即去』的DLL



前面我们提到,DLL必须具备『招之即来,挥之即去』的基本特性。那麽要

如何载入及释放DLL呢?关於此点,我们必须分为两方面来探讨;即所谓的

明确呼叫及不明确呼叫。



明确呼叫(explicited linked):所谓明确呼叫(explicited linked)是

使用LoadLibrary函式来载入

DLL。使用FreeLibrary函式来释放 DLL。这种方式是由使用者主动透过

LoadLibrary 载入该

DLL,然後以GetProcAddress来取得函式位址,再呼叫该函式。最後在不使

用该DLL之後,再将其释放。使用明确呼叫的优点在於,你可以完全控制该

DLL的载入及释放,最有效地利用系统资源:缺点则是,必须自行利用

GetProcAddress来取得叫使用的函式位址,但也由於使用了GetProcAddress

来取得函式位址,因此在使用上增加许多弹性。由於此种使用方式载入函式

程式是主动且可见的,因此名之为明确呼叫。





不明确呼叫 ( implicited linked):所谓不明确呼叫则是利用链结DLL函

式库所相对应的输出函式库 ( export

library),来达成呼叫函式的目的。因此载入DLL以及释放DLL的程序是不

可见的,当使用该输出函式库的程式载入後,系统即将该DLL载入,当使用

该输出函式的程式结束後,系统即将该DLL释放。使用不明确呼叫的优点在

於,使用翟诙ㄒ迳鲜遣幻魅返摹N谗崮兀恳蛭狣LL的载

入及释放尚牵涉到多行程使用时的载入及释放。由於DLL是动态链结的,因

此可以同时有许多程式在使用同一个

DLL,举例来说:若一个X.DLL同时被A、B、C三个程式使用着,则X.DLL

会被载入三次。然而系统为了结省资源,当然不会重复载入,因此此时在系

统内会有一个表格来记载X.DLL的使用次数。所以当A程式载入X.DLL後,

B、C程式再次载入X.DLL时,此时X.DLL并没有被重复地载入,系统只是将

X.DLL的使用次数加一,然後将先前载入的X.DLL位址传回给

B、C两个程式使用,如此就可以达到共享函式库的目的了。同样地在释放X.DLL

时,若该DLL同时有多人使用时,系统纯粹只是将该DLL的使用次数减一,

当其使用次数等於0时,系统才会『真正』地将它由系统中释放。否则若是

系统不分青红皂白即将DLL释放,会造成系统的灾难。





由以上可知,无论我们使用明确呼叫或是不明确呼叫,DLL的载入及释放都

和它的使用次数有关。所以DLL的生与死其实和它的使用次数有关,当它的

使用次数不为0时,就表示其『阳寿未尽』,系统就会维持其活动状态;反

之,若其使用次数为0时,则表示它该『寿终正寝』了,系统就将其释放,

并回收其使用的资源。然而若使用该DLL的程式当掉,导致该DLL没被释放

时,该DLL就会因为使用次数没有被适时减少,而一直在系统内『阴魂不散』

了。这种利用使用次数来管理共享资源的方法,也同时使用在OLE之中。



新知识的实践



现在我们已了解DLL的使用,尚有另一种明确呼叫的方式,我们可以将前面

的范例程式修改为使用明确呼叫的方法来使用 DLL。



void (*ShowImage)(void);

void __fastcall TForm1::ShowButtonClick(TObject *Sender)

{

HINSTANCE hInst;

hInst = LoadLibrary("DLLSAMP.DLL");

(FARPROC &)ShowImage=GetProcAddress(hInst,"ShowImage");

ShowImage();

FreeLibrary(hInst);

}



以上就是修改後的程式,因为程式已改成明确呼叫的方式,因此不需要使用

DLLSAMP.LIB了,所以关於BCB和VC所使用的LIB档格式不同的问题也不存

在了。在此我简单地说明所使用的几个函式





hInst = LoadLibrary("DLLSAMP.DLL") 是用来载入DLLSAMP.DLL

,同时传回该DLL的HINSTANCE值,它是据以使用DLL的权杖。



(FARPROC &)ShowImage=GetProcAddress(hInst,"ShowImage")

利用前面得到的HINSTANCE值,呼叫GetProcAddress来得到ShowImage函

式的位址,因为GetProcAddress所传回的值为Fimage.h即可。如此一来算是解决

了利用 __declspec(dllimport) 和

__declspec(dllexport) 的不便了。



    必也正名乎的DLL函式命名

    谈完了标准写法,再来我们要谈谈一个更容易搞混的函式命名原则。本

来在正常情况下,我们是不需要理会编译器的函式命名规则的,因为在使用

同一样编译器的情况下,不会有什麽太大的问题。然而问题来了,由於DLL

是动态连结函式库,因此它的目标就是希望可以让多个程式共享程式及资

源。所以若是DLL只能为同一种编译器所使用,那麽它的用途就大打折扣了。

因此我们还是必须了解函式的命名方法。同时由於函式命名方式在各种不同

的编译器各不相同,因此我们也必须了解其相异处,最重要的是,我们必须

找出其沟通的方式。



C++ Builder的命名规则



    除了前面提到的 __declspec编译指令之外,在C++ Builder尚有几种

修饰字会影响到函数的命名, 它们就是

    __cdecl,__stdcall,__pascal,__fastcall四个修饰字。为了了解该

修饰字对於函式命名的影响,我们可以用以下的程式来测试之:



#ifndef _DLLNAME01_H_

#define _DLLNAME01_H_

#ifndef DLLNAME

#define EXTERN __declspec(dllimport)

#else

#define EXTERN __declspec(dllexport)

#endif

EXTERN void DllName01(void);

EXTERN void _stdcall DllName02(void);

EXTERN void _cdecl DllName03(void);

EXTERN void _pascal DllName04(void);

EXTERN void _fastcall DllName05(void);

};

#endif



以上为程式的定义,同时我们可以在 .CPP档中撰写相对应的空函式,然後

将其编译成DLL档,再利用TDUMP.EXE或是VC++

内的DUMPBIN.EXE来观察其内容,由於TDUMP会将函式命名解码,反而会使

混淆原来的名称,因此以下的输出是由DUMPBIN.EXE得来。











函式定义 DLL内的函式名 摘要说明

void DllName01(void) @DllName01$qv 因为是CPP程式码

void _stdcall DllName02(void) @DllName02$qqsv 所以函式名都被修

void _cdecl DllName03(void) @DllName03$qv 饰过。

void _pascal DllName04(void) @DLLNAME04$QV

void _fastca名称後加上其使

用叁数的性质,如叁数类别等。这在C++ 中有一个特别的名称,叫做mangled

    name,这是一种为了要实作出多载函式所发出的命名规则。(注:在C++ 中

Add(int) 和Add(double)

    可以同时存在,因此必须在object

    code区分之)。同时这种命名方式由於各个编译器厂商使用的方式各不

相同,因此在撰写DLL时要避免使用之。为了要避开以上问题,我们改以下

列的宣告方式:

#define _DLLNAME01_H_

#ifndef DLLNAME

#define EXTERN __declspec(dllimport)

#else

#define EXTERN __declspec(dllexport)

#endif
直接以下列方式宣告之:

extern "C" void __stdcall ShowImage();



现在我们可以检视除去mangled name後的函式名称:



函式定义 DLL内的函式名 摘要说明

void DllName01(void) _DllName01 名称加底线

void _stdcall DllName02(void) DllName02 名称未变

void _cdecl DllName03(void) _DllName03 名称加底线

void _pascal DllName04(void) DLLNAME04 名称大写

void _fastcall DllName05(void) @DllName05 名称加@



以上我们可得知,在未加修饰字时和使用_cdecl修饰字时的名称是一样的。

而 _pascal修饰字所产生的函式名则和16位元的标准DLL 函式名相同(这

在VC++

是不被接受的),__fastcall的函式名称则加上 @。



其中在WIN32中使用最多的是 _stdcall修饰字,这也是你要撰写一个可以

和其他语言共同使用时所使用的修饰字,其次则为

__cdecl修饰字,这是用来传送不定叁数型别的函式如printf、sprintf等

使用的。其馀两者几乎在DLL没有机会使用。



结论:由上可知,在C++Builder中撰写DLL时必须注意以下事项:

    使用 __declspec(dllimport)及 __declspec(dllexport)的标准型

式。

    注意C++ 的函式名称编码(mangled name)。

    注意修饰字的使用。除非使用不定叁数的函式,否则必使用 __stdcall



饰字。

(4) 不要把 __declspec的使用和 __stdcall混淆了。此二者并没有绝对

的相关性。即使是程式老手都可能栽在此处,切记,切记!



怎麽样,在看完了以上的介绍後,是否有晃然大悟的感觉。DLL

#define EXTERN __declspec(dllimport)

#else

#define EXTERN __declspec(dllexport)

#endif

extern "C" EXTERN void __stdcall ShowImage(void);

#endif



语言双雄' C++Builder 和Visual C++ 连结



前面我们已经把关於C++Builder撰写DLL所应注意到的事项介绍完了,现

在我们来谈另一个重点 - C++Builder和Visual C++

的连结。若是你没有使用过Visual C++ 的话,可以将此部份略去。若是你

在程式设计时必须使用到Visual C++ 的DLL或是必须提供DLL给VC++

或是VB使用时,也许会带给你意想不到的收获。



VC++ 使用C++Builder的DLL函式



在Visual C++ 中使用C++Builder的DLL的函式方法和在C++Builder中使

用大同小异,唯有几件事情必须要注意。



(一)Visual C++ 的LIB档格式和C++Builder的LIB格式不同,因此你必

须重新产生一个 LIB。不过,可惜的是VC++

在32位元的版本中并未提供IMPLIB.EXE函式(这点一直令许多人百思不

解),因此你无法很方便地产生LIB档。解决方法有二:其一是在VC++

内撰写一个同名称的空的DLL函式,令其产生LIB档,其二则是使用

LoadLibrary、GetProcAddress式的明确呼叫方式。

(二)使用前面提到的标准写法。



C++Builder中使用VC++ 的DLL函式



在C++Builder中使用VC++ 的DLL函式时要注意的是Microsoft在Visual C++

中使用的特殊命名规则。在VC++

中命名规则除了前面谈到的几项之外,它还使用了一个特殊的叁数命名法,

简言之,就是在函数名称後面加上叁数的大小,这种命名方法会造成

C++Builder,VB,Delphi使用的上的困扰。举例来说





extern "C" _declspec(dllexport) void __stdcall ShowImage(void);



在VC++ 中产生的函式名称为ShowImage@0(其中0表示叁数大小),而不是

如在C++Builder中产生的ShowImage,这是VC++

已知的问题,这个问题也造成了很多使用non-VC++ 的使用者的问题,解决

之道是在该DLL的DEF档中加上以下的叙述

EXPORTS

ShowImage=ShowImage@0

如此便可以产生正确的函式名了,若是你不想修改DEF档,你也可以在程式

中加入以下的连结指引

#pragma comment(linker,"/exports:ShowImage=ShowImage@0")

假设你不确定其正确的名称,可以利用DumpBin或是TDump观察之。



以上是针对VC++ 的程式设计的所作的额外说明。最後我们以一个VC++ 程

式呼叫本单元的About Dialog DLL做为结束。







此程式的关键程式码如下:

void CVcusedllApp::OnAppAbout()

{

void (*ShowImage)(void);

HINSTANCE hInst;



hInst = LoadLibrary("DLLSAMP2.DLL");

(FARPROC &)ShowImage=GetProcAddress(hInst,"ShowImage");

ShowImage();

FreeLibrary(hInst);

}



--
※ 来源:·BBS 荔园晨风站 bbs.szu.edu.cn·[FROM: 192.168.1.118]


[回到开始] [上一篇][下一篇]

荔园在线首页 友情链接:深圳大学 深大招生 荔园晨风BBS S-Term软件 网络书店