概述:
本文介绍了以C++dll与.NET运行时主机为桥梁的方法,提供了一种C++的业务系统调用.NET平台的功能的思路,有时候业务系统可能是很多年前建立的或者有性能要求需要使用C++,但是我们又想使用C#编码的高效性,功能的多样性,那么本文就能给你很好的帮助。
参考文章:
Visual Studio 2022 Community
先决条件:
由于主机是本机应用程序,所以本教程将介绍如何构造 C++ 应用程序以托管 .NET。 将需要一个 C++ 开发环境(例如,Visual Studio 提供的环境)。
还需要生成 .NET 组件以用来测试主机,因此你应该安装 .NET SDK。
1.找到安装在电脑中的运行时主机
若你的电脑已经具备.NET6开发环境,那么你应该能从以下目录中找到我们所需的依赖文件
这意味着我们可以用x86或x64来编写我们的c++dll,你可以按你实际需要来进行选择。
以下文章我将以x86为示例
2.创建一个C++动态链接库项目(c++dll)
在项目的目录中创建一个文件夹Runtime,这个文件夹跟dllmain.cpp同层就行,把运行时主机的相关文件复制过来
把项目修改为x86
把符合语言模式设置为否
选择不需要预编译头
把我们刚刚复制过来的运行时主机的目录包含进项目中
$(SolutionDir)XG_EWindowPass\Runtime
配置链接器,用于编译时关联运行时主机
$(SolutionDir)XG_EWindowPass\Runtime
nethost.lib
项目中我们会使用到Json,如何安装和使用jsoncpp可以参考以下文章:
接下来添加一个头文件dotnetHostFun.h,代码如下:
#pragma once
using string_t = std::basic_string<char_t>;
const string_t root_path(L".\\");
const string_t config_path = root_path + L"XG_EWindowPass.runtimeconfig.json";//.NET 6
const string_t dotnetlib_path = root_path + L"XG_EWindowPassCSharp.dll";
const char_t* dotnet_type = L"XG_EWindowPassCSharp.Lib, XG_EWindowPassCSharp";
load_assembly_and_get_function_pointer_fn load_assembly_and_get_function_pointer = nullptr;
hostfxr_initialize_for_runtime_config_fn init_fptr;
hostfxr_get_runtime_delegate_fn get_delegate_fptr;
hostfxr_close_fn close_fptr;
void* load_library(const char_t* path)
{
HMODULE h = ::LoadLibraryW(path);
assert(h != nullptr);
return (void*)h;
}
void* get_export(void* h, const char* name)
{
void* f = ::GetProcAddress((HMODULE)h, name);
assert(f != nullptr);
return f;
}
bool load_hostfxr()
{
// Pre-allocate a large buffer for the path to hostfxr
char_t buffer[MAX_PATH];
size_t buffer_size = sizeof(buffer) / sizeof(char_t);
int rc = get_hostfxr_path(buffer, &buffer_size, nullptr);
if (rc != 0)
return false;
// Load hostfxr and get desired exports
void* lib = load_library(buffer);
init_fptr = (hostfxr_initialize_for_runtime_config_fn)get_export(lib, "hostfxr_initialize_for_runtime_config");
get_delegate_fptr = (hostfxr_get_runtime_delegate_fn)get_export(lib, "hostfxr_get_runtime_delegate");
close_fptr = (hostfxr_close_fn)get_export(lib, "hostfxr_close");
return (init_fptr && get_delegate_fptr && close_fptr);
}
load_assembly_and_get_function_pointer_fn get_dotnet_load_assembly(const char_t* config_path)
{
// Load .NET Core
void* load_assembly_and_get_function_pointer = nullptr;
hostfxr_handle cxt = nullptr;
int rc = init_fptr(config_path, nullptr, &cxt);
if (rc != 0 || cxt == nullptr)
{
std::cerr << "Init failed: " << std::hex << std::showbase << rc << std::endl;
close_fptr(cxt);
return nullptr;
}
// Get the load assembly function pointer
rc = get_delegate_fptr(
cxt,
hdt_load_assembly_and_get_function_pointer,
&load_assembly_and_get_function_pointer);
if (rc != 0 || load_assembly_and_get_function_pointer == nullptr)
std::cerr << "Get delegate failed: " << std::hex << std::showbase << rc << std::endl;
close_fptr(cxt);
return (load_assembly_and_get_function_pointer_fn)load_assembly_and_get_function_pointer;
}
上述代码第四行中提到的XG_EWindowPass.runtimeconfig.json,把这个文件放到运行目录就行,代码如下:
{
"runtimeOptions": {
"tfm": "net6.0",
"framework": {
"name": "Microsoft.NETCore.App",
"version": "6.0.0"
}
}
}
上述代码第6行中提到的
const char_t* dotnet_type = L"XG_EWindowPassCSharp.Lib, XG_EWindowPassCSharp";
XG_EWindowPassCSharp.Lib的意思是我们将要调用的C#dll中的命名空间 + 类名
接下来我们写dllmain.cpp里的内容,我们添加一些头引用
//一些常规的引用
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include <iostream>
#include <wchar.h>
#include <Windows.h>
//运行时主机
#include <nethost.h>
#include <coreclr_delegates.h>
#include <hostfxr.h>
#include "dotnetHostFun.h"//用于加载.NET运行时,作为主机运行托管代码
//json tool
#include "json\json.h"
#include "dllmian.h"
#include <comdef.h>
using namespace std;
#define STR(s) L ## s
#define CH(c) L ## c
#define DIR_SEPARATOR L'\\'
我们需要在dll的主入口函数中启动运行时主机
//引用的头文件
//....
//主入口函数
BOOL APIENTRY DllMain( HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
//启动.NET主机
if (!load_hostfxr())
{
assert(false && "Failure: load_hostfxr()");
return EXIT_FAILURE;
}
return TRUE;
}
我们需要定义一个结构体用于传递参数给C#dll,此处顺便给出json字符串转对象的方法
//引用的头文件
//....
//主入口函数
//....
//用于传递函数的结构体
struct lib_args
{
const char_t* message;
int number;
};
//读取json字符串,转为可操作的json对象
Json::Value readJsonFromString(const char* mystr)
{
//1.创建工厂对象
Json::CharReaderBuilder ReaderBuilder;
ReaderBuilder["emitUTF8"] = true;//utf8支持,不加这句,utf8的中文字符会变成\uxxx
//2.通过工厂对象创建 json阅读器对象
std::unique_ptr<Json::CharReader> charread(ReaderBuilder.newCharReader());
//3.创建json对象
Json::Value root;
//4.把字符串转变为json对象,数据写入root
std::string strerr;
bool isok = charread->parse(mystr, mystr + strlen(mystr), &root, &strerr);
if (!isok || strerr.size() != 0) {
std::cerr << "json解析出错";
}
//5.返回有数据的json对象,这个json对象已经能用了
return root;
}
接下来我们需要写一些函数以实现我们的业务逻辑,先添加一个头文件dllmian.h声明函数指针,其中包含了三类典型的函数,这意味着外部程序可以按以下方式调用我们的C++dll
XG_LoginByHtml:只有const char*类型的入参
XG_EvaluateByHtml:有两种入参与 char*类型的抛出参数
XG_LogoutByHtml:没有参数
#pragma once
#define DLL_API __declspec(dllexport)
#ifdef __cplusplus
extern "C"
{
#endif
int __stdcall XG_LoginByHtml(const char* userName, const char* userNo, const char* userStar, const char* userPosition, const char* orgName, const char* headBase64);
int __stdcall XG_EvaluateByHtml(int outTime, const char* businessName, char* evaluateValue);
int __stdcall XG_LogoutByHtml();
#ifdef __cplusplus
};
#endif
由于我们使用的是__stdcall的约定方式,而x86项目默认的是__cdecl约定,这样会导致编译后对外的函数名会发生一些改变,这会造成外部程序找不到函数的问题,若想深入了解可以自行搜索C++函数调用约定
为解决以上问题,我们需要做一些处理,首先为项目添加一个模块定义文件dllmain.def
这个模块定义文件的内容我们修改为如下代码,这样外部程序就能找到我们的函数了
LIBRARY
EXPORTS
XG_LoginByHtml
XG_EvaluateByHtml
XG_LogoutByHtml
在上面的dllmain.h中我们声明了三个函数指针,接下来我们在dllmain.cpp中实现他们,以达到使用运行时主机调用C#dll中的函数的功能。
//引用的头文件
//....
//主入口函数
//....
//用于传递函数的结构体
//....
//读取json字符串,转为可操作的json对象
//....
//以下为函数实现
int __stdcall XG_LoginByHtml(const char* userName, const char* userNo, const char* userStar, const char* userPosition, const char* orgName, const char* headBase64)
{
//获取运行时主机中提供的函数的指针,这个函数将用于获取C#dll中的函数的指针
if (load_assembly_and_get_function_pointer == nullptr) {
load_assembly_and_get_function_pointer = get_dotnet_load_assembly(config_path.c_str());
assert(load_assembly_and_get_function_pointer != nullptr && "Failure: get_dotnet_load_assembly()");
}
//C#dll中的函数名
const char_t* dotnet_type_method = STR("LoginByHtml");
//获取C#dll中的LoginByHtml函数的指针
component_entry_point_fn login = nullptr;
int rc = load_assembly_and_get_function_pointer(
dotnetlib_path.c_str(),
dotnet_type,
dotnet_type_method,
nullptr /*delegate_type_name*/,
nullptr,
(void**)&login);
assert(rc == 0 && login != nullptr && "Failure: load_assembly_and_get_function_pointer()");
//开始拼接传参,把传参以json的方式放入结构体中
Json::Value root;
root["userName"] = userName;
root["userNo"] = userNo;
root["userStar"] = userStar;
root["userPosition"] = userPosition;
root["orgName"] = orgName;
root["headBase64"] = headBase64;
//关键在于对builder的属性设置
Json::StreamWriterBuilder builder;
static Json::Value def = []() {
Json::Value def;
Json::StreamWriterBuilder::setDefaults(&def);
def["emitUTF8"] = true;
return def;
}();
builder.settings_ = def;//Config emitUTF8
std::string jsonString = Json::writeString(builder, root);
lib_args args
{
(_bstr_t)jsonString.c_str(),
1
};
//调用C#dll中的函数
int result = login(&args, sizeof(args));
return result;
}
int __stdcall XG_EvaluateByHtml(int outTime, const char* businessName, char* evaluateValue)
{
if (load_assembly_and_get_function_pointer == nullptr) {
load_assembly_and_get_function_pointer = get_dotnet_load_assembly(config_path.c_str());
assert(load_assembly_and_get_function_pointer != nullptr && "Failure: get_dotnet_load_assembly()");
}
const char_t* dotnet_type_method = STR("EvaluateByHtml");
component_entry_point_fn evaluate = nullptr;
int rc = load_assembly_and_get_function_pointer(
dotnetlib_path.c_str(),
dotnet_type,
dotnet_type_method,
nullptr /*delegate_type_name*/,
nullptr,
(void**)&evaluate);
assert(rc == 0 && evaluate != nullptr && "Failure: load_assembly_and_get_function_pointer()");
Json::Value root;
root["outTime"] = outTime;
root["businessName"] = businessName;
//关键在于对builder的属性设置
Json::StreamWriterBuilder builder;
static Json::Value def = []() {
Json::Value def;
Json::StreamWriterBuilder::setDefaults(&def);
def["emitUTF8"] = true;
return def;
}();
builder.settings_ = def;//Config emitUTF8
std::string jsonString = Json::writeString(builder, root);
lib_args args
{
(_bstr_t)jsonString.c_str(),
1
};
const char* result = (const char*)evaluate(&args, sizeof(args));
//参数抛出
Json::Value jsonValue = readJsonFromString(result);
std::string str = jsonValue.get("evaluateValue", "Null").asString();
strcpy_s(evaluateValue, str.length() + 1, str.c_str());
return 0;
}
int __stdcall XG_LogoutByHtml()
{
if (load_assembly_and_get_function_pointer == nullptr) {
load_assembly_and_get_function_pointer = get_dotnet_load_assembly(config_path.c_str());
assert(load_assembly_and_get_function_pointer != nullptr && "Failure: get_dotnet_load_assembly()");
}
const char_t* dotnet_type_method = STR("LogoutByHtml");
component_entry_point_fn logout = nullptr;
int rc = load_assembly_and_get_function_pointer(
dotnetlib_path.c_str(),
dotnet_type,
dotnet_type_method,
nullptr /*delegate_type_name*/,
nullptr,
(void**)&logout);
assert(rc == 0 && logout != nullptr && "Failure: load_assembly_and_get_function_pointer()");
int result = logout(nullptr, 0);
return result;
}
以XG_LoginByHtml为例,先获取到运行时主机提供给我们的方法,用这个方法去获取到C#dll中指定名称的函数的指针,然后我们调用它即可。
3.创建一个C#类库项目(C#dll)
添加一个类Lib.cs,我们将在这里实现我们的业务逻辑,代码如下
using System.Runtime.InteropServices;
namespace XG_EWindowPassCSharp
{
public static class Lib
{
[StructLayout(LayoutKind.Sequential)]
public struct LibArgs
{
public IntPtr Message;
public int Number;
}
public static int LoginByHtml(IntPtr arg, int argLength)
{
//获取到的参数
LibArgs libArgs = Marshal.PtrToStructure<LibArgs>(arg);
string message = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? Marshal.PtrToStringUni(libArgs.Message) : Marshal.PtrToStringUTF8(libArgs.Message);
LoginByHtmlRequest model = Newtonsoft.Json.JsonConvert.DeserializeObject<LoginByHtmlRequest>(message);
Console.WriteLine("LoginByHtml入参:" + message);
//此处写业务逻辑
//....
return 0;
}
public static int EvaluateByHtml(IntPtr arg, int argLength)
{
//获取到的参数
LibArgs libArgs = Marshal.PtrToStructure<LibArgs>(arg);
string message = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? Marshal.PtrToStringUni(libArgs.Message) : Marshal.PtrToStringUTF8(libArgs.Message);
EvaluateByHtmlRequest model = Newtonsoft.Json.JsonConvert.DeserializeObject<EvaluateByHtmlRequest>(message);
Console.WriteLine("EvaluateByHtml入参:" + message);
//此处写业务逻辑
//....
//在此处编写返回数据
EvaluateByHtmlResponse result = new EvaluateByHtmlResponse()
{
evaluateValue = "value of evaluateValue"
};
//字符串转换为指针返回去
return Marshal.StringToHGlobalAnsi(result.ToJson()).ToInt32();
}
public static int LogoutByHtml(IntPtr arg, int argLength)
{
//此处写业务逻辑
//....
return 0;
}
}
}
附Models.cs的代码
public class LoginByHtmlRequest
{
public string userName { get; set; }
public string userNo { get; set; }
public string userStar { get; set; }
public string userPosition { get; set; }
public string orgName { get; set; }
public string headBase64 { get; set; }
}
public class EvaluateByHtmlRequest
{
public int outTime { get; set; }
public string businessName { get; set; }
}
我在JsonSerialize.cs中给object类型添加了扩展方法object.ToJson(),代码如下
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace XG_EWindowPassCSharp
{
public static class JsonSerialize
{
public static string ToJson(this object inputValue)
{
if (inputValue is null)
return null;
return Newtonsoft.Json.JsonConvert.SerializeObject(inputValue);
}
}
}
以上便是简单的C#类库中的方法示例,接下来我们需要用C++控制台来调用C++dll,看一看实际的运行效果,看看能否成功调用到C#dll中的函数。
4.创建一个C++控制台项目(C++exe)
添加一个头文件FprDeviceFun.h,声明函数指针,与我们上面提到的C++dll中的函数指针对应,代码如下:
#pragma once
#define DLL_EVALUATE TEXT("XG_EWindowPass.dll")
typedef int(__stdcall* XG_LoginByHtml)(const char* userName, const char* userNo, const char* userStar, const char* userPosition, const char* orgName, const char* headBase64);
typedef int(__stdcall* XG_EvaluateByHtml)(int outTime, const char* businessName, char* evaluateValue);
typedef int(__stdcall* XG_LogoutByHtml)();
此处我们同样使用_stdcall约定,与C++dll中声明的函数指针的约定一致,以确保我们能正确调用动态库中的函数。
接下来我们在main函数中调用我们的动态库,代码如下:
#include <iostream>
#include <Windows.h>
#include "FprDeviceFun.h"
int main()
{
//加载动态库
HMODULE hModule = LoadLibrary(DLL_EVALUATE);
if (hModule == NULL)
{
std::cerr << "Failed to load DLL\n";
return -1;
}
//开始调用测试
XG_LoginByHtml login = (XG_LoginByHtml)GetProcAddress(hModule, "XG_LoginByHtml");
if (login == NULL)
{
std::cerr << "Failed to get function address\n";
FreeLibrary(hModule);
return -1;
}
int result1 = login("测试1", "测试2", "测试3", "测试4", "测试5", "测试6");
printf("动态调用,result1 = %d\n", result1);
XG_EvaluateByHtml evaluate = (XG_EvaluateByHtml)GetProcAddress(hModule, "XG_EvaluateByHtml");
char msg1[256] = { 0 };
int result2 = evaluate(30, "测试业务", msg1);
printf("%s\n", msg1);
XG_LogoutByHtml logout;
logout = (XG_LogoutByHtml)GetProcAddress(hModule, "XG_LogoutByHtml");
int result3 = logout();
printf("动态调用,result3 = %d\n", result3);
//释放动态库资源
FreeLibrary(hModule);
return 0;
}
以上便是全部的代码说明了,接下来我们需要编译这三个项目,由于编译的时候我们需要把运行时主机的全部文件以及三个项目的输出文件全部复制到同一个目录中,所以我给两个动态库项目添加了编译事件,以下附上我的编译生成事件命令,你需要根据你的实际情况进行修改,确保目录的正确
5.(附)编译命令
C++dll项目的生成命令如下:
copy $(SolutionDir)XG_EWindowPass\Runtime\nethost.dll $(OutDir)
copy $(SolutionDir)XG_EWindowPass\Runtime\apphost.exe $(OutDir)
copy $(ProjectDir)Runtime\comhost.dll $(OutDir)
copy $(ProjectDir)Runtime\ijwhost.dll $(OutDir)
copy $(ProjectDir)Runtime\singlefilehost.exe $(OutDir)
copy $(ProjectDir)Runtime\ijwhost.lib $(OutDir)
copy $(ProjectDir)Runtime\libnethost.lib $(OutDir)
copy $(ProjectDir)Runtime\nethost.lib $(OutDir)
copy $(ProjectDir)XG_EWindowPass.runtimeconfig.json $(OutDir)
C#dll项目的生成命令如下:
copy $(OutDir)XG_EWindowPassCSharp.dll D:\Git源码库\XXXXXX\XG_EWindowPass\Debug
copy $(OutDir)XG_EWindowPassCSharp.pdb D:\Git源码库\XXXXXX\XG_EWindowPass\Debug
copy $(OutDir)XG_EWindowPassCSharp.deps.json D:\Git源码库\XXXXXX\XG_EWindowPass\Debug
copy D:\.nuget\packages\newtonsoft.json\13.0.3\lib\net6.0\Newtonsoft.Json.dll $(OutDir)
copy D:\.nuget\packages\newtonsoft.json\13.0.3\lib\net6.0\Newtonsoft.Json.dll D:\Git源码库\XXXXXX\XG_EWindowPass\Debug
最终我们的输出目录中有以下这些文件
在此目录中按住Shift+鼠标右键打开cmd,用命令运行我们的控制台程序,效果如下
可以看到我们的程序没有中文乱码的情况
6.最后
以上便是通过C++dll与.NET运行时主机为桥梁,实现C++调用C#的方法了,可以根据实际情况把以上x86项目转换为x64项目,记得C++dll项目转换目标平台后,需要重新配置关联目录、链接器、生成事件命令等,希望本文能帮助到你。