59

背景:作为更大任务的一部分,我需要使非托管 C++ 和 C 代码可以访问 C# 库。为了自己回答这个问题,过去几天/几周我一直在学习 C++/CLI。

似乎有许多不同的方法可以使用非托管 C++ 和 C 中的 C# dll 来实现。一些简短的答案似乎是:使用 Interlope 服务,使用 .com。和 regasm,使用 PInvoke(似乎只从 C# 到 C++),并在 C++/CLR 中使用 IJW(这似乎是 Interlope 服务)。我认为最好设置一个库,它可能是一个 CLR 包装器,它使用 IJW 代表本机 C++ 和 C 代码调用我的 C# dll。

细节:我需要将字符串和 int 的值从 c++ 代码传递给 C# dll,然后返回 void。

相关性:许多公司有很多借口混合和匹配 C++、C 和 C#。性能:非托管代码通常更快,接口:托管接口通常更易于维护、部署,并且通常更容易看,经理们也告诉我们。遗留代码也迫使我们。它就在那里(就像我们爬过的山一样)。虽然如何从 C# 调用 C++ 库的示例很多。通过谷歌搜索很难找到如何从 C++ 代码调用 C# 库的示例,尤其是如果您想查看更新的 4.0+ 代码。

软件: C#、C++/CLR、C++、C、Visual Studio 2010 和 .NET 4.0

问题详情: OK 多部分问题:

  1. 使用 com 对象有优势吗?还是 PInvoke?还是别的什么方法?(我觉得这里的学习曲线同样陡峭,尽管我确实在 Google Land 中找到了有关该主题的更多信息。IJW 似乎承诺我想要它做的事情。我是否应该放弃寻找 IJW 解决方案和而是专注于这个?)(优势/劣势?)

  2. 我想象有一个解决方案是否正确,我编写了一个在 C++/CLR 中利用 IJW 的包装器?我在哪里可以找到关于这个主题的更多信息,不要说我没有足够的谷歌/或查看 MSDN 而不告诉我你在哪里看到它。(我想我更喜欢这个选项,努力编写清晰简单的代码。)

  3. 缩小问题范围:我觉得我真正的问题和需要是回答以下较小的问题:如何设置非托管 C++ 文件可以在 Visual Studio 中使用的 C++/CLR 库。我认为,如果我可以简单地在非托管 C++ 代码中实例化一个托管 C++ 类,那么我可能能够解决剩下的问题(接口和包装等)。我希望我的主要愚蠢之处在于尝试在 Visual Studio 中设置引用/#includes 等,我清楚地认为我可能有其他误解。也许整个事情的答案可能只是指向帮助我的教程或说明的链接。

研究:我一遍又一遍地用谷歌搜索和搜索并取得了一些成功。我找到了许多链接,这些链接向您展示了如何从 C# 代码中使用非托管库。而且我承认已经有一些链接显示了如何使用 com 对象来做到这一点。针对 VS 2010 的结果并不多。

参考资料: 我一遍又一遍地阅读了很多帖子。我试图通过最相关的工作。有些似乎非常接近答案,但我似乎无法让它们发挥作用。我怀疑我缺少的东西非常小,例如误用关键字 ref,或缺少 #include 或 using 语句,或误用名称空间,或未正确使用 IJW 功能,或缺少 VS需要正确处理编译等。所以你想知道,为什么不包含代码?好吧,我觉得我不在一个我理解并期望我必须工作的代码的地方。我想在一个我理解它的地方,当我到达那里时,我可能需要帮助修复它。我将随机包含两个链接,但我不允许在我当前的 Hitpoint 级别显示它们。

http://www.codeproject.com/Articles/35437/Moving-Data-between-Managed-Code-and-Unmanaged-Cod

这从 C++ 到 Visual Basic 并通过 C++CLR 返回的两个方向从托管和非托管代码调用代码,当然我对 C# 感兴趣。:http: //www.codeproject.com/Articles/9903/Calling -托管代码来自非托管代码和副

4

6 回答 6

43

你可以很容易地做到这一点。

  1. 创建一个 .h/.cpp 组合
  2. 在新创建的 .cpp 文件上启用 /clr。(CPP -> 右键单击​​ -> 属性)
  3. 将“其他#using 目录”的搜索路径设置为指向您的 C# dll。

本机.h

void NativeWrapMethod();

本机.cpp

#using <mscorlib.dll>
#using <MyNet.dll>

using namespace MyNetNameSpace;

void NativeWrapMethod()
{
    MyNetNameSpace::MyManagedClass::Method(); // static method
}

这就是将 C++\CLI 中的 C# 库与本机代码一起使用的基础知识。(只需在需要的地方引用 Native.h 并调用该函数。)

将 C# 代码与托管 C++\CLI 代码一起使用大致相同。

关于这个主题有很多错误信息,因此,希望这可以为人们节省很多麻烦。:)


我已经这样做了:VS2010 - VS2012(它可能也适用于 VS2008。)

于 2012-11-16T00:16:43.930 回答
26

2018 年更新

似乎该解决方案不适用于 Visual Studio 2017 及更高版本。不幸的是,我目前没有使用 Visual Studio,因此无法自己更新这个答案。但是kaylee发布了我的答案的更新版本,谢谢!

更新结束

如果你想使用 COM,这是我对这个问题的解决方案:

C# 库

首先,您需要一个兼容 COM 的库。

  • 你已经有了?完美,你可以跳过这部分。

  • 您可以访问图书馆吗?按照以下步骤确保它与 COM 兼容。

    1. 确保您在项目的属性中选中了“注册 COM 互操作”选项。属性 -> 构建 -> 向下滚动 -> 注册 COM 互操作

以下屏幕截图显示了您可以找到此选项的位置。

截图项目属性构建

  1. 所有应该可用的接口和类都需要有一个GUID

    namespace NamespaceOfYourProject
    {
        [Guid("add a GUID here")]
        public interface IInterface
        {
            void Connect();
    
            void Disconnect();
        }
    }
    
    namespace NamespaceOfYourProject
    {
         [Guid("add a GUID here")]
         public class ClassYouWantToUse: IInterface
         {
             private bool connected;
    
             public void Connect()
             {
                 //add code here
             }
    
             public void Disconnect()
             {
                 //add code here
             }
         }
    }
    

所以这几乎就是你必须用你的 C# 代码做的事情。让我们继续 C++ 代码。

C++

  1. 首先,我们需要导入 C# 库。

编译 C# 库后,应该有一个 .tlb 文件。

#import "path\to\the\file.tlb"

如果您将这个新创建的文件导入到您的 file.cpp,您可以将您的对象用作局部变量。

#import "path\to\the\file.tlb"

int _tmain(int argc, _TCHAR* argv[])
{
    CoInitialize(NULL);

    NamespaceOfYourProject::IInterfacePtr yourClass(__uuidof(NamespaceOfYourProject::ClassYouWantToUse));

    yourClass->Connect();

    CoUninitialize();
}
  1. 使用您的类作为属性。

您会注意到第一步仅适用于局部变量。以下代码显示了如何将其用作属性。与这个问题有关。

您将需要位于 atlcomcli.h 中的 CComPtr。将此文件包含在您的头文件中。

CPlusPlusClass.h

#include <atlcomcli.h> 
#import "path\to\the\file.tlb"

class CPlusPlusClass
{
public:
    CPlusPlusClass(void);
    ~CPlusPlusClass(void);
    void Connect(void);

private:
    CComPtr<NamespaceOfYourProject::IInterface> yourClass;
}

CPlusPlusClass.cpp

CPlusPlusClass::CPlusPlusClass(void)
{
    CoInitialize(NULL);

    yourClass.CoCreateInstance(__uuidof(NamespaceOfYourProject::ClassYouWantToUse));

}

CPlusPlusClass::~CPlusPlusClass(void)
{
    CoUninitialize();
}

void CPlusPlusClass::Connect(void)
{
    yourClass->Connect();
}

就是这样!在 C++ 和 COM 中享受 C# 类的乐趣。

于 2015-05-01T14:26:16.587 回答
16

我发现做到这一点的绝对最佳方法是创建一个将 c# 代码连接到本机 C++ 的 c++/cli 桥。您可以使用 3 个不同的项目来执行此操作。

  • 第一个项目:C# 库
  • 第二个项目:C++/CLI 桥(这包含了 C# 库)
  • 第三个项目:使用第二个项目的本机 C++ 应用程序

我最近创建了一个简单的 GitHub 教程,说明如何在此处执行此操作。用一点勇气阅读该代码,您应该能够敲定创建一个 C++/CLI 桥接器,该桥接器允许您在本机 C++ 中使用 C# 代码。

作为奖励,我添加了如何将 C# 事件包装到可以订阅的 C++ 中的函数指针。

于 2014-08-20T16:44:59.117 回答
16

不幸的是,来自 0lli.rocks的答案要么已过时,要么不完整。我的同事帮助我完成了这项工作,坦率地说,其中一两个实施细节并不明显。这个答案纠正了差距,应该可以直接复制到 Visual Studio 2017 中供您自己使用。

警告:我无法让它为 C++/WinRT 工作,仅供参考。由于接口不明确导致的各种编译错误IUnknown。我也遇到了问题,使其仅适用于库实现,而不是在应用程序的主体中使用它。我专门尝试按照 0lli.rocks 的说明进行操作,但始终无法编译。

步骤 01:创建 C# 库


这是我们将用于演示的那个:

using System;
using System.Runtime.InteropServices;

namespace MyCSharpClass
{
    [ComVisible(true)] // Don't forget 
    [ClassInterface(ClassInterfaceType.AutoDual)] // these two lines
    [Guid("485B98AF-53D4-4148-B2BD-CC3920BF0ADF")] // or this GUID
    public class TheClass
    {
        public String GetTheThing(String arg) // Make sure this is public
        {
            return arg + "the thing";
        }
    }
}

步骤 02 - 为 COM 可见性配置 C# 库


子步骤 A - 注册 COM 互操作性

在此处输入图像描述

子步骤 B - 使程序集 COM 可见

在此处输入图像描述

.tlb第 3 步 - 为文件构建库


你可能只想这样做ReleaseAnyCPU除非你真的需要更具体的东西。

第 4 步 - 将.tlb文件复制到 C++ 项目的源位置


在此处输入图像描述

第 5 步 - 将.tlb文件导入您的 C++ 项目


#include "pch.h"
#include <iostream>
#include <Windows.h>
#import "MyCSharpClass.tlb" raw_interfaces_only

int wmain() {
    return 0;
}

第 6 步 - 当 Intellisense 失败时不要惊慌


在此处输入图像描述

它仍然会建立。一旦我们将实际类实现到 C++ 项目中,您将看到更多红线代码。

第 7 步 - 构建您的 C++ 项目以生成.tlh文件


一旦您第一次构建,此文件将进入您的中间对象构建目录

在此处输入图像描述

第 8 步 - 评估.tlh文件以获取实施说明


这是.tlh在中间对象文件夹中生成的文件。不要编辑它。

// Created by Microsoft (R) C/C++ Compiler Version 14.15.26730.0 (333f2c26).
//
// c:\users\user name\source\repos\consoleapplication6\consoleapplication6\debug\mycsharpclass.tlh
//
// C++ source equivalent of Win32 type library MyCSharpClass.tlb
// compiler-generated file created 10/26/18 at 14:04:14 - DO NOT EDIT!

//
// Cross-referenced type libraries:
//
//  #import "C:\Windows\Microsoft.NET\Framework\v4.0.30319\mscorlib.tlb"
//

#pragma once
#pragma pack(push, 8)

#include <comdef.h>

namespace MyCSharpClass {

//
// Forward references and typedefs
//

struct __declspec(uuid("48b51671-5200-4e47-8914-eb1bd0200267"))
/* LIBID */ __MyCSharpClass;
struct /* coclass */ TheClass;
struct __declspec(uuid("1ed1036e-c4ae-31c1-8846-5ac75029cb93"))
/* dual interface */ _TheClass;

//
// Smart pointer typedef declarations
//

_COM_SMARTPTR_TYPEDEF(_TheClass, __uuidof(_TheClass));

//
// Type library items
//

struct __declspec(uuid("485b98af-53d4-4148-b2bd-cc3920bf0adf"))
TheClass;
    // [ default ] interface _TheClass
    // interface _Object

struct __declspec(uuid("1ed1036e-c4ae-31c1-8846-5ac75029cb93"))
_TheClass : IDispatch
{
    //
    // Raw methods provided by interface
    //

      virtual HRESULT __stdcall get_ToString (
        /*[out,retval]*/ BSTR * pRetVal ) = 0;
      virtual HRESULT __stdcall Equals (
        /*[in]*/ VARIANT obj,
        /*[out,retval]*/ VARIANT_BOOL * pRetVal ) = 0;
      virtual HRESULT __stdcall GetHashCode (
        /*[out,retval]*/ long * pRetVal ) = 0;
      virtual HRESULT __stdcall GetType (
        /*[out,retval]*/ struct _Type * * pRetVal ) = 0;
      virtual HRESULT __stdcall GetTheThing (
        /*[in]*/ BSTR arg,
        /*[out,retval]*/ BSTR * pRetVal ) = 0;
};

} // namespace MyCSharpClass

#pragma pack(pop)

在该文件中,我们看到了我们想要使用的公共方法的这些行:

virtual HRESULT __stdcall GetTheThing (
        /*[in]*/ BSTR arg,
        /*[out,retval]*/ BSTR * pRetVal ) = 0;

这意味着导入的方法将需要一个类型为 的输入字符串BSTR,以及一个指向BSTR输出字符串的指针,导入的方法将在成功时返回该指针。您可以像这样设置它们,例如:

BSTR thing_to_send = ::SysAllocString(L"My thing, or ... ");
BSTR returned_thing;

在我们可以使用导入的方法之前,我们必须构造它。从.tlh文件中,我们看到以下几行:

namespace MyCSharpClass {

//
// Forward references and typedefs
//

struct __declspec(uuid("48b51671-5200-4e47-8914-eb1bd0200267"))
/* LIBID */ __MyCSharpClass;
struct /* coclass */ TheClass;
struct __declspec(uuid("1ed1036e-c4ae-31c1-8846-5ac75029cb93"))
/* dual interface */ _TheClass;

//
// Smart pointer typedef declarations
//

_COM_SMARTPTR_TYPEDEF(_TheClass, __uuidof(_TheClass));

//
// Type library items
//

struct __declspec(uuid("485b98af-53d4-4148-b2bd-cc3920bf0adf"))
TheClass;
    // [ default ] interface _TheClass
    // interface _Object

首先,我们需要使用类的命名空间,即MyCSharpClass

接下来,我们需要从命名空间中确定智能指针,即_TheClass+ Ptr;这一步并不明显,因为它不在.tlh文件中。

最后,我们需要为类提供正确的构造参数,即__uuidof(MyCSharpClass::TheClass)

结束,

MyCSharpClass::_TheClassPtr obj(__uuidof(MyCSharpClass::TheClass));

第 9 步 - 初始化 COM 并测试导入的库


您可以使用CoInitialize(0)特定的 COM 初始化程序或任何您的特定 COM 初始化程序来执行此操作。

#include "pch.h"
#include <iostream>
#include <Windows.h>
#import "MyCSharpClass.tlb" raw_interfaces_only

int wmain() {
    CoInitialize(0); // Init COM
    BSTR thing_to_send = ::SysAllocString(L"My thing, or ... ");
    BSTR returned_thing;
    MyCSharpClass::_TheClassPtr obj(__uuidof(MyCSharpClass::TheClass)); 
    HRESULT hResult = obj->GetTheThing(thing_to_send, &returned_thing);

    if (hResult == S_OK) {
        std::wcout << returned_thing << std::endl;
        return 0;
    }
    return 1;
}

再一次,当 Intellisense 出现故障时不要惊慌。你在黑魔法、巫毒和 Thar Be Dragons 的领域,所以继续前进!

在此处输入图像描述

于 2018-10-26T22:18:31.967 回答
4

我找到了至少开始回答我自己的问题的东西。以下两个链接包含来自 Microsoft 的 wmv 文件,这些文件演示了在非托管 C++ 中使用 C# 类。

第一个使用 COM 对象和 regasm:http: //msdn.microsoft.com/en-us/vstudio/bb892741

第二个使用 C++/CLI 的特性来包装 C# 类:http: //msdn.microsoft.com/en-us/vstudio/bb892742。我已经能够从托管代码中实例化 ac# 类并检索视频中的字符串。它非常有帮助,但它只回答了我问题的 2/3,因为我想将一个带有字符串周界的类实例化为 ac# 类。作为概念证明,我将示例中提供的代码更改为以下方法,并实现了这一目标。当然,我还添加了一个修改后的 {public string PickDate(string Name)} 方法来对名称字符串做一些事情,以向自己证明它有效。

wchar_t * DatePickerClient::pick(std::wstring nme)
{
    IntPtr temp(ref);// system int pointer from a native int
    String ^date;// tracking handle to a string (managed)
    String ^name;// tracking handle to a string (managed)
    name = gcnew String(nme.c_str());
    wchar_t *ret;// pointer to a c++ string
    GCHandle gch;// garbage collector handle
    DatePicker::DatePicker ^obj;// reference the c# object with tracking handle(^)
    gch = static_cast<GCHandle>(temp);// converted from the int pointer 
    obj = static_cast<DatePicker::DatePicker ^>(gch.Target);
    date = obj->PickDate(name);
    ret = new wchar_t[date->Length +1];
    interior_ptr<const wchar_t> p1 = PtrToStringChars(date);// clr pointer that acts like pointer
    pin_ptr<const wchar_t> p2 = p1;// pin the pointer to a location as clr pointers move around in memory but c++ does not know about that.
    wcscpy_s(ret, date->Length +1, p2);
    return ret;
}

我的部分问题是:什么更好?根据我在许多研究中阅读的内容,答案是 COM 对象被认为更易于使用,而使用包装器可以实现更大的控制。在某些情况下,使用包装器可以(但并非总是)减小 thunk 的大小,因为 COM 对象自动具有标准大小的占用空间,并且包装器只有它们需要的大小。

thunk(正如我在上面使用的)指的是在 COM 对象的情况下在 C# 和 C++ 之间使用的空间时间和资源,以及在使用 C++/CLI 进行编码的情况下在 C++/CLI 和本机 C++ 之间使用的空间时间和资源包装。所以我的答案的另一部分应该包括一个警告,即超过绝对必要的跨越 thunk 边界是不好的做法,不建议在循环内访问 thunk 边界,并且可能错误地设置包装器,以便它加倍 thunk (越过边界两次,只需要一个 thunk),而对于像我这样的新手来说,代码似乎不正确。

关于 wmv 的两个注意事项。第一:有些镜头在两者中都重复使用,不要上当。起初它们看起来相同,但它们确实涵盖了不同的主题。其次,还有一些额外的功能,例如编组,现在是 CLI 的一部分,而 wmv 中没有涵盖。

编辑:

请注意,您的安装有一个后果,CLR 将找不到您的 c++ 包装器。您必须确认 c++ 应用程序安装在任何/每个使用它的目录中,或者在安装时将库(然后需要强命名)添加到 GAC。这也意味着在开发环境中的任何一种情况下,您都可能必须将库复制到应用程序调用它的每个目录。

于 2012-12-11T15:59:12.627 回答
0

我做了一堆环顾四周,发现了微软的一篇相对较新的文章,详细介绍了它是如何完成的(有很多旧的信息漂浮在周围)。从文章本身:

代码示例使用 CLR 4 托管 API 在本机 C++ 项目中托管 CLR,加载和调用 .NET 程序集

https://code.msdn.microsoft.com/CppHostCLR-e6581ee0

基本上它分两步描述它:

  1. 将 CLR 加载到进程中
  2. 加载您的程序集。
于 2016-10-16T23:02:43.343 回答