在Native C++中调用C#代码
在关于C++与C#互操作的大多中文文章中,介绍都是在C#中如何使用C++的功能,本文将为大家介绍在C++中如何调用C#的功能。
首先,简单介绍一下C#如何使用C++的功能,以作者所了解到的,主要的方式有两种,一种是平台调用(P/Invoke),与更加强大的C++/CLI。P/Invoke更加简单,无需链接C++库,即可使用C++ DLL中定义的函数,它是目前最主流的方式,但是P/Invoke存在一个比较大的限制,即无法使用C++ DLL中的类类型,在这种情况下,只能借助C++/CLI作为中间层,将原C++ DLL的功能通过链接,编译的方式封装成.net程序集以提供给C#使用,这种方式稍显麻烦,但是也更加强大。C++/CLI同时具备托管和非托管的操作能力,那么能不能通过C++/CLI这个工具将C#编写的功能提供给C++使用呢?答案是可以的。
本文假设读者已经对于C#和C++同时具有一定的了解能力,当然,笔者的C++水平也非常的基础(这玩意太麻烦了),文章中有什么需要改进的地方,请读者大大尽情在评论区指出,笔者将看心情改进。
先简单介绍一下流程,笔者将会建立三个项目,它们是:
1.一个C#项目(CSharpLibrary),此项目的代码将会被C++项目所消费,目标框架为当前最新的稳定版本.net 6。
2.一个C++/CLI项目(CSharpToCPPBridge),此项目将C#项目封装成为非托管的封装以供C++项目调用。
3.一个C++控制台项目(CPPNativeLibrary),此项目是将最终使用C#代码的消费者。
本文所使用的IDE为Visual Studio 2022 Professional.
以下是详细过程:
一.建立C#项目(CSharpLibrary)后,将会建立一个ShowCase类,该类的功能非常简单,即一个求和方法,与一个根据姓和名求取全名(即字符串合并)的方法,此类的定义如下:
namespace CSharpLibrary
{
/// <summary>
/// The class to show some of features of c sharp;
/// </summary>
public class ShowCase
{
/// <summary>
/// Get sum of two numbers;
/// </summary>
/// <param name="a"></param>
/// <param name="b"></param>
/// <returns></returns>
public int Sum(int a,int b) => a + b;
/// <summary>
/// Get full name;
/// </summary>
/// <param name="firstName"></param>
/// <param name="lastName"></param>
/// <returns></returns>
public string GetFullName(string firstName, string lastName) => $"{firstName} {lastName}";
}
}
二.C#项目建立完成后,建立一个C++ DLL项目(CSharpToCPPBridge)作为到Native C++的中间项目,建立完成后,修改该项目的项目属性,使得项目支持.net 6.
小提示:此处如果选择不了目标框架的时候,需要上面先选择clr:netcore之后,点击一下应用按钮,然后目标框架里面就出来列表了,感谢评论区的 猫叔Vincent 的补充。
在项目CSharpToCPPBridge引用中添加对CSharpLibrary的引用:
这样,便可以在项目CSharpToCPPBridge中使用C#项目的代码了,接下来,将在CSharpToCPPBridge中封装C#项目的ShowCase类。
1.在CSharpToCPPBridge中添加头文件ShowCase.h,修改如下:
#include "pch.h"
#ifdef CSHARPTOCPPBRIDGE_EXPORTS
#define DLLEXPORT __declspec(dllexport)
#else
#define DLLEXPORT __declspec(dllimport)
#endif
#ifndef EXAMPLE_OBJECT_H
#define EXAMPLE_OBJECT_H
class DLLEXPORT ShowCase
{
public:
/// <summary>
/// Get sum of two numbers;
/// </summary>
/// <param name="a"></param>
/// <param name="b"></param>
/// <returns></returns>
int Sum(int a,int b);
/// <summary>
/// Get full name;
/// </summary>
/// <param name="firstName"></param>
/// <param name="lastName"></param>
/// <returns></returns>
LPCWSTR GetFullName(LPCWSTR firstName, LPCWSTR lastName);
};
#endif
根据笔者经验,这个头文件中需要注意两个地方:
(1).此处的宏定义DLLEXPORT,依赖了预定义常量CSHARPTOCPPBRIDGE_EXPORTS,在我使用的VS2022中,创建CSharpToCPPBridge项目时,IDE自动为项目加入了预定义常量,若没有请手动检查项目的预定义常量中是否加入了此常量,请确保所有的配置都含有此常量:
(2).定义ShowCase的class DLLEXPORT ShowCase 的书写书写顺序必须是DLLEXPORT在class之后,否则CSharpToCPPBridge可能无法生成.lib文件(.lib文件对于静态链接编译的C++的库引用非常重要),这和我之前的直觉认知相反,导致我在这里停留了很久才找到问题,不知道在其它编译器里会不会也是这样还是只是MSVC会这样,哎,C++就是这么的古怪。
2.添加ShowCase.cpp文件到CSharpToCPPBridge中,此文件实现了ShowCase.h中定义的ShowCase类,它的内容如下:
#include "pch.h"
#include <vcclr.h>
#include "ShowCase.h"
using namespace System;
gcroot<CSharpLibrary::ShowCase^> _showCase = gcnew CSharpLibrary::ShowCase();
int ShowCase::Sum(int a, int b)
{
return _showCase->Sum(a, b);
}
LPCWSTR ShowCase::GetFullName(LPCWSTR firstName, LPCWSTR lastName)
{
auto firstNameStr = gcnew System::String(firstName);
auto lastNameStr = gcnew System::String(lastName);
auto str = _showCase->GetFullName(firstNameStr, lastNameStr);
pin_ptr<const WCHAR> nativeStr = PtrToStringChars(str);
return (LPCWSTR)nativeStr;
}
构建CSharpToCPPBridge项目,将会得到如下的几个文件:
除了生成的.pdb,.exp文件可以删除外,其它的文件均是运行时所需要的文件,都不能删除。
在以上的构建过程前,我将C++项目的生成路径统一为了$(SolutionDir)$(Platform)\$(Configuration)\,下一个项目将
至此,为消费者项目CPPNativeLibrary所封装的工作已经完成,在创建CPPNativeLibrary项目之前,为了方便在下一个项目中引用CSharpToCPPBridge的链接文件,需要将生成的.lib,dll复制到一个独立的目录中,在下一个项目中引用该目录的.lib文件,此处笔者选择了在解决方案目录下的lib文件夹作为复制的目标目录,并在CSharpToCPPBridge的项目生成后事件中使用一个powershell脚本来完成dll和lib的拷贝:
PostBuild.ps1内容如下:
[CmdletBinding()]
param(
[Parameter(Mandatory=$True)]
[string]$TargetDir,
[Parameter()]
[string]$LibDir = "..\lib"
)
$ErrorActionPreference = "Stop"
if (!(Test-Path $LibDir)) {
mkdir $LibDir | Out-Null
}
cp "$TargetDir\CSharpToCPPBridge.dll" $LibDir
cp "$TargetDir\CSharpToCPPBridge.lib" $LibDir
当然,使用powershell脚本只是笔者本身的偏好,读者也可以自行选择自己熟悉的方式去拷贝到对应的目录。
三,创建C++控制台项目CPPNativeLibrary作为最终的消费者项目.
创建完成项目后,依次执行下面的操作以引用之前的工作成果:
1.在属性的C/C++ -> General -> Additional Include Directories中添加附加包含目录:"../CSharpToCPPBridge"
如此一来便可引用之前创建的头文件ShowCase.h.
2.在VC++Directories中添加"..\lib"到Reference Directories和Library Directories中。
3.在Linker->Input->Addtional Dependencies中指定.lib文件的名称CSharpToCPPBridge.lib.以使得链接时链接器能找到该.lib。
4.为CPPNativeLibrary的项目引用添加CSharpToCPPBridge,这一步并不是必须的,只是为了在生成CPPNativeLibrary时,自动生成CSharpToCPPBridge,不执行这一步也是没有关系的,这需要先手动生成CSharpToCPPBridge.
5.编写CPPNativeLibrary的主程序文件如下:
#include <iostream>
#include "ShowCase.h"
using namespace std;
int main()
{
auto showCase = new ShowCase();
auto sum = showCase->Sum(1, 2);
wcout << "1 + 2 = " << sum << endl;
auto firstName = L"Billy";
auto lastName = L"Herrington";
auto fullName = showCase->GetFullName(firstName, lastName);
wcout << "Full name is " << fullName << endl;
delete showCase;
}
生成,执行后可以看到输出如下:
至此,一个简单的C++调用C#的例子便完成了,此例子所使用的C#语法特性和类型非常少,仅仅使用了int和string,对于int C++和C#定义一致,对于string,我是用了LPCWSTR/LPWSTR,即WCHAR *来和string对应,对于更多复杂的CLR类型(比如泛型),可能类型映射的难度就更高了,但是基本思路是差不多的。
本项目的源码开源在https://github.com/JanusTida/CSharpToCPP
希望对读者有所启发。