• C#作为GUI开发工具,C++作为业务核心模块的实现方式记录


    C#作为GUI开发工具,C++作为业务核心模块的实现方式记录

    这里主要技术基础就是要实现C#调用C++的dll。
    这里主要讲几种方式:

    方式一、利用命名管道实现

    1. 主要原理:

    C++业务端作为命名管道服务端,负责监听C#的GUI客户端发送的命名管道命令

    这里需要注意的点是

    a.管道路径规则:
    // .: 表示当前计算机
    // 注:windows中VC++管道创建放到 …pipe\的路径下
    // windows中C#管道创建不需放到 …pipe\的路径下
    // e.g. VC++ \\\\.\\pipe\\testPipe <==> C# .\testPipe

    1. C++命名管道服务端实现
    ///
    /// serverName: 服务端主机地址 默认"."表示当前主机
    /// pipeName: 命名管道名字 不能为空
    /// bMultiPipe: 是否可以多线程监听多个客户端
    ///
    int Listen(const std::string& serverName, const std::string& pipeName, bool bMultiPipe)
    {
    #ifdef WIN_USE
    	// 1.组装管道路径
    	// e.g. \\\\.\\pipe\\testPipe 
    	std::string pipe;
    	if (serverName.empty()) {
    		pipe.assign("\\\\.\\pipe\\"s);
    	}
    	else {
    		pipe.assign("\\\\"s).append(serverName).append("\\pipe\\"s);
    	}
    	pipe.append(pipeName);
    
    	// 2.监听业务
    	::HANDLE hPipe;
    	BOOL bConnected;
    	while (true) {
    		// 2.1.创建命名管道Windows句柄
    		hPipe = ::CreateNamedPipeA(pipe.c_str(), PIPE_ACCESS_DUPLEX, PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_WAIT, PIPE_UNLIMITED_INSTANCES, PIPE_CELL_BUFFER_SIZE, PIPE_CELL_BUFFER_SIZE, 0, NULL);
    
    		// 2.2.等待客户端的连接 此处存在阻塞 直到有客户端连接
    		// Wait for the client to connect; if it succeeds,   
    		// the function returns a nonzero value. If the function  
    		// returns zero, GetLastError returns ERROR_PIPE_CONNECTED. 
    		bConnected = ::ConnectNamedPipe(hPipe, NULL) 
    			? TRUE 
    			: (GetLastError() == ERROR_PIPE_CONNECTED); // 当没有客户端连接的时候 ConnectNamedPipe会阻塞等待客户端连接
    		if (bConnected) {
    			// 2.3.阻塞等待接收客户端发送的数据
    			// 此处实现考虑:接收管理上下文的事件,比如开启一个业务通讯、结束服务端等
    			std::byte buffer[bytes] = {0};	// 接收数据的缓冲
    			DWORD nReadBytes;	// 表示当前实际接收到的数据的字节大小
    			::ReadFile(hPipe, buffer, bytes, &nReadBytes, NULL);
    
    			// 2.4.处理管理上下文的事件
    			std::string event = parse_event(buffer);
    			if (event = "start service") {
    
    				// Interaction: 业务通讯处理
    				// 也是通过::ReadFile接收客户端发送的业务事件
    				// 也是通过::WriteFile响应客户端结果
    				
    				if (!bMultiPipe) {
    					// 只能单个实例
    					// 开始业务通讯交互
    					Interaction(hPipe);	//	业务通讯处理
    					continue;
    				}
    	
    				// 多个实例支持
    				// Create a thread for this client.   
    				std::thread hThread(&Interaction, this, hPipe);
    				HANDLE thHand = hThread.native_handle();
    				::SetThreadPriority(thHand, THREAD_PRIORITY_HIGHEST);
    				hThread.detach();
    			}
    
    			// 2.5.响应客户端处理结果
    			std::byte responseBuffer[responseByteSize]; // 响应缓冲
    			DWORD cbWritten;	// 表示实际发送的数据
    			::WriteFile(hPipe, responseBuffer, responseByteSize, &cbWritten, NULL);
    		}
    	}
    #endif  // WIN_USE
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    1. C#命名管道客户端实现
    // 1.创建命名管道句柄
    // e.g. \\\\.\\pipe\\testPipe 
    // 等价于C++的::CreateNamedPipeA("\\\\.\\pipe\\testPipe ",...)
    var pipeClient = new NamedPipeClientStream(
        ".",
        "testPipe",
        PipeDirection.InOut
    );
    
    // 2.连接服务端
    // 客户端发起连接请求 C++服务端::ConnectNamedPipe阻塞直到此连接成功
    pipeClient.Connect();
    
    using (StreamReader sr = new StreamReader(pipeClient))
    {
    	// 3.向服务端发送数据
        var data = new byte[10240];
        data = System.Text.Encoding.Default.GetBytes("send to server");
        pipeClient.Write(data, 0, data.Length);
    
    	// 4.接收服务端的响应
        string temp;
        while ((temp = sr.ReadLine()) != null) ;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    方式二、利用SWIG转换C++代码调用

    SWIG如何实现让C#调用C++的dll,原理是使用C#的互操作技术P/Invoke,SWIG在此基础上对C++代码进行的包装,使开发者更易于调用。

    这里需要注意的点是

    a.只需用SWIG重新生成接口dll:
    e.g. C#GUI.exe --》 C++ facade.dll --》C++ kernel.dll
    上述例子只需用SWIG来操作facade.dll

    假设C#GUI.exe,C++ facade.dll,C++ kernel.dll,在VS中已经写好代码:
    其中C++ facade.dll组织形式如下:

    C++ facade.dll
    Facade.h
    Facade.cpp
    1. 自定义一个example.i文件,用于SWIG自动生成调用的代码文件
      example.i内容如下:
    /*--- File: example.i ---*/
    %module invoke_dll_name
    %{
    #include "Facade.h"	
    %}
    
    %include "std_string.i"
    %include "Facade.h"
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    %module invoke_dll_name: 表示C#调用C++的dll名字
    %{}%: 表示C#需要调用的C++函数、变量、类等类型申明
    %include指需要进行分装的类型,std_string.i是SWIG封装好的用来处理C++的std::string。此处也表示路径,比如std_string.i位于SWIG的上下文环境下,Facade.h必须位于example.i同级别目录(可将Facade.h单独拷贝到example.i所在目录)

    1. SWIG启动,调用example.i,自动生成调用代码文件
      SWIG命令:

    swig -c++ -csharp example.i

    生成结果:

    Facade_wrap.cxx: // 添加到C++ facade.dll模块后,重新编译,自动生成C#可调用的dll程序集
    facade.cs
    facadePINVOKE.cs
    FacadeClass.cs // *.cs文件,添加到C#GUI.exe模块后,即可调用C++的功能

    1. 重新配置C++ facade.dll代码组成
      将2步骤生成的Facade_wrap.cxx添加到此模块,重新编译生成。
      此时C++ facade.dll组织形式如下:
    C++ facade.dll
    Facade.h
    Facade.cpp
    Facade_wrap.cxx
    1. 重新配置C#GUI.exe,将2步骤生成所有*.cs文件添加到此项目,即可调用C++的功能
    2. 编译运行。
      这里需要注意:

    需要将所有C++依赖的相关dll都拷贝到GUI.exe所在目录,否则不能执行

    运行报错的常见原因:

    • BadImageFormatException: 试图加载格式不正确的程序
      原因:一般情况是,C++的dll是64位,但是 C#GUI.exe编译时设置了“Any CPU”的目标平台来生成
      解决:C#GUI.exe编译设置目标平台与C++的dll保持一致
    • DllNotFoundException: 无法加载 DLL“xxx”:
      原因:一般情况是,没有将C++ facade.dll拷贝到C#GUI.exe所在目录,或者没有将C++ facade.dll依赖的dll拷贝到C#GUI.exe所在目录
      解决:先用Dependency查看缺失的依赖,补齐C++的dll
    • 编写.i接口文件时,运算符重载和右值引用构造函数需要特殊处理
    // module后面定义接口的名字 最后C#调用的dll必须与此定义同名
    %module point2
    %{
    #include "point.h"
    %}
    
    // 引用typemaps.i 可以使用%apply来约定参数的输入输出属性
    %include "typemaps.i"
    
    // Point2是point.h定义的类型
    template <class T>
    struct Point2
    {
    // 忽略swig不支持的语法 或者 在C#语言中不用支持的语法
    %ignore _v;
    %ignore Point2(Point2&& rhs);				// 右值引用 在C#中不需要
    %ignore operator == (const Point2& v) const;		// ==重载 在C#中不支持
    %ignore operator != (const Point2& v) const;		// <=重载 在C#中不支持
    %ignore operator <  (const Point2& v) const;		// <重载 在C#中不支持
    %ignore operator [] (int i);							// []重载 在C#中不支持
    %ignore operator [] (int i) const;						// []重载 在C#中不支持
    %ignore x();	// 在C#中不需要 如果保留 返回这类型会变成DWIGType_p_double 不知道如何使用
    %ignore y();
    %ignore operator=;				// =重载 在C#中不支持
    %ignore operator *=;
    %ignore operator /=;
    %ignore operator +=;
    %ignore operator -=;
    
    // 保留的一些运算符重载 利用%rename指令 保证目标语言可以调用
    %rename(Mult) operator*(const Point2& rhs) const;
    %rename(MultScalar) operator * (T rhs) const;
    %rename(DivScalar) operator/(T rhs) const;
    %rename(Add) operator+(const Point2& rhs) const;
    %rename(Minus) operator-(const Point2& rhs) const;
    %rename(Negate) operator-() const;
    
    // 扩展实现 定义为目标语言可以调用的语义
    %extend {
    	bool Equals(const Point2& v) const {
    		// 调用C++类中实现的==重载函数
    		return (*$self) == v;	// (*$self)类似C++类中的*this
    	}
    
    	int CompareTo(const Point2& v) const {
    		// 调用C++类中实现的==重载函数
    		if ((*$self) == v) return 0;
    		// 调用C++类中实现的<重载函数
    		return ((*$self) < v ? -1: 1);
    	}
    
    	T Compoent(int i) { return (*$self)._v[i]; }
    	void SetCompoent(int i, const T& v) { (*$self)._v[i] = v; }
    
    	// 利用%apply指令来约定,指定名字的参数是输出参数还是输入参数
    	// 目前不能使用%apply指令来约定自定义类型
    	//%apply Class1 & OUTPUT { Class1 &c1, Class1 &c2};
    	%apply double & OUTPUT { double& height };	// 最后形成的C#代码: Get(..., out double height)...
    	void Get(Point3<T>& p3, Point4<T>& p4, double& height) {
    		...
    	}
    	// 利用%clear指令 可以确保移除%apply对某个参数的约定
    	//%clear Class1 &c1;	// Remove all typemaps for Class1& c1
    	//%clear Class1 &c2;	// Remove all typemaps for Class1& c2
    	%clear double &height;
    }
    
    	/// coordinate
    	T _v[2];
    	Point2() {...}
    	explicit Point2(T xNew, T yNew) {...}
    	~Point2() {...}
    	Point2(const Point2& rhs) {...}
    	Point2(Point2&& rhs) {...}
    
    	inline bool operator == (const Point2& v) const { ...}
    	inline bool operator != (const Point2& v) const { ...}
    	inline bool operator <  (const Point2& v) const { ...}
    	inline void Set(T x, T y) { ... }
    	inline void Set(const Point2& rhs) { ... }
    	inline T & operator [] (int i) { ... }
    	inline T operator [] (int i) const { ... }
    	inline T & x() { ... }
    	inline T & y() { ... }
    	inline T x() const { ... }
    	inline T y() const { ... }	
    	// 其他运算符重载忽略
    	...
    }
    
    // 必须具象化 才能给目标语言调用
    // C#支持调用的模板具象化
    %template(Point2i) Point2<int>;
    %template(Point2f) Point2<float>;
    %template(Point2d) Point2<double>;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 存在多个类时,可以编写多个.i接口文件,最后再定义一个总的.i接口文件,来include所有类的.i接口文件
      比如:

      • class1.i
      • class2.i
      • 总的.i接口文件: facade.i 内容包含
        • %include “class1.i”
        • %include “class2.i”
    • 当C++函数的参数存在引用类型,需要在函数中返回值时,可以按照如下处理

    // 针对C++的简单类型 
    // e.g. double
    %include "typemaps.i"
    
    // 将 double & 映射为 out double 类型
    %apply double & OUTPUT { double& result };	// OUTPUT: 表示参数需要输出值 result表示参数名
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    // 针对C++的 std
    // e.g. std::string
    %include "typemaps.i"
    
    // 过程复杂 参照:
    // https://stackoverflow.com/questions/13022051/how-to-swig-stdstring-to-c-sharp-ref-string
    // https://www.coder.work/article/571827
    namespace std {
    
    %naturalvar string;
    
    class string;
    
    // string &
    
    %typemap(ctype) std::string & "char**"
    %typemap(imtype) std::string & "/*imtype*/ out string"
    %typemap(cstype) std::string & "/*cstype*/ out string"
    
    //C++
    %typemap(in, canthrow=1) std::string &
    %{  
    	//typemap in
        std::string temp;
        $1 = &temp; 
    %}
    
    //C++
    %typemap(argout) std::string & 
    %{ 
        //Typemap argout in c++ file.
        //This will convert c++ string to c# string
        *$input = SWIG_csharp_string_callback($1->c_str());
    
    	//#if defined(SWIGPYTHON)
    	typemap argout std::string&
        //PyObject* obj = PyUnicode_FromStringAndSize((*$1).c_str(),(*$1).length());
        //$result=SWIG_Python_AppendOutput($result, obj);
    	//#endif
    %}
    
    %typemap(argout) const std::string & 
    %{ 
        //argout typemap for const std::string&
    %}
    
    %typemap(csin) std::string & "out $csinput"
    
    %typemap(throws, canthrow=1) string &
    %{ 
    	SWIG_CSharpSetPendingException(SWIG_CSharpApplicationException, $1.c_str());
       return $null; 
    %}
    
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • SWIG命令指定*.cs生成文件的输出目录

    选项: -outdir


    e.g. swig -c++ -csharp -outdir swig example.i

  • 相关阅读:
    为何说AI是网络安全的未来?
    Cy5反式环辛烯,TCO-Cy5,Cy5 trans-cyclooctene标记生物分子
    xss过waf的小姿势
    [课程][原创]yolov7训练自己数据集实例分割模型
    Spring5完整版详解
    CSS BFC介绍
    Vue的自定义指令
    socket,tcp,http三者之间的原理和区别
    《TCP/IP网络编程》阅读笔记--多播与广播
    【C++】多态 ④ ( 多态实现原理 | C++ 联编概念 | 链接属性 | 内部链接 | 外部链接 | 联编与链接 | 静态联编 | 动态联编 | 联编 与 virtual 关键字 )
  • 原文地址:https://blog.csdn.net/xys206006/article/details/131145779