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

这里主要技术基础就是要实现C#调用C++的dll。

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

  1. 主要原理:

这里需要注意的点是

  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. 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) ;
}

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

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

这里需要注意的点是

假设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. SWIG启动,调用example.i,自动生成调用代码文件
    SWIG命令:

生成结果:

  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. 编译运行。
    这里需要注意:
  • 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
06-11 04:21