chromium源码阅读-进程间通信IPC.消息的接收与应答
介绍
chromium进程间通信在win32下是通过命名管道的方式实现的,最后的数据都是以二进制流的方式进行传播,pickle类就是负责消息的封包与解包功能,它将各种数据已二进制的形式写到内存缓冲区中,在实际通信的时候通过与其他一些辅助类与模板函数来实现具体数据结构的写与读。本文主要介绍的是chromium在将消息发送与接收过程中,以及chromium如何通过各种消息宏与C++模板机制实现消息的分派与应答,限于篇幅此处忽略命名管道部分消息的发送与接收。
demo
以下代码是截取自chromium源代码里面关于ipc同步消息(SYNMSG)的测试用例,在chromium中所谓的ipcsynmessage就是说: 当@A发送消息a给@B,@B接收到a之后,需要返回一个消息b给@A确认 通过下面代码分析消息的接收与应答过程。 在TestMessageReceiver中定义了多个消息形式为On_$(IN)_$(OUT)格式的函数,它的作用是对接收的消息进行处理以及返回相应的结果给远端,其中的IN表示远端发送过来的参数个数,OUT表示应答给远端的参数个数。
#define MESSAGES_INTERNAL_FILE "chrome/common/ipc_sync_message_unittest.h" #include "chrome/common/ipc_message_macros.h" static IPC::Message* g_reply; class TestMessageReceiver { public: void On_0_1(bool* out1) { *out1 = false; } void On_0_2(bool* out1, int* out2) { *out1 = true; *out2 = 2; } void On_0_3(bool* out1, int* out2, std::string* out3) { *out1 = false; *out2 = 3; *out3 = "0_3"; } void On_1_1(int in1, bool* out1) { DCHECK(in1 == 1); *out1 = true; } //..... bool Send(IPC::Message* message) { // gets the reply message, stash in global DCHECK(g_reply == NULL); g_reply = message; return true; } void OnMessageReceived(const IPC::Message& msg) { IPC_BEGIN_MESSAGE_MAP(TestMessageReceiver, msg) IPC_MESSAGE_HANDLER(Msg_C_0_1, On_0_1)//在ipc_enum中定义 IPC_MESSAGE_HANDLER(Msg_C_0_2, On_0_2) IPC_MESSAGE_HANDLER(Msg_C_0_3, On_0_3) IPC_MESSAGE_HANDLER(Msg_C_1_1, On_1_1) //..... IPC_END_MESSAGE_MAP() //经过宏展开变为 /* { typedef TestMessageReceiver _IpcMessageHandlerClass; const IPC::Message& ipc_message__ = msg; bool msg_is_ok__ = true; switch (ipc_message__.type()) { case Msg_C_0_1::ID: msg_is_ok__ = Msg_C_0_1::Dispatch(&ipc_message__, this, &On_0_1); break; .... DCHECK(msg_is_ok__); } } */ } }; TEST(IPCSyncMessageTest, Main) { bool bool1 = true; int int1 = 0; std::string string1; Send(new Msg_C_0_1(&bool1)); DCHECK(!bool1); Send(new Msg_C_0_2(&bool1, &int1)); DCHECK(bool1 && int1 == 2); Send(new Msg_C_0_3(&bool1, &int1, &string1)); DCHECK(!bool1 && int1 == 3 && string1 == "0_3"); // .... }
其中OnMessageReceived是消息的接收函数,里面定义的宏格式跟MFC定义的消息处理宏的功能是一样的,都是响应消息函数。如果接收到对应的消息比如消息MsgC01那么就会调用成员函数On01,成员函数中设置了参数out1值为false,设置的这个值经过处理会转换为Message,然后将其传递给成员函数Send发送给远端,这里只是测试用例,因此没有实际的发送出去只是将这个消息暂时赋值给全局变量greply。
IPC消息宏
chromium消息机制通过头文件chrome/common/ipcmessagemacros.h定义了许多有用的消息宏以及模板函数、模板类。这个头文件编写的非常巧妙,通过多次嵌套头文件本身,最后经过编译器预处理宏展开,方便的定义了用户定义的消息枚举类型以及消息类。
在上面的代码中首先定义了一个宏文件MESSAGESINTERNALFILE,这是用户自己定义的消息类型文件,里面也include了文件chrome/common/ipcmessagemacros.h,用户需要在这个文件定义自己需要的消息类型,这里定义了MsgC01,MsgC02等消息,在头文件chrome/common/ipcmessagemacros.h中有如下的预处理操作:
//头文件没有ifndef define endif,因此能重复的嵌套包含 #include "chrome/common/ipc_message_utils.h" #ifndef MESSAGES_INTERNAL_FILE #error This file should only be included by X_messages.h, which needs to define MESSAGES_INTERNAL_FILE first. #endif // Trick scons and xcode into seeing the possible real dependencies since they // don‘t understand #include MESSAGES_INTERNAL_FILE. See http://crbug.com/7828 /@1 //注意这里用了header guard,只会包含一次 #ifndef IPC_MESSAGE_MACROS_INCLUDE_BLOCK #define IPC_MESSAGE_MACROS_INCLUDE_BLOCK // Multi-pass include of X_messages_internal.h. Preprocessor magic allows // us to use 1 header to define the enums and classes for our render messages. #define IPC_MESSAGE_MACROS_ENUMS //嵌套包含:@2 #include MESSAGES_INTERNAL_FILE #define IPC_MESSAGE_MACROS_CLASSES //嵌套包含:@2 #include MESSAGES_INTERNAL_FILE #ifdef IPC_MESSAGE_MACROS_LOG_ENABLED #define IPC_MESSAGE_MACROS_LOG #include MESSAGES_INTERNAL_FILE #endif #undef MESSAGES_INTERNAL_FILE #undef IPC_MESSAGE_MACROS_INCLUDE_BLOCK #endif // IPC_MESSAGE_MACROS_INCLUDE_BLOCK @A #undef IPC_BEGIN_MESSAGES //..... //下面是定义消息枚举类型,消息类,以及日志类(日志类可选) #if defined(IPC_MESSAGE_MACROS_ENUMS) //消息枚举类型定义 //这里undef很重要,因为在@2嵌套包含之后下面的代码通过@2展开在了@1里面 //如果不undef的话,在@1中就会重复的包含下面的代码 #undef IPC_MESSAGE_MACROS_ENUMS //.... //定义枚举消息类型 #define IPC_BEGIN_MESSAGES(label) enum label##MsgType { label##Start = label##MsgStart << 12, label##PreStart = (label##MsgStart << 12) - 1, ///...... #define IPC_END_MESSAGES(label) label##End }; //.... #elif defined(IPC_MESSAGE_MACROS_LOG) //消息日志类定义 #undef IPC_MESSAGE_MACROS_LOG //.... //定义日志消息类型 #define IPC_BEGIN_MESSAGES(label) void label##MsgLog(uint16 type, std::wstring* name, const IPC::Message* msg, std::wstring* params) { switch (type) { #define IPC_END_MESSAGES(label) default: if (name) *name = L"[UNKNOWN " L ## #label L" MSG"; } } class LoggerRegisterHelper##label { public: LoggerRegisterHelper##label() { g_log_function_mapping[label##MsgStart] = label##MsgLog; } }; LoggerRegisterHelper##label g_LoggerRegisterHelper##label; //.... #elif defined(IPC_MESSAGE_MACROS_CLASSES) //消息类定义 #undef IPC_MESSAGE_MACROS_CLASSES // .... //定义消息类 #define IPC_BEGIN_MESSAGES(label) #define IPC_END_MESSAGES(label) // 定义消息类,枚举msg_class__ID在IPC_MESSAGE_MACROS_ENUMS 中的IPC_MESSAGE_CONTROL0定义 // 定义的这个类继承自IPC::Message,初始化构造的时候传入该消息的ID #define IPC_MESSAGE_CONTROL0(msg_class) class msg_class : public IPC::Message { public: enum { ID = msg_class##__ID }; msg_class() : IPC::Message(MSG_ROUTING_CONTROL, ID, PRIORITY_NORMAL) {} }; //.... #endif
上面代码IPCMESSAGEMACROSINCLUDEBLOCK中我们可以看到也多次include了用户自定义的文件MESSAGESITERNALFILE,上面的作用实际上就是*方便用户定义自己的消息类型,通过编写一个头文件就能一次性同时定义消息的枚举类型,实际的消息类*
参看自定义的消息头文件MESSAGESINTERNALFILE以及添加的注释可以大致了解如何通过多次嵌套包含头文件同时实现消息枚举类型与类:
#include "chrome/common/ipc_message_macros.h" // 下面两次(或者3次)宏召开中,定义enum时在IPC_BEGIN_MESSAGES用到一个枚举 //变量TestMsgStart 在ipc_message_utils.h中定义 //ipc_enum 宏展开 /* enum TestMsgType { TestStart = TestMsgStart << 12, TestPreStart = (TestMsgStart << 12) - 1, SyncChannelTestMsg_NoArgs__ID, SyncChannelTestMsg_AnswerToLife__ID, SyncChannelTestMsg_Double__ID, Msg_C_0_1__ID, ...., Msg_R_3_2__ID, Msg_R_3_3__ID, TestEnd }; */ //ipc_classes宏展开 /* class SyncChannelTestMsg_NoArgs{...}; class SyncChannelTestMsg_AnswerToLife{...}; ... class Msg_C_0_1 : public IPC::MessageWithReply<Tuple0, Tuple1<bool&> > { public: enum { ID = Msg_C_0_1__ID }; //看TestMsgType Msg_C_0_1(bool* arg1) : IPC::MessageWithReply<Tuple0, Tuple1<bool&> >( MSG_ROUTING_CONTROL, ID, MakeTuple(), MakeRefTuple(*arg1)) {} }; .... class Msg_R_3_2{...}; class Msg_R_3_3{...}; */ /* SYNC消息机制 消息映射宏的形式为IPC_SYNC_MESSAGE_CONTROL$(IN)_$(OUT),表示的是同步消息, 意思是:当@A发送消息a给@B,@B接收到a之后,需要返回一个消息b给@A。 其中宏中的$IN和$OUT分别表示输入参数的个数 以及输出参数的个数,通过宏展开我们可以知道 */ IPC_BEGIN_MESSAGES(Test) IPC_SYNC_MESSAGE_CONTROL0_0(SyncChannelTestMsg_NoArgs) IPC_SYNC_MESSAGE_CONTROL0_1(SyncChannelTestMsg_AnswerToLife, int /* answer */) IPC_SYNC_MESSAGE_CONTROL1_1(SyncChannelTestMsg_Double, int /* in */, int /* out */) // out1 is false IPC_SYNC_MESSAGE_CONTROL0_1(Msg_C_0_1, bool) //经过ipc_enum宏展开变为 //Msg_C_0_1__ID //经过ipc_class宏展开变为 /******************************************************************* class Msg_C_0_1 : public IPC::MessageWithReply<Tuple0, Tuple1<bool&> > { public: enum { ID = Msg_C_0_1__ID }; Msg_C_0_1(bool* arg1) : IPC::MessageWithReply<Tuple0, Tuple1<bool&> >( MSG_ROUTING_CONTROL, ID, MakeTuple(), MakeRefTuple(*arg1)) {} }; *******************************************************************/ // out1 is true, out2 is 2 IPC_SYNC_MESSAGE_CONTROL0_2(Msg_C_0_2, bool, int)
这里定义的消息类有连个模板参数,一个输入参数tuple,与输出参数tuple,通过这样区分就可以实现消息分派处理函数中输入参数与输出参数,在测试用例中非指针参数就是输入参数,指针参数就是输出参数
消息的接收与应答
TestMessageReceiver::OnMessageReceived在接收到一个消息之后,通过IPCBEGINMESSAGEMAP与IPCENDMESSAGEMAP定义的消息处理宏会将消息分派到相依的消息处理函数中,第一个消息宏
IPC_MESSAGE_HANDLER(Msg_C_0_1, On_0_1)//在ipc_enum中定义
我们进一步了解如何处理接收到的消息,上面定义的宏经过展开变为如下的代码:
{ typedef TestMessageReceiver _IpcMessageHandlerClass; const IPC::Message& ipc_message__ = msg; bool msg_is_ok__ = true; switch (ipc_message__.type()) { case Msg_C_0_1::ID: msg_is_ok__ = Msg_C_0_1::Dispatch(&ipc_message__, this, &On_0_1); break; .... DCHECK(msg_is_ok__); } }
可以看到在接收到消息MsgC01::ID之后调用了MsgC01::Dispatch函数, MsgC01继承自头文件chrome/common/ipcmessageutils.h中定义的消息类
template <class SendParamType/*输入参数*/, class ReplyParamType/*输出参数*/> class MessageWithReply : public SyncMessage { public: typedef SendParamType SendParam; typedef typename SendParam::ParamTuple RefSendParam; typedef ReplyParamType ReplyParam; //..... static bool Dispatch(const Message* msg, T* obj, Method func) { SendParam send_params; void* iter = GetDataIterator(msg); Message* reply = GenerateReply(msg);// bool error; if (ReadParam(msg, &iter, &send_params)) {//读取输入参数tuple typename ReplyParam::ValueTuple reply_params; DispatchToMethod(obj, func, send_params, &reply_params);//函数重载与模板特化 WriteParam(reply, reply_params);//将tuple写入msg error = false; #ifdef IPC_MESSAGE_LOG_ENABLED if (msg->received_time() != 0) { std::wstring output_params; LogParam(reply_params, &output_params); msg->set_output_params(output_params); } #endif } else { NOTREACHED() << "Error deserializing message " << msg->type(); reply->set_reply_error(); error = true; } obj->Send(reply);//调用TestMessageReceiver::Send return !error; } }
在上面的过程大致是:
- 读入输入参数存放到tuple sendparams中,比如测试用例中On11的输入参数Tuple<int>
- 调用模板特化函数DispatchToMethod
该函数有一系列的模板特化类型,如果接受到的消息类型为MsgC01,那么该特化函数版本为
template<class ObjT, class Method, class OutA> inline void DispatchToMethod(ObjT* obj, Method method, const Tuple0& in, Tuple1<OutA>* out) { (obj->*method)(&out->a); }
这里的代码 (obj->*method)(&out->a)调用的就是TestMessageReceiver::On01, 如果接受的消息是On11,那么该函数的特化版本为:
template<class ObjT, class Method, class InA, class OutA> inline void DispatchToMethod(ObjT* obj, Method method, const Tuple1<InA>& in, Tuple1<OutA>* out) { (obj->*method)(in.a, &out->a); }
这里的代码 (obj->*method)(in.a, &out->a)调用的就是TestMessageReceiver::On11,
- 得到结果,调用obj->Send将结果返回给远端
通过上面代码可看到,调用完用户自定义消息处理函数后,得到输出参数tuple replyparam,用户自定义的消息处理函数如On11中的输出参数(指针参数)写入的指针变量指向的内存地址实际就是replyparam, 得到出参数之后通过模板特化函数WriteParam将该replayparam写入Message(继承自Pickle),然后调用OnMessageReceived::Send将消息处理结果返回给远端。
结束
阅读chromium中消息的接收、处理以及将结果返回给远端的代码,可以返现里面大量使用到了宏预处理、函数重载、模板特化。
- 用户自定义消息,然后chromium通过一个头文件chrome/common/ipcmessagemacros.h嵌套包含自己以及用户自定义消息头文件,这样就同时了消息的枚举类型与类。
- 在自定义消息中将输出参数与输出参数分离成两个tuple,在接收到远端消息的时候读取输入参数tuple,构造一个临时变量输出参数tuple
- 将输入tuple与输出tuple讲过函数DispatchToMethod传递给用户的消息处理函数
- 用户消息处理函数结束后,用户返回的输出参数写入到了前面构造的临时变量输出参数tuple中,将tupel序列化写入Message然后调用用户的Send函数将输出参数发送给远端