VS2019 C++的跨平台开发——C# WPF

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


项目准备

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

VS2019 C++的跨平台开发——C# WPF_第1张图片

VS2019 C++的跨平台开发——C# WPF_第2张图片

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

C++项目的提醒事项:

VS2019 C++的跨平台开发——C# WPF_第3张图片


C++的导出设置

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

VS2019 C++的跨平台开发——C# WPF_第4张图片

NativeInterface

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

/////////////////////////////////////////////
////////////NativeInterface.h////////////////
/////////////////////////////////////////////
#pragma once

extern "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();
	};
	//一个纯虚的interface
	class 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 

static const char* name = "Name is :Test Manager";
static const char* SGetModuleName()
{
	SafeLog(const_cast(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#部分

首先看下工程结构:

VS2019 C++的跨平台开发——C# WPF_第5张图片

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)来转换到 string
        private 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(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(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是个函数,需要用一个托管函数变量来赋值后传入 log
            private 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(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;
            }
        }
        //构造私有化,禁止使用new
        private 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
{
    /// 
    /// MainWindow.xaml 的交互逻辑
    /// 
    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}");
        }
    }
}

效果展示

VS2019 C++的跨平台开发——C# WPF_第6张图片

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

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

你可能感兴趣的:(VS2019 C++的跨平台开发——C# WPF)