C#作为GUI开发工具,C++作为业务核心模块的实现方式记录
这里主要技术基础就是要实现C#调用C++的dll。
这里主要讲几种方式:
方式一、利用命名管道实现
- 主要原理:
C++业务端作为命名管道服务端,负责监听C#的GUI客户端发送的命名管道命令
这里需要注意的点是
a.管道路径规则:
// .: 表示当前计算机
// 注:windows中VC++管道创建需放到 …pipe\的路径下
// windows中C#管道创建不需放到 …pipe\的路径下
// e.g. VC++ \\\\.\\pipe\\testPipe <==> C# .\testPipe
- 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
}
- 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++代码进行的包装,使开发者更易于调用。
这里需要注意的点是
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
- 自定义一个example.i文件,用于SWIG自动生成调用的代码文件
example.i内容如下:
/*--- File: example.i ---*/
%module invoke_dll_name
%{
#include "Facade.h"
%}
%include "std_string.i"
%include "Facade.h"
%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所在目录)
- 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++的功能
- 重新配置C++ facade.dll代码组成
将2步骤生成的Facade_wrap.cxx添加到此模块,重新编译生成。
此时C++ facade.dll组织形式如下:
-
C++ facade.dll
- Facade.h
- Facade.cpp
- Facade_wrap.cxx
- 重新配置C#GUI.exe,将2步骤生成所有*.cs文件添加到此项目,即可调用C++的功能
- 编译运行。
这里需要注意:
需要将所有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>;
-
存在多个类时,可以编写多个.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表示参数名
// 针对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;
%}
}
- SWIG命令指定*.cs生成文件的输出目录
选项: -outdir <dir path>
e.g. swig -c++ -csharp -outdir swig example.i