一、概述
Delphi是Borland公司研制的高效的可视化开发工具,它既可用于开发系统软件,也适合于应用软件的开发。随着韩国网络游戏《传奇》的红火,其开发工具delphi也被更多的软件开发人员所熟知;C语言也是目前IT界运用最广泛的开发语言之一。最近由于工作方面的原因,需要提供delphi调用c/c++编写的动态库的例子。在不同的开发语言之间相互调用,除了要熟练掌握各种语言的编写技巧之外,还要了解各种语言对函数的处理机制与调用规则。本文讲述了delphi调用c语言的动态库的例子,其中介绍了一些函数调用方面的基础知识,以及如何将c++中的结构、数组、字符串等数据类型转换为delphi中对应数据类型,还引入了常用的但网络上不常见的一些技术,比如回调函数的定义与注册,并通过实例展示了delphi调用c语言的动态库的细节,供大家在学习开发过程中参考。
开发工具:vc++6.0、delphi7.0
二、基础知识
2.1、回调函数
软件模块之间总是存在着一定的接口,从调用方式上,可以把他们分为三类:同步调用、回调和异步调用。其中回调是一种双向调用模式,也就是说,被调用方在接口被调用时也会调用对方的接口;异步调用是一种类似消息或事件的机制,不过它的调用方向刚好相反,接口的服务在收到某种讯息或发生某种事件时,会主动通知客户方(即调用客户方的接口)。回调和异步调用的关系非常紧密,通常我们使用回调来实现异步消息的注册,通过异步调用来实现消息的通知。同步调用是三者当中最简单的,而回调又常常是异步调用的基础。
回调函数与普通函数的申明并无区别,但如果是不同的开发语言之间通过回调函数来传递与处理数据,则一定要注意在不同语言对回调函数的申明与实现时其函数调用约定必须保持一致。
2.2、函数调用约定
声明过程或函数时,可以指定调用约定(calling convention),尤其在以接口形式提供给其它语言工具使用时,则必须指定过程或函数的调用方式。
Delphi指定调用约定可以使用的指示字包括register、pascal、cdecl、stdcall以及safecall。C/C++调用约定有stdcall、cdecl、fastcall、thiscall、naked call等。
决定了传递给例程的参数的顺序,还影响参数从栈中的解除、参数传递时对寄存器的使用以及处理错误和异常等。缺省的调用约定是register。
下表是调用约定的简要概括:
指示字 | 参数顺序 | 参数删除者 | 寄存器传递参数 |
register | 从左到右 | 例程 | 是 |
pascal | 从左到右 | 例程 | 否 |
cdecl | 从右到左 | 调用者 | 否 |
stdcall | 从右到左 | 例程 | 否 |
safecall | 从右到左 | 例程 | 否 |
fastcall | 函数前两个参数通过ecx和edx传递,其他参数从右向左的顺序压栈 | 例程 | 部分是 |
thiscall (非关键词) | 从右到左 (this指针保存在ecx,仅用于c++) | 例程 |
|
naked call | 一般用于实模式驱动程序设计 | - | - |
补充说明:
·cdecl调用约定又称为C调用约定,是C语言缺省的调用约定.
·register约定最多可以使用三个CPU寄存器传递参数,而其他的约定都通过栈传递所有的参数。
·safecall约定实现了异常防火墙。在Windows中,这一实现在内部处理COM错误通知。
·缺省的register约定是效率最高的,因为它通常避免了栈中新帧的创建。
·调用来自用C或C++编写的共享库的函数时,cdecl约定是很有用的;而对外部代码的调用,一般而言,推荐使用stdcall和safecall约定。
·在Windows中,操作系统API函数使用的是stdcall和safecall约定。其他操作系统通常使用cdecl约定。(注意,stdcall约定比cdecl约定具有更高的效率。)
·safecall约定必需用于声明双重接口的方法。pascal约定用于维持向后(旧版本)兼容。
三、实例分析
3.1、实例功能概述
动态库功能:C语言开发,修正将某个工友的工号以及薪资,将其工号增加10,薪资增加1000。当然,实际生活中估计不会出现这种情况,这里只是为了展示这种调用技术而假想的例子。
Delphi程序功能:调用动态库,将工友的资料作为入参传入动态库,由动态库对工友资料做相关调整,然后动态库通过回调函数将处理后的数据传给调用程序并在控制台显示出来。
C调用程序功能:与Delphi程序功能一致,提供它主要便于大家对比参照。
3.2、代码分析
完整的代码在附件中,如果大家要实践的话,可以参考它。
3.2.1、C动态库实现说明
3.2.1.1、testdll.h:
这里定义了一个工友结构信息_TEST_WORKER,由工号、姓名、年龄、薪水组成,定义了一个回调函数结构,用于注册外部调用的处理函数;同时申明了一个动态库导出函数。详细的注释列在代码中:
//定义工友结构信息_TEST_WORKER
typedef struct _TEST_WORKER
{
int nNO;//工号
char chName[16];//姓名
int nAge;//年龄
int nSalary;//薪水
} TEST_WORKER,*PTEST_WORKER;
//定义回调函数结构,用于将处理后的工友结构信息传递给调用程序
typedef struct
{
void (*CallbackAlertRes)(TEST_WORKER* );
}CallbackFuncDef;
//申明动态库导出函数:
//注意,这里没有stdcall修饰,使用的是cdecl
TESTDLL_API int fnTestdll(PTEST_WORKER *p,CallbackFuncDef *pCallbackFuncDef);
//其中TESTDLL_API的定义如下:
#ifdef TESTDLL_EXPORTS
#define TESTDLL_API __declspec(dllexport)
#else
#define TESTDLL_API __declspec(dllimport)
#endif
3.2.1.2、testdll.cpp:
//导出函数的函数体
TESTDLL_API int fnTestdll(PTEST_WORKER *p,CallbackFuncDef *pCallbackFuncDef)
{
FILE *fp;
char szStr[100];
//以下用于测试 跟踪传入的参数,显示数据在文本文件中
fp=fopen("zhouys.txt","a+"); //this for test
if(!fp) return -1;
memset(szStr,'/0',sizeof(szStr));
sprintf(szStr,"%d %s %d %d/n",(*p)->nNO,(*p)->chName,(*p)->nAge,(*p)->nSalary);
fwrite(szStr,sizeof(szStr),1,fp);
fclose(fp);
//操作、处理
(*p)->nNO+=10;
(*p)->nSalary+=1000;
//回调通知,将处理后的信息传递给外部注册的函数
if(pCallbackFuncDef!=NULL)
pCallbackFuncDef->CallbackAlertRes(*p);
return 0;
}
3.2.2、C调用程序
全部处理代码在calldll.cpp中,为控制台程序
3.2.2.1、先申明函数接口
//动态调用方式
typedef int (*pfnTestdll)(PTEST_WORKER *p,CallbackFuncDef *pCallbackFuncDef);
3.2.2.2、再编写回调处理函数:
//用于接收动态库处理后的数据,以做相关处理。此处在控制台上打印处理后的结果信息
void CallbackAlertRes(TEST_WORKER* p)
{
printf("%d %s %d %d/n",p->nNO,p->chName,p->nAge,p->nSalary);
}
3.2.2.3、主函数内容:
//申明结构指针
PTEST_WORKER p;
//加载动态库
HINSTANCE _hDll = LoadLibrary("mydll.dll");
if(_hDll==NULL )
{
printf("Load Library error,please check mydll.dll file is exist?/n");
return -1;
}
//获取动态库函数指针
pfnTestdll mycall=(pfnTestdll)GetProcAddress(_hDll, "fnTestdll");
if(!mycall)
{
FreeLibrary(_hDll);
printf("get dll function pointer faild,please check!/n");
return -1;
}
//生成需要处理的数据
p=new TEST_WORKER;
//赋初始值
p->nNO=10;
strcpy(p->chName,"李宇春");
p->nAge=20;
p->nSalary=2000;
//注册回调函数
CallbackFuncDef callback_fun;
callback_fun.CallbackAlertRes=CallbackAlertRes;
//调用动态库函数处理
mycall(&p,&callback_fun);
//释放内存
delete p;
return 0;
运行程序:可以看到控制台输出了处理后的信息,
可以看到生成了zhouys.txt文本文件,记录了初始数据信息
3.2.3、delphi调用程序
3.2.3.1、转换数据结构类型:
本例有三处需要转换的地方:
1、 C结构体转换为Delphi的记录结构
2、 C字符串转换为Delphi的字符数组结构
3、 C结构指针转换为Delphi的指针对象
转换结构如下
type {此处引入结构变量}
TEST_WEORKER=record
nNO:integer;
cName:array[1..16] of char;
nAge:integer;
nSalary:integer;
end;
PTEST_WEORKER=^TEST_WEORKER;
PPTEST_WEORKER=^PTEST_WEORKER;
3.2.3.2、申明函数:
定义回调函数结构,并申明回调结构指针:
//{此处引入函数指针}
TCallbackAlertRes=procedure(p:PTEST_WEORKER);
{回调函数结构}
CallbackFuncDef=record
CallbackAlertRes:TCallbackAlertRes;
end;
PCallbackFuncDef=^CallbackFuncDef;
//申明调用函数指针,注意此处申明为cdecl调用方式,与动态库中函数一致
TFunTestDLL=function(p:PPTEST_WEORKER;pp:PCallbackFuncDef):integer; cdecl;
注意:
TFunTestDLL为调用函数指针,它将被赋值为指向动态库所导出的函数的地址,由于C语言的函数调用参数入栈规则是从右到左,即stdcall,所以此处需申明为C调用方式stdcall。
3.2.3.3、回调处理函数:
//生成回调处理函数体,以cdecl修饰符申明,遵循C的调用方式
procedure myCallbackAlertRes(p:PTEST_WEORKER);cdecl;
begin
//打印到控制台
Writeln(Format('%d %s %d %d',[p^.nNO,string(p^.cName),p^.nAge,p^.nSalary]));
end;
注意:
3.2.3.4、申明全局变量:
var
_hDll:THandle;
pdll_fun: TFunTestDLL;
p:PTEST_WEORKER;
pmycallback:PCallbackFuncDef;
3.2.3.5、调用函数:
_hDll:=LoadLibrary('mydll.dll');
if _hDll <> 0 then
begin
@pdll_fun := GetProcAddress(_hDll, 'fnTestdll');
if @pdll_fun<>nil then
begin
New(p);
New(pmycallback);
//符初值
p^.nNO:=10;
p^.nAge:=20;
p^.nSalary:=2000;
FillChar(p^.cName, SizeOf(p^.cName), Ord(' '));
StrCopy(@(p^.cName),'李宇春');
//注册回调函数
pmycallback^.CallbackAlertRes:=@myCallbackAlertRes;
//调用动态库函数进行处理
pdll_fun(@p,pmycallback);
end;
//释放资源
FreeLibrary(_hDll);
Dispose(pmycallback);
Dispose(p);
end;
四、结束语
通过上面的代码展示,相信大家都明白了delphi是如何调用c动态库的。其实在实际工作过程中,也有delphi调用C静态库,以及kylix调用linux下C开发的.so和.a库的情况,我自己也做了一些实践,有一定的了解,感觉各种情况下的调用原理都大同小异。有兴趣的同志可以和我联系一同探讨,这里就不赘述了。
五、参考资料
1、delphi7帮助系统
2、Object Pascal Reference.chm
3、Programming with Delphi.chm