主要有2種方法,非托管和托管,2種都需要具備一定C++及DLL的基礎:
1.通過一個間接層DLL來封裝接口對象的方法調用
先來創建一個dll項目,用來生成一個給C#調用的dll:
項目結構如下:(部分文件是自行添加的如模塊定義文件def)
各個文件的內容如下:
// CppLibDll.h是接口定義頭文件 #pragma once // 下列 ifdef 塊是創建使從 DLL 導出更簡單的 // 宏的標准方法。此 DLL 中的所有文件都是用命令行上定義的 CPPLIBDLL_EXPORTS // 符號編譯的。在使用此 DLL 的 // 任何其他項目上不應定義此符號。這樣,源文件中包含此文件的任何其他項目都會將 // CPPLIBDLL_API 函數視為是從 DLL 導入的,而此 DLL 則將用此宏定義的 // 符號視為是被導出的。 #ifdef CPPLIBDLL_EXPORTS #define CPPLIBDLL_API __declspec(dllexport) #else #define CPPLIBDLL_API __declspec(dllimport) #endif #include <string> // 注釋掉VS自動生成的示例代碼 #if 0 // 此類是從 CppLibDll.dll 導出的 class CPPLIBDLL_API CCppLibDll { public: CCppLibDll(void); // TODO: 在此添加您的方法。 }; extern CPPLIBDLL_API int nCppLibDll; CPPLIBDLL_API int fnCppLibDll(void); #endif // 導出的接口類 class IExport { public: // 返回值:成功:0,失敗:非0,失敗信息保存在D:/Log.txt virtual int OnInit(std::string strSaveFilePath) = 0; // 返回值:成功:0,失敗:非0,失敗信息保存在D:/Log.txt virtual int OnTest() = 0; virtual ~IExport() {} }; // 假設這是原來的DLL暴露的接口函數 // 這種返回接口類對象的指針的導出函數,對於C++來說沒有什么問題,但是對於C#沒辦法直接用對象指針調用接口方法 extern "C" CPPLIBDLL_API IExport* __stdcall ExportObjectFactory(); extern "C" CPPLIBDLL_API void __stdcall DestroyExportObject(IExport* obj); // 通過建立一個接口層,幫C#完成間接調用接口方法 // 這2個方法可以單獨做成一個間接層dll(此處只是為了方便,一般情況也只能自己另外寫一個dll,因為你不能修改別人的dll源碼)
// 下面strSaveFilePath變量類型不要用string,C#中的string類型和C++的string不匹配
extern "C" CPPLIBDLL_API int __stdcall CallOnInit(IExport* obj, const char* strSaveFilePath); extern "C" CPPLIBDLL_API int __stdcall CallOnTest(IExport* obj);
// CppLibDll.cpp是接口實現頭文件 // CppLibDll.cpp : 定義 DLL 應用程序的導出函數。 // #include "stdafx.h" #include "CppLibDll.h" #include "ExportImpl.h" // 實現了接口類的具體子類 #if 0 // 這是導出變量的一個示例 CPPLIBDLL_API int nCppLibDll=0; // 這是導出函數的一個示例。 CPPLIBDLL_API int fnCppLibDll(void) { return 42; } // 這是已導出類的構造函數。 // 有關類定義的信息,請參閱 CppLibDll.h CCppLibDll::CCppLibDll() { return; } #endif extern "C" CPPLIBDLL_API IExport* __stdcall ExportObjectFactory() { return new ExportImpl(); } extern "C" CPPLIBDLL_API void __stdcall DestroyExportObject(IExport* obj) { if (obj) { delete obj; obj = nullptr; } } extern "C" CPPLIBDLL_API int __stdcall CallOnInit(IExport* obj, const char* strSaveFilePath) { if (obj) { return obj->OnInit(strSaveFilePath); } else { return -1; } } extern "C" CPPLIBDLL_API int __stdcall CallOnTest(IExport* obj) { if (obj) { return obj->OnTest(); } else { return -1; } }
Source.def是模塊定義文件,用於導出dll接口函數名,並保證其不被重命名:
LIBRARY "CppLibDll" EXPORTS ExportObjectFactory @ 1 DestroyExportObject @ 2 CallOnInit @ 3 CallOnTest @ 4
以下2個文件是實現了接口的一個具體派生類:
// ExportImpl.h #pragma once #include "CppLibDll.h" // 實現接口 class ExportImpl : public IExport { public: ExportImpl(); ~ExportImpl(); virtual int OnInit(std::string strSaveFilePath) override; virtual int OnTest() override; enum InfoType { InitError, InitInfo, TestError, TestInfo }; private: std::string m_strFilePath; void Log(InfoType info, std::string infoMessage); };
// ExportImpl.cpp #include "stdafx.h" #include "ExportImpl.h" #include <fstream> #include <ctime> const std::string logpath = "D:/Log.txt"; ExportImpl::ExportImpl() { m_strFilePath = ""; } ExportImpl::~ExportImpl() { // 如有資源需要釋放 } int ExportImpl::OnInit(std::string strSaveFilePath) { if (strSaveFilePath == "") { Log(InfoType::InitError, "The given save file path is empty!"); return -1; } m_strFilePath = strSaveFilePath; Log(InfoType::InitInfo, "Init Ok!"); return 0; } int ExportImpl::OnTest() { if (m_strFilePath == "") { Log(InfoType::TestError, "The save file path is empty!"); return -1; } std::ofstream outFile(m_strFilePath, std::ios::app); if (!outFile) { Log(InfoType::TestError, "Open save file failed!"); return -2; } Log(InfoType::TestInfo, "Start test!"); Log(InfoType::TestInfo, "Testing..."); Log(InfoType::TestInfo, "Testing Over!"); Log(InfoType::TestInfo, "Result: Pass"); return 0; } void ExportImpl::Log(InfoType info, std::string infoMessage) { // 獲取當前時間 std::time_t rawtime; char buffer[64]; std::time(&rawtime); // 獲取系統時間 std::tm *localTm = localtime(&rawtime); // 生成本地時間 std::strftime(buffer, 64, "%Y-%m-%d %H:%M:%S", localTm); std::string strFilePath = logpath; std::string strInfo = ""; switch (info) { case InfoType::InitError: strInfo = "Init Error"; break; case InfoType::InitInfo: strInfo = "Init Info"; strFilePath = m_strFilePath; break; case InfoType::TestError: strInfo = "Test Error"; break; case InfoType::TestInfo: strInfo = "Test Info"; strFilePath = m_strFilePath; break; default: strInfo = "Undefine"; break; } std::ofstream of(strFilePath, std::ios::app); if (of) { of << "[" << strInfo << "]" << buffer << " :" << infoMessage << std::endl; } of.close(); }
編譯生成后,先用一個C++的控制台項目測試以下這個dll是否有問題:
// LibTestByCpp.cpp : 定義控制台應用程序的入口點。 // #include "stdafx.h" #include "lib/CppLibDll.h" #pragma comment(lib, "lib/CppLibDll.lib") // 隱式調用 int main() {
// C++很簡單,直接通過工廠方法生成接口對象,然后調用接口中定義的虛方法即可 IExport *p = ExportObjectFactory(); p->OnInit("D:/TestInfo.txt"); p->OnTest(); DestroyExportObject(p); system("pause"); return 0; }
下面是C#封裝並調用這個dll的代碼:
using System; using System.Collections.Generic; using System.Linq; using System.Runtime.InteropServices; using System.Text; using System.Threading.Tasks; namespace Disposable { class Program { static void Main(string[] args) { // CUseCppInterfaceObject obj = new CUseCppInterfaceObject(); // obj.OnInit(@"D:\TestInfo.txt"); // obj.OnTest(); // obj.Dispose();
using (CUseCppInterfaceObject obj = new CUseCppInterfaceObject())
{
obj.OnInit(@"D:\TestInfo.txt");
obj.OnTest();
//obj.Dispose();
}
Console.ReadLine(); } } public class CUseCppInterfaceObject : IDisposable { #region PInvokes // DLL內部函數 [DllImport("CppLibDll.dll")] static private extern IntPtr ExportObjectFactory(); [DllImport("CppLibDll.dll")] static private extern void DestroyExportObject(IntPtr pObj); [DllImport("CppLibDll.dll")] static private extern int CallOnInit(IntPtr pObj, string strSaveFilePath); [DllImport("CppLibDll.dll")] static private extern int CallOnTest(IntPtr pObj); #endregion PInvokes #region Members private IntPtr m_pNativeObject; // 保存創建的C++接口對象的指針 #endregion Members public CUseCppInterfaceObject() { // 通過dll導出接口創建C++接口對象實例 this.m_pNativeObject = ExportObjectFactory(); } // Finalizer is called when Garbage collection occurs, but only if // the IDisposable.Dispose method wasn't already called. ~CUseCppInterfaceObject() { Dispose(false); } public void Dispose() { Dispose(true); } protected virtual void Dispose(bool bDisposing) { if (this.m_pNativeObject != IntPtr.Zero) { // 非空指針,調用dll的接口銷毀創建的接口對象 DestroyExportObject(this.m_pNativeObject); this.m_pNativeObject = IntPtr.Zero; } if (bDisposing) { // 已經清理非托管內存,無需再調用終結器 GC.SuppressFinalize(this); } } #region Wrapper public int OnInit(string strSaveFilePath) { return CallOnInit(this.m_pNativeObject, strSaveFilePath); } public int OnTest() { return CallOnTest(m_pNativeObject); } #endregion Wrapper } }
編譯運行這個控制台程序,最終結果如下,成功調用了dll:
參考:
https://www.codeproject.com/Articles/18032/How-to-Marshal-a-C-Class
https://stackoverflow.com/questions/9211128/p-invoke-how-to-call-unmanaged-method-with-marshalling-from-c