在Delphi調用由C++導出的類的對象
原理
調用方式是C++中使用純虛函數,在Delphi中也就是純虛類,他們有着相同的佈局的虛方法表。每個Delphi的類都有一張VMT表,VMT中包含了一些基礎信息、一些獲得運行時信息的方法以及虛方法指針。因爲佈局相同,所以可以互相調用。
但是C++的類還是C++的類,Delphi類都繼承於TObject,而C++沒有這個概念。所以,獲得了C++的類,不能去嘗試調用TObject的方法。一般情況下都是將一些功能型模塊封裝出接口,導出給Delphi用。
方式
將C++的類封裝成DLL,並導出一個新建對象接口,Delphi層加載DLL後,調用該接口去創建C++類的對象,這樣在Delphi層就可以直接使用這個對象。
環境說明
Delphi XE8
Visual studio 2017
特別注意
- 注意內存管理問題,哪一層申請的內存,最好在哪一層釋放,涉及到傳遞內存塊或者指針的時候,最好的方式是做一次內存拷貝操作。
- 傳值時,注意類型要對應,XE8中的string和C++層的string完全不一樣,應該是對應C串,即char*。
- 申明結構體需要注意雙方的對齊方式。在Delphi裏,record前面如果加了packed,就不會被編譯器強制對齊。
- 注意編碼,XE8中的字符一個char佔用兩個字節,而C++中char佔用一個字節。比如:XE8傳字符串 ‘aaa’給C++,C++層接收到的內容是:#97#0#97#0#97#0,碰到第一個#0就截斷了,只能接到第一個字符了。遇到這個問題,解決辦法就是將它視爲一塊無類型內存塊。(Delphi2007中char佔用一個字節,就不用考慮這個問題了)
如下代碼所示:
struct TEntityNode
{
...
void* key; //指向一塊內存塊
int lenKey; //這塊內存塊的size
...
};
C++層
demo是在以前的代碼上修改的
- 首先定義一個類,包含一些需要導出的純虛方法。
//Interface.h
#pragma once
#ifndef INTERFACE_H
#define INTERFACE_H
#include "stdafx.h"
#define API_EXPORT __declspec(dllexport)
#pragma pack(push, 1)
struct TEntityNode
{
TEntityNode* xPrev;
TEntityNode* xNext;
TEntityNode* yPrev;
TEntityNode* yNext;
void* key;
int lenKey;
int x;
int y;
};
#pragma pack(pop)
#pragma pack(push, 1)
struct TAddNode
{
void* key;
int lenKey;
int x;
int y;
};
#pragma pack(pop)
typedef void (*Fun_Print)(TEntityNode*);
struct TCallBackFuncRcd
{
Fun_Print print;
};
class TScene {
public:
virtual bool __stdcall Init() = 0;
virtual void __stdcall Free() = 0;
virtual void __stdcall registerCallBack(TCallBackFuncRcd rcd) = 0;
virtual TEntityNode* __stdcall Add(TAddNode* node) = 0;
virtual TEntityNode* __stdcall Get(void* key, int lenKey) = 0;
virtual bool __stdcall Leave(void* key, int lenKey) = 0;
virtual TEntityNode* __stdcall Move(void* key, int lenKey, int x, int y) = 0;
virtual void __stdcall PrintAll(bool isPrintX) = 0;
virtual void __stdcall PrintAOI(TEntityNode* node, int xArea, int yArea) = 0;
};
extern "C" API_EXPORT TScene *NewScene();
#endif
- 這裏定義好了類的全部內容,但未實現方法,末尾處導出了一個方法(方法的實現見後面),用於創建實現類的對象。
extern "C" API_EXPORT TScene *NewScene();
- 然後,再看實現類的實現部分。實現類中可以添加一些自己額外的成員,但僅限於C++類的內部調用,Delphi層調用不到。
//Handler.h
class TSceneHandler : public TScene {
public:
virtual bool __stdcall Init();
virtual void __stdcall Free();
virtual void __stdcall registerCallBack(TCallBackFuncRcd rcd);
virtual TEntityNode* __stdcall Add(TAddNode* node);
virtual TEntityNode* __stdcall Get(void* key, int lenKey);
virtual bool __stdcall Leave(void* key, int lenKey);
virtual TEntityNode* __stdcall Move(void* key, int lenKey, int x, int y);
virtual void __stdcall PrintAll(bool isPrintX);
virtual void __stdcall PrintAOI(TEntityNode* node, int xArea, int yArea);
private:
void _add(TEntityNode* node);
private:
TEntityNode * head;
TEntityNode * tail;
Fun_Print fPrint;
};
- 比較有意思的是,我們還可以將Delphi的方法作爲回調註冊到C++的類中來,做完一系列操作後,可以調用這個回調函數回調內容回去。
方式也很簡單,只需要加載Dll的時候,將回調函數的指針傳給C++層即可。
typedef void (*Fun_Print)(TEntityNode*);
struct TCallBackFuncRcd
{
Fun_Print print;
};
void TSceneHandler::registerCallBack(TCallBackFuncRcd rcd)
{
fPrint = rcd.print;
}
這裏我註冊的是一個打印函數。
- 下面貼出C++層Add方法的實現(全部代碼放在最後)。
//Handler.cpp
TEntityNode* TSceneHandler::Add(TAddNode * node)
{
//這裏將內容拷貝過來,以防在Delphi層被釋放,進而引發內存訪問異常的問題
TEntityNode* retNode = new TEntityNode();
retNode->lenKey = node->lenKey;
retNode->key = new char(node->lenKey);
memcpy(retNode->key, node->key, node->lenKey);
retNode->x = node->x;
retNode->y = node->y;
_add(retNode);
return retNode;
}
- 最後,實現類寫好以後。我們需要提供一個接口,用於創建實現類的對象,上面已經將這個接口導出了,我們看看這個接口的具體實現。
//Interface.cpp
#include "stdafx.h"
#include "Interface.h"
#include "handler.h"
API_EXPORT TScene* NewScene() {
return new TSceneHandler();
}
直接返回新創建的對象
Delphi層
- 首先,把純虛類寫好。類中的方法前後順序要與上面的一致,方法的參數類型要一致,沒有對應的類型,使用無類型指針、內存塊傳遞。
純虛類定義如下
unit dll;
interface
type
PEntityNode = ^TEntityNode;
TEntityNode = packed record
xPrev : PEntityNode;
xNext : PEntityNode;
yPrev : PEntityNode;
yNext : PEntityNode;
Key : Pointer;
lenKey : Integer;
X : Integer;
Y : Integer;
end;
PAddNode = ^TAddNode;
TAddNode = packed record
Key : Pointer;
lenKey : Integer;
X : Integer;
Y : Integer;
end;
TCallBackFuncRcd = record
print: Pointer;
end;
type
TScene = class
public
function Init: Boolean; virtual; stdcall; abstract;
procedure Free; virtual; stdcall; abstract;
procedure registerCallBack(rcd: TCallBackFuncRcd);virtual; stdcall; abstract;
function Add(const node: PAddNode): PEntityNode; virtual; stdcall; abstract;
function Get(const key: Pointer; const lenKey: Integer): PEntityNode; virtual; stdcall; abstract;
function Leave(const key: Pointer; const lenKey: Integer): Boolean; virtual; stdcall; abstract;
function Move(const key: Pointer; const lenKey: Integer; const x, y: Integer): PEntityNode; virtual; stdcall; abstract;
procedure PrintAll(const isPrintX: Boolean);virtual; stdcall; abstract;
procedure PrintAOI(const node: PEntityNode; xArea, yArea: Integer); virtual; stdcall; abstract;
end;
implementation
end.
- 然後從DLL中導入創建類的對象的接口
function NewScene(): TScene; external 'Scene.dll';
調用NewScene方法就可以獲得對象了
- 初始化的時候,調用NewScene,並注入回調函數
procedure TForm2.print(pNode: PEntityNode);
var
lvKey: string;
begin
if not Assigned(pNode) then
begin
mmo1.Lines.Add('Print-->節點不存在');
Exit;
end;
//這裏除以了2是因爲一個char佔用兩個字節
//且,內存長度的計算是用的ByteLength()方法,而不是Length()方法
SetString(lvKey, PChar(pNode.Key), pNode.lenKey div 2);
mmo1.Lines.Add(Format('%s - (%d, %d)', [lvKey, pNode.x, pNode.y]));
end;
procedure TForm2.FormCreate(Sender: TObject);
var
rcd: TCallBackFuncRcd;
begin
FScene := NewScene;
if not FScene.Init then
begin
mmo1.Lines.Add('初始化失敗');
Exit;
end;
rcd.print := @GPrint;
FScene.registerCallBack(rcd);
end;
- 記得要釋放,釋放時候要調用DLL中的類的自己定義的釋放方法。
procedure TForm2.FormDestroy(Sender: TObject);
begin
FScene.Free;
end;
- 同樣的,這裏也貼出Delphi層的Add方法(全部的代碼放在最後)
function TForm2.AddNode(const Key: string; const X, Y: Integer): PEntityNode;
var
addNode: PAddNode;
begin
New(addNode);
addNode.Key := PChar(Key);
//注意使用ByteLength(),Length()函數會少算一半長度
addNode.lenKey := ByteLength(Key);
addNode.X := X;
addNode.Y := Y;
Result := FScene.Add(addNode);
//C++層做了拷貝操作
Dispose(addNode);
end;
全部代碼,我放在了我的資源裏,歡迎下載
//download.csdn.net/download/I_can_/12007246
如有不足,請多指教