深入Delphi下的DLL编程

深入Delphi下的DLL编程

作者:岑心

引 言 
相信有些计算机知识的朋友都应该听说过“DLL”。尤其是那些使用过windows操作系统的人,都应该有过多次重装系统的“悲惨”经历——无论再怎样小心,没有驱动损坏,没有病毒侵扰,仍然在使用(安装)了一段时间软件后,发现windows系统越来越庞大,操作越来越慢,还不时的出现曾经能使用的软件无法使用的情况,导致最终不得不重装系统。这种情况常常是由于dll文件的大量安装和冲突造成的。这一方面说明DLL的不足,另一方面也说明DLL的重要地位,以至我们无法杜绝它的使用。 
DLL(动态链接库,Dynamic Link Library)简单来说是一种可通过调用执行的已编译的代码模块。DLL是windows系统的早期产物。当时的主要目的是为了减少应用程序对内存的使用。只有当某个函数或过程需要被使用时,才从硬盘调用它进入内存,一旦没有程序再调用该DLL了,才将其从内存中清除。光说整个windows系统,就包括了成百上千个dll文件,有些dll文件的功能是比较专业(比如网络、数据库驱动)甚至可以不安装的。假如这些功能全部要包括在一个应用程序(Application program)里,windows将是一个数百M大小的exe文件。这个简单的例子很容易解释DLL的作用,而调用DLL带来的性能损失则变得可被忽略不计。 
多个应用程序调用同一个DLL,在内存里只有一个代码副本。而不会象静态编译的程序那样每一个都必须全部的被装入。装载DLL时,它将被映射到进程的地址空间,同时使用DLL的动态链接并非将库代码拷贝,而仅仅记录函数的入口点和接口。 
同时DLL还能带来的共享的好处。一家公司开发的不同软件可能需要一些公用的函数/过程,这些函数/过程可能是直接的使用一些内部开发的DLL;一些常用的功能则可以直接使用windows的标准DLL,我们常说的windows API就是包含在windows几个公用DLL文件里的函数/过程;理论上(如果不牵涉作者的版权),知道一个DLL的声明及作用(函数定义的输入参数及返回值),我们完全可以在不清楚其实现(算法或编译方式)的情况下直接使用它。 
假如一个DLL中函数/过程的算法得到了更新,BUG得到了修正,整个dll文件会得到升级。一般来说为了保证向下兼容,调用声明与返回结果应该保持不变。但实际上,即使是同一家开发的DLL,随着功能的改善,也很难保证某个调用执行完全不变。在使用其他人开发的DLL时这种糟糕情况更加的严重。比如我在一个绘图程序里使用了某著名图形软件商旧版本的DLL包,我所有的调用都是根据他发布的旧版的声明来执行的。假设用户安装了该软件商的一个新软件,导致其中部分DLL被更新升级,假如这些DLL已经有过改动,直接后果将是我的软件不再稳定甚至无法运行!不要轻视这种情况,事实上它是很普遍的,比如windows在修正BUG和升级过程中,就不断改动它包含的那些DLL。往往新版DLL不是简单的增加新的函数/过程,而是更换甚至取消了原有的声明,这时候我们再也无法保证所有程序都运行正常。 
DLL除了上面提到的改善计算机资源利用率、增加开发效率、隐藏实现细节外,还可以包含数据和各种资源。比如开发一个软件的多国语言版,就可以使用DLL将依赖于语言的函数和资源分离出来,然后让各地的用户安装不同对应的DLL,以获取本地字符集的支持。再比如一个软件必须的图形、图标等资源,也可以直接放在dll文件中统一安装管理。

创建一个DLL

在进行后面的讲解之前,我想大家应该先清楚一个概念:例程声明的是一个指针变量,调用函数/过程,其实是通过指针转入该函数/过程的执行代码。 
我们先尝试用Delphi来建立一个自己的DLL文件。这个DLL包含一个标准的目录删除(包含子目录及文件)函数。 
建立DLL 
通过Delphi建立一个DLL是很容易的。New一个新Project,选择DLL Wizard,然后会生成一个非常简单的单元。该单元不象一般的工程文件以program开始,而是以library开始的。 
该工程单元缺省引用了SysUtils、Classes两个单元。可以直接在该单元的uses之后,begin … end部分之前添加函数/过程代码,也可以在工程中添加包含代码的单元,然后该单元将会被自动uses。 
接下来是编写DLL例程的代码。如果是引用单元里的例程,需要通过声明时添加export后缀引出。假如是直接写在library单元中的,则不必再写export了。 
最后一步是在library单元的begin语句之上,uses部分及函数定义之下添加exports部分,并列举需要引出的例程名称。注意仅仅是名称,不包含procedure或function关键字,也不需要参数、返回值和后缀。 
exports语句后的语法有三种形式(例程指具体的函数/过程): 
exports例程名; 
exports例程名 index 索引值; 
exports例程名 name新名称; 
索引值和新名称便于其他程序确定函数地址;也可以不指定,如果没有使用Index关键字,Delphi将按照exports后的顺序从1开始自动分配索引号。Exports后可跟多个例程,之间以逗号分隔。 
编译,build最终的dll文件。 
需注意的格式 
为了保证生成的DLL能正确与C++等语言兼容,需要注意以下几点: 
尽量使用简单类型或指针作为参数及返回值的类型。这里的简单类型是指C++的简单类型,所以string字符串类型最好转换成Pchar字符指针。直接使用string的DLL例程在Delphi开发的程序中调用是没有问题的(有资料指出需加入ShareMem做为第一单元以确保正确),但如果使用C++或其他语言开发的程序调用,则不能保证参数传递正确; 
虽然过程是允许的,但是最好习惯全部写成函数。过程则返回执行正确与否的true/false; 
对于参数的指示字比如const(只读)、out(只写)等等,为保证调用的兼容性,最好使用缺省方式(缺省var,即可读写的地址); 
使用stdcall声明后缀,以保证正确的异常处理。16位DLL无法通过这种方式处理异常,所以还得在例程最外层用Try … Except将异常处理掉; 
一般不使用far后缀,除非为了保持与16位兼容。 
范例代码 
DLL工程单元:

library FileOperate;

uses
  SysUtils,
  Classes,
  uDirectory in ‘uDirectory.pas‘;
{$R *.res}

exports
  DeleteDir;

begin

end.函数功能实现单元 :
unit uDirectory;

interface

uses
  Classes, SysUtils;
function DeleteDir( DirName : Pchar ) : boolean; export; stdcall;

implementation

function DeleteDir( DirName : Pchar ) : boolean;
var
  FindFile : TSearchRec;
  s : string;
begin
  s := DirName;
  if copy( s, length( s ), 1 ) <> ‘/‘ then
    s := s + ‘/‘;
  if DirectoryExists( s ) then
  begin
    if FindFirst( s + ‘*.*‘, faAnyFile, FindFile ) = 0 then
    begin
      repeat
        if FindFile.Attr <> faDirectory then
        begin
          // 文件则删除
          DeleteFile( s + FindFile.Name );
        end else begin
          // 目录则嵌套自身
          if ( FindFile.Name <> ‘.‘ ) and ( FindFile.Name <> ‘..‘ ) then
            DeleteDir( Pchar( s + FindFile.Name ) );
        end;
      until FindNext( FindFile ) <> 0;
      FindCLose( FindFile );
    end;
  end;
  Result := RemoveDir( s );
end;

end.

初始化及释放资源

Delphi中初始化有几种方法。一种是利用Unit的Initalization与Finalization这两个小节(不知道“单元小节”?你该先去恶补Delphi语法了)进行该单元中变量的初始化工作。注意,DLL虽然在内存中只有一个副本,但是例程隶属于调用者的不同进程空间。如果想初始化公共变量来达到多进程共享是不可行的,同时也要注意公共变量带来的冲突问题。

二是在library单元的begin … end部分进行DLL的初始化。假如想在DLL结束时有对应代码,则可以利用DLL自动创建的一个ExitProc过程变量,这是一个退出过程的指针。建立一个自己的过程,并将该过程的地址赋与ExitProc。因为ExitProc是DLL创建时就存在的,所以在begin … end部分就应该进行此步操作。同时建立一个临时指针变量保存最初的ExitProc值,在自己的退出过程中将ExitProc值赋回来。这样做是为了进行自己的退出操作后,能完成缺省的DLL退出操作(与在重载的Destory方法中inherated的意义是一样的,完成了自己的destory,还需要进行缺省的父类destory才完整)。 
示例如下:

library MyDLL;

var
  OldExitProc : pointer; // 公共变量,为的保存最初的ExitProc指针以便赋回

procedure MyExitProc;
begin
  // 对应初始化的结束代码
  ExitProc := OldExitProc; // 自己的退出过程中要记住将ExitProc赋回
end;

begin
  // 初始化代码
  OldExitProc := ExitProc;
  ExitProc := @MyExitProc;
end.

第三种方法和ExitProc类似,在System单元中预定义了一个指针变量DllProc(该方法需要引用 Windows单元)。在使用DLLProc时, 必须先写好一个具有以下原型的程序:

procedure DLLHandler(dwReason: DWORD); stdcall; 

并在library的begin..end.之间, 将这个DLLHandler程序的执行地址赋给DLLProc中, 这时就可以根据参数Reason的值分别作出相应的处理。示例如下:

library MyDLL;

procedure MyDLLHandler( dwReason : DWORD );
begin
  case dwReason of
    DLL_Process_Attach :
      ; // 进程进入时
    DLL_Process_Detach :
      ; // 进程退出时
    DLL_Thread_Attach :
      ; // 线程进入时
    DLL_Thread_Detach :
      ; // 线程退出时
  end;
end;

begin
  // 初始化代码
  DLLProc := @MyDLLHandler;
  MyDLLHandle( DLL_Process_Attach );
end.

可见,通过DLLProc 在处理多进程时比ExitProc更加强大和灵活。

静态(隐式)调用DLL

DLL已经有了,接下来我们看如何调用并调试它。普通的DLL是不需要注册的,但是要包含在windows搜索路径中才能被找到。搜索路径的顺序是:当前目录;Path路径;windows目录;widows系统目录(system、system32)。 
引入DLL例程的声明方法 
在需要使用外部例程(DLL函数/过程)的代码之前预定义该函数/过程。即按DLL中的定义原样声明,且仅需要声明。同时加上external后缀引入,与export引出相对应。根据exports的三种索引语法,也有三种确定例程的方式(以函数声明为例): 
function 函数名(参数表):返回值;external ’DLL文件名’; 
function 函数名(参数表):返回值;external ’DLL文件名’ index 索引号; 
function 函数名(参数表):返回值;external ’DLL文件名’ name 新名称; 
如果不确定例程名称,可以用索引方式引入。如果按原名引入会发生冲突,则可以用“新名称”引入。 
进行声明后DLL函数的使用就和一般函数相同了。静态调用方式简单,但在启动调用程序时即调入DLL作为备用过程。如果此DLL文件不存在,那么启动时即会提示错误并立刻终止程序,不管定义是否使用。 
快速查看DLL例程定义可以使用Borland附带的工具tdump.exe(在Delphi或BCB的bin目录下),示例如下: 
Tdump c:/windows/system/user32.dll > user32.txt 
然后打开user32.txt文件,找到Exports from USER32.dll行,之下的部分就是DLL例程定义了,比如:

     VA      Ord. Hint Name
    -------- ---- ---- ----
    00001371    1 0000 ActivateKeyboardLayout
    00005C20    2 0001 AdjustWindowRect
    0000161B    3 0002 AdjustWindowRectEx 

Name列就是例程的名称,Ord就是该例程索引号。注意,该工具是不能得到例程的参数表的。如果参数错误,调用DLL例程会引起堆栈错误而导致调用程序崩溃。

调用代码

建立一个普通工程,在Main窗体上放置一个TShellTreeView控件(Samples页),再放置一个按钮,添加代码如下:

function DeleteDir( DirName : Pchar ) : boolean; stdcall;
  external ‘FileOperate.dll‘;

procedure TForm1.Button1Click( Sender : TObject );
begin
  if DirectoryExists( ShellTreeView.Path ) then
    if Application.MessageBox( Pchar( ‘确定删除目录‘ + QuotedStr( ShellTreeView.Path )
      + ‘吗?‘ ), ‘Information‘, MB_YESNO ) = IDYes then
      if DeleteDir( Pchar( ShellTreeView.Path ) ) then
        showmessage( ‘删除成功‘ );
end;

该范例调用的就是前面建立的DLL。 
注意,声明时要包括stdcall后缀,这样才能保证调用Delphi开发的DLL的例程中类似PChar这样的参数值传递正确。大家有兴趣可以试验一下,不加入stdcall或者safecall后缀执行上面代码,将不能保证成功传递字符串参数给DLL函数。

调试方法

在Delphi主菜单Run项目中选择Parameters,打开“Run Parameters”对话框。

在Host Application中填入一个宿主程序(该程序调用了将要调试的DLL),还可以在Parameters中输入参数。保存内容,然后就可以在DLL工程中设置断点、跟踪/单步执行了。 
Run该DLL工程,然后将运行宿主程序。执行会调用DLL的操作,然后就能跟踪进入该DLL的代码,接下来的调试操作和普通程序是一样的。 
因为操作系统或其他软件影响的原因,可能会出现进行了上述步骤仍然无法正常跟踪/中断DLL代码的情况。

这时可以试试在菜单Project |Options 对话框的 Linker 页面里将 EXE and DLL Options 中的Include TD32 debug info及include remote debug symbols两个选项选中。 
假如还是不能中断 那只好另外建立一个引用执行代码单元的应用程序,写代码调用例程调试完成后再编译DLL了(其实该方法有时候蛮方便的,但有时候亦非常麻烦)。

引入文件 
DLL比较复杂时,可以为它的声明专门创建一个引入单元,这会使该DLL变得更加容易维护和查看。引入单元的格式如下:

unit MyDllImport; { Import unit for MyDll.dll }

interface

procedure MyDllProc;

implementation

procedure MyDllProc; external ‘MyDll‘ index 1;

end.

这样以后想要使用MyDll中的例程时,只要简单的在程序模块中的uses子句中加上MyDllImport即可。其实这仅仅是种方便开发的技巧,大家打开Windows等引入windows API的单元,可以看到类似的做法。

动态(显式)调用DLL

前面讲述静态调用DLL时提到,DLL会在启动调用程序时即被调入。所以这样的做法只能起到公用DLL以及减小运行文件大小的作用,而且DLL装载出错会立刻导致整个启动过程终止,哪怕该DLL在运行中只起到微不足道的作用。 
使用动态调用DLL的方式,仅在调用外部例程时才将DLL装载内存(引用记数为0时自动将该DLL从内存中清除),从而节约了内存空间。而且可以判断装载是否正确以避免调用程序崩溃的情况,最多损失该例程功能而已。 
动态调用虽然有上述优点,但是对于频繁使用的例程,因DLL的调入和释放会有额外的性能损耗,所以这样的例程则适合使用静态引入。 
调用范例 
DLL动态调用的原理是首先声明一个函数/过程类型并创建一个指针变量。为了保证该指针与外部例程指针一致以确保赋值正确,函数/过程的声明必须和外部例程的原始声明兼容(兼容的意思是1、参数名称可以不一样;2、参数/返回值类型至少保持可以相互赋值,比如原始类型声明为Word,新的声明可以为Integer,假如传递的实参总是在Word的范围内,就不会出错)。 
接下来通过windows API函数LoadLibrary引入指定的库文件,LoadLibrary的参数是DLL文件名,返回一个THandle。如果该步骤成功,再通过另一个API函数GetProcAddress获得例程的入口地址,参数分别为LoadLibrary的指针和例程名,最终返回例程的入口指针。将该指针赋值给我们预先定义好的函数/过程指针,然后就可以使用这个函数/过程了。记住最后还要使用API函数FreeLibrary来减少DLL引用记数,以保证DLL使用结束后可以清除出内存。这三个API函数的Delphi声明如下:

Function LoadLibrary(LibFileName:PChar):THandle;
Function GetProcAddress(Module:THandle;ProcName:PChar):TfarProc;
Procedure FreeLibrary(LibModule:THandle); 

将前面静态调用DLL例程的代码更改为动态调用,如下所示:

type
  TDllProc = function( PathName : Pchar ) : boolean; stdcall;

var
  LibHandle : THandle;
  DelPath : TDllProc;

begin
  LibHandle := LoadLibrary( Pchar( ‘FileOperate.dll‘ ) );
  if LibHandle >= 32 then
  begin
    try
      DelPath := GetProcAddress( LibHandle, Pchar( ‘DeleteDir‘ ) );
      if DirectoryExists( ShellTreeView.Path ) then
        if Application.MessageBox
          ( Pchar( ‘确定删除目录‘ + QuotedStr( ShellTreeView.Path ) + ‘吗?‘ ),
          ‘Information‘, MB_YESNO ) = IDYes then
          if DelPath( Pchar( ShellTreeView.Path ) ) then
            showmessage( ‘删除成功‘ );
    finally
      FreeLibrary( LibHandle );
    end;
  end;
end;

Delphi开发DLL常见问题

字符串参数 
前面曾提到过,为了保证DLL参数/返回值传递的正确性,尤其是为C++等其他语言开发的宿主程序使用时,应尽量使用指针或基本类型,因为其他语言与Delphi的变量存储分配方法可能是不一样的。C++中字符才是基本类型,串则是字符型的线形链表。所以最好将string强制转换为Pchar。 
如果DLL和宿主程序都用Delphi开发,且使用string(还有动态数组,它们的数据结构类似)作为导出例程的参数/返回值,那么添加ShareMem为工程文件uses语句的第一个引用单元。ShareMem是Borland共享的内存管理器Borlndmm.dll的接口单元。引用该单元的DLL的发布需要包括Borlndmm.dll,否则就得避免使用string。 
在DLL中建立及显示窗体 
凡是基于窗体的Delphi应用程序都自动包含了一个全局对象Application,这点大家是很熟悉的。值得注意的是Delphi创建的DLL同样有一个独立的Application。所以若是在DLL中创建的窗体要成为应用程序的模式窗体的话,就必须将该Application替换为应用程序的,否则结果难以预料(该窗体创建后,对它的操作比如最小化将不会隶属于任何主窗体)。在DLL中要避免使用ShowMessage而用MessageBox。 
创建DLL中的模式窗体比较简单,把Application.Handle属性作为参数传递给DLL例程,将该句柄赋与Dll的Application.Handle,然后再用Application创建窗体就可以了。 
无模式窗体则要复杂一些,除了创建显示窗体例程,还必须有一个对应的释放窗体例程。对于无模式窗体需要十分小心,创建和释放例程的调用都需在调用程序中得到控制。这有两层意思:一要防止同一个窗体实例的多次创建;二由应用程序创建一个无模式窗体必须保证由应用程序释放,否则假如DLL中有另一处代码先行释放,再调用释放例程将会失败。 
下面是DLL窗体的代码:

unit uSampleForm;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, ExtCtrls, StdCtrls;

type
  TSampleForm = class( TForm )
    Panel : TPanel;
  end;

procedure CreateAndShowModalForm( AppHandle : THandle; Caption : PChar );
  export; stdcall;
function CreateAndShowForm( AppHandle : THandle ) : LongInt; export; stdcall;
procedure CloseShowForm( AFormRef : LongInt ); export; stdcall;

implementation

{$R *.dfm}

// 模式窗体
procedure CreateAndShowModalForm( AppHandle : THandle; Caption : PChar );
var
  Form : TSampleForm;
  str : string;
begin
  Application.Handle := AppHandle;
  Form := TSampleForm.Create( Application );
  try
    str := Caption;
    Form.Caption := str;
    Form.ShowModal;
  finally
    Form.Free;
  end;
end;

// 非模式窗体
function CreateAndShowForm( AppHandle : THandle ) : LongInt;
var
  Form : TSampleForm;
begin
  Application.Handle := AppHandle;
  Form := TSampleForm.Create( Application );
  Result := LongInt( Form );
  Form.Show;
end;

procedure CloseShowForm( AFormRef : LongInt );
begin
  if AFormRef > 0 then
    TSampleForm( AFormRef ).Release;
end;

end.

DLL工程单元的引出声明:

exports
  CloseShowForm,
  CreateAndShowForm,
  CreateAndShowModalForm; 

应用程序调用声明:

procedure CreateAndShowModalForm(Handle : THandle;Caption : PChar);stdcall;external ‘FileOperate.dll‘;
function  CreateAndShowForm(AppHandle : THandle):LongInt;stdcall;external ‘FileOperate.dll‘;
procedure CloseShowForm(AFormRef : LongInt);stdcall;external ‘FileOperate.dll‘; 

除了普通窗体外,怎么在DLL中创建TMDIChildForm呢?

其实与创建普通窗体类似,不过这次需要传递调用程序的Application.MainForm作为参数:

function ShowForm( mainForm : TForm ) : integer;
stdcall
var
  Form1 : TForm1;
  ptr : PLongInt;
begin
  ptr := @( Application.mainForm ); // 先把DLL的MainForm句柄保存起来,也无须释放,只不过是替换一下
  ptr^ := LongInt( mainForm ); // 用调用程序的mainForm替换DLL的MainForm
  Form1 := TForm1.Create( mainForm ); // 用参数建立
end;

代码中用了一个临时指针的原因在Application.MainForm是只读属性。MDI窗体的FormStyle不用设为fmMDIChild。 
初始化COM库 
如果在DLL中使用了TADOConnection之类的COM组件,或者ActiveX控件,调用时会提示 “标记没有引用存储”等错误,这是因为没有初始化COM。DLL中不会调用CoInitilizeEx,初始化COM库被认为是应用程序的责任,这是Borland的实现策略。 
你需要做的是1、引用Activex单元,保证CoInitilizeEx函数被正确调用了

2、在单元级加入初始化和退出代码:

initialization
   Coinitialize(nil);
finalization
   CoUninitialize;
end. 

3、 在结束时记住将连接和数据集关闭,否则也会报地址错误。 
引出DLL中的对象 
从DLL窗体的例子中可以发现,将句柄做为参数传递给DLL,DLL能指向这个句柄的实例。同样的道理,从DLL中引出对象,基本思路是通过函数返回DLL中对象的指针,将该指针赋值到宿主程序的变量,使该变量指向内存中某对象的地址。对该变量的操作即对DLL中的对象的操作。 
本文不再详解代码,仅说明需要注意的几点规则: 
应用程序只能访问对象中的虚拟方法,所以要引用的对象方法必须声明为虚方法; 
DLL和应用程序中都需要相同的对象及方法定义,且方法定义顺序必须一致; 
DLL中的对象无法继承; 
对象实例只能在DLL中创建。 
声明虚方法的目的不是为了重载,而是为了将该方法加入虚拟方法表中。对象的方法与普通例程是不同的,这样做才能让应用程序得到方法的指针。 
DLL毕竟是结构化编程时代的产物,基于函数级的代码共享,实现对象化已经力不从心。现在类似DLL功能,但对对象提供强大支持的新方式已经得到普遍应用,象接口(COM/DCOM/COM+)之类的技术。进程内的服务端程序从外表看就是一个dll文件,但它不通过外部例程引出应用,而是通过注册发布一系列接口来提供支持。它与DLL从使用上有两个较大区别:需要注册,通过创建接口对象调用服务。可以看出,DLL虽然通过一些技巧也可以引出对象,但是使用不便,而且常常将对象化强制转为过程化的方式,这种情况下最好考虑新的实现方法。 
注:本文代码在Delphi6、7中调试通过。 
附:本文参考了“Delphi5开发人员指南”等书及资料。

delphi编写dll心得,感恩前辈的总结(外一篇)

1。每个函数体(包括exports和非exports函数)后面加 ‘stdcall;‘, 以编写出通用的dll 
2。exports函数后面必须加‘export;‘(放在‘stdcall;‘前面) 
3。对于非exports函数可以使用string类型,而且建议使用string类型进行参数传递 
4。对于exports函数请使用PChar类型做参数传递 
5。如果exports调用其他函数,建议在exports函数体内使用变量过渡,然后再调用其他函数;  也就说:尽量不要把exports的参数再作为参数调用其他函数。 
6。exports函数中如果有回传参数:如果是非地址型的(如integer,boolean等基本类型)请  使用var前缀,如果是地址型的请不要使用var前缀(如PChar或数组等)。  对不使用var前缀要回传的参数请使用内存拷贝类函数,如StrPCopy,CopyMemory,Move等。  原因:dll和主应用程序并不能很好的共用一块内存,所以必须进行内存拷贝才能正确将dll  中的内容回传(拷贝)到主应用程序中。也因此对回传的地址标识类参数,在调用dll之前必须  进行内存分配,例如Delphi中:AllocMem(n integer),Pb中:Space(nlong)。  注意在调用dll处dll函数声明时,若是delphi参数声明同dll中的参数声明(回传地址型的参数无需加  var),若是pb回传参数必须加ref 前缀。 
7。非exports函数的参数必须遵循规则:回传参数加前缀var,你完全可以对待非exports函数同在Delphi应用  里写函数一样 
8。非exports函数中如果有数组参数,无论是否回传,请加var前缀,它是地址调用 
9。在dll中布尔型请注意bool和boolean的区别,在调用方环境中将可能引起不同的结果 
10。在dll函数中尽量避免使用delphi特有的数据类型或类,如TStringList等 
11。减少use列表中不必要单元的引用,以减少dll的大小 
12。dll的调试:可以使用showmessage(需use dialogs)来调试,也可以[run]->[Parameters]中配置宿主  程序来单步跟踪dll的执行情况 
13。请注意dll中申请的所有内存必须正确释放,否则dll可能在被调用n次之后会出现地址引用错误 
14。在调用dll时候: 
     1)运行环境:可以直接放在应用程序同目录下,也可以放在一个文件夹下,如果放在一个文件夹下 
 你必须将此文件夹路径设置到环境变量中,你可以在应用程序中设置,也可以在dll中设置:

var
  PathBuf : array [ 0 .. 2048 ] of Char;
  Pathstr : string;

begin
  FillChar( PathBuf, 2048, ‘ ‘ );
  windows.GetEnvironmentVariable( ‘PATH‘, PathBuf, 2048 );
  Pathstr := string( PathBuf );
  Pathstr := Trim( Pathstr );
  if Pos( lowerCase( AppPath + ‘tuxedo/dll‘ ), lowerCase( Pathstr ) ) <= 0 then
  begin
    Pathstr := Pathstr + ‘;‘ + AppPath + ‘tuxedo/dll‘;
    SetEnvironmentVariable( ‘PATH‘, PAnsiChar( Pathstr ) );
  end;
end;

2)开发环境:若delphi同运行环境没什么区别,它是直接编译生成应用程序,并运行应用程序;     
 若PB,必须将dll的路径相对PB的开发工具的应用程序来设置,如放到pb9.0.exe同目录下,当然你可以 
设置[HKEY_LOCAL_MACHINE/SOFTWARE/Microsoft/Windows/CurrentVersion/App Paths/]下面对应你的开 
发工具的应用程序名称目录下设置dll所在的路径(分号隔开添加既可,不要将原来的路径覆盖) 
  
在dll中获取dll的路径:

var
    Buffer:array [0..255] of char;
    tmpstr:String;
begin
    GetModuleFileName(HInstance, Buffer, SizeOf(Buffer));
    tmpstr:=ExtractFilePath(Buffer);
    //...
end; 

提示信息尽量不要在dll中showmessage,最好是作为信息参数传回,宿主程序再根据结果来进行信息提示,  这样也可以不引用Dialogs单元

本文来自CSDN博客,转载请标明出处: http://blog.csdn.net/Sunshyfangtiange/archive/2009/07/21/4365444.aspx

时间: 2024-10-22 22:21:30

深入Delphi下的DLL编程的相关文章

DELPHI下的SOCK编程

本文是写给公司新来的程序员的,算是一点培训的教材.本文不会涉及太多的编程细节,只是简单讲解在DELPHI下进行Winsock编程最好了解的知识. 题外话:我认为学习编程就如同学习外语一样,最好的方式是你先学会如何去运用它,然后才是了解它的语言特性.语法之类的东西.不过很可惜,我们以前的外语教育使用了相反的过程.软件编程也是一样,在很多人的大学阶段,你更多的是学习那些理论知识,学习“语法”,这里,我丝毫没有贬低理论知识重要性的意思.理论知识和实践是相辅相成的,但一个恰当的学习方式,很多时候可以让学

DELPHI下的SOCK编程(转)

DELPHI下的SOCK编程      本文是写给公司新来的程序员的,算是一点培训的教材.本文不会涉及太多的编程细节,只是简单讲解在DELPHI下进行Winsock编程最好了解的知识. 题外话:我认为学习编程就如同学习外语一样,最好的方式是你先学会如何去运用它,然后才是了解它的语言特性.语法之类的东西.不过很可惜,我们以前的外语教育使用了相反的过程.软件编程也是一样,在很多人的大学阶段,你更多的是学习那些理论知识,学习“语法”,这里,我丝毫没有贬低理论知识重要性的意思.理论知识和实践是相辅相成的

MFC下的DLL编程学习

1.DLL库与LIB库对比: 静态链接库Lib(Static Link Library),是在编译的链接阶段将库函数嵌入到应用程序的内部.如果系统中运行的多个应用程序都包含所用到的公共库函数,则必然造成很大的浪费.这样即增加了链接器的负担,也增大了可执行程序的大小,还加大了内存的消耗.Lib的好处是应用程序可以独立运行,而不需要在操作系统中另外安装对应的DLL. 而DLL采用动态链接,对公用的库函数,系统只有一个拷贝(一般是位于系统目录的*.DLL文件),而且只有在应用程序真正运行阶段调用时,才

Delphi 中的DLL 封装和调用对象技术(刘艺,有截图)

Delphi 中的DLL 封装和调用对象技术本文刊登2003 年10 月份出版的Dr.Dobb's 软件研发第3 期刘 艺摘 要DLL 是一种应用最为广泛的动态链接技术但是由于在DLL 中封装和调用对象受到对象动态绑定机制的限制使得DLL 在封装对象方面有一定的技术难度导致有些Delphi 程序员误以为DLL 只支持封装函数不支持封装对象本文着重介绍了DLL 中封装和调用对象的原理和思路并结合实例给出了多种不同的实现方法关键字动态链接库DLL 对象接口虚方法动态绑定类引用面向对象1 物理封装与动

Delphi下利用WinIo模拟鼠标键盘详解

http://www.cnblogs.com/rogee/archive/2010/09/14/1827248.html 本文最早在编程论坛上发表,文章地址:http://programbbs.com/bbs/view12-17207-1.htm,相关文件可以在上述地址的页面中下载.转载时请注明出处. 前言 一日发现SendInput对某程序居然无效,无奈只好开始研究WinIo.上网查了很多资料,发现关于WinIo模拟鼠标键盘的资料很少,有的也只是支言片语讲的不是很详细,而且大部分都是关于模拟键

Java中调用Delphi编写的DLL

有些时候,要写一些程序,在 JAVA 里面好难实现, 但如果使用其它编程语言却又比较容易时,我们不妨通过 JNI 来让不同语言的程序共同完成. JNI 的教程, 网上 C 的比较多,Java 也提供了 javah.exe 为 C 语言的 JNI 程序生成头文件, 如果你是一个 Delphi 编程员, 能否让 JAVA 与 Delphi 程序交互呢? 答案是肯定的,今天我们就来看一下一个简单的例子. Helloworld. 主要是来认识一下, JAVA 怎样调用 Delphi 程序的方法. 好的,

VC++动态链接库(DLL)编程深入浅出(zz)

1.概论 先来阐述一下DLL(Dynamic Linkable Library)的概念,你可以简单的把DLL看成一种仓库,它提供给你一些可以直接拿来用的变量.函数或类.在仓库的发展史上经历了"无库-静态链接库-动态链接库"的时代. 静态链接库与动态链接库都是共享代码的方式,如果采用静态链接库,则无论你愿不愿意,lib中的指令都被直接包含在最终生成的EXE文件中了.但是若使用DLL,该DLL不必被包含在最终EXE文件中,EXE文件执行时可以"动态"地引用和卸载这个与E

Elite Container DELPHI下的一个轻量级IoC对象容器

一.简介: Elite Container是DELPHI下的一个轻量级IoC对象容器(IoC:Inverse of Control,反转控制).它是参考了Java中的Spring框架(主要是配置文件的写法),并结合DELPHI的特点来构建的.相比Spring的对象容器,它提供的功能更为精简常用(如对象延迟创建.对象属性自动注入等),降低了学习的难度,并且提供了很多扩展点,你只需简单地写一个插件实现类,并在配置文件中进行简单配置,就可以让Elite Container拥有你的自定义功能! Elit

winsock教程- windows下的socket编程(c语言实现)

winsock教程- windows下的socket编程(c语言实现) 使用winsock进行socket 编程 这是一个学习windows下socket编程(c语言)的快速指南.这是因为一下代码片段只能运行在windows下.windows API中的socket编程部分叫做winsock. 你电脑上做出的任何网络通信背后基本上都有socket,它是一个网络的基本组成部分.举个例子说当你在浏览器键入www.google.com的时候,socket连接到google.com并且取回那个页面然后才