【问题标题】:How to wrap a C++ class in a C based dll or a CLI based dll?如何将 C++ 类包装在基于 C 的 dll 或基于 CLI 的 dll 中?
【发布时间】:2013-07-28 12:55:54
【问题描述】:

我被告知将 C++ 中的 writen 类导入 dll,然后在 c# 应用程序中使用该 dll。在this guide 之后,我创建了 dll,但我不能简单地在 C# 应用程序中使用它,因为它存在一些问题:

  1. 我应该为我的工厂函数的返回类型放置什么?

  2. 我的构造函数参数类型const wchar_t* 的等价物是什么?

  3. 如何检索和使用 vector< wstring> 类型的函数返回类型?

这些是阻止我在 C# 应用程序中使用 C++ DLL 的问题。有人告诉我,我需要使用 C++/CLI 创建一个包装器,然后在我的 C# 中使用它。但遗憾的是我对此一无所知,我不知道 C++.net。

目前唯一对我来说似乎更耸人听闻的事情是让它以某种方式与 C 兼容,然后创建一个 C DLL 并在我的 C# 应用程序中使用它。我读过在 C 中,类对象指针可以通过HANDLEs 访问,所以我认为在不进行大量更改的情况下让事情顺利进行是个好主意。

所以问题是如何使用 Handles 在 C 中访问我的类对象并使用它们?以及如何将 vector<wstring> 转换为 C 对应项? 如果我想使用 CLI 为我的 C++ DLL 创建一个包装器(DLL?),以便在其他 dotnet 应用程序中使用,我应该怎么做?

【问题讨论】:

  • windows HANDLE 类型只是void *s
  • const wchar_t * 是一个极其模糊的参数规范。也许它是一个 UTF-16LE 编码的 Unicode 字符串,带有 NUL 终止符,正在传递给构造函数。如果是这样,它接近于 .NET 的 System::String,这是一个以代码单元计数的 UTF-16LE 编码的 Unicode 字符串。 (至少它有const,所以我们知道它是不可变的,因此不是输入输出参数。)
  • .NET char 封送至wchar_t
  • 还不清楚const wchar_t *参数指向的数据是必须在对象的生命周期内维护还是仅在构造函数调用期间维护。
  • @Joel char.NET 的 CharC# 别名。不涉及编组。 Charu16char_t,弱表示为 wchar_t。 (wchar_t 是一种可移植类型,没有指定大小或编码/字符集,但在 VC++ 中,它是 2 个字节,通常包含一个 UTF-16LE 代码单元,即整个或一半的 Unicode 代码点。)跨度>

标签: c++ c dll c++-cli


【解决方案1】:

直接从 C# 使用本机 C++ 类在技术上是可行的,但这并非易事,甚至都不是一个好主意。对于初学者,您必须知道用于从 DLL 导入的名称,这将是 C++ 名称修饰之后的名称。您也不能从 C# 中直接访问 vector 之类的内容。

基本上有两个不错的选择:

首先是编写一个带有 C 接口的 DLL,该接口只使用可以编组为 CLR 类型的类型。您可以将指针与 IntPtr 类型一起使用,但您不能真正取消引用这些指针。您几乎可以将它们存储在您的 C# 代码中,然后在需要时将它们传递回本机 DLL。你也可以使用简单的struct 类型,只要你不需要深拷贝来处理它们。此选项涉及使用 P/Invoke。

第二种选择是编写一个混合模式的 C++/CLI 程序集,它实现了需要访问您的本机代码的所有逻辑。此程序集可以直接访问 C# 代码中的类和数据,也可以直接访问本机代码,但应预先警告您存在无法将两者混合使用的烦人中断。例如,C++/CLI 中的 ref class 不能有 shared_ptr 成员。但是,它可以有一个原始 C++ 指针作为成员。 (混合模式)本机类也可以访问 CLR 句柄类型并通过它调用 C# 代码。此选项涉及使用 C++ 互操作。

值得注意的是,您还可以使用 C++ Interop 进行其他操作。您可以让您的 C# 代码访问混合模式 C++/CLI 程序集,该程序集为某些本机代码提供 .NET 接口。但是,在这种情况下,您仍然需要进行一些翻译,因此它并不比第一个选项好多少。

有关 C++ 互操作的完整教程会相当冗长。我建议您阅读 here 并进一步调查 Google 上的 C++ Interop。

【讨论】:

    【解决方案2】:

    C++/CLI 引入了托管对象,其指针标记 * 应替换为 ^,'new' 应替换为 'gcnew',完成后无需删除这些对象有了它们,它们将被垃圾回收,[edit] 托管类在其定义中具有 ref 关键字 [/edit]。

    在 C++/CLI 包装类 WrapperCLass 中包装 C++ MyClass 类可能如下所示:

    #include <stdio.h>
    
    class MyClass
    {
    public:
        void ShowStuff(const wchar_t *a)
        {
            wprintf(a);
        }
    };
    
    public ref class WrapperClass
    {
        MyClass *wrapped;
    public:
        WrapperClass()
        {
            wrapped = new MyClass;
    
        }
        ~WrapperClass()
        {
            delete wrapped;
        }
        void ShowStuff(IntPtr string)
        {
            wrapped->ShowStuff((const wchar_t *)string.ToPointer());
        }
    };
    

    如果您使用它生成一个 dll,您将能够在您的 C# 项目中将其用作参考 而且您不必使用工厂功能机制。 在 C++/CLI 中可用,所以 const wchar_t * 也是如此。

    要将 System::String 转换为 const wchar_t *,您可以使用如下内容:

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    
    namespace Client
    {
        class Program
        {
            static void Main(string[] args)
            {
                WrapperClass w = new WrapperClass();
                IntPtr tmp;
                w.ShowStuff(tmp = System.Runtime.InteropServices.Marshal.StringToHGlobalUni("Test"));
                System.Runtime.InteropServices.Marshal.FreeHGlobal(tmp);
            }
        }
    }
    

    (可能有更好的方法来做到这一点......)

    对于您的返回类型,您必须在包装类中进行转换。制作一些 .net 集合,遍历你的向量,将 wstring 转换为 System::String,并将其添加到 .net 集合中,然后返回。

    【讨论】:

    • 技术上,ref 不是关键字; ref class 是所谓的空白关键字之一。关键是ref 不是class 的修饰符,因为ref classclass 是完全不同的东西。
    • 对于编组字符串,我可能会建议将它们作为 System::String^ 传递到 C++/CLI,然后使用基于 RAII 的编组类。否则,您会遇到异常安全问题。当然,你也可以在这里使用finally,但我个人觉得 RAII 要简单得多。
    • @Pruyque: 非常感谢。但是如果我准备好一个 dll 并想使用它而不是重写托管类中的本机类怎么办?
    • 您可以LoadLibrary您的dll,并使用您的工厂函数来实例化包装的对象,而不是直接创建一个new MyClass。对于要在包装类中调用的每个方法,您仍然必须在包装器中编写一个方法(我知道,这不好笑……)。 @Tom:酷,我还不知道(+1)...
    【解决方案3】:

    要为 C++ 类创建 C wrapper 以用于例如 C# 应用程序,您可以执行以下操作。

    在 Visual Studio 中选择 Win32 Console Application 并输入名称,然后单击下一步,在下一个窗格中选择 DLL 并单击完成。完成后,您会看到一个包含 3 个文件的 DLL 项目。

    testdll.h 
    testdll.cpp
    dllmain
    

    删除testdll.htestdll.cpp 文件中存在的所有内容,并将以下内容分别复制到每个文件中。将这些行添加到您的 testdll.h

    // Our C wrapper for creating a dll to be used in C# apps
    
    // The following ifdef block is the standard way of creating macros which make exporting 
    // from a DLL simpler. All files within this DLL are compiled with the TESTDLL_EXPORTS
    // symbol defined on the command line. This symbol should not be defined on any project
    // that uses this DLL. This way any other project whose source files include this file see 
    // TESTDLL_API functions as being imported from a DLL, whereas this DLL sees symbols
    // defined with this macro as being exported.
    #ifdef  TESTDLL_EXPORTS
    #define  TESTDLL_API __declspec(dllexport)
    #else
    #define  TESTDLL_API __declspec(dllimport)
    #endif
    
    extern "C"
    {
         TESTDLL_API int OurTestFunction(int x, int y);                         
    }
    

    在这个 extern "C" 块中定义你的接口,访问你的类成员函数的函数。注意函数原型之前的TESTDLL。您的所有功能都必须按此进行。

    将这些添加到您的 testdll.cpp 文件中:

    #include "testdll.h"
    #include "ourClass.h"
    
    #define DLL_EXPORT
    
    extern "C"
    {
        OurClass ourObject;
        TESTDLL_API int OurTestFunction(int x, int y)
        {
            return ourObject.Add(x,y);
        }
    }
    

    您编译它并获得一个可用于 C# 应用程序的基于 C 的 dll。
    不过有几点需要注意,更重要的是:

    1. 您需要了解您用作代理的代码 - 我的意思是 testdll.h 中的函数定义,只能使用 C 兼容的类型,毕竟是 C 而不是 C++。
    2. 是您希望能够分配您的新对象 类而不是仅使用一个全局对象来访问所有方法。

    为此,如果您需要在成员函数之间传递您的类对象,您需要首先将其转换为 C 可以理解的 void*,然后传递它并使用它来访问您的任何成员函数。

    例如,为了让用户能够间接管理对象,我会在我的testdll.h 中添加类似的内容:

    #ifdef TESTDLL_EXPORTS
    #define TESTDLL_API __declspec(dllexport)
    #else
    #define TESTDLL_API __declspec(dllimport)
    #endif
    
    extern "C"
    {
        TESTDLL_API int OurTestFunction(int x, int y);                      
    
        TESTDLL_API void*  CreateHandle();
        TESTDLL_API void*  GetCurrentHandle();
        TESTDLL_API void   DisposeCurrentHandle();
        TESTDLL_API void   SetCurrentHandle(void* handle);
        TESTDLL_API void*  GetHandle();
        TESTDLL_API void   DisposeHandle(void*);
        TESTDLL_API void   DisposeArrayBuffers(void);
    }
    

    在我的 testdll.cpp 中,我会将它们定义为:

    #include "testdll.h"
    #include "ourClass.h"
    
    #define DLL_EXPORT
    
    extern "C"
    {
        OurClass *ourObject;
    
        TESTDLL_API int OurTestFunction(int x, int y)
        {
            //return ourObject.Add(x,y); -- not any more !!
            ourObject = reinterpret_cast<OurClass *>(GetHandle());
        }
    
        //Handle operations
        TESTDLL_API void* CreateHandle()
        {
            if (ourObject == nullptr)
            {
                ourObject = new OurClass ;
            }
            else
            {
                delete ourObject ;
                ourObject = new OurClass ;
            }
            return reinterpret_cast<void*>(ourObject);
        }
    
        TESTDLL_API void* GetCurrentHandle()
        {
            return reinterpret_cast<void*>(ourObject );
        }
    
        TESTDLL_API void  DisposeCurrentHandle()
        {
            delete ourObject ;
            ourObject = nullptr;
        }
    
        TESTDLL_API void  SetCurrentHandle(void* handle)
        {
            if (handle != nullptr)
            {
                ourObject = reinterpret_cast<OurClass *>(handle);
            }
            else
            {
                ourObject = new OurClass ;
            }
    
        }
    
        //factory utility function
        TESTDLL_API void* GetHandle()
        {
            void* handle = GetCurrentHandle();
            if (handle != nullptr)
            {
                return handle;
            }
            else
            {
                ourObject = new OurClass ;
                handle = reinterpret_cast <void*>(ourObject );
            }
            return handle;
        }
    
        CDLL_API void  DisposeHandle(void* handle)
        {
            OurClass * tmp = reinterpret_cast<OurClass *>(handle);
            delete tmp;
        }
    
        TESTDLL_API void DisposeArrayBuffers(void)
        {
            ourObject = reinterpret_cast<OurClass *>(GetHandle());
            return ourObject ->DisposeBuffers();//This is a member function defined solely for this purpose of being used inside this wrapper to delete any allocated resources by our class object.
        }
    }
    

    当我们编译这个Dll 时,我们可以很容易地在我们的 C# 应用程序中使用它。在能够使用我们在这个 dll 中定义的函数之前,我们需要使用适当的[ImportDll()]。因此,对于我们的 TestDll,我们将编写:

    [DllImport(@"TestDll.dll", CallingConvention = CallingConvention.Cdecl)]
    public static extern int OurTestFunction(int firstNumber,int secondNumber); 
    

    最后像这样使用它:

    private void btnReadBigram_Click(object sender, EventArgs e)
    {
        int x = OurTestFunction(10,50);
        MessageBox.Show(x.ToString());
    }
    

    这就是我所做的一切,以使我的 C++ 类成员函数可以在 C# 应用程序中轻松访问。

    注意
    编译 C# 应用程序时,请确保您选择了 x86 平台来编译您的项目,而不是 AnyCpu。您可以通过属性更改您的平台。

    注2
    要了解如何为您的本机 C++ 类创建 C++/CLI 包装器,请阅读以下内容:C++/CLI wrapper for your native C++ class

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-04-11
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多