用Delphi制作DLL

手把手教 delphi: 写你的 dll 文件
一、开使你的第一个 DLL 专案
   1.File->Close all->File->New �z DLL �{
代码 :
   // 自动产生 Code 如下
   library Project2;
   // 这有段废话
   uses
   SysUtils,
   Classes;

   {$R *.RES}

   begin
   end.
   2. 加个 Func 进来:
  代码 :
   library Project2;
   uses
   SysUtils,
   Classes;

Function MyMax ( X , Y : integer ) : integer ; stdcall ;
begin
   if X > Y then
   Result := X
   else
   Result := Y ;
end ;
//
切记: Library 的名字大小写没关系,可是 DLL-Func 的大小写就有关系了。
//
DLL-Func-Name 写成 MyMax myMAX 是不同的。如果写错了,立即
//
的结果是你叫用到此 DLL AP 根本开不起来。
//
参数的大小写就没关系了。甚至不必同名。如原型中是 (X,Y:integer) 但引
//
用时写成 (A,B:integer) ,那是没关系的。
//
切记:要再加个 stdcall 。书上讲,如果你是用 Delphi DLL ,且希望不仅给
// Delphi-AP
也希望 BCB/VC-AP 等使用的话,那你最好加个 Stdcall ; 的指示
//
参数型态: Delphi 有很多种它自己的变量型态,这些当然不是 DLL 所喜欢的
//
Windows/DLL 的母语应该是 C 。所以如果要传进传出 DLL 的参数,我们
//
尽可能照规矩来用。这两者写起来,后者会麻烦不少。如果你对 C 不熟
//
的话,那也没关系。我们以后再讲。

   {$R *.RES}

   begin
   end.
   3. 将这些可共享的 Func 送出 DLL ,让外界�z就是你的 Delphi-AP 啦�{使用:光如此,你的 AP 还不能用到这些,你还要加个 Exports 才行。
  代码 :
   {$R *.RES}
   exports
   MyMax ;
   begin
   end.
   4. 好了,可以按 Ctrl-F9 编译了。此时可不要按 F9 DLL 不是 EXE┌ 不可单独执行的,如果你按 F9 ,会有 ErrorMsg 的。这时如果 DLL Error ,请修正之。再按 Ctrl-F9 。此时可能有 Warning ,不要紧,研究一下,看看就好。再按 Ctrl-F9 ,此时就『 Done , Compiled 』。同目录就会有个 *.dll 。恭喜,大功告成了。
二、进行测试:开个新 application
   1. 加个 TButton
  代码 :
   ShowMessage ( IntToStr(MyMax(30,50)) ) ;
   2. 告知 Exe 到那里抓个 Func
  代码 :
   // Form,interface,var 后加
   Function MyMax ( X , Y : integer ) : integer ; stdcall ; external 'MyTestDLL.dll' ;
   // MyTestDLL.dll 为你前时写的 DLL 项目名字
   // DLL 名字大小写没关系。不过记得要加 extension .DLL 。在 Win95 NT
   // 是不必加 extension ,但这两种 OS ,可能越来越少了吧。要加 extension
  可以了,简单吧。
  上面的例子是不是很简单?熟悉 Delphi 的朋友可以看出以上代码和一般的 Delphi 程序的编写基本是相同的,只是在 TestDll 函数后多了一个 stdcall 参数并且用 exports 语句声明了 TestDll 函数。只要编译上面的代码,就可以玫揭桓雒 �� �D elphi.dll 的动态链接库。现在,让我们来看看有哪些需要注意的地方:
   1. DLL 中编写的函数或过程都必须加上 stdcall 调用参数。在 Delphi 1 Delphi 2 环境下该调用参数是 far 。从 Delphi 3 以后将这个参数变为了 stdcall ,目的是为了使用标准的 Win32 参数传递技术来代替优化的 register 参数。忘记使用 stdcall 参数是常见的错误,这个错误不会影响 DLL 的编译和生成,但当调用这个 DLL 时会发生很严重的错误,导致操作系统的死锁。原因是 register 参数是 Delphi 的默认参数。
   2. 所写的函数和过程应该用 exports 语句声明为外部函数。
  正如大家看到的, TestDll 函数被声明为一个外部函数。这样做可以使该函数在外部就能看到,具体方法是单激鼠标右键用 快速查看( Quick View 功能查看该 DLL 文件。(如果没有 快速查看 选项可以从 Windows CD 上安装。) TestDll 函数会出现在 Export Table 栏中。另一个很充分的理由是,如果不这样声明,我们编写的函数将不能被调用,这是大家都不愿看到的。
   3. 当使用了长字符串类型的参数、变量时要引用 ShareMem
   Delphi 中的 string 类型很强大,我们知道普通的字符串长度最大为 256 个字符,但 Delphi string 类型在默认情况下长度可以达到 2G 。(对,您没有看错,确实是两兆。)这时,如果您坚持要使用 string 类型的参数、变量甚至是记录信息时,就要引用 ShareMem 单元,而且必须是第一个引用的。既在 uses 语句后是第一个引用的单元。如下例:
   uses
   ShareMem,  SysUtils,  Classes;
还有一点,在您的工程文件( *.dpr )中而不是单元文件( *.pas )中也要做同样的工作,这一点 Delphi 自带的帮助文件没有说清楚,造成了很多误会。不这样做的话,您很有可能付出死机的代价。避免使用 string 类型的方法是将 string 类型的参数、变量等声明为 Pchar ShortString (如: s:string[10] )类型。同样的问题会出现在当您使用了动态数组时,解决的方法同上所述。
Delphi 制作 DLL 的方法
Dll 的制作一般步骤
  二 参数传递
  三 DLL 的初始化和退出清理 [ 如果需要初始化和退出清理 ]
  四 全局变量的使用
  五 调用静态载入
  六 调用动态载入
  七 DLL 建立一个 TForM
  八 DLL 中建立一个 TMDIChildForM
  九 示例:
  十 Delphi 制作的 Dll 与其他语言的混合编程中常遇问题:
  十一 相关资料
Dll 的制作一般分为以下几步:
   1 . 在一个 DLL 工程里写一个过程或函数
   2 . 写一个 Exports 关键字,在其下写过程的名称。不用写参数和调用后缀。
  二 参数传递
   1 . 参数类型最好与 window C++ 的参数类型一致。不要用 DELPHI 的数据类型。
   2 . 最好有返回值 [ 即使是一个过程 ] ,来报出调用成功或失败,或状态。成功或失败的返回值最好为 1[ 成功 ] 0[ 失败 ]. 一句话,与 windows c++ 兼容。
   3 . stdcall 声明后缀。
   4 . 最好大小写敏感。
   5 . 无须用 far 调用后缀,那只是为了与 windows 16 位程序兼容。
  三 DLL 的初始化和退出清理 [ 如果需要初始化和退出清理 ]
   1 .DLLProc[SysUtils 单元的一个 Pointer] DLL 的入口。在此你可用你的函数替换了它的入口。但你的函数必须符合以下要求 [ 其实就是一个回调函数 ] 。如下:
   procedure DllEnterPoint(dwReason: DWORD);far;stdcall;
   dwReason 参数有四种类型:
   DLL_PROCESS_ATTACH: 进程进入时
   DLL_PROCESS_DETACH 进程退出时
   DLL_THREAD_ATTACH 线程进入时
   DLL_THREAD_DETACH 线程退出时
  在初始化部分写 :
   DLLProc := @DLLEnterPoint;
   DllEnterPoint(DLL_PROCESS_ATTACH);
   2 . Form 上有 TdcomConnection 组件 , Uses Activex, 在初始化时写一句 CoInitialize (nil);
   3 . 在退出时一定保证 DcomConnection.Connected := False, 并且数据集已关闭。否则报地址错。
  四 全局变量的使用
  在 widnows 32 位程序中,两个应用程序的地址空间是相互没有联系的。虽然 DLL 在内存中是一份,但变量是在各进程的地址空间中,因此你不能借助 dll 的全局变量来达到两个应用程序间的数据传递,除非你用内存映像文件。
调用静态载入
1 客户端函数声名 :  
1) 大小写敏感。
2) DLL 中的声明一样。
如: showform(form:Tform);Far;external'yproject_dll.dll';
3) 调用时传过去的参数类型最好也与 windows c++ 一样。
4) 调用时 DLL 必须在 windows 搜索路径中,顺序是:当前目录; Path 路径;    
windows;widows\system;windows\ssystem32;
  六 调用动态载入
   1 . 建立一种过程类型 [ 如果你对过程类型的变量只是一个指针的本质清楚的话,你就知道是怎么回事了 ] 。如:
   type
      mypointer=procedure(form:Tform);Far;external;
   var
 
   Hinst:Thandle;
      showform:mypointer;
   begin
      Hinst:=loadlibrary('yproject_dll');//Load 一个 Dll, 按文件名找。
      showform:=getprocaddress(Hinst,'showform');// 按函数名找,大小写敏感。如果你知道自动化对象的本质就清楚了。
      showform(application.mainform);// 找到函数入口指针就调用。
 
   Freelibrary(Hinst);
   end;
  七 . DLL 建立一个 TForM
   1 把你的 Form Uses Dll 中,你的 Form 用到的关联的单元也要 Uses 进来 [ 这是最麻烦的一点,因为你的 Form 或许 Uses 了许多特殊的单元或函数 ]
   2 传递一个 Application 参数,用它建立 Form.
  八 . DLL 中建立一个 TMDIChildForM
   1 Dll 中的 MDIForm.FormStyle 不用为 fmMDIChild.
   2 CreateForm 后写以下两句:
   function ShowForm(mainForm:TForm):integer;stdcall
   var
      Form1: TForm1;
 
   ptr:PLongInt;
   begin
 
   ptr:=@(Application.MainForm);// 先把 dll MainForm 句柄保存起来,也无须释放,只不过是替换一下
      ptr^:=LongInt(mainForm);// 用主调程序的 mainForm 替换 DLL MainForm MainForm 是特殊的 WINDOW ,它专门管理 Application 中的 Forms 资源 .
      // 为什么不直接 Application.MainForm := mainForm, 因为 Application.MainForm 是只读属性
      Form1:=TForm1.Create(mainForm);// 用参数建立
   end;
  备注:参数是主调程序的 Application.MainForm
. 示例:
   DLL 源代码:
   library Project2;
   uses
   SysUtils,  Classes,   Dialogs,   Forms,
   Unit2 in 'Unit2.pas' {Form2};
   {$R *.RES}
   var
      ccc: Pchar;
   procedure OpenForm(mainForm:TForm);stdcall;
   var
     Form1: TForm1;
 
   ptr:PLongInt;
   begin
 
   ptr:=@(Application.MainForm);
 
   ptr^:=LongInt(mainForm);
      Form1:=TForm1.Create(mainForm);
   end;
   procedure InputCCC(Text: Pchar);stdcall;
   begin
     ccc := Text;
   end;
   procedure ShowCCC;stdcall;
   begin
 
   ShowMessage(String(ccc));
   end;
   exports
      OpenForm;
      InputCCC,
 
   ShowCCC;
   begin
   end.
  调用方源代码:
   unit Unit1;
   interface
   uses
      Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
     StdCtrls;
   type
   TForm1 = class(TForm)
   Button1: TButton;
   Button2: TButton;
   Edit1: TEdit;
   procedure Button1Click(Sender: TObject);
   procedure Button2Click(Sender: TObject);
   private
   { Private declarations }
   public
   { Public declarations }
   end;
   var
      Form1: TForm1;
   implementation
   {$R *.DFM}
   procedure OpenForm(mainForm:TForm);stdcall;External'project2.dll';
   procedure ShowCCC;stdcall;External'project2.dll';
   procedure InputCCC(Text: Pchar);stdcall;External'project2.dll';
   procedure TForm1.Button1Click(Sender: TObject);
   var
 
   Text: Pchar;
   begin
 
   Text := Pchar(Edit1.Text);
 
   // OpenForm(Application.MainForm);// 为了调 MDICHILD
 
   InputCCC(Text);// 为了实验 DLL 中的全局变量是否在各个应用程序间共享
   end;
   procedure TForm1.Button2Click(Sender: TObject);
   begin
 
   ShowCCC;// 这里表明 WINDOWS 32 位应用程序 DLL 中的全局变量也是在应用程序地址空间中, 16 位应用程序或许不同,没有做实验。
   end;
  十 Delphi 制作的 Dll 与其他语言的混合编程中常遇问题:
   1 . PowerBuilder 混合编程
  在定义不定长动态数组方面在函数退出清理堆栈时老出现不可重现的地址错,原因未明,大概与 PB 的编译器原理有关,即使 PB 编译成二进制代码也如此。
Delphi 中静态调用 DLL
  调用一个 DLL 比写一个 DLL 要容易一些。首先给大家介绍的是静态调用方法,稍后将介绍动态调用方法,并就两种方法做一个比较。同样的,我们先举一个静态调用的例子。
unit Unit1;

interface

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

type
  TForm1 = class(TForm)
    Edit1: TEdit;
    Button1: TButton;
    procedure Button1Click(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
end;

var
  Form1: TForm1;

implementation

{$R *.DFM}

//
本行以下代码为我们真正动手写的代码
function TestDll(i:integer):integer;stdcall;
external ’Delphi.dll’;

procedure TForm1.Button1Click(Sender: TObject);
begin
   Edit1.Text:=IntToStr(TestDll(1));
end;

end.
上面的例子中我们在窗体上放置了一个编辑框( Edit )和一个按钮( Button ),并且书写了很少的代码来测试我们刚刚编写的 Delphi.dll 。大家可以看到我们唯一做的工作是将 TestDll 函数的说明部分放在了 implementation 中,并且用 external 语句指定了 Delphi.dll 的位置。(本例中调用程序和 Delphi.dll 在同一个目录中。)让人兴奋的是,我们自己编写的 TestDll 函数很快被 Delphi 认出来了。您可做这样一个实验:输入 “TestDll ,很快 Delphi 就会用 fly-by 提示条提示您应该输入的参数是什么,就像我们使用 Delphi 中定义的其他函数一样简单。注意事项有以下一些:
一、调用参数用 stdcall
  和前面提到的一样,当引用 DLL 中的函数和过程时也要使用 stdcall 参数,原因和前面提到的一样。
二、用 external 语句指定被调用的 DLL 文件的路径和名称
  正如大家看到的,我们在 external 语句中指定了所要调用的 DLL 文件的名称。没有写路径是因为该 DLL 文件和调用它的主程序在同一目录下。如果该 DLL 文件在 C:\ ,则我们可将上面的引用语句写为 external ’C:\Delphi.dll’ 。注意文件的后缀 .dll 必须写上。
三、不能从 DLL 中调用全局变量
  如果我们在 DLL 中声明了某种全局变量,如: var s:byte 。这样在 DLL s 这个全局变量是可以正常使用的,但 s 不能被调用程序使用,既 s 不能作为全局变量传递给调用程序。不过在调用程序中声明的变量可以作为参数传递给 DLL
四、被调用的 DLL 必须存在
  这一点很重要,使用静态调用方法时要求所调用的 DLL 文件以及要调用的函数或过程等等必须存在。如果不存在或指定的路径和文件名不正确的话,运行主程序时系统会提示 启动程序时出错 找不到 *.dll 文件 等运行错误。
Delphi 中动态调用 DLL
  动态调用 DLL 相对复杂很多,但非常灵活。为了全面的说明该问题,这次我们举一个调用由 C++ 编写的 DLL 的例子。首先在 C++ 中编译下面的 DLL 源程序。
#include

extern ”C” _declspec(dllexport)
int WINAPI TestC(int i)
{
return i;
}
  编译后生成一个 DLL 文件,在这里我们称该文件为 Cpp.dll ,该 DLL 中只有一个返回整数类型的函数 TestC 。为了方便说明,我们仍然引用上面的调用程序,只是将原来的 Button1Click 过程中的语句用下面的代码替换掉了。
procedure TForm1.Button1Click(Sender: TObject);
type
  TIntFunc=function(i:integer):integer;stdcall;
var
  Th:Thandle;
  Tf:TIntFunc;
  Tp:TFarProc;
begin
  Th:=LoadLibrary(’Cpp.dll’); {
装载 DLL}
  if Th>0 then
    try
      Tp:=GetProcAddress(Th,PChar(’TestC’));
      if Tp<>nil then
      begin
        Tf:=TIntFunc(Tp);
        Edit1.Text:=IntToStr(Tf(1)); {
调用 TestC 函数 }
      end
      else
        ShowMessage(’TestC
函数没有找到 ’);
    finally
      FreeLibrary(Th); {
释放 DLL}
    end
  else
    ShowMessage(’Cpp.dll
没有找到 ’);
end;
  大家已经看到了,这种动态调用技术很复杂,但只要修改参数,如修改 LoadLibrary(’Cpp.dll’) 中的 DLL 名称为 ’Delphi.dll’ 就可动态更改所调用的 DLL
一、定义所要调用的函数或过程的类型
  在上面的代码中我们定义了一个 TIntFunc 类型,这是对应我们将要调用的函数 TestC 的。在其他调用情况下也要做同样的定义工作。并且也要加上 stdcall 调用参数。
二、释放所调用的 DLL
  我们用 LoadLibrary 动态的调用了一个 DLL ,但要记住必须在使用完后手动地用 FreeLibrary 将该 DLL 释放掉,否则该 DLL 将一直占用内存直到您退出 Windows 或关机为止。
  现在我们来评价一下两种调用 DLL 的方法的优缺点。静态方法实现简单,易于掌握并且一般来说稍微快一点,也更加安全可靠一些;但是静态方法不能灵活地在运行时装卸所需的 DLL ,而是在主程序开始运行时就装载指定的 DLL 直到程序结束时才释放该 DLL ,另外只有基于编译器和链接器的系统(如 Delphi )才可以使用该方法。动态方法较好地解决了静态方法中存在的不足,可以方便地访问 DLL 中的函数和过程,甚至一些老版本 DLL 中新添加的函数或过程;但动态方法难以完全掌握,使用时因为不同的函数或过程要定义很多很复杂的类型和调用方法。对于初学者,笔者建议您使用静态方法,待熟练后再使用动态调用方法。
使用 DLL 的实用技巧
一、编写技巧
   1 、为了保证 DLL 的正确性,可先编写成普通的应用程序的一部分,调试无误后再从主程序中分离出来,编译成 DLL
   2 、为了保证 DLL 的通用性,应该在自己编写的 DLL 中杜绝出现可视化控件的名称,如: Edit1.Text 中的 Edit1 名称;或者自定义非 Windows 定义的类型,如某种记录。
   3 、为便于调试,每个函数和过程应该尽可能短小精悍,并配合具体详细的注释。
   4 、应多利用 try-finally 来处理可能出现的错误和异常,注意这时要引用 SysUtils 单元。
   5 、尽可能少引用单元以减小 DLL 的大小,特别是不要引用可视化单元,如 Dialogs 单元。例如一般情况下,我们可以不引用 Classes 单元,这样可使编译后的 DLL 减小大约 16Kb
二、调用技巧
   1 、在用静态方法时,可以给被调用的函数或过程更名。在前面提到的 C++ 编写的 DLL 例子中,如果去掉 extern ”C” 语句, C++ 会编译出一些奇怪的函数名,原来的 TestC 函数会被命名为 @TestC$s 等等可笑的怪名字,这是由于 C++ 采用了 C++ name mangling 技术。这个函数名在 Delphi 中是非法的,我们可以这样解决这个问题:
改写引用函数为
function TestC(i:integer):integer;stdcall;
external ’Cpp.dll’;name ’@TestC$s’;
其中 name 的作用就是重命名。
   2 、可把我们编写的 DLL 放到 Windows 目录下或者 Windows\system 目录下。这样做可以在 external 语句中或 LoadLibrary 语句中不写路径而只写 DLL 的名称。但这样做有些不妥,这两个目录下有大量重要的系统 DLL ,如果您编的 DLL 与它们重名的话其后果简直不堪设想,况且您的编程技术还不至于达到将自己编写的 DLL 放到系统目录中的地步吧!
三、调试技巧
   1 、我们知道 DLL 在编写时是不能运行和单步调试的。有一个办法可以,那就是在 Run|parameters 菜单中设置一个宿主程序。在 Local 页的 Host Application 栏中添上宿主程序的名字就可进行单步调试、断点观察和运行了。
   2 、添加 DLL 的版本信息。开场白中提到了版本信息对于 DLL 是很重要的,如果包含了版本信息, DLL 的大小会增加 2Kb 。增加这么一点空间是值得的。很不幸我们如果直接使用 Project|options 菜单中 Version 选项是不行的,这一点 Delphi 的帮助文件中没有提到,经笔者研究发现,只要加一行代码就可以了。如下例:
library Delphi;

uses
  SysUtils,  Classes;

{$R *.RES}
//
注意,上面这行代码必须加在这个位置

function TestDll(i:integer):integer;stdcall;
begin
  Result:=i;
end;

exports
  TestDll;

begin
end.
3 、为了避免与别的 DLL 重名,在给自己编写的 DLL 起名字的时候最好采用字符数字和下划线混合的方式。如: jl_try16.dll
   4 、如果您原来在 Delphi 1 Delphi 2 中已经编译了某些 DLL 的话,您原来编译的 DLL 16 位的。只要将源代码在新的 Delphi 3 Delphi 4 环境下重新编译,就可以得到 32 位的 DLL 了。

[ 后记 ] 除了上面介绍的 DLL 最常用的使用方法外, DLL 还可以用于做资源的载体。例如,在 Windows 中更改图标就是使用的 DLL 中的资源。另外,熟练掌握了 DLL 的设计技术,对使用更为高级的 OLE COM 以及 ActiveX 编程都有很多益处。
 
 
 
 
 
 
 
对使用 Delphi 制作 DLL 复用文件的建议
 
在公司里有一些需要制作 DLL 的场合,因为熟悉、方便和简易,大多数使用 Delphi 来制作。现在就这个主题提出一些个人建议。
尽量使用标准 DLL 接口。指的是传递的参数类型及函数返回类型不能是 Delphi 特有的,比如 string AnsiString ),以及动态数组和含有这些类型成员的复合类型(如记录),也不能是包含有这些类型成员数据成员的对象类型,以避免可能的错误。如果使用了 string 类型或动态数组类型,且调用方不是 Delphi 程序,则基本上会报错。如果调用方是 Delphi 但调用方或被调用方没有在工程文件的第一包含单元不是 ShareMem ,也可能会出错。
  如果调用方是 Delphi 应用程序,则可能可以使用不包含禁止类型( string, 动态数组)数据成员的对象作为参数或返回值,但也应尽量避免。
  如果调用方与被调用方都是 Delphi 程序,而且要使用 string 或动态数组作参数,则双方工程文件的第一包含单元必须是 ShareMem 。( C++Builder 程序的情况可能与此相同,不过没有测试过。)
  如果调用方不是 Delphi 程序,则 string 、动态数组、包含 string 或动态数组的复合数据类型及类实例,都不能作为参数及返回值。
  因此,为了提高 DLL 的复用范围,避免可能存在的错误,应当使用标准 WIN32 API 标准参数类型,以前使用 string 的变量,可以使用 PChar(s) 转换。动态数组则转换为指针类型( @array[0] ),并加上数组的长度。
  如果因为调用方与被调用方都是 Delphi 程序,为了编写方便,不想进行上述转换,则推荐使用运行时包的形式。运行时包可以保证动态分配数据的正确释放。这样因为其扩展名( .bpl ),显出该文件仅限于 Delphi/C++Builder 使用(不象 DLL )。
  其次,尽量避免使用 overload 的函数 / 过程作输出,如果同一操作有多个方式,则可以让函数 / 过程名有少许差别,类似于 Delphi 中的 FormatXXXX CreateXXXX 等函数及方法,如 CreateByDefaultFile, CreateDefault
  最后,作为 DLL 的提供者,应当提供直接编程的接口文件,如 Delphi 中的 .pas .dcu (最好是 .pas ,因为可以有注释)、 C C++ 中的 .h .lib 。而不是让使用者们自己创建。如果非要有 overload 的函数 / 过程,这一点显得特别重要。另外,作为 Delphi 应用,提供的 .pas 文件可以是提前连接的(使用 external 指定 DLL 中的输出函数),也可以是后期连接的(使用 LoadLibrary GetProcAddress ), DLL 提供者提供编程接口文件,既显得正式(或 HiQoS ),又有保障。

你可能感兴趣的:(职场,dll,休闲)