引入基于 Windows 消息的 IPC 组件
基于Windows消息的简单而可靠的IPC模块,由于Windows的限制,最多支持19轮递归调用。
引言
在大多数情况下,我们使用管道进行IPC。 使用管道时,通常会使用额外的线程来监视数据的到达,这会导致UI线程和监视线程之间的同步工作。 此外,使用管道,递归调用将变得出乎意料地复杂。
在这里,我提出了一个新的IPC模块,我将其命名为SIPC。 使用SIPC,可以轻松地进行IPC调用,就像两个进程在同一线程中一样。
背景
我深入研究了构建自己的输入法编辑器(IME)。 我将IME分为两个部分:IME接口和UI模块。 IME接口由系统加载并在宿主进程中运行。 UI模块是一个单独的可执行模块。 为了尽可能轻松地连接这两个模块,我设计了SIPC。
Using the Code
构建演示完成后,运行该演示,系统会要求您选择运行模式。 选择是以服务器身份运行,选择否以客户端身份运行。 请注意,服务器必须首先运行。 服务器和客户端都有UI。 在服务器UI中,日志窗口记录客户端信息。 在客户端UI中,可以从UI调用三个IPC函数。“add int
”和“add string
”演示了int
和string
参数类型如何在演示中工作。“sum
”演示了如何进行递归调用。
该项目的基本思想是使用Windows消息在两个进程之间进行通信。 使用Windows消息,可以调用IPC函数,而无需担心线程同步问题。 尽管WM_COPYDATA
可以在大多数情况下做到这一点,但在某种程度上使用WM_COPYDATA
会更复杂。 例如,必须手动序列化输入参数,并且只能接收整数返回值。 实际上,可以通过将参数包装到一个类中来简化序列化和反序列化过程。
为了自动序列化和反序列化参数,这里使用了三个类
struct IShareBuffer {
enum SEEK {
seek_set= 0, /* seek to an absolute position */
seek_cur, /* seek relative to current position */
seek_end /* seek relative to end of file */
};
virtual int Write(const void * data, UINT nLen) = 0;
virtual int Read(void * buf, UINT nLen) = 0;
virtual UINT Tell() const = 0;
virtual UINT Seek(SEEK mode, int nOffset) = 0;
virtual void SetTail(UINT uPos) = 0;
};
class SParamStream
{
public:
SParamStream(IShareBuffer *pBuf) :m_pBuffer(pBuf)
{
}
IShareBuffer * GetBuffer() {
return m_pBuffer;
}
template<typename T>
SParamStream & operator<<(const T & data)
{
Write((const void*)&data, sizeof(data));
return *this;
}
template<typename T>
SParamStream & operator >> (T &data)
{
Read((void*)&data, sizeof(data));
return *this;
}
public:
int Write(const void * data, int nLen)
{
return m_pBuffer->Write(data, nLen);
}
int Read(void * buf, int nLen)
{
return m_pBuffer->Read(buf, nLen);
}
protected:
IShareBuffer * m_pBuffer;
};
struct IFunParams
{
virtual UINT GetID() = 0;
virtual void ToStream4Input(SParamStream & ps) = 0;
virtual void ToStream4Output(SParamStream & ps) = 0;
virtual void FromStream4Input(SParamStream & ps) = 0;
virtual void FromStream4Output(SParamStream & ps) = 0;
};
IShareBuffer
用于包装共享内存支持。 SParamStream
用于将任何类型的参数写入或读取到共享内存。 IFunParams
是一个接口,用于包装IPC调用的实际参数。
此外,我们设计了一组辅助宏,以简化IFunParams
接口的实现。
#pragma once
#define FUNID(id) \
enum{FUN_ID=id};\
UINT GetID() {return FUN_ID;}
#define FUN_BEGIN \
bool HandleFun(UINT uMsg, SOUI::SParamStream &ps){ \
bool bHandled = false; \
#define FUN_HANDLER(x,fun) \
if(!bHandled && uMsg == x::FUN_ID) \
{\
x param; \
GetIpcHandle()->FromStream4Input(¶m,ps.GetBuffer());\
ps.GetBuffer()->Seek(SOUI::IShareBuffer::seek_cur,sizeof(int));\
fun(param); \
GetIpcHandle()->ToStream4Output(¶m,ps.GetBuffer());\
bHandled = true;\
}
#define FUN_END \
return bHandled; \
}
#define CHAIN_MSG_MAP_2_IPC(ipc) \
if(ipc)\
{\
BOOL bHandled = FALSE;\
lResult = (ipc)->OnMessage((ULONG_PTR)hWnd,uMsg,wParam,lParam,bHandled);\
if(bHandled)\
{\
return true;\
}\
}
/////////////////////////////////////////////////////////////////////
template<typename P1>
void toParamStream(SOUI::SParamStream & ps, P1 &p1)
{
ps << p1;
}
template<typename P1>
void fromParamStream(SOUI::SParamStream & ps, P1 & p1)
{
ps >> p1;
}
#define PARAMS1(type,p1) \
void ToStream4##type(SOUI::SParamStream & ps){ toParamStream(ps,p1);}\
void FromStream4##type(SOUI::SParamStream & ps){fromParamStream(ps,p1);}\
/////////////////////////////////////////////////////////////
template<typename P1, typename P2>
void toParamStream(SOUI::SParamStream & ps, P1 &p1, P2 & p2)
{
ps << p1 << p2;
}
template<typename P1, typename P2>
void fromParamStream(SOUI::SParamStream & ps, P1 & p1, P2 &p2)
{
ps >> p1 >> p2;
}
#define PARAMS2(type,p1,p2) \
void ToStream4##type(SOUI::SParamStream & ps){ toParamStream(ps,p1,p2);}\
void FromStream4##type(SOUI::SParamStream & ps){fromParamStream(ps,p1,p2);}\
////////////////////////////////////////////////////////////////////
template<typename P1, typename P2, typename P3>
void toParamStream(SOUI::SParamStream & ps, P1 &p1, P2 & p2, P3 & p3)
{
ps << p1 << p2 << p3;
}
template<typename P1, typename P2, typename P3>
void fromParamStream(SOUI::SParamStream & ps, P1 & p1, P2 &p2, P3 & p3)
{
ps >> p1 >> p2 >> p3;
}
#define PARAMS3(type,p1,p2,p3) \
void ToStream4##type(SOUI::SParamStream & ps){ toParamStream(ps,p1,p2,p3);}\
void FromStream4##type(SOUI::SParamStream & ps){fromParamStream(ps,p1,p2,p3);}\
///////////////////////////////////////////////////////////////////
template<typename P1, typename P2, typename P3, typename P4>
void toParamStream(SOUI::SParamStream & ps, P1 &p1, P2 & p2, P3 & p3, P4 & p4)
{
ps << p1 << p2 << p3<<p4;
}
template<typename P1, typename P2, typename P3, typename P4>
void fromParamStream(SOUI::SParamStream & ps, P1 & p1, P2 &p2, P3 & p3, P4 & p4)
{
ps >> p1 >> p2 >> p3>>p4;
}
#define PARAMS4(type,p1,p2,p3,p4) \
void ToStream4##type(SOUI::SParamStream & ps){ toParamStream(ps,p1,p2,p3,p4);}\
void FromStream4##type(SOUI::SParamStream & ps){fromParamStream(ps,p1,p2,p3,p4);}\
/////////////////////////////////////////////////////////////////////////
template<typename P1, typename P2, typename P3, typename P4, typename P5>
void toParamStream(SOUI::SParamStream & ps, P1 &p1, P2 & p2, P3 & p3, P4 & p4, P5 &p5)
{
ps << p1 << p2 << p3 << p4 <<p5;
}
template<typename P1, typename P2, typename P3, typename P4, typename P5>
void fromParamStream(SOUI::SParamStream & ps, P1 & p1, P2 &p2, P3 & p3, P4 & p4, P5 &p5)
{
ps >> p1 >> p2 >> p3 >> p4>>p5;
}
#define PARAMS5(type,p1,p2,p3,p4,p5) \
void ToStream4##type(SOUI::SParamStream & ps){ toParamStream(ps,p1,p2,p3,p4,p5);}\
void FromStream4##type(SOUI::SParamStream & ps){fromParamStream(ps,p1,p2,p3,p4,p5);}\
如您所见,以上助手最多可以支持5个输入和输出参数。
使用宏,要定义一个IPC调用,例如int add(int a, int b)
,可以通过定义一个类Param_AddInt
来完成序列化和反序列化,该类看起来像
struct Param_AddInt : FunParams_Base
{
int a, b;
int ret;
FUNID(CID_AddInt)
PARAMS2(Input, a,b)
PARAMS1(Output,ret)
};
FUNID(CID_AddInt)
定义IPC调用ID。 PARAMS2(Input, a,b)
定义如何将a
和b
序列化到共享内存,PARAMS1(Output,ret)
定义如何反序列化返回值。
要调用IntAdd
IPC,伪代码可能如下所示
int CClientConnect::Add(int a, int b)
{
Param_AddInt params;
params.a = a;
params.b = b;
m_ipcHandle->CallFun(¶ms);
return params.ret;
}
现在,让我们解释一下此演示的工作方式。 在这里,让我们关注服务器端点如何响应IPC调用。
首先,我们定义了Param_AddInt
。 在向服务器发送消息之前,我们将Param_AddInt
序列化到共享内存。
然后,我们使用SendMessage
向服务器发送消息。 服务器收到消息后,它读取函数ID和参数,然后调用响应过程并将输出参数写回共享缓冲区并返回。
客户端收到服务器的返回后,客户端从共享缓冲区读取输出参数。 这样就完成了一个IPC调用。
LRESULT SIpcHandle::OnMessage
(ULONG_PTR idLocal, UINT uMsg, WPARAM wp, LPARAM lp, BOOL &bHandled)
{
bHandled = FALSE;
if ((HWND)idLocal != m_hLocalId)
return 0;
if (UM_CALL_FUN != uMsg)
return 0;
bHandled = TRUE;
IShareBuffer *pBuf = GetRecvBuffer();
assert(pBuf->Tell()>= 4); //4=sizeof(int)
pBuf->Seek(IShareBuffer::seek_cur,-4);
int nLen=0;
pBuf->Read(&nLen, 4);
assert(pBuf->Tell()>=(UINT)(nLen+ 4));
pBuf->Seek(IShareBuffer::seek_cur,-(nLen+ 4));
int nCallSeq = 0;
pBuf->Read(&nCallSeq,4);
UINT uFunId = 0;
pBuf->Read(&uFunId,4);
SParamStream ps(pBuf);
bool bReqHandled = m_pConn->HandleFun(uFunId, ps);
return bReqHandled?1:0;
}
收到IPC调用请求后,SIpcHandler
调用IConnection::HandleFun(uFunId,ps)
。 在辅助宏的帮助下,HandleFun
函数可以分解为一组宏,并将不同的调用映射到不同的处理函数。 例如
void OnAddInt(Param_AddInt & param);
void OnAddStr(Param_AddString & param);
void OnSum(Param_Sum & param);
FUN_BEGIN
FUN_HANDLER(Param_AddInt, OnAddInt)
FUN_HANDLER(Param_AddString, OnAddStr)
FUN_HANDLER(Param_Sum,OnSum)
FUN_END
FUN_BEGIN
和FUN_END
是函数HandleFun
的主体,FUN_HANDLER
将由其第一个参数标识的IPC调用映射到其第二个参数。
请注意,在尝试发送IPC请求之前,在消息队列中,可能存在来自另一端的一些待处理请求正在等待处理。 为了确保所有IPC调用都按顺序处理,IIpcHande::CallFunc
将如下所示
bool SIpcHandle::CallFun(IFunParams * pParam) const
{
if (m_hRemoteId == NULL)
return false;
//pay attention, here we need to make sure msg queue empty.
MSG msg;
while(::PeekMessage(&msg, NULL, UM_CALL_FUN, UM_CALL_FUN, PM_REMOVE))
{
if(msg.message == WM_QUIT)
{
PostQuitMessage(msg.wParam);
return false;
}
DispatchMessage(&msg);
}
int nCallSeq = m_uCallSeq ++;
if(m_uCallSeq>100000) m_uCallSeq=0;
IShareBuffer *pBuf = &m_sendBuf;
DWORD dwPos = pBuf->Tell();
pBuf->Write(&nCallSeq,4); //write call seq first.
UINT uFunId = pParam->GetID();
pBuf->Write(&uFunId,4);
if(!ToStream4Input(pParam, pBuf))
{
pBuf->Seek(IShareBuffer::seek_set, dwPos);
m_sendBuf.SetTail(dwPos);
assert(false);
return false;
}
int nLen = m_sendBuf.Tell()-dwPos;
m_sendBuf.Write(&nLen,sizeof(int));//write a length of params to stream,
//which will be used to locate param header
LRESULT lRet = SendMessage(m_hRemoteId, UM_CALL_FUN, pParam->GetID(),
(LPARAM)m_hLocalId);
if (lRet != 0)
{
m_sendBuf.Seek(IShareBuffer::seek_set,
dwPos+nLen+sizeof(int)); //output param must follow input params
BOOL bRet = FromStream4Output(pParam,&m_sendBuf);
assert(bRet);
}
//clear params.
m_sendBuf.Seek(IShareBuffer::seek_set, dwPos);
m_sendBuf.SetTail(dwPos);
return lRet!=0;
}
以上提到的都是关键点。 借助SIPC,可以轻松进行IPC调用,而无需考虑线程同步问题等。
实际上,SIPC是来自我的另一个开源项目SOUI的组件之一。 SOUI是一个直接的UI框架,它借鉴了包括WTL,QT,flash,Android,Chrome和CEGUI等在内的思想。 可以从https://github.com/soui3/soui克隆源代码。 如果您对SOUI感兴趣,请随时与我联系。
关注点
- 通过使用宏构造
IFunParams
,SIPC调用会自动执行参数序列化。 - 通过使用宏构造
HandleFun
,SIPC调用会自动将来自参数类型的IPC调用映射到处理函数。 - 所有IPC调用都在同一线程中完成。
历史
- 1.0 2019.8.12: 初始版本