簡體   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