Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / Win32

Introduce an IPC Component Based on Windows Message

3.55/5 (3 votes)
12 Aug 2019MIT4 min read 7.2K   138  
Simple and reliable IPC module based on Windows message, which supports recursive call up to 19 rounds because of the limitation of Windows.

select mode

server

client

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:

C++
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 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.

C++
#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:

C++
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:

C++
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.

C++
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;
}

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:

C++
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:

C++
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;
}

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

  1. By constructing IFunParams using macros, SIPC call auto does parameters serialization.
  2. By constructing HandleFun using macros, SIPC call automatically maps IPC call from parameter type to handle function.
  3. All IPC calls are done in the same thread.

History

  • 1.0 2019.8.12: Initial version

License

This article, along with any associated source code and files, is licensed under The MIT License