繁体   English   中英

如何从本机 C++ 调用 C# 库(使用 C++\\CLI 和 IJW)

[英]How to call a C# library from Native C++ (using C++\CLI and IJW)

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

似乎有许多不同的方法可以使用来自非托管 C++ 和 C 的 C# dll 来实现。一些简短的答案似乎是:使用 Interlope 服务、使用 .com。 和 regasm,使用 PInvoke(似乎仅从 C# 转到 C++),并在 C++/CLR(似乎是 Interlope 服务)中使用 IJW。 我认为最好设置一个库,它可能是一个 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 等,但显然我可能会有其他误解。 也许这整个事情的答案可能只是一个指向帮助我解决这个问题的教程或说明的链接。

研究:我一次又一次地在谷歌上搜索和 Bing 并取得了一些成功。 我找到了许多向您展示如何使用 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 -Managed-Code-from-Unmanaged-Code-and-Vice

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

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

本地文件

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。)

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# 类玩得开心。

我发现最好的方法是创建一个 c++/cli 桥,将 c# 代码连接到你的本地 C++。 你可以用 3 个不同的项目来做到这一点。

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

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

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

不幸的是,来自 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 可见

在此处输入图片说明

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


除非您确实需要更具体的东西,否则您可能只想将其作为AnyCPU 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文件中,我们看到以下.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 初始值设定项来做到这一点。

#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 出现问题时不要惊慌。 你在 Black Magic、Voodoo 和 Thar Be Dragons 领域,所以继续前进!

在此处输入图片说明

我发现一些东西至少开始回答我自己的问题。 以下两个链接包含来自 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。 这也意味着在开发环境中的任何一种情况下,您可能都必须将库复制到应用程序调用它的每个目录。

我环顾四周,发现微软最近发表的一篇文章详细介绍了如何做到这一点(有很多旧的信息漂浮在周围)。 从文章本身:

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

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

基本上它分两步描述:

  1. 将 CLR 加载到进程中
  2. 加载您的程序集。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM