首页 > 编程语言 >在Native C++中调用C#代码

在Native C++中调用C#代码

时间:2022-10-20 22:33:21浏览次数:57  
标签:lib 项目 C# ShowCase C++ CSharpToCPPBridge Native

  在关于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.

 

 

 

   在项目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

  希望对读者有所启发。

 

 

 

  

  

标签:lib,项目,C#,ShowCase,C++,CSharpToCPPBridge,Native
From: https://www.cnblogs.com/ponus/p/16811130.html

相关文章

  • C++ 中 const 关键字的作用总结
    const的含义相信大部分程序员都对const不陌生,英文翻译中作为形容词意思为恒定的,不变的,作为名词翻译为常量,恒量,其实,这在很大程度上已经说明了这个关键字的含义。接下来,让......
  • Spring Batch 中的 chunk
    我们都知道SpringBatch有2种任务方式。主要是在Step阶段,在Step阶段,我们可以执行一个Tasklet,我们也可以按照Chunk来执行。主要区别如果使用Tasklet的话,我们可......
  • Spring Batch 中的 chunk
    我们都知道SpringBatch有2种任务方式。主要是在Step阶段,在Step阶段,我们可以执行一个Tasklet,我们也可以按照Chunk来执行。主要区别如果使用Tasklet的话,我......
  • LCA
    #include<bits/stdc++.h>usingnamespacestd;#defineintlonglong#defineullunsignedlonglong#defineendl"\n"#definesfscanf#definepfprintf#define......
  • 嵌入式-C语言基础:理解形参和实参的区别
    #include<stdio.h>//实参:函数原型中声明函数后面带的参数inttest(intx)//函数原型{//函数体printf("test里面的x地址=%p",&x);returnx;}//变量......
  • CF916E 解题报告
    被这道题搞了一个晚上,还好搞出来了qwq令人耳目一新的阅读体验题目简述翻译已经很简单了。前置知识DFS序,LCA,线段树,不需要标签中的树剖!DFS序更新信息及判断祖先如果你......
  • Spring中Transactional注解使用的心得
    今天看黑马redis的课,里面讲到了一个事务注解不生效的问题。究其原因,就在于Spring中事务注解生效的条件。那么接下来就说一下自己的心得。查了一下资料,就是说如果想让@Tr......
  • defaultdict创建字典
    因为字典的hash性所以key一定是唯一的-在创建字典的时候可以想象它的可以做到的事情是,unique后对数据分类或统计-但是字典中有多少唯一的值并不确定-此时引入default......
  • Activate & Set WLAN for MB SD C4 XENTRY Software
    MBStarDiagnosticXentrysoftwarehasbeenupdatedtoV09.2022.Ithasbeentestedandverified100%workingfineonWin7andWin10,andcanbeusedforMBSD......
  • Scala题目
    Scala题目数据在bigdata19-scala/data中      题目:基础1、统计班级人数[班级,人数]2、统计学生的总分[学号,学生姓名,学生年龄,总分]进阶1、统计年级......