Delphi是市场上最好的RAD工具,但是现在C++占据着主导地位,有时针对一个问题很难找到Delphi或Pascal的解决方案.可是却可能找到了一个相关的C++类.本文描述几种在Delphi代码中使用C++类的方法.
Delphi is one of the greatest RAD tools on the market, but it in this currently C++-dominated world, it can sometimes be hard to find a Delphi or Pascal solution to your problem. There is, however, a chance you'll find a C++ class instead. This article describes several ways that enable you to use C++ classes from your Delphi code. 坏消息是:不能直接在Delphi代码中引入C++类.Delphi连接器不能将C++的对象文件连接到应用程序中.可以连接C对象文件,但这不是本文的主题(请见another article).你需要使用C++编译器创建DLL,然后在DLL中使用这些类. First the bad news: you won't be able to directly link the classes into your Delphi code. The Delphi linker can't link C++ object files into your application. You can link C object files, but that is the subject ofanother article. You'll need access to a C++ compiler that can create DLLs, and use the classes from the DLL. 不同的对象内部格式 Delphi和C++接口最大不同之处在于其对象的内部格式.所有的Delphi对象从TObject类继承,在堆中创建.C++的类更像Delphi中带方法的结构体,可以静态或动态创建.他们与Borland和Turbo Pascal遗留在Delphi中的object类型很相似. The greatest difficulty in interfacing Delphi and C++ is, that their object structures differ. All Delphi classes descend from TObject, and are created on the heap. C++ classes are more like Delphi records with methods, and can be created statically or dynamically. They are similar to the legacy object types, carried over into Delphi from Borland and Turbo Pascal. 幸运的是他们的区别不太大.唯一的不同是Delphi类和C++类的内存布局,Delphi类的第一个域是指向虚拟方法表(VMT)的指针.C++类没有像Tobject这样的通用基类,因此不会总存在虚方法(C++术语中叫做虚成员函数),所以VMT域可能不存在或在对象首地址的其他偏移量处.但是你可以强制C++类有一个与Tobject相同偏移量的VMT指针,只需要简单的令其包括一个非数据域(成员),并创建至少一个虚方法(虚成员函数). Fortunately there is not such a big difference internally. The only thing that distinguishes the memory layout of a Delphi class and a C++ class is, that the Delphi class always has a pointer to the Virtual Method Table (VMT), as its first field. C++ classes don't have a common ancestor like TObject, and don't always have virtual methods (or virtual member functions, as they are called in C++-speak), so the VMT field can be missing or at a different offset in the object. But you can force the C++ class to have one, at the same offset as TObject, by simply telling it to contain no data fields (members), and by makingat least one method (member function) virtual. 导入方法 另一个问题是从Dll中导入函数.这里有两种基本方法:第一个是将C++类退化为C函数集合,每个函数的第一个参数为指向类的对象实例的指针;第二种方式是使用虚拟,抽象方法. Another problem is exporting methods from a DLL. There are basically two ways: the first one "flattens" the C++ class into a set of C functions, which all take an object as first parameter; the second one uses virtual, abstract methods. 假设你有如下简单的C++对象:一个控制台(Console)类,使用conio.h中的函数实现简单的控制台功能.在构造函数中保存当前屏幕信息,并在析构过程中恢复回来. Say you have the following simple C++ object: a Console class, that uses the functions in conio.h to achieve simple console functionality. On creation, it saves the current screen and, on destruction, restores it again. enum TextColors { tcBLACK, tcBLUE, tcGREEN, tcCYAN, tcRED, tcMAGENTA, tcBROWN, tcLIGHTGRAY, tcDARKGRAY, tcLIGHTBLUE, tcLIGHTGREEN, tcLIGHTCYAN, tcLIGHTRED, tcLIGHTMAGENTA, tcYELLOW, tcWHITE }; class Console { private: text_info oldState; char *screenBuffer; public: Console(void); virtual ~Console(void); void reset(void); void clearScreen(void); void gotoXY(int x, int y); void textColor(TextColors newColor); void textAttribute(int newAttribute); void textBackground(int newBackground); int readKey(void); bool keyPressed(void); void write(const char *text); void writeLn(const char *text); }; 这里类有10个方法,一个构造函数,一个析构函数.现在展示在Delphi中使用C++类的两种方法. This class has 10 methods, a destructor and a constructor. I will now demonstrate the two main ways of using this C++ class from Delphi. 退化对象 要退化一个类,将其每个方法都作为简单的C函数进行导出,包括构造函数和析构函数.函数(除了构造函数和析构函数外)的第一个参数必须是指向对象实例的指针.要退化Console类,需要声明12个函数: To "flatten" a class, you export a simple C function for each method, as well as functions for the constructor and the destructor. The first parameter of the functions (except for the "constructor" function) should be a pointer to the object. To flatten the Console class, you would declare 12 functions like this: #include <windows.h> #include "console.h"
typedef Console *ConsoleHandle;
// define a macro for the calling convention and export type #define EXPORTCALL __declspec(dllexport) __stdcall
extern "C" {
ConsoleHandle EXPORTCALL NewConsole(void) { return new Console(); }
void EXPORTCALL DeleteConsole(ConsoleHandle handle) { delete handle; }
void EXPORTCALL ConsoleReset(ConsoleHandle handle) { handle->reset(); }
void EXPORTCALL ConsoleClearScreen(ConsoleHandle handle) { handle->clearScreen(); }
void EXPORTCALL ConsoleGotoXY(ConsoleHandle handle, int x, int y) { handle->gotoXY(x, y); }
void EXPORTCALL ConsoleTextColor(ConsoleHandle handle, TextColors newColor) { handle->textColor(newColor); }
void EXPORTCALL ConsoleTextAttribute(ConsoleHandle handle, int newAttribute) { handle->textAttribute(newAttribute); }
void EXPORTCALL ConsoleTextBackground(ConsoleHandle handle, int newBackground) { handle->textBackground(newBackground); }
int EXPORTCALL ConsoleReadKey(ConsoleHandle handle) { return handle->readKey(); }
bool EXPORTCALL ConsoleKeyPressed(ConsoleHandle handle) { return handle->keyPressed(); }
void EXPORTCALL ConsoleWrite(ConsoleHandle handle, const char *text) { handle->write(text); }
void EXPORTCALL ConsoleWriteLn(ConsoleHandle handle, const char *text) { handle->writeLn(text); }
} // extern "C"
#pragma argsused int WINAPI DllEntryPoint(HINSTANCE hinst, unsigned long reason, void* lpReserved) { return 1; } 现在编译代码得到DLL文件,可以在Delphi中以调用API的方式调用这个对象了.接口单元如下所示: Now you only have to compile this to a DLL, and your object can be used from Delphi in the same manner as any API call. The interface unit would look like this: unit ConsoleFlat; interface uses SysUtils; type ConsoleHandle = Pointer; // no need to know the real type TTextColor = ( tcBLACK, tcBLUE, tcGREEN, tcCYAN, tcRED, tcMAGENTA, tcBROWN, tcLIGHTGRAY, tcDARKGRAY, tcLIGHTBLUE, tcLIGHTGREEN, tcLIGHTCYAN, tcLIGHTRED, tcLIGHTMAGENTA, tcYELLOW, tcWHITE ); function NewConsole: ConsoleHandle; stdcall; procedure DeleteConsole(handle: ConsoleHandle); stdcall; procedure ConsoleReset(handle: ConsoleHandle); stdcall; procedure ConsoleClearScreen(handle: ConsoleHandle); stdcall; procedure ConsoleGotoXY(handle: ConsoleHandle; x, y : Integer); stdcall; procedure ConsoleTextColor(handle: ConsoleHandle; newColor: TTextColor); stdcall; procedure ConsoleTextAttribute(handle: ConsoleHandle; newAttribute: Integer); stdcall; procedure ConsoleTextBackground(handle: ConsoleHandle; newBackground: TTextColor); stdcall; function ConsoleReadKey(handle: ConsoleHandle): Integer; stdcall; function ConsoleKeyPressed(handle: ConsoleHandle): Boolean; stdcall; procedure ConsoleWrite(handle: ConsoleHandle; text: PChar); stdcall; procedure ConsoleWriteLn(handle: ConsoleHandle; text: PChar); stdcall; implementation const DLLName = 'ConsoleFlat.dll'; function NewConsole; external DLLName; procedure DeleteConsole; external DLLName; procedure ConsoleReset; external DLLName; procedure ConsoleClearScreen; external DLLName; procedure ConsoleGotoXY; external DLLName; procedure ConsoleTextColor; external DLLName; procedure ConsoleTextAttribute; external DLLName; procedure ConsoleTextBackground; external DLLName; function ConsoleReadKey; external DLLName; function ConsoleKeyPressed; external DLLName; procedure ConsoleWrite; external DLLName; procedure ConsoleWriteLn; external DLLName; end. 现在可以随便调用了.缺点是需要使用函数,而在C++中可以使用封装好的类.而不是 This can then be used as you please. The disadvantage is, of course, that you are using functions, where the C++ user can use a class. So instead of Console := TConsole.Create; try Console.TextBackground(tcRED); Console.TextColor(tcYELLOW); Console.ClearScreen; Console.WriteLn('Yellow on red'); Console.ReadKey; finally Console.Free; end; 必须按如下形式: you must do the following: Console := NewConsole; try ConsoleTextBackground(Console, tcRED); ConsoleTextColor(Console, tcYELLOW); ConsoleClearScreen(Console); ConsoleWriteLn(Console, 'Yellow on red'); ConsoleReadKey(Console); finally DeleteConsole(Console); end;
这就需要使用另一种更加灵活的方式在Delphi中使用类. This leads to the next way of using the class from Delphi, one which is a bit more convenient. 使用纯虚类 纯虚类就是Delphi程序员所说的纯抽象类.只有虚方法而不存在数据成员.纯Delphi抽象类有相似的布局.任何类都有一个成员指向VMT.VMT是一个数组,每个类都会创建一个(而不是每个对象),包含指向类中虚方法实际实现的指针.这是实现多态的机制.函数调用被编码为跳转到类VMT中的特定索引指向的函数.因此不同的子类,有不同的VMT,可以按需实现虚方法;虚函数对应的指针在VMT中有相同的索引,但是在每个类中其指向不同的函数. Pure virtual classes are what the Delphi programmer would call pure abstract classes. These have the advantage that they only have virtual methods, and absolutely no data members. Pure Delphi abstract classes have a similar layout. The only member of both classes is therefore a pointer to the VMT. The VMT is an array, one for each class (fortunately, not for each object), that contains pointers to the actual implementation of each virtual method for that particular class. This is how polymorphism is implemented. Function calls are only coded as jumps to the function in the VMT of the class at a specific index. So different descendant classes, which have different VMTs, can implement a virtual function differently; the function pointer found at the same index in the VMT will always be used, but it will point to different functions in each class. 现在大家可能会问:Tobject的虚拟函数呢?是不是已经在数组中填充了一系列的槽(函数指针).这是正确的,但是幸运的是,Delphi对象的VMT方式布局为:预先定义的虚方法位于VMT指向地址的负偏移量.用户定义的第一个虚方法在第一个槽上,即零偏移量的位置. But now some of you may say: what about the virtual functions of TObject? These must probably already fill quite a few slots of the array. That is true, but fortunately, the VMT of Delphi objects is layed out in such a way, that these predefined virtual methods are all at a negative offset from the address to which the VMT pointer points. The first slot, at offset 0, contains the first user defined virtual method. 这种设计使传递所有方法(只要是虚方法)地址变得非常简单,只需获取VMT中的指针.唯一需要注意的事情就是,这里假设两种语言中定义的纯抽象类的方法声明顺序必须严格一致.这也是在Delphi3以前版本定义接口的方式. This fortunate circumstance makes it rather simple to pass the addresses of all methods (as long as they are virtual) in one step, as one simple pointer to the VMT. The only thing you must take care of is to assure that there is a pure abstract base class defined in both languages, and that the order of declaration of the methods is exactly the same. This is actually how interfaces were defined in versions of Delphi prior to Delphi 3. 注意这种方式下,可以按Delphi类的方式使用C++类.但其还是一个C++类,因此其没有Tobject类的方法或属性,如InstanceSize 或AfterConstruction.不能尝试去调用TObject类的方法.必须按C++类的方式去使用,或作为一个轻量级的接口,但不必涉及COM函数. Note that this way, you'll be using a C++ class as if it were a Delphi class.But it remains a C++ class, so it doesnot have theTObject methods or properties likeInstanceSize or AfterConstruction.You should not try to call them! You should really use the class as a C++ class, or as some kind of lightweight interface, without the COM functions. 如果遵守上面的忠告就可以安全的使用C++类了. Only if you follow this advice, you will be able to use the C++ class safely. 这里有个问题.如果要导出的类不是一个纯虚类,而是一个正常的类,拥有数据成员和非虚函数.有两种可能,如果有源码,可以为类定义一个抽象祖先,然后将待导出的类作为其子类: There is one problem. The class you want to export is not a pure virtual class, it is a normal class, with a data member and non-virtual functions. There are two possiblities. If you have the source code, you can define your own abstract ancestor of the class, and then make your class a descendant of it:
#define STDCALL __stdcall
class AbstractConsole { public: virtual void STDCALL reset(void) = 0; virtual void STDCALL clearScreen(void) = 0; virtual void STDCALL gotoXY(int x, int y) = 0; virtual void STDCALL textColor(TextColors newColor) = 0; virtual void STDCALL textAttribute(int newAttribute) = 0; virtual void STDCALL textBackground(int newBackground) = 0; virtual int STDCALL readKey(void) = 0; virtual bool STDCALL keyPressed(void) = 0; virtual void STDCALL write(const char *text) = 0; virtual void STDCALL writeLn(const char *text)= 0; virtual void STDCALL free(void) = 0; };
#ifndef DLLCODE #define EXTERN __declspec(dllimport) __stdcall #else #define EXTERN __declspec(dllexport) __stdcall #endif
extern "C" AbstractConsole* EXTERN NewConsole(void); 注意:一些编译器,如Microsoft Visual C++,并不总是将成员函数默认编译为cdecl,而是使用其自己的非标准调用约定.因此最好将所有成员函数编译为stdcall. NOTE: some compilers, like Microsoft Visual C++, don't always compile member functions ascdecl by default, but use their own, non-standard calling convention instead. So it is best to compile all member functions asstdcall. 现在简单的修改Console类声明的第一行代码,并重新编译. Now you could simply change the first line of the declaration of your original Console class, and recompile. class Console : public AbstractConsole { // etc...
不需要再次声明Console类的所有虚方法.在C++中子类拥有基类中相同签名的虚方法,并将自动重写这些函数.因此原来的类中成员方法将自动变为虚方法. There is no need to declare all the functions of Console virtual. In C++, in descendant classes, a member function with the same signature as a virtual member function as the ancestor class, will automatically override that function. So the member functions of our original class will automatically become virtual. 可是,通常不可能或不希望将所有类的方法声明为虚方法,或修改祖先类.这种情况下必须使用多继承或聚合的方式使用Console类的功能. However, often it is not possible, or desired, to make all functions of a class virtual, or to change the ancestor. In that case you will have to use multiple inheritance or aggregation to use the functionality of Console. #include "console.h" #include "aconsole.h"
class ConcreteConsole : public AbstractConsole, private Console { void reset(void); void clearScreen(void);
// etc...
void free(void); }; 然后函数简单的调用Console的函数来实现任务.如下是摘录的实现代码. And the functions simply call the Console functions to perform their tasks. Below is an excerpt of the implementing code. #include "console.h" #include "cconsole.h"
void ConcreteConsole::reset(void) { Console::reset(); }
void ConcreteConsole::clearScreen(void) { Console::clearScreen(); }
// etc...
void ConcreteConsole::free(void) { if (this) delete this; }
当然现在还是需要导出函数的.如果你选择了继承等方式,使Console 类继承于AbstractConsole ,形式如下: Of course there is still a need for an export function. If you chose to change the inheritance, i.e. decided to let the Console class inherit from AbstractConsole, it will look like this: AbstractConsole* EXTERNCALL NewConsole() { return new Console(); } 如果使用聚合和包装的方式,形式如下: If, on the other hand, you decided to use aggregation and a wrapper, it would look like: AbstractConsole* EXTERNCALL NewConsole() { return new ConcreteConsole(); } Delphi类不必知道AbstractConsole 或 ConcreteConsole,可以简单的按需要调用Tconsole或其他对象.导入单元如下: The Delphi class doesn't have to know about AbstractConsole or ConcreteConsole, and can simply be called TConsole, or whatever you like. The import unit will look like this: unit ConsoleDLL;
interface
type TTextColor = ( tcBLACK, tcBLUE, tcGREEN, tcCYAN, tcRED, tcMAGENTA, tcBROWN, tcLIGHTGRAY, tcDARKGRAY, tcLIGHTBLUE, tcLIGHTGREEN, tcLIGHTCYAN, tcLIGHTRED, tcLIGHTMAGENTA, tcYELLOW, tcWHITE );
TConsole = class
procedure Reset; virtual; cdecl; abstract; procedure ClearScreen; virtual; cdecl; abstract; procedure GotoXY(x, y : Integer); virtual; cdecl; abstract; procedure TextColor(newColor: TTextColor); virtual; cdecl; abstract; procedure TextAttribute(newAttribute: Integer); virtual; cdecl; abstract; procedure TextBackground(newBackground: TTextColor); virtual; cdecl; abstract; function ReadKey: Integer; virtual; cdecl; abstract; function KeyPressed: Boolean; virtual; cdecl; abstract; procedure Write(text: PChar); virtual; cdecl; abstract; procedure WriteLn(text: PChar); virtual; cdecl; abstract; procedure Free; virtual; cdecl; abstract; end;
function NewConsole: TConsole; stdcall;
implementation
function NewConsole; external 'ConsoleDLL.dll';
end. 这就是使用DLL中的C++类Console需要做的全部工作.按上述方法就可以很简单的进行调用了. This is all you need to use the C++ Console class from the DLL. It can be used in a similar fashion as I described above: Console := NewConsole; try Console.TextBackground(tcRED); Console.TextColor(tcYELLOW); Console.ClearScreen; Console.WriteLn('Yellow on red'); Console.ReadKey; finally Console.Free; end; 结论 虽然经常说C++类不能在Delphi中调用,如本文所述这只有部分正确.但是上述的两种方式中,哪种导入C++类的方式更好呢? Although it is often said that C++ classes can't be used in Delphi, this is only partly true, as this article demonstrates. But which of the two ways of importing C++ classes is preferrable? 第二种方式使用虚类,当然更容易调用.使用方式上与Delphi中的类差不多.但是不能从这个类中继承并添加自己的函数,因为无法调用基类的构造函数,也不能调用任何Tobject的函数,或依赖于他们的代码. NewConsole 总是返回一个ConcreteConsole 对象(或Console,如果改变继承方式).同时更接近于C++的运行机制. The second way, using virtual classes, is of course much more convenient to use. It is almost as if your class was written in Delphi. But you cannot inherit from the class and add your own functionality, since you can't call the inherited constructor.You can't call any of the TObject functions either, or call code that relies on them. NewConsole will always return a ConcreteConsole (or a Console, if you changed the inheritance). Also, it is a bit more work on the C++ side. 退化类的方式缺少便利性,但少了一个层次的间接调用(因为调用虚方法总需要一次额外的间接调用),提高了运行速度. The "flat" variety is less convenient, but has one level of indirection less (since calling virtual functions is also an extra level of indirection), and that makes it a bit faster. 当然,两种方式,都可以写一个Delphi类来包装抽象类或导入函数.但会导致其他的间接调用. Of course, with both approaches, you could write a Delphi class that wraps either the abstract class, or the functions. But that would introduce yet another level of indirection. DLL的C++源码和Delphi的Demo小程序可以在Downloads页面下载. The C++ source code for both DLLs and small demo programs in Delphi can be downloaded from theDownloads page. Rudy Velthuis http://rvelthuis.de/downloads.html#cppobjszip http://blog.csdn.net/henreash/article/details/7352335 |
2023-10-27
2022-08-15
2022-08-17
2022-09-23
2022-08-13
请发表评论