【问题标题】:How can I marshall a vector<int> from a C++ dll to a C# application?如何将 vector<int> 从 C++ dll 编组到 C# 应用程序?
【发布时间】:2011-02-14 09:51:36
【问题描述】:

我有一个 C++ 函数,它生成一个有趣的矩形列表。我希望能够将该列表从 C++ 库中取出并返回到调用它的 C# 应用程序中。

到目前为止,我正在像这样对矩形进行编码:

struct ImagePatch{ 
   int xmin, xmax, ymin, ymax;
}

然后编码一些向量:

void MyFunc(..., std::vector<int>& rectanglePoints){
   std::vector<ImagePatch> patches; //this is filled with rectangles
   for(i = 0; i < patches.size(); i++){
       rectanglePoints.push_back(patches[i].xmin);
       rectanglePoints.push_back(patches[i].xmax);
       rectanglePoints.push_back(patches[i].ymin);
       rectanglePoints.push_back(patches[i].ymax);
   }
}

与 C# 交互的标头看起来像(并且适用于一堆其他函数):

extern "C" {
    __declspec(dllexport) void __cdecl MyFunc(..., std::vector<int>& rectanglePoints);
}

我可以做一些关键字或其他事情来得到这组矩形吗?我发现this article 用于在 C# 中编组对象,但它似乎太复杂且解释不足。整数向量是正确的方法吗,还是有其他技巧或方法?

【问题讨论】:

  • 这并不是真正的“编组”。编组将获取您的 C++ 对象,编写一些表示它的二进制数据,并让 C# 读取该二进制数据以在其他环境中构造相应的对象。您正在尝试将参数从 C# 代码传递到 C++ 代码,并让 C++ 代码对其进行修改。
  • 好的,那么我怎样才能做到这一点,使我从 C++ 端得到的内容是任意长度的?
  • 好吧,如果您想避免在 C++ 中编写“适当的”托管 .NET 对象,如该 MSDN 文章中所述,则可以:从 C++ 返回一个 malloced 缓冲区(并在您'已经完成了它);重新设计您的 API,以便 C# 可以传入要填充的缓冲区和长度,并且 C++ 代码可以以某种方式告诉 C# 该缓冲区需要多大;重新设计 API,以便为每个值从 C++ 回调到 C# 一次,并将其添加到 C# 集合中。我对 C#(或与 C# 中的其他语言的接口)了解不足,无法判断哪个更容易/更好。
  • 如果您的 C++ 代码可以在 .NET 中运行,或者使用 C# 和 C++ 中的 .NET 容器之一。
  • 是的,我已经决定将一个大缓冲区传递给 C++,并希望它足以满足实际用途。不过,对于 .NET 和本机代码之间的互操作服务而言,这似乎是一个相当大的漏洞。

标签: c# .net c++ stl marshalling


【解决方案1】:

是的。你可以。实际上,不只是std::vectorstd::stringstd::wstring,任何标准的 C++ 类或您自己的类都可以被编组或实例化并从 C#/.NET 调用。

在 C# 中包装 std::vector&lt;any_type&gt; 确实可以使用常规的 P/Invoke Interop,但它很复杂。甚至任何类型的std::map 都可以在 C#/.NET 中完成。

public class SampleClass : IDisposable
{    
    [DllImport("YourDll.dll", EntryPoint="ConstructorOfYourClass", CharSet=CharSet.Ansi,          CallingConvention=CallingConvention.ThisCall)]
    public extern static void SampleClassConstructor(IntPtr thisObject);

    [DllImport("YourDll.dll", EntryPoint="DestructorOfYourClass", CharSet=CharSet.Ansi,          CallingConvention=CallingConvention.ThisCall)]
    public extern static void SampleClassDestructor(IntPtr thisObject);

    [DllImport("YourDll.dll", EntryPoint="DoSomething", CharSet=CharSet.Ansi,      CallingConvention=CallingConvention.ThisCall)]
    public extern static void DoSomething(IntPtr thisObject);

    [DllImport("YourDll.dll", EntryPoint="DoSomethingElse", CharSet=CharSet.Ansi,      CallingConvention=CallingConvention.ThisCall)]
    public extern static void DoSomething(IntPtr thisObject, int x);

    IntPtr ptr;

    public SampleClass(int sizeOfYourCppClass)
    {
        this.ptr = Marshal.AllocHGlobal(sizeOfYourCppClass);
        SampleClassConstructor(this.ptr);  
    }

    public void DoSomething()
    {
        DoSomething(this.ptr);
    }

    public void DoSomethingElse(int x)
    {
        DoSomethingElse(this.ptr, x);
    }

    public void Dispose()
    {
        if (this.ptr != IntPtr.Zero)
        {
            // The following 2 calls equals to "delete object" in C++
            // Calling the destructor of the C++ class will free the memory allocated by the native c++ class.
            SampleClassDestructor(this.ptr);

            // Free the memory allocated from .NET.
            Marshal.FreeHGlobal(this.ptr);

            this.ptr = IntPtr.Zero;
        }
    }
}

请看下面的链接,

C#/.NET PInvoke Interop SDK

(我是SDK工具的作者)

一旦为 C++ 类准备好 C# 包装类,就很容易实现 ICustomMarshaler,以便您可以从 .NET 编组 C++ 对象。

http://msdn.microsoft.com/en-us/library/system.runtime.interopservices.icustommarshaler.aspx

【讨论】:

  • 你在这里进入了未定义行为的领域。做对了,使用 C++/CLI。
  • 感谢 cmets。但是一旦你知道了包装一个 C++ 类的步骤,它就一点也不复杂了,我添加了一个简单的例子来完成我的答案。 C++/CLI 对开发人员不太友好,尤其是语法。
  • 恭喜,您编写了一个潜在的内存泄漏、潜在的双重释放以及在没有正确支持系统的情况下调用内部函数的类。坏坏主意。此外,正确的 C++/CLI 解决方案将是 1/3 的代码。
  • 非常感谢 cmets。潜在的内存泄漏?你能指出在哪里以及如何?在处理 P/Invoke 时,无论如何您都必须注意 native 和 .NET 的区别。你在说什么内部功能?这些函数在我的示例中导出,这只是说明它如何工作的示例,它不是具有生产质量代码的类。双倍免费?您可能在 C++ 代码本身中犯同样的错误,对吧?当涉及垃圾收集时,这也是在 C++ 和 .NET 世界之间架桥时的常见问题。
  • 当然,C++ 端调用约定很重要,因为调用成员函数的约定不止一种。 thiscallstdcallcdecl 都是可能的。依赖 makefile 使用的默认约定是脆弱的。您在示例中如此方便地省略的损坏名称很脆弱,因为它们随每个编译器版本而变化。然后你声称这适用于标准库类?这些也会改变。如果你在做代码生成,生成 C++/CLI,没有人会关心奇怪的语法。
【解决方案2】:

STL 是一个特定于 C++ 的库,因此您不能将它作为一个对象直接传递给 C#。

关于 std::vector 保证的一件事是 &v[0] 指向第一个元素并且所有元素都线性地位于内存中(换句话说,就内存布局而言,它就像一个 C 数组)

所以编组为 int 数组...这应该不难 - 网络上有很多示例。

添加

假设您只将数据从 C++ 传递到 C#:

C# 无法处理 C++ 向量对象,因此不要尝试通过引用传递它:相反,您的 C++ 代码必须返回指向整数数组的指针...

如果你不打算在多个线程中使用这个函数,你可以使用静态存储:

int *getRects(bool bClear)
{
    static vector<int> v; // This variable persists across invocations
    if(bClear)
    {
        v.swap(vector<int>());
    }
    else
    {
        v.clear();
        // Fill v with data as you wish
    }

    return v.size() ? &v[0] : NULL;
}

如果返回的数据很大,则调用 getRects(true),因此你释放 v 中的内存。

为简单起见,无需传递矢量数据的大小,只需在末尾添加一个标记值(例如 -1),以便 C# 代码检测数据的结束位置。

【讨论】:

  • 但是这将如何运作?如果我编组为一个数组,该数组如何通过推回在 C++ 端增长?该内存尚未分配,因此第一个元素之外的任何元素都会失败。如果我在 C# 端预先分配数组,那么我还不如使用数组而不是向量,如果我有很多矩形可能会很糟糕。
  • @rep_movsd 这对于非 int 的东西(如结构)同样适用吗?
  • 是的,结构体是值类型,被视为原始类型。
【解决方案3】:

我很确定你不能这样做。您必须能够将 C++ 代码直接转换为 C# 类,因此您至少必须复制向量类的内部结构以正确编组它。我也很确定您将无法跨边界移动引用,您必须使用 IntPtr (原始指针)。我知道可行的方法是编组结构的原始数组。

【讨论】:

  • 我知道我可以跨边界移动引用——我在其他函数中使用 int& 等。在 C# 方面,使用 dllimport 时,只需将 'ref' 关键字放在函数签名中即可。
  • 另请注意,像vector这样的C++对象不应该跨越模块边界——不能保证不同的DLL或exe使用完全相同版本的STL。
  • 那么我如何将一堆整数(我不知道的确切数量)返回到 C# 中?
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-02-07
  • 1970-01-01
  • 2018-02-24
  • 1970-01-01
  • 2019-07-19
相关资源
最近更新 更多