本篇介绍如何使用C++开发DLL给WPF的C#脚本调用。本文虽然以C#的WPF窗体应用为例子,但不限于此,.net平台都可以使用,包括Unity的C#脚本。


项目准备

首先VS2019相对于VS2017最明显的变化就是创建新建工程的界面,创建C++ DLL 工程和C# WPF如下图所示:

C++项目的配置就参考之前的文章https://blog.csdn.net/luoyu510183/article/details/83999548,下面就不详细说明了。C#的UI代码我也不说明,主要是讲C#调用C++ DLL的部分,其他部分可以下载我的工程看源码理解。

C++项目的提醒事项:


C++的导出设置

先看看C++项目的文件结构:

NativeInterface

先看下NativeIterface的代码,这个文件是把这个DLL下所有的导出接口都以一个类的形式进行导出。代码如下:

/
NativeInterface.h
/
#pragma onceextern "C" {//本类把本DLL下的所有接口都统一在这里,使用单例的方式创建指针给C#使用//以下的成员函数皆为函数指针的形式,因为,类的函数实际上还有一个隐藏this的指针参数,//不便于C#解析。ITest1Manager这个类也举了怎么导出类的成员函数的例子。//注意下面的函数指针和函数都是_stdcall,这是微软的示例里面使用的默认调用方式,即//从右到左参数入栈,并且被调用方清理堆栈。这个是windows独有的,包括_thiscall,_clrcall,__cdecl等。//关注于Windows平台的需要好好理解,其他默认使用_stdcall就行class NativeManager{public://字符串传递测试const char* (_stdcall *GetModuleName)();//设置回调函数测试void (_stdcall* SetLogHandler)(LogHandler handler);//导出其他类测试class CTestManager* TestManager;//测试成员变量,错误int Count;//正确的获取总数int (_stdcall *GetCount)();//非函数指针,虚函数导出测试class ITest1Manager* Test1Manager;NativeManager();~NativeManager();};//导出不能直接导出变量,所以用这个函数导出类的指针,用单例的方式创建_declspec(dllexport) NativeManager* _stdcall CreateNativeManager();//本函数用于释放导出类_declspec(dllexport) void _stdcall ReleaseNativeManager();//测试设置C#的回调函数_declspec(dllexport) void (_stdcall ExSetLogHandler)(LogHandler handler);
}
//测试不使用 extern "C"的导出函数名
_declspec(dllexport) void (_stdcall ReleaseNativeManager1)(int num);/
NativeInterface.cpp//
/
#include "pch.h"
#include "NativeInterface.h"
#include "CTestManager.h"
static const char* name = "Name is Native Interface";
static NativeManager* Instance = nullptr;
static int NameCount = 0;
static const char* _stdcall SGetModuleName()
{//SafeLog 作为C++项目的日志打印,编译定义在pch.cpp中,整个工程全局可用。//调用的是C#的Log回调函数,打印字符串会通过C#回调函数显示在界面上Instance->Count = NameCount++;SafeLog("%s Count:%d", name,NameCount);return name;
}
static int _stdcall SGetCount()
{return NameCount;
}
//LogHandler 定义在pch.h中:typedef void (_stdcall * LogHandler)(const char*);
//__stdcall是Windows的回调函数的统一调用约定,被调用方清理堆栈
//
void (_stdcall ExSetLogHandler)(LogHandler handler)
{//Log定义在pch.cpp中,作为全局的Log函数指针Log =(handler);
}static void (_stdcall SSetLogHandler)(LogHandler handler)
{char temp[256];sprintf_s(temp, "Log Handler %p", handler);Log = handler;SafeLog(temp);
}NativeManager::NativeManager()
{SafeLog("NativeManager()");GetModuleName = SGetModuleName;SetLogHandler = SSetLogHandler;TestManager = new CTestManager();GetCount = SGetCount;Count= NameCount;Test1Manager = new CTest1Manager();
}NativeManager::~NativeManager()
{SafeLog("~NativeManager()");Instance = nullptr;delete TestManager;delete (CTest1Manager*)Test1Manager;
}NativeManager* _stdcall CreateNativeManager()
{if (Instance==nullptr){Instance = new NativeManager();}return Instance;
}void _stdcall ReleaseNativeManager()
{if (Instance){SafeLog("ReleaseNativeManager");delete Instance;}
}void (_stdcall ReleaseNativeManager1)(int num)
{if (Instance){SafeLog("ReleaseNativeManager%d",num);delete Instance;}
}

NativeInterface这部分要注意以下几点:

  1. 导出只能导出函数,变量和指针只能通过函数去获取。
  2. 类和结构体的成员函数改用函数指针的形式,为什么?成员函数的地址不连续,不确定,只能通过符号去定位。成员函数都有this的隐藏传入参数,C#解析麻烦。
  3. 都默认使用_stdcall的调用方式,避免使用va_list,像printf这样样不确定传入参数数量的函数,实在无法避免可以选择_cdecl的调用方式。但是回调函数一定是_stdcall。这一条是Windows平台独有的,其他可以忽略。
  4. 类的成员变量的值是C#解析类指针时的值,并不对应C++的变量,不会随C++变化而改变。需要调用函数去获取。
  5. SafeLog的部分在pch中,这个文件很简单不介绍,自己看源码。

CTestManager

这个文件主要是测试以函数指针的形式导出类和以虚函数列表的形式导出类。

/
///CTestManager.h
/
#pragma once
#include "pch.h"
extern "C" {//这里和NativeInterface一样,主要是演示NativeManager的成员变量里可以有别的类指针class CTestManager{public:const char* (*GetModuleName)();CTestManager();~CTestManager();};//一个纯虚的interfaceclass ITest1Manager{public://虚函数列表里面函数的地址是连续的virtual const char* GetModuleName() = 0;virtual int Add(int a, int b) = 0;};class CTest1Manager :public ITest1Manager{public://这两个函数接口可以导出给C#使用virtual const char* GetModuleName();virtual int Add(int a, int b);//这两个函数不可以导出,这两个函数实际上等效于// void CTest1Manager::test1(CTest1Manager* this)//如果要导出那么需要导出这个类,并且根据导出的符号去加载这个成员函数void test1();void test2();CTest1Manager();~CTest1Manager();};
}/
///CTestManager.cpp//
/
#include "pch.h"
#include "CTestManager.h"
#include <iostream>static const char* name = "Name is :Test Manager";
static const char* SGetModuleName()
{SafeLog(const_cast<char*>(name));return name;
}CTestManager::CTestManager()
{this->GetModuleName = SGetModuleName;
}CTestManager::~CTestManager()
{
}
static const char* name1 = "Name is :Test1 Manager";const char*  CTest1Manager::GetModuleName()
{//测试两个非虚成员函数的地址SafeLog("Func1 Adds:%p Func2 Adds:%p", (&CTest1Manager::test1), (&CTest1Manager::test2));return name1;
}int  CTest1Manager::Add(int a, int b)
{return a+b+1;
}void CTest1Manager::test1()
{
}void CTest1Manager::test2()
{
}CTest1Manager::CTest1Manager()
{
}CTest1Manager::~CTest1Manager()
{
}

这部分没啥好注意的,重点上面都说了,需要自己好好理解什么是虚函数列表,为什么使用这个导出解析很方便?


C#部分

首先看下工程结构:

NativeInterface.cs C++接口的封装类

这个类要完成所有从C++到C#的操作,即完成所有的Marshal操作,从非托管到托管的转换。其他的.cs脚本调用C++接口都通过这个类。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;namespace WpfApp1
{//用一个类统一封装从C++到C#的所有接口,声明为partial,可以在多个.cs里面完成这个类public partial class NativeInterface{#region C++到C#的结构转换//C++的char* 需要使用Marshal.PtrToStringAnsi(ptr)来转换到 stringprivate delegate IntPtr GetNameHandler();//缓冲类,保证内存的顺序和C++的一样//下面这个StructLayout是所有C++指针到C#结构体必需的[StructLayout(LayoutKind.Sequential)]private class NTestManager{//对应C++的函数指针public GetNameHandler GetModuleName;}//应用类,外部C#代码实际调用的类//将所有C++到C#的类型转换在这个类中完成public class TestManager{//重新封装成员函数,避免外部代码再次使用IntPtr和Marshal操作public string GetModuleName{get{var ptr = NTestManager.GetModuleName();return Marshal.PtrToStringAnsi(ptr);}}//禁止除了以C++指针以外的方式创建实例private TestManager(IntPtr intptr){IntPtr = intptr;NTestManager = Marshal.PtrToStructure<NTestManager>(IntPtr);}private NTestManager NTestManager;private IntPtr IntPtr;//显示转换public static explicit operator TestManager(IntPtr intPtr){return new TestManager(intPtr);}}//以下是This call的方式解析类的成员函数,即带this传参的函数,需要用IntPtr把this参数补上[UnmanagedFunctionPointer(CallingConvention.ThisCall)]private delegate IntPtr ThiscallStringHandler(IntPtr intPtr);[UnmanagedFunctionPointer(CallingConvention.ThisCall)]private delegate int ThiscallAddHandler(IntPtr intPtr, int a, int b);[StructLayout(LayoutKind.Sequential)]private class NTest1Manager{public ThiscallStringHandler GetModuleName;public ThiscallAddHandler Add;}public class Test1Manager{public string GetModuleName(){return Marshal.PtrToStringAnsi(NTest1Manager.GetModuleName(IntPtr));}public int Add(int a, int b){return NTest1Manager.Add(IntPtr, a, b);}//这里是关键,Marshal.ReadIntPtr读取基类的虚函数列表private Test1Manager(IntPtr intPtr){IntPtr = intPtr;IntPtr NTest1Vtbl = Marshal.ReadIntPtr(intPtr, 0);NTest1Manager = Marshal.PtrToStructure<NTest1Manager>(NTest1Vtbl);}private NTest1Manager NTest1Manager;private IntPtr IntPtr;public static explicit operator Test1Manager(IntPtr intPtr){return new Test1Manager(intPtr);}}public delegate void LogHandler(string str);private delegate void SetLogHandler(LogHandler h);private delegate int IntHandler();[StructLayout(LayoutKind.Sequential)]private class NNativeInterface{public GetNameHandler GetModuleName;public SetLogHandler SetLogHandler;public IntPtr NTestManager;public int Count;public IntHandler GetCount;public IntPtr NTest1Manager;}public class NativeManager{public string GetModuleName{get{return Marshal.PtrToStringAnsi(NNativeInterface.GetModuleName());}}//这里需要注意,NNativeInterface.SetLogHandler(value);是不可以的//value是个函数,需要用一个托管函数变量来赋值后传入 logprivate LogHandler log;public LogHandler Log{set{log = value;NNativeInterface.SetLogHandler(log);}}public void ReleaseTest(){ReleaseNativeManager();instance = null;}public void ReleaseTest1(){ReleaseNativeManager1(12);instance = null;}public TestManager TestManager { get; private set; }public int Count{get{return NNativeInterface.Count;}}public int TrueCount{get{return NNativeInterface.GetCount();}}public Test1Manager Test1Manager { get; private set; }private NNativeInterface NNativeInterface;private IntPtr IntPtr;private NativeManager(IntPtr ptr){NNativeInterface = Marshal.PtrToStructure<NNativeInterface>(ptr);TestManager = (TestManager)NNativeInterface.NTestManager;Test1Manager = (Test1Manager)NNativeInterface.NTest1Manager;}public static explicit operator NativeManager(IntPtr ptr){return new NativeManager(ptr);}}private NativeManager NativeMgr;#endregion//这个是最终外部cs代码调用的唯一接口,方便调用,省略单例的部分public static NativeManager WrapInterface{get{return Instance.NativeMgr;}}#region 单例private static NativeInterface instance = null;private static NativeInterface Instance{get{if (instance == null){instance = new NativeInterface();}return instance;}}//构造私有化,禁止使用newprivate NativeInterface(){NativeMgr = (NativeManager)CreateNativeManager();}~NativeInterface(){//如像上面手动释放,NativeInterface这里就不需要再调用释放//因为上面的手动释放Instance=null 会再次调用本函数//ReleaseNativeManager();}#endregion#region Dll函数加载部分//extern "C" 的函数名不需要指定某个,EntryPoint = "CreateNativeManager",只需要选择正确的CallingConvention[DllImport("Dll1.dll", CallingConvention = CallingConvention.StdCall)]static extern IntPtr CreateNativeManager();[DllImport("Dll1.dll", CallingConvention = CallingConvention.StdCall)]static protected extern void ReleaseNativeManager();//没有extern "C"的是C++的导出函数形式,它的函数符号包含传入参数和返回类型,比如 ?ReleaseNativeManager1@@YAXH@Z//这个名称根据你的声明会一直改变,所以用序号来表示相对简单,#1,表示导出的第一个函数//这两种EntryPoint都是可以的:函数符号"?ReleaseNativeManager1@@YAXH@Z",序号 "#1"//这两个都可以使用dumpbin来查看[DllImport("Dll1.dll", CallingConvention = CallingConvention.StdCall, EntryPoint = "#1")]static protected extern void ReleaseNativeManager1(int num);[DllImport("Dll1.dll", CallingConvention = CallingConvention.StdCall)]static extern void ExSetLogHandler(LogHandler setLogHandler);#endregion}
}

这是最重要的部分,需要注意以下几点:

  1. 不要使用unsafe代码,如void*,char[]这样的代码。C#是托管安全的语言,不要因为导入C++DLL就污染了整个工程。
  2. 考虑逻辑的分布,如在WPF调用C++的需求下,C#这边更多的是对接UI显示部分。所以C++提供的接口应该是以界面更新为驱动的,即都是每秒访问低于100次的代码。有些频繁使用的接口可以考虑直接使用C#代码,一般是IO相关的函数。

封装类的使用:

简单看下界面几个按键的点击事件里的封装类的使用方法:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;namespace WpfApp1
{/// <summary>/// MainWindow.xaml 的交互逻辑/// </summary>public partial class MainWindow : Window{public MainWindow(){InitializeComponent();//完成初始化后为C++DLL设置回调函数NativeInterface.WrapInterface.Log=Log;}//提供一个全局Log方法,可以供C++和C#共同使用public static void Log(string str){Application.Current.Dispatcher.Invoke(() =>{var mainw = Application.Current.MainWindow as MainWindow;mainw.TextLog.Text += str + "\n";mainw.TextLog.ScrollToEnd();});}private void BtnTest_Click(object sender, RoutedEventArgs e){//以属性的方式获取C++的变量名var name = NativeInterface.WrapInterface.TestManager.GetModuleName;TbTestName.Text = name;Log( "C# Log:" + name);}private void BtnNative_Click(object sender, RoutedEventArgs e){var name = NativeInterface.WrapInterface.GetModuleName;TbNativeName.Text = name;//测试成员变量的Count和通过Get Count函数获取的Count,在C++中它们指向的是同一个变量Log( $"C# Log: {name} Name Count: {NativeInterface.WrapInterface.Count} True Name Count:{NativeInterface.WrapInterface.TrueCount} ");}private void BtnRelease1_Click(object sender, RoutedEventArgs e){NativeInterface.WrapInterface.ReleaseTest1();}private void BtnRelease_Click(object sender, RoutedEventArgs e){NativeInterface.WrapInterface.ReleaseTest();}private void BtnTest1_Click(object sender, RoutedEventArgs e){//这里是以方法的形式获取名称var name = NativeInterface.WrapInterface.Test1Manager.GetModuleName();TbTest1Name.Text = name;Log("C# Log:" + name);}private void BtnTest1Add_Click(object sender, RoutedEventArgs e){Random random = new Random();int a = random.Next(100);int b = random.Next(100);int c = NativeInterface.WrapInterface.Test1Manager.Add(a, b);Log($"C# Test1 Add {a}+{b}={c}");}}
}

效果展示

完整解决方案:https://download.csdn.net/download/luoyu510183/11260393

注意,需要手动生成C++项目,再运行C#窗体

VS2019 C++的跨平台开发——C# WPF相关推荐

  1. VS2019 C++的跨平台开发——Android .so开发

    这篇介绍下怎么用VS开发Android使用的.so动态链接库文件. Android环境配置 1.先打开VS installer ​ 2.选中C++移动开发​ 3.如果VS没有下载NDK和SDK的,需要 ...

  2. VS2019 C++的跨平台开发——Linux开发

    前言 由于前段时间正好买了一个服务器来跑Tensorflow的推理模型,所以借这个机会把Linux的开发也一并补上. 先声明我的服务器是Ubuntu16.04,下面文章的内容也是基于Ubuntu16. ...

  3. VS远程调试(Visual Studio)远程连接到linux cmake(跨平台开发)(适用于VS2019,且远程目标平台cmake版本大于等于3.8)

    参考文章:使用vs2019进行Linux远程开发 vs进行远程开发分为三步: 1.创建远程环境的连接,随后让vs将远程环境中的系统头文件同步到本地(也可以指定其他地方的头文件,后面会讲解),c++的代 ...

  4. C++跨平台开发(VS2019+WSL(Ubuntu))

    由于实验室任务要求,特学习了一下如何跨平台开发项目,时间有点长了,如有疏漏,还请包涵指正! 我们在开发项目的过程中,有时候会进行跨平台开发,例如,我们需要开发Linux项目,但是受到条件限制,目前只有 ...

  5. VS2019 C++跨平台开发 Android So 库

    一.VS2019 软件配置 1.1 下载VS2019 Android 开发工具 在获取工具和功能中,勾选下载 Android 开发工具(包含SDK)和 NDK. 1.2 配置 Android SDK ...

  6. 基于VS2019 C++的跨平台(Linux)开发(2.1)——网络基础

    一.引言 首先,来聊聊我们现实中的QQ聊天,如下图所示,两个客户端分别表示聊天的两方,那么可能有人会想为什么中间多了个腾讯公司的服务器呢?因为我们的QQ软件是从腾讯公司下载下来的,它其实起到了中转站的 ...

  7. 基于VS2019 C++的跨平台(Linux)开发(1.2.2)——设备管理及文件IO

    接上一篇文章,我们来回顾作业以及学习其他的一些系统调用等等 Linux--设备管理及文件IO(一) 一.回顾作业 1.知识点 1.文件IO分类 ①纯文本文件:不能进行任何修饰,且只有文字的文件;如.c ...

  8. 基于VS2019 C++的跨平台(Linux)开发(1.5)——管道

    一.管道概述 1.1.管道概念 管道是Unix中最古老的进程间通信的形式.我们把从一个进程连接到另一个进程的一个数据流(文件IO流)称为一个"管道". 1.2.管道特点 管道是半双 ...

  9. 基于VS2019 C++的跨平台(Linux)开发(1.3.3)——进程管理

    接上一篇文章,先来回顾作业,再来学习守护进程 基于VS2019 C++的跨平台(Linux)开发(1.3.2)--进程管理 一.回顾作业 详解见以下链接 c++ 文件拆分与合并--结合linux进程管 ...

最新文章

  1. pcl_filters模块api代码解析
  2. 手把手教你学Kotlin (2):task1-6 函数,Java to Kotlin Convert,(持续更新中)
  3. 前端中会用到的设计模式之单一职责原则
  4. linux qt creator git,Building Qt Creator from Git/zh
  5. ai里为什么不能随意放大缩小_平面设计基础知识:平面设计师应该熟练掌握的软件之AI。...
  6. Linux fork()一个进程内核态的变化
  7. 基于Boost::beast模块的同步HTTP客户端
  8. 机器学习(十一)——机器学习中的矩阵方法(1)LU分解、QR分解
  9. Django 遇到的错误:expected str, bytes or os.PathLike object, not _io.TextIOWrapper
  10. 一加8 Pro渲染图曝光:骁龙865+挖孔屏+后置四摄
  11. 千人千面之3D立体个人数据营销
  12. Python学习入门基础教程(learning Python)--5.6 Python读文件操作高级
  13. java调用libreoffice_使用Open / LibreOffice开始使用UNO和Java
  14. cmd中通过winsat命令测试硬盘、CPU、内存、3d性能等
  15. LCD高抗干扰液晶段码屏显示驱动芯片:VK2C21A/B/BA/C/D 大量应用于音箱/音响面板LCD显示驱动
  16. c语言函数递归相关知识及应用
  17. 转载老码农教你学英语
  18. 大数据毕设选题 - 旅游数据分析可视化系统(python 大数据)
  19. 520|用Python绘制自定义照片墙
  20. 最大子矩阵问题悬线法 学习笔记

热门文章

  1. Visual Studio code 运行c++/c语言
  2. 公安部 权威发布:2021年全国机动车保有量达3.95亿 新能源汽车同比增59.25%
  3. [附源码]java毕业设计火车票预订系统论文2022
  4. 能动性增强的学习环境中学生情绪研究及发表经验
  5. 天九集团卢俊卿:一位热衷慈善的成功企业家
  6. 连接数据库报com.microsoft.sqlserver.jdbc.SQLServerException: 驱动程序无法通过使用安全套接字层(SSL)加密与 SQL Server 建立安全连接
  7. Java中的条件语句
  8. 龙芯linux开发板,龙芯1b开发板环境及系统搭建
  9. uniapp——Android 异常: failed to connect to localhost/127.0.0.1
  10. react 父子组件传值校验 设置默认值