【问题标题】:Calling C++ function from C#, with lots of complicated input and output parameters从 C# 调用 C++ 函数,具有大量复杂的输入和输出参数
【发布时间】:2013-03-28 00:49:29
【问题描述】:

我是 C# 新手,但我曾广泛使用 C++。我有一个需要从 C# 调用的 C++ 函数。在阅读了一些 SO 的答案和一些谷歌搜索后,我得出结论,我需要为该函数创建一个纯 C 接口。我已经这样做了,但仍然对如何从 C# 调用它感到困惑。

C++ 中的函数如下所示:

int processImages(
    std::string& inputFilePath,                      // An input file
    const std::vector<std::string>& inputListOfDirs, // Input list of dirs
    std::vector<InternalStruct>& vecInternalStruct,  // Input/Output struct
    std::vector<std::vector< int > >& OutputIntsForEachFile,
    std::vector< std::vector<SmallStruct> >& vecVecSmallStruct, // Output
    int verboseLevel
    );

同样的函数,用 C 语言转换,如下所示:

int processImagesC(
    char* p_inputFilePath,               // An input file
    char** p_inputListOfDirs,            // Input list of dirs
    size_t* p_numInputDirs,              // Indicating number of elements
    InternalStruct* p_vecInternalStruct, // Input/Output struct
    size_t* p_numInternalStructs, 
    int** p_OutputIntsForEachFile,       // a 2d array each row ending with -1
    size_t* p_numOutputIntsForEachFile //one number indicating its number of rows
    SmallStruct** p_vecVecSmallStruct,   // Output
    size_t* p_numInVecSmallStruct,
    int verboseLevel
    );

这是基于this 的建议。

现在我需要从 C# 调用它,这就是混乱的地方。我已尽力转换结构。

C# 代码如下所示:

[DllImport(
    @"C:\path\to\cppdll.dll", CallingConvention=CallingConvention.Cdecl, 
    EntryPoint="processImagesC", SetLastError=true)]
[return: MarshalAs(UnmanagedType.I4)]
unsafe public static extern int processImagesC(
    String inputFilePath,
    String[] inputListOfDirs,
    ref uint numInputListOfDirs,

    // Should I use ref InternalStruct * vecInternalStruct?
    ref InternalStruct[] vecInternalStruct, 

    ref uint numInternalStruct,

    // Or ref int[] or ref int[][] or int[][]?
    ref int[][] OutputIntsForEachFile, 

    ref uint numOutputIntsForEachFile,

    // again, ref ..[], [][], or ref [][]?
    ref SmallStruct[][] vecVecSmallStruct, 

    int verboseLevel
);

在 C/C++ 代码中为所有输出变量(指针)分配内存。这可能意味着我们需要将代码声明为不安全,对吗?

我们如何处理内存释放?我是否应该编写另一个 API(函数)来释放 C/C++ 分配的对象/数组?

C++ 代码需要符合标准且独立于平台,因此我无法在其中插入任何特定于 Windows 的内容。

我希望有人能理解这一点并提供答案,或者至少为我指明正确的方向。

【问题讨论】:

  • 你永远不应该从 C# 调用 C++。名称修饰格式在编译之间并不相同。
  • 如果太复杂,您可以围绕原生 C++ 创建托管 C++/CLI 包装器,以便更轻松地从 C# 调用。
  • 必须同意 C++/CLI 包装器方法。框架中有一些实用程序可以在 STL 类型之间进行转换。
  • @Romoku 你能解释一下我该如何创建这个托管的 C++/CLI 包装器吗?我没有使用托管 C++ 的经验...
  • C++/CLI 就是答案。

标签: c# c++ dll interop pinvoke


【解决方案1】:

由于似乎有人对将 It Just Works (IJW) 与 C++/CLI 结合使用有些兴趣,因此我将发布一些关于此的信息,需要进行进一步的谷歌搜索和研究才能弄清楚这一切。 C++/CLI 可以使用单个编译器标志启用(/CLI,通过属性页->常规->公共语言运行时支持启用)。 C++/cli 不是 c++,而只是另一种托管语言。 C++/CLI 类可以编译成 dll 并直接从其他 .NET 项目(C#、VB.NET 等)调用。但是,与其他 .NET 语言不同,它可以直接与 C++ 代码交互。

This 是学习 C++/CLI 的良好开端。要学习的重要内容是告诉您该类是托管的(.NET 类)而不是 Vanila C++ 的装饰。 “ref”关键字将定义标记为 .NET 定义:

public ref class Foo{ public: void bar();};//Managed class, visible to C#
public ref struct Foo{};//Managed struct, visible to C#

所有引用类都使用句柄​​而不是指针或引用来引用。句柄由 ^ 运算符表示。要创建一个新句柄,请使用 gcnew,要访问句柄的函数/成员,请使用 -> 运算符。

//in main
Foo^ a = gcnew Foo();
a->bar();

您经常需要将常见的结构从 C# 移动到本机类型,然后再返回。 (例如托管 Array^ 或 String^ 到 void* 或 std::string)。这个过程称为编组。 This handy table 对于解决这个问题非常有用。

一个常见的任务是为原生类创建一个包装器,如下所示:

//Foo.h
#include <string>
namespace nativeFoo
{
    class Foo
    {
     private:
        std::string fooHeader;
     public:
        Foo() {fooHeader = "asdf";}
        std::string Bar(std::string& b) {return fooHeader+b;}
    };
}
//ManagedFoo.h
#include "foo.h"
namespace managedFoo
{
    public ref class Foo
    {
        private:
             nativeFoo::Foo* _ptr;
        public:
             Foo(){_ptr = new nativeFoo::Foo();}
             ~Foo(){!Foo();}
             !Foo(){if (_ptr){delete ptr;ptr = NULL;}}

             String^ bar(String^ b)
             {
                 return marshal_as<String^>(_ptr->bar(marshal_as<std::string>(b)));
             }
    };
}

警告:我完全遗漏了一堆#include 和#using 语句,这只是为了给出如何使用它的一般要点。

【讨论】:

  • 你确实帮他处理了 std::string 类型,他的函数有复杂的类型,比如 std::vector<:vector>>&, std::vector<:vector int>>,他也需要从 C# 访问它们。你能在 C++/CLI 中做同样的事情吗?谢谢!
  • @Dave,一般来说,这取决于你想怎么做。您只能真正编组结构:类(例如 std::vector)有点棘手。如果您可以对 System::Generic::List 进行深层复制,我建议:(vector&lt;NativeStruct&gt; s; List&lt;ManagedSmallStruct&gt;^ l = gcnew List&lt;ManagedSmallStruct&gt;(); l-&gt;add((ManagedSmallStruct)s[0]);)。这可能会导致大型阵列的性能问题。否则,您将不得不为向量创建一个包装类(就像我们为 Foo 所做的那样),它会很快变旧。
  • 2 小点:编译器开关是/clr,而不是/cli。 C++/CLI 与其他 .NET 语言的不同之处在于它可以生成 混合模式 程序集,其中包含托管代码和本机代码。在您的示例中,nativeFoo::Foo 编译为本机代码。如果要使用 C++/CLI 创建纯托管程序集,则必须使用 /clr:pure/clr:safe
【解决方案2】:

从这里开始:

还有一些关于编组的事情:

请注意,Marshal.Copy 也会重载以供数组使用。通过编组,您可以摆脱ref,除非您确实想要。只需按照他们的方式编写 C/C++。

下面有点复杂:

【讨论】:

    【解决方案3】:

    我经常看到的两种处理方法是使用“FreeResource”风格的 API,或者在函数中指定输出缓冲区的大小。

    方法一

    C++

    void GetName(char ** _str)
    {
        if (!_str)
            return; // error
        *_str = new char[20];
        strcpy(*str, "my name");
    }
    
    void FreeString(char * _str)
    {
        delete str;
    }
    

    客户(任何语言)

    char * name;
    GetName(&name);
    ...
    FreeString(name);
    

    方法二

    C++

    void GetName(char * _str, size_t _len)
    {
        if (_len < 20)
            return; // error
        strcpy(str, "my name");
    }
    

    客户(任何语言)

    char * name = new char[20];
    GetName(name, 20);
    ...
    

    【讨论】:

    • 你的回答让事情更清楚了,但是,这将有助于查看用于调用 dll 函数的特定 C# 代码。
    【解决方案4】:

    如果您愿意使用第三方工具,有一个名为 C#/.NET PInvoke Interop SDK 的工具可能对您有所帮助。但你也可以自己做。对于具有几个方法的简单类,您可以在托管 C# 代码中编写自己的代码。

    从 .NET 世界中实例化 C++ 对象的基本思想是从 .NET 中分配 C++ 对象的确切大小,然后调用从 C++ DLL 导出的构造函数来初始化对象,然后您就可以调用任何函数来访问该 C++ 对象,如果任何方法涉及其他 C++ 类,您还需要将它们包装在 C# 类中,对于具有原始类型的方法,您可以简单地 P/Invoke 它们。如果你只有几个方法可以调用,那会很简单,手动编码不会花很长时间。完成 C++ 对象后,调用 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="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()
        {
            Marshal.FreeHGlobal(this.ptr);
        }
    }
    

    详情请看以下链接,

    C#/.NET PInvoke Interop SDK

    该工具,xInterop NGen++ 2.0 has been released. 如果您有兴趣为原生 C++ DLL 创建 C# 包装器,请查看它。

    (我是SDK工具的作者)

    【讨论】:

      猜你喜欢
      • 2018-07-28
      • 2010-12-25
      • 1970-01-01
      • 1970-01-01
      • 2015-01-16
      • 2019-01-26
      • 2018-06-21
      • 2016-11-19
      • 1970-01-01
      相关资源
      最近更新 更多