Introduction
In most cases, we use pipe to do IPC. Using pipe, commonly an extra thread is used to monitor data arrive, which results in sync work between UI thread and monitor thread. Furthermore, using pipe, recursive calls will become unexpected complicated.
Here, I propose a new IPC module, I name it SIPC. With SIPC, one can make IPC calls easily as if the two processes are in the same thread.
Background
I dived into building my own input method editor (IME). I divided IME into two parts: IME interface and UI module. IME interface is loaded by the system and is run in the host process. UI module is a separate executable module. To connect the two modules as easily as possible, I designed the SIPC.
Using the Code
After building the demo finished, run the demo and you will be asked to select a run mode. Choose Yes to run as server, and No to run as client. Note that server must run first. Both of server and client have a UI. In server UI, it's log window records client info. In client UI, three IPC functions can be called from the UI. "add int
" and "add string
" demonstrate how int
and string
parameter types work in the demo. "sum
" demonstrates how to make call recursively.
The basic idea of this project is to communicate between two processes using windows message. Using windows message, one can call IPC functions without worrying about thread sync problem. Although WM_COPYDATA
can do this at most cases, using WM_COPYDATA
is complexer to some extent. For example, one has to serialize input parameters manually, and can only receive an integer return value. Actually, the serialize and deserialize procedures can be simplified by wrapping parameters into a class.
To automatically serialize and deserialize parameters, three classes are used here:
struct IShareBuffer {
enum SEEK {
seek_set= 0,
seek_cur,
seek_end
};
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
is used to wrap shared memory support. SParamStream
is used to write or read any type of parameters to shared memory. IFunParams
is an interface used to wrap actual parameters for an IPC call.
Besides, we designed a group of helper macros to simplify the implementation of interfaces of 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);}\
As you can see, the above helpers can support up to 5 parameters for both input and output parameters.
Using the macros, to define an IPC call such as int add(int a, int b)
, both serialize and deserialize can be done by defining a class Param_AddInt
, which looks like:
struct Param_AddInt : FunParams_Base
{
int a, b;
int ret;
FUNID(CID_AddInt)
PARAMS2(Input, a,b)
PARAMS1(Output,ret)
};
FUNID(CID_AddInt)
defines the IPC call id. PARAMS2(Input, a,b)
defines how to serialize a
and b
to share memory, and PARAMS1(Output,ret)
define how to deserialize return value.
To call IntAdd
IPC, fake code may look like:
int CClientConnect::Add(int a, int b)
{
Param_AddInt params;
params.a = a;
params.b = b;
m_ipcHandle->CallFun(¶ms);
return params.ret;
}
Now, let's explain how this demo works. Here, let's focus on how an IPC call is response by server endpoint.
At first, we defined Param_AddInt
. Before sending a message to server, we serialize Param_AddInt
to share memory.
Then, we send message to server using SendMessage
. After server received the message, it read function id and parameters, than call response procedure and write back output parameters to share buffer and return.
After client received return from server, client read output parameters from share buffer. Thus an IPC call finished.
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); 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;
}
After receiving IPC call request, SIpcHandler
calls IConnection::HandleFun(uFunId,ps)
. With the help of helper macro, the HandleFun
function could be decomposed to a group of macros and maps different calls to different handle functions. For example:
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
and FUN_END
is the body of function HandleFun
, and FUN_HANDLER
map IPC call identified by its first parameter to its second parameter.
Note that, before one tries to send an IPC request, in the message queue, there may be some pending requests from the other side that were waiting to be handled. To make sure all IPC calls will be handled in turn, the IIpcHande::CallFunc
will look like:
bool SIpcHandle::CallFun(IFunParams * pParam) const
{
if (m_hRemoteId == NULL)
return false;
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); 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)); 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)); BOOL bRet = FromStream4Output(pParam,&m_sendBuf);
assert(bRet);
}
m_sendBuf.Seek(IShareBuffer::seek_set, dwPos);
m_sendBuf.SetTail(dwPos);
return lRet!=0;
}
That the above mentioned are all key points. With the SIPC, one can make IPC call easy without considering thread sync problems, etc.
Actually, SIPC is one of the components that come from my other open source project SOUI. SOUI is a direct UI framework, which borrows ideas including WTL, QT, flash, Android, Chrome and CEGUI, etc. The source code can be cloned from https://github.com/soui3/soui. If you are interested in SOUI, feel free to contact me at any time.
Points of Interest
- By constructing
IFunParams
using macros, SIPC call auto does parameters serialization. - By constructing
HandleFun
using macros, SIPC call automatically maps IPC call from parameter type to handle function. - All IPC calls are done in the same thread.
History
- 1.0 2019.8.12: Initial version