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
