【问题标题】:Calling C++ from D从 D 调用 C++
【发布时间】:2014-01-02 22:55:04
【问题描述】:

我已经阅读了解释如何从 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 中的第一个位置?这不依赖于实现/编译器吗?

【问题讨论】:

    标签: c++ d


    【解决方案1】:

    我不希望 D 的垃圾收集器知道如何释放 C++ 对象。这意味着(至少)D 运行时:

    1. 对 C++ 运行时做出假设,即如何删除 C++ 对象
    2. 其他 C++ 代码不再需要该对象

    我确定您必须提供另一个 C++ 函数来调用传递给它的对象。事实上,许多 C++ 库(即使也从 C++ 中使用)在从库内部调用构造函数的情况下具有相同的模式。即使在直接 C 中,在一个 dll/exe 中分配内存并在另一个中释放它通常也是一个坏主意。如果两个二进制文件不共享同一个运行时库,这可能会很糟糕。

    【讨论】:

      【解决方案2】:

      实现的具体方式是 D 对象简单地具有一个与 C++ 兼容的 vtable。所以只有它上面的虚函数起作用,而且由于表是按索引排列的,所以它们必须以相同的顺序出现。

      D 对 C++ 构造函数、析构函数或任何其他特殊方法一无所知,但如果它们是虚拟的,它可以丢弃 vtable。

      我现在编写了一个名为 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++ 程序更新了它,请确保它在 C++ 中也被删除。您在 D 中创建并传递给 C++ 的任何对象也应被 D... 销毁,您可能需要手动执行此操作。如果您的 C++ 函数保留对 D 对象的引用,但在 D 中没有一个,它可能会被垃圾回收!因此,您要么希望确保在 D 中始终有一个在对象的生命周期内存在的引用,要么自己使用 malloc 和 free 等函数创建一个销毁它。​​

      我不喜欢让调用者使用通用的 free(),因为版本不一定匹配。我说总是从你的图书馆提供一种方法来释放东西。即使它的实现只是 free(ptr);,提供您自己的函数也会明确表明应该使用它,并为您提供防止此类不匹配的保护。

      【讨论】:

      • 那么是不是说我可以给C++类加一个析构函数,只要不是virtual,就不会和D接口对应的方法搞混了?我不太明白的另一点是如何保证方法的顺序匹配。例如,如果一个 D 接口定义如下: interface D{ void foo(); }
      • 是的,应该可以,添加 dtor。但是,D 不会对此一无所知,也永远不会打电话给它。方法的顺序不能保证匹配,这是您的责任,通过确保函数在 D 和 C++ 中以相同的顺序出现。
      • 我认为对于 C++,vtbl 中虚拟方法的顺序取决于编译器。换句话说,不能保证 vtbl 方法的顺序与它们在类定义中的声明顺序相同。我错过了什么吗?
      • 我不确定标准是怎么说的,但我已经尝试了几次,它在实践中有效。
      • 这不是标准的一部分:stackoverflow.com/questions/3674929/… 这就是为什么我有点担心如果我切换编译器/版本我的代码会默默地中断......
      【解决方案3】:

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

      另外,不要忘记任何与其他语言的接口都必须声明为 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方法的范围子句。

      【讨论】:

      • 你不一定需要 extern C,因为 D 有(部分但足够好)extern(C++) 支持来理解修改。
      • 因此,在您使用 D 从未见过的新 c++ 编译器之前,您的代码将一直有效。或者你的 c++ 编译器在更新时改变了它的修改方案。
      • 是的,这些可能是问题(虽然他们也会在野外破坏 C++ 代码,但嘿,以前也发生过)。我同意最大的兼容性是使用 C 接口。
      【解决方案4】:

      由于您的问题标题为“从 D 调用 C++”,我假设您正在尝试 interface to C++

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

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

      D 中的 C++ 支持非常有限,这是有充分理由的。 - 完整的 C++ 支持意味着一个成熟的 C++ 编译器(带有 C++ 预处理器)应该包含在 D 编译器中。这将使 D 编译器的实现更加困难。

      我还注意到,C++ 类必须以与在 D 接口中声明的顺序完全相同的顺序声明成员函数(例如,如果我在 bar() 方法之前添加析构函数,则无法从D,但是如果在 bar() 方法之后声明了析构函数,一切正常)。

      在这种特殊情况下,我相信您首先编写 C++ 类,记住它将在 D 项目中使用,然后您编写 D 接口。 D 接口应该与 C++ 类中的方法紧密匹配,因为 D 编译器将生成与 C++ 兼容的virtual table

      C++ 支持将会改进,但 D 极不可能完全支持 C++。已完成支持 C++ 命名空间的工作(D 社区要求的一项改进)。

      由于 D 完全支持 C,最好的办法是将复杂的 C++ 代码“扁平化”为 C,其方式类似于文章“Mixed C and C++”中的做法。很久以前,我使用类似的方法从 Delphi 调用 C++ 方法。

      【讨论】:

      • 嗯,我不同意成熟的 C++ 编译器,至少在大多数情况下不同意。读取 C++ 头文件确实如此,但调用该函数并不需要那么多。它主要是调用约定(得到它)、修改(大部分都有)和数据布局(在没有标题的情况下很棘手,但可以完成——实际上与使用 C 结构没有什么不同)。甚至 C++ 模板也主要归结为修改,因为加载 C++ 共享库实际上并没有实例化模板,它只是像任何其他函数一样查找它。我想这与这里的问题无关。
      • 我不会将“C++ 支持”简化为调用 C++ 函数... :) 关于你的想法,我完全同意 Walter 的观点。 (与 C++ 100% 兼容意味着或多或少地向 D 中添加了一个功能齐全的 C++ 编译器前端。轶事证据表明,编写这样的项目至少需要 10 个人年的项目,本质上是制作具有这种能力的 D 编译器无法实现。)
      • 嗯,我也不会那么简化它,但是 D 可以通过将它与它自己的析构函数能力相结合来非常接近。当然不是 100% 支持 C++,但至少在那时足以充分利用 C++ 共享库。
      • 这就是为什么我认为当前的方法非常好——就像 D 语言本身一样实用。我等不及 C++ 命名空间支持了。这是我目前唯一真正需要的东西。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-05-01
      • 1970-01-01
      • 1970-01-01
      • 2012-11-30
      相关资源
      最近更新 更多