3

我是 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
    );

这是基于建议。

现在我需要从 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 的内容。

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

4

4 回答 4

4

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

是学习 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)。这个过程称为编组。这个方便的表格对于弄清楚这一点非常有用。

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

//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 语句,这只是为了给出如何使用它的一般要点。

于 2013-03-28T14:58:16.563 回答
1

从此开始:

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

请注意,Marshal.Copy还为数组使用重载。通过编组,您可以摆脱ref您确实想要的例外情况。只需按照他们的方式编写 C/C++。

以下有点复杂:

于 2013-03-28T01:54:56.943 回答
0

我经常看到处理这种情况的两种方法是使用“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);
...
于 2013-03-28T01:11:07.750 回答
0

如果您愿意使用第三方工具,有一个名为 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 互操作 SDK

该工具xInterop NGen++ 2.0 已发布。如果您有兴趣为本地 C++ DLL 创建 C# 包装器,请查看它。

(我是SDK工具的作者)

于 2013-04-14T03:24:34.523 回答