将一个Delphi类传递给一个期望具有__thiscall方法的类的C++函数/方法

6
我有一些MSVC++编译的DLL,为其创建了COM-类似(精简版)接口(抽象Delphi类)。其中一些类具有需要对象指针的方法。这些C++方法使用__thiscall调用约定声明(我无法更改),它与__stdcall非常相似,只是将this指针传递到ECX寄存器上。
我在Delphi中创建类实例,然后将其传递给C++方法。我可以在Delphi中设置断点,并看到它命中我的Delphi类中公开的__stdcall方法,但很快我就会遇到STATUS_STACK_BUFFER_OVERRUN,并且应用程序必须退出。在Delphi方面是否可能模拟/处理__thiscall?如果我传递由C++系统实例化的对象,则一切正常,并且调用该对象的方法(如预期所示),但这是无用的 - 我需要传递Delphi对象。

编辑于2010年04月19日18:12 更详细地说,发生了以下情况:第一个被调用的方法(setLabel)退出时没有错误(虽然它是一个存根方法)。第二个被调用的方法(init)进入后,在尝试读取vol参数时失败。

C++端

#define SHAPES_EXPORT __declspec(dllexport) // just to show the value
class SHAPES_EXPORT CBox
{
  public:
    virtual ~CBox() {}
    virtual void init(double volume) = 0;
    virtual void grow(double amount) = 0;
    virtual void shrink(double amount) = 0;
    virtual void setID(int ID = 0) = 0;
    virtual void setLabel(const char* text) = 0;
};

Delphi Side

IBox = class
public
  procedure destroyBox; virtual; stdcall; abstract;
  procedure init(vol: Double); virtual; stdcall; abstract;
  procedure grow(amount: Double); virtual; stdcall; abstract;
  procedure shrink(amount: Double); virtual; stdcall; abstract;
  procedure setID(val: Integer); virtual; stdcall; abstract;
  procedure setLabel(text: PChar); virtual; stdcall; abstract; 
end;

TMyBox = class(IBox)
protected
  FVolume: Double;
  FID: Integer;
  FLabel: String; //
public
  constructor Create;
  destructor Destroy; override;
  // BEGIN Virtual Method implementation
  procedure destroyBox; override; stdcall;             // empty - Dont need/want C++ to manage my Delphi objects, just call their methods
  procedure init(vol: Double); override; stdcall;      // FVolume := vol;
  procedure grow(amount: Double); override; stdcall;   // Inc(FVolume, amount);
  procedure shrink(amount: Double); override; stdcall; // Dec(FVolume, amount);
  procedure setID(val: Integer); override; stdcall;    // FID := val;
  procedure setLabel(text: PChar); override; stdcall;  // Stub method; empty.
  // END Virtual Method implementation      
  property Volume: Double read FVolume;
  property ID: Integer read FID;
  property Label: String read FLabel;
end;

我本来以为只使用stdcall就足够了,但有些东西出问题了,不确定是什么,也许与使用ECX寄存器有关?非常感谢您的帮助。
编辑2010-04-19 17:42:是不是必须在进入函数时保留ECX寄存器并在函数退出时恢复它?C++需要“this”指针吗?目前我可能只是基于一些强烈的谷歌搜索。我找到了something related,但它似乎处理的是这个问题的反向情况。

你说你很快就会得到一个STATUS_STACK_BUFFER_OVERRUN错误。多快?你可以发布一些样本代码以显示错误发生的位置吗?这个错误发生在所有方法中吗?(你测试过所有的方法吗?) - Mason Wheeler
1
你的诊断是正确的,Alan。C++代码将它的this参数放在ECX中,但你的Delphi代码希望它在第一个堆栈参数中。所有其他堆栈参数也都错了一个。我有一种技术可以解决这个问题。我唯一担心使用它来处理这段代码的是类的析构函数。我会尝试在今天晚些时候写一篇描述。我的方法将基于我在无窗口富文本API上的工作:http://pages.cs.wisc.edu/~rkennedy/windowless-rtf - Rob Kennedy
那太棒了,Rob,期待那个。但是为什么析构函数可能会有问题呢?接口存根析构函数 destroyBox() 不会被 C++ 端调用,从而什么也不做,还是真正的 Delphi 析构函数 Destroy() 最终被调用了呢? - Atorian
那就是这样,Alan。构造函数和析构函数在C++和Delphi中都是特殊的,它们在语言之间不能互换。当C++代码调用C++析构函数时,Delphi端需要有一个看起来和行为像C++析构函数的方法。一个Delphi析构函数可能不够用。 - Rob Kennedy
好的,那么类头部的桩程序 "procedure destroyBox; virtual; stdcall; abstract" 不足以吗?它占据了 VMT 中与 C++ 析构函数 ~CBox() 相同的位置吗? - Atorian
显示剩余4条评论
6个回答

3
假设您已经创建了一个带有VMT的MSVC++类,它可以完美地映射到Delphi类的VMT(我从未这样做过,但我相信这是可能的)。现在,您可以从MSVC++代码调用Delphi类的虚拟方法,唯一的问题是__thiscall调用约定。由于Delphi不支持__thiscall,可能的解决方案是在Delphi端使用代理虚拟方法:
type
  TTest = class
    procedure ECXCaller(AValue: Integer);
    procedure ProcProxy(AValue: Integer); virtual; stdcall;
    procedure Proc(AValue: Integer); stdcall;
  end;

implementation

{ TTest }

procedure TTest.ECXCaller(AValue: Integer);
asm
  mov   ecx,eax
  push  AValue
  call  ProcProxy
end;

procedure TTest.Proc(AValue: Integer);
begin
  ShowMessage(IntToStr(AValue));
end;

procedure TTest.ProcProxy(AValue: Integer);
asm
   pop  ebp            // !!! because of hidden delphi prologue code
   mov  eax,[esp]      // return address
   push eax
   mov  [esp+4],ecx    // "this" argument
   jmp  Proc
end;

我看到在 init() 之前调用 setLabel(),这让我想知道 VMT 映射是否有误。我希望不是这种情况。 - Atorian
1
@Serg,你也可以使用Delphi类来实现COM接口。毕竟这就是Delphi 2所做的。你甚至可以使用普通的老式记录来实现它们。这就是C语言所做的。 - Rob Kennedy
@Alan,你看到方法被错误调用的原因是Delphi的析构函数在VMT中没有正偏移。C++代码期望析构函数是第一个虚方法,在VMT中的偏移为0,因此你需要声明一个新的方法来代替。Delphi的析构函数在偏移-4处。 - Rob Kennedy
@Serg,是的,我所指的COM-like/lite更多地是指ABI方面,即根据顺序匹配VMT条目。顺便说一句,我尝试了你的答案,但它不起作用,尽管逻辑对我来说似乎很好;它似乎是一个很好的整体方向。我可以看到ECX包含Self/this指针,但在从ProcProxy进入Proc之后,在jmp Proc之后,我遇到了访问冲突问题。 - Atorian
@Alan G.:我已经找到了一个错误并更新了帖子。我已经在Delphi端测试了代码 - 现在它可以正常工作了。 - kludg
显示剩余2条评论

1

不要这样做

正如Ori所提到的,C++ ABI没有标准化。你不能也不应该期望这能够工作,如果你确实成功了,那么它将是一个极其不可移植的hack。

在跨语言边界引导C++函数调用的标准方法是使用静态函数,在其中显式传递this参数:

class SHAPES_EXPORT CBox
{
  public:
    virtual void init(double volume) = 0;
    static void STDCALL CBox_init(CBox *_this, double volume) { _this->init(volume); }
    // etc. for other methods
};

实际上,静态方法在技术上应该声明为extern "C",因为不能保证使用C ABI来实现静态类方法,但几乎所有现有的编译器都这样做。

我完全不了解Delphi,所以我不知道如何在Delphi方面处理这个问题,但这是你在C++方面需要做的。如果Delphi支持cdecl调用约定,则可以删除上面的STDCALL

是的,这很烦人,因为你必须从Delphi中调用CBox_init而不是init,但这只是你必须处理的事情。当然,如果你愿意,你可以将CBox_init重命名为更合适的名称。


1

我认为你不能合理地期望这能够工作。C++没有标准化的ABI,而Delphi没有任何标准化的东西。你可能会找到一种方法来破解出一些可以工作的东西,但是不能保证它将在未来的Delphi版本中继续工作。

如果你可以修改MSVC方面的内容,你可以尝试使用COM(这正是COM设计的目的)。这将是丑陋和不愉快的,但我真的感觉你现在并不开心...所以也许这会有所改善。

如果你不能这样做,似乎你要么必须编写一个thunking层,要么就不能使用Delphi。


3
实际上,Delphi很好地支持了标准ABI调用规范。只是 thiscall 不属于其中之一。 - Mason Wheeler
1
-1 对于“标准化任何事物”持反对态度,+1 对于双方都支持COM(嘿,那算是一个标准,对吧?)。 - Jeroen Wiert Pluimers
嘿,我和其他人一样喜欢Delphi - 可能更多,因为我实际上使用过它,并认为它是一个非常好的工具。但是,它实现的Pascal方言并没有像C ++那样标准化。Delphi使用自己的库格式、对象格式和实现者可以自由定义ABI、调用约定或任何其他任何方式。一个绝佳的工具?当然。符合标准的环境?不是这样。 - Ori Pessach
1
@Jeroen Pluimers - COM算作是一种临时标准,也许可以与Java世界中的Ant使用方式进行比较。支持Ant的IDE(我非常确定所有的IDE都支持)可以用于构建任何使用Ant的东西。拥有一个明确定义的标准允许这种互操作性。这会带来愉快的体验吗?在我看来不是。只要我接受必须坚持使用Delphi开发我的项目,我宁愿选择Delphi的IDE而不是标准的Java IDE。 - Ori Pessach
1
许多工具供应商都有同样的故事:有时他们的工具在标准制定之前就出现了,有时他们不够大而无法制定标准,但仍对其他标准的发展产生了重大影响。有时他们超前于时代。想想“网络即计算机”与云计算的比较。许多工具(包括VB6和Delphi 3)是专门开发以遵守COM标准的。在.NET 4.0中,COM已成为一流公民!大多数Windows API仍然使用由Anders Hejlsberg等人引入的调用约定。 - Jeroen Wiert Pluimers

0
你可以尝试使用C++ Builder编译那些DLL,C++ Builder具有与Delphi互操作的语言支持。自BDS 2006版本以来,在C++Builder中制作的组件可以在Delphi中访问,所以普通的旧类可以正常工作。
如果您只打算使用MSVC,则COM可能是在两个环境之间进行接口的最佳方法。

我以前用C++ Builder编译过一些主要的库,它们从未被设计成像这个一样,而且付出的努力与结果相比是巨大的。追踪晦涩的编译器和链接器消息并修复MS特定的代码比尝试找到更直接(希望合理)的解决方案要少得多。我知道修复可能最终会变得不太标准/干净,但如果它能够工作,我将非常高兴。 - Atorian
C++ Builder的语言兼容性从C++ Builder 6时代开始大幅提高,也许会更容易解决问题。这可能比尝试通过汇编操纵堆栈来修补调用约定更为清晰。 - rep_movsd

0
我已经创建了类似COM的接口(抽象Delphi类)。
为什么不使用通常的COM接口?它们在C++和Delphi中保证二进制兼容性。
唯一的问题是你不能在Delphi中避免AddRef/Release/QueryInterface。但如果你将引用计数实现为空操作(就像TComponent一样)-那么你可以在C++端忽略这些方法。

0
作为对使用c++Builder建议的补充,由于预算/版本可用性/“构建人员”的反对可能会导致问题,我建议使用一个简单的MSVC包装器将调用传递给Delphi dll(s)。在那一点上,您可以选择使用COM或不使用。
您可以几乎按原样使用现有代码,而无需深入研究汇编语言以修复调用约定不匹配的问题。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接