从D语言中调用C++

17

我已经阅读了这里讲解如何从 D 语言中调用 C++ 的文档:http://dlang.org/cpp_interface.html,但还有一些事情不是很清楚。

以 D 语言网站提供的示例为例:

#include <iostream>

using namespace std;

class D {
   public:
   virtual int bar(int i, int j, int k)
   {
       cout << "i = " << i << endl;
       cout << "j = " << j << endl;
       cout << "k = " << k << endl;
       return 8;
   }
};

D *getD() {
   D *d = new D();
   return d;
}

然后可以像下面展示的那样,从 D 中调用 C++ 类:

extern (C++) {
    interface D {
        int bar(int i, int j, int k);
    }

    D getD();
}

void main() {
   D d = getD();
   d.bar(9,10,11);
}
我不太清楚的是,C++对象是如何被删除的。D语言的垃圾回收器是否会调用C++对象的delete,还是我们需要提供一个“删除器”函数来手动删除对象并在D中调用它?如果我向C++类添加析构函数,好像它从未被调用过。此外,我还注意到C++类必须以与它们在D接口中声明的顺序完全相同的顺序声明成员函数(例如,如果我在bar()方法之前添加了析构函数,则无法从D中调用C++对象,但如果在bar()方法之后声明析构函数,则一切正常)。
另外,如果D接口定义为:
extern(C++){
   interface D{
       int bar();
       int foo();
   }
}

相应的C++类如下:

class D{
public:
   virtual int bar(){};
   virtual int foo(){};

};

你如何确保C++虚函数表(vtbl)的创建顺序与D接口中声明方法的顺序相同?对我来说,这没有任何保证。换句话说,我们如何确保D :: bar()将出现在vtbl的第一个位置?这不是取决于实现/编译器吗?

4个回答

7
我不认为D的垃圾回收器知道如何释放C++对象。这至少意味着D运行时需要:
  1. 做出关于C++运行时的假设,即如何删除C++对象
  2. 对象不再被其他C++代码使用
我相信你将不得不提供另一个C++函数来调用传递给它的对象。实际上,许多C++库(即使也从C++中使用)在构造函数从库内部调用时具有相同的模式。即使在纯C中,在一个dll/exe中分配内存,然后在另一个dll/exe中释放内存通常是个坏主意。如果两个二进制文件没有共享相同的运行时库,这可能会导致严重问题。

4
具体实现方式是D对象简单地有一个C++兼容的虚函数表。因此,只有虚函数可以使用,并且由于该表按索引排列,它们必须以相同的顺序出现。
D不知道C++构造函数、析构函数或任何其他特殊方法,但如果它们是虚拟的,它可能会影响虚函数表。
我写了一个名为dtoh的小程序,目前正在等待审核,它可以从D源代码自动生成C++头文件,以保持简单。它还没有完成,但它可能仍然有帮助: https://github.com/adamdruppe/tools/blob/7d077b26d991dd5705e834900f66bea737a233b2/dtoh.d 首先编译它,dmd dtoh.d,然后从D文件创建JSON:dmd -X yourfile.d,然后运行dtoh yourfile.json,它应该会生成可用的yourfile.h。但是,正如我所说,它还没有完成,仍在等待整体设计审查,所以它可能非常糟糕。不过你现在可以像现在正在做的那样,自己来完成它。
无论如何,在D中看到的对象就像C ++中的Class *一样。你总是通过指针传递它,所以没有构造或复制构造。
D和C++也不理解彼此的内存分配系统。我遵循的规则是,你的库创建的任何东西,你的库都应该能够销毁。所以如果你的C++程序newed它,请确保在C++中删除它。你为传递到C++的任何对象也应该由D销毁...而且你可能想要手动执行。如果你的C++函数保留对D对象的引用,但在D中没有引用,它可能会被垃圾回收!所以你要么确保在对象的生命周期中始终有一个活动引用,或者使用像malloc和free这样的函数自己创建和销毁它。

我不喜欢使用通用的free(),因为版本可能不匹配。我建议总是提供一个从你的库中释放资源的方法。即使它的实现只是free(ptr);,提供你自己的函数将明确表明应该使用它,并保护你免受这种不匹配的影响。


那么这是否意味着,只要不是虚拟的,我就可以向C++类中添加析构函数,以便它不会干扰与D接口对应的方法?另一个我不太明白的问题是如何保证方法的顺序匹配。例如,如果定义了一个D接口,如下所示:interface D{ void foo(); } - BigONotation
是的,加上 dtor 应该可以解决问题。但是,D 不会知道它的存在,因此永远不会调用它。方法的顺序不能保证匹配,这是您的责任,通过确保函数在 D 和 C++ 中以相同的顺序出现来正确处理。 - Adam D. Ruppe
我认为对于C++,虚方法在vtbl中的顺序取决于编译器。换句话说,在类定义中声明的顺序与vtbl方法的顺序没有保证是相同的。我有什么遗漏吗? - BigONotation
我不确定标准是怎么说的,但我已经尝试过几次了,在实践中它是有效的。 - Adam D. Ruppe
这不是标准的一部分:https://dev59.com/lnA65IYBdhLWcg3wsw8X,这就是为什么我有点担心如果我切换编译器/版本我的代码会悄悄地中断... - BigONotation
是的,我想它可能可以。如果您想要最大兼容性,我建议使用具有明确定义的ABI的东西,例如C不透明指针加函数或Windows上的COM。 - Adam D. Ruppe

2

你需要在D类中添加一个调用c ++ delete运算符的方法。或者,您可以使用全局销毁方法。

此外,请不要忘记任何与另一种语言的接口必须声明为extern "C",以避免编译器函数名称混淆。

#include <iostream>

using namespace std;

class D {
   public:
   virtual int bar(int i, int j, int k)
   {
       cout << "i = " << i << endl;
       cout << "j = " << j << endl;
       cout << "k = " << k << endl;
       return 8;
   }

   // option 1
   virtual void destroy()
   {
       delete this;
   }
};

extern "C"
D *getD() {
   D *d = new D();
   return d;
}

// option 2
extern "C"
void killD(void* d) {
   delete d;
   return;
}

然后在你的D语言代码中,你需要创建一个作用域子句来调用destroy方法。

1
你不一定需要使用extern C,因为D语言有(部分但足够好的)extern(C++)支持来理解名称修饰。 - Adam D. Ruppe
因此,您的代码将一直工作,直到您使用 D 从未见过的新 C++ 编译器。或者您的 C++ 编译器在更新时更改了其名称重整方案。 - Noishe
是的,这些可能会引起问题(尽管它们也会破坏C++代码,但嘿,这以前发生过)。我同意最大兼容性应该使用C接口。 - Adam D. Ruppe

1

由于您的问题标题为“从D中调用C ++”,因此我将假设您正在尝试与C ++进行接口

D垃圾收集器是否会对C ++对象调用delete,还是我们需要提供一个“删除器”函数来手动从D中删除对象?

通过“C++对象”,我假设您指的是使用new运算符分配的对象。D不知道使用C++ new运算符创建的C++对象。因此,每当您需要删除由C++分配的对象时,必须提供自己的代码来释放内存。

D中的C ++支持非常有限,这是有原因的。完整的C ++支持意味着必须在D编译器中包含一个完整的C ++编译器(带有C ++预处理器)。这将使D编译器的实现变得更加困难。

另外,我注意到C ++类必须按照与其在D接口中声明的顺序相同的顺序声明成员函数(例如,如果我在bar()方法之前添加了析构函数,则无法从D中调用C ++对象,但如果在bar()方法之后声明析构函数,则一切正常)。

在这种情况下,我认为您首先编写C++类,考虑到它将用于D项目,然后编写D接口。 D接口应该与C++类中的方法紧密匹配,因为D编译器将生成一个与C++兼容的虚拟表
C++支持将会提高,但D极不可能完全支持C++。已经做了一些工作来支持C++命名空间(这是D社区提出的改进)。
由于D完全支持C,最好的想法是将复杂的C++代码“扁平化”为C,类似于文章“混合C和C ++”中所做的方式。很久以前,我就使用类似的方法从Delphi调用C++方法。

嗯,我不同意完整的C++编译器的事情,至少对于大多数事情来说是这样。阅读C++头文件可能是真的,但调用函数并不需要那么多。它主要只是调用约定(搞定了),名称修饰(大部分都有了)和数据布局(没有头文件很棘手,但可以完成 - 实际上与使用C结构体没有什么区别)。即使是C++模板,大多数情况下也只涉及名称修饰,因为加载c++共享库实际上并不会实例化模板,它只是像任何其他函数一样查找它。我猜这对这里的问题并不重要。 - Adam D. Ruppe
我不会将“C++支持”简化为调用C++函数... :) 不知道你怎么看,但我完全同意Walter的观点。(与C++完全兼容意味着在D中添加一个完全功能的C++编译器前端。轶事证据表明,编写这样的编译器至少需要10个人年的项目,基本上使得具有这种能力的D编译器无法实现。 - DejanLekic
嗯,我也不会简化它,但是通过将其与D自己的析构函数能力相结合,D可以非常接近。当然并不是100%的C++支持,但至少足以充分利用C++共享库。 - Adam D. Ruppe
这就是为什么我认为当前的方法非常好 - 实用就像 D 语言本身一样。我迫不及待地想要 C++ 的命名空间支持,因为那是我目前唯一需要的东西。 - DejanLekic

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