转载:使用Dll中的类
1. 在多种语言之间使用dll的主要方法是定义函数和过程接口,一方提供实现,一方提供声明。各语言都能在dll中查到符合声明的实现,条件是参数具有同样规范,包括参数类型相容,数目相同,位置相同,压栈方式相同。返回值类型不在考虑当中,实际返回类型由使用模块中的声明决定。
2. 除了通过函数和过程接口,还可以在dll中export类,这样可以直接从dll中引入类。这只在VC中才行,但要求产生dll和使用dll时使用的是同一版本的VC。例如:
// dll.h 文件
class __declspec(dllexport) dll {
private:
int t;
public:
int i;
dll();
void myshow(char* string);
void seti(int k);
int geti();
};
// dll.cpp 文件
#include
#include "dll.h"
dll::dll(){
i=3;
}
void dll::myshow(char* string){
printf(string);
}
void dll::seti(int k){
i=k;
}
int dll::geti(){
return i;
}
编译即可产生该dll。
使用时,只须
// dll.h
class dll {
private:
int t;
public:
int i;
dll();
void myshow(char* string);
void seti(int k);
int geti();
};
#pragma comment (lib,"dll.lib") // 链入dll
// main.cpp
#include "dll.h"
void main(){
dll dl;
dl.myshow("Hello DLL/n");
}
这样可以直接使用这个类。
3. 对于不能直接export和import类的情况,只能使用过程和函数来传递。
(1)将类的功能封装在一个函数中,用函数传递。如用一个TForm获取参数、密码,可提供一个函数getParam,在这个函数中使用该TForm,只把参数、密码返回。
(2)将类的实例通过提供方的函数产生并返回,利用dll中忽略返回值的特性让使用方得到该实例指针,只需保证使用方声明与提供方定义一致即可。在该实例中必须传递类的成员变量和成员函数指针。问题只是采用何种机制放置这些变量和指针。
a. 通过虚拟表传出。虚拟函数和普通函数的不同之处在于,前者的函数指针登记在类的每个实例中的一个虚拟函数表当中,后者的地址在编译时由编译器填入。有虚拟函数的类的大小是所有成员变量大小与一个虚表指针大小(4)的总和,这点可从sizeof函数中看出。dll提供方和使用方把要传出的函数登记在虚表中,这样双方就可以通过虚表传递函数指针。使用这种方法必须注意参数规范(是否_cdecl),定义成虚拟函数,注意成员函数顺序。实际上,如果对编译器比较熟的话,可以欺骗编译器,把变量伪装成虚拟函数传出,这样也就可以传出变量了。
例如:(这里只讨论函数的export,对变量应该可以采用某种方法同样export)
// dll.h 文件
class dll {
private:
int t;
public:
int i;
dll();
void myshow(char* string);
void seti(int &k);
int geti();
};
// dll.cpp 文件
#include
#include "dll.h"
dll::dll(){
i=3;
}
void dll::myshow(char* string){
printf("the string:%s/n",str);
}
void dll::seti(int &k){
i=k;
}
int dll::geti(){
return i;
}
// 用类封装 wrap_dll.h
#include "dll.h"
class wrap_dll {
dll* thedll;
public :
wrap_dll();
virtual void _cdecl myshow(char* string);
virtual void _cdecl seti(int &k);
virtual int _cdecl geti();
virtual void _cdecl Delete();
// 提供释放空间的函数
};
extern "C" __declspec(dllexport) wrap_dll* newdll();
// wrap_dll.cpp ,产生wrap_dll实例
wrap_dll::wrap_dll(){
thedll = new dll;
}
void _cdecl wrap_dll:
elete(){
delete thedll;
::delete this;
}
void _cdecl wrap_dll::myshow(char* string){
thedll->myshow(string);
}
void _cdecl wrap_dll::seti(int &k){
thedll->seti(k);
}
int _cdecl wrap_dll::geti(){
return thedll->geti();
}
wrap_dll* newdll(){
return new wrap_dll;
}
在使用方使用Delphi:
// a.pas
unit a;
interface
//声明类
type
delphdll = class
public
procedure myshow(p
char) cdecl;virtual;abstract;
procedure seti(var k:integer) cdecl;virtual;abstract;
function geti:integer cdecl;virtual;abstract;
procedure Delete cdecl:virtual;abstract;
// 用abstract可不定义函数体而通过编译
end;
// 链入dll
function newdll:delphdll cdecl;external 'dll.dll';
implementation
// nothing
end.
//main.dpr
program main;
uses ..., a in 'a.pas', ...;
var del_dll:delphdll;
k :integer;
begin
k := 10;
del_dll:=newdll;
del_dll.myshow('hello DLL');
del_dll.seti(k);
del_dll.Delete;
end.
b. 直接通过类传出。将类定义成包含函数指针,不用虚拟函数表。这提供了更直接的方式,但也更为复杂。把类中每个要export的函数都用一个函数指针存储其位置,在构造函数中为其赋值。其他的成员变量保持不变。在接受方同样声明一个这样的类以便使指针不错位。有以下注意事项:
A 保证成员变量和函数指针在定义和使用方声明中顺序相同。
B Delphi中的类在每个实例的地址开始处有四个字节的内容,可能是结束后的跳转地址、指向本类中函数定义的指针或其他一些东西(如果是record而不是类,这四个字节填入固定的数0x0000000E)。若由C++ export类,须为这些字节空出空间来,由于自己管理释放而不使用Delphi的机制,这些字节不会被用到。
C 编译器(不论是C++还是Delphi)在调用类的成员函数时会将当前实例的指针作为参数压栈,该指针会成为最左边的一个参数,如 p->xx(i,str)会变成xx(p,i,str)。当我们使用函数指针来调用函数以模仿调用类中成员函数时,应当显式地加入此参数。
例如:
// dll.h
class dll {
unsigned nonuse;
// 给Delphi空位
private :
int kk;
public:
int ii;
dll();
void __cdecl myshow(char* string);
void (__cdecl dll::*myshow1)(char* string);// 准备存储函数指针,dll::是为了赋值时通过编译
void __cdecl setii(int i);
void (__cdecl dll::*setii1)(int i);
int __cdecl getii();
int (__cdecl dll::*getii1)();
void __cdecl Delete();
void (__cdecl dll::*Delete1)();
};
extern "C" __declspec(dllexport) dll* newdll();
//dll.cpp
#include
#include "dll.h"
void dll::myshow(char* str){
printf("the string:%s/n",str);
}
int dll::getii(){
return ii;
}
void dll::setii(int i){
ii = i;
}
void dll:
elete(){
::delete this;
}
dll::dll(){
ii = 32;
myshow1=myshow;
// 给函数指针赋值
getii1 = getii;
setii1 = setii;
Delete1=Delete;
}
dll* newdll(){
return new dll;
}
使用方使用Delphi
//a.pas
unit a;
interface
type
delphdll = class
private // 这里会有一个指针
kk:integer;
public
ii:integer;
myshow1
rocedure (p:mydll;str
char);cdecl;
// 过程指针,p为显式参数--实例指针
setii1
rocedure (p:mydll;i:integer);cdecl;
getii1 :function (p:mydll):integer;cdecl;
// 函数指针
Delete1
rocedure (p:mydll);
cdecl;
procedure myshow(str
char);
cdecl;
procedure setii(i:integer);cdecl;
function getii:integer;cdecl;
procedure Delete;
cdecl;
end;
function newdll:delphdll cdecl;external 'dll.dll';
implementation
procedure delphdll.myshow(str
char);cdecl;
begin
myshow1(self,str);
// 传入实例指针
end;
procedure delphdll.setii(i:integer);cdecl;
begin
setii1(self,i);
end;
function delphdll.getii:integer;cdecl;
begin
getii:= getii1(self);
end;
procedure delphdll.Delete;cdecl;
begin
Delete1(self);
end;
end.
//main.dpr
program main;
uses ...,a in 'a.pas', ...;
var del_dll:delphdll;
k :integer;
begin
k := 10;
del_dll:=newdll;
del_dll.myshow('hello DLL');
del_dll.setii(k);
k:=del_dll.getii;
del_dll.Delete;
end.
这些仅是几个例子而已,知道了上述原则后,在C,C++,Delphi之间导出和引入类应该没有什么问题了。比较上述方法,在C++与Delphi之间共享类时,用虚表比较好,因为二者的虚表结构完全一样,传参也一样,不会有诸如将实例指针压栈、在实例起始地址处存在四字节等情况,而且,我还没有测试上面直接传类方法中有虚函数即有虚表指针时,以及类似int &k的参数在C风格的函数指针中用int* k 代替时,是否会有影响。但若有时间应该可以完善该方法。它提供了完全的类映射,更具有真实的传递类的性质。