手把手教
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
),又有保障。