問題
我正做的一個項目需要在Erlang 節點和C++ 節點之間傳輸大量的事件,在C++這一側,使用struct儲存這些消息。
自然的,我需要很多struct,例如:
struct msg_greeting{
std::string sender;
std::string content;
int content_length;
std::string content_type;
};
struct msg_bye{
std::string sender;
};
在Erlang這一側,使用tuple儲存,由於Erlang是動態類型的,所以不需要定義,這裡只是說明:
view sourceprint?1 {greeting, Sender ::string(), Content ::string(), ContentLength ::int(), ContentType ::atom() }
2 {bye, Sender ::string() }
消息的傳輸可以使用tinch_pp (http://www.adampetersen.se/code/tinchpp.htm)
如果你第一次使用tinch_pp,下面這一段是一個簡單的接收和匹配的過程,即使不了解tinch_pp也可以看懂:
void connect::msg_receiver_routine()
{
try{
while(1) {
matchable_ptr msg = mbox->receive();
int token;
std::string type;
matchable_ptr body;
if(msg->match(
make_e_tuple(atom("event"),
e_string(&type)),
any(&body)))
//do something here
else
//some log here
}
}catch(boost::thread_interrupted e){
// @todo output some log here
}
};
我們使用event標識一個erlang事件,type是這個事件的類型,body是事件內容,也就是我們之前定義的greeting或者bye。
接下來,我們需要實現事件的處理,首先,我們需要把tinch_pp匹配出來的tuple填入我們的c++結構。
我們這樣做:
msg_ptr on_greeting(matchable_ptr p){
std::string sender;
std::string content;
int contentLength;
std::string contentType;
bool matched = p->match(make_e_tuple(
erl::string(&sender),
erl::string(&content),
erl::int_(&contentLength),
erl::atom(&contentType)
));
if(matched){
msg_ptr = shared_ptr<msg_greeting>(new msg_greeting());
msg_ptr->Sender = sender;
msg_ptr->Content = content;
msg_ptr->ContentLength = contentLength;
msg_ptr->ContentType = contentType;
return msg_ptr;
}
return shared_ptr<msg_greeting>();
}
問題在於,我們需要為每個消息寫這麼一大段代碼。假如我們的C Node需要處理幾十種消息,我們就需要把這個代碼重復幾十遍,而實際上只有一小部分才是有用的(有差異的)。
提取通用代碼
怎樣才能省去重復的部分,只保留其中的精華呢?這就需要元編程和預處理器了,我們稍後再介紹。
首先,最顯著的差異就是不同的消息中的信息不一樣,用c++的說法是:他們具有不同的成員。
去掉這個差異後,我們的代碼可以簡化為:
msg_ptr on_greeting(matchable_ptr p){
if(matched){
msg_ptr mp = msg_greeting::make(p);
return mp;
}
return shared_ptr<msg_greeting>();
}
看似簡潔了許多,但實際上,我們只是把msg_greeting特有的處理(差異)隱藏在msg_greeting定義的靜態方法裡了。
至少,我們的on_xxxx方法看起來干淨點了。
但是,我們還是需要在某處(msg_greeting內部)定義這些代碼。
更好的方案
反射是很多語言都具有的特性,反射意味著類具有了自省的能力,即一個類知道自己有哪些成員,這些成員有哪些屬性。
如果C++支持反射,我們這個問題就好解決了,我們可以定義一個msg_fill方法,按照msg成員的屬性,從matchable_ptr獲取成員的值。等等,C++可沒有反射支持,至少我不知道。
那麼,我們自己來實現一個吧。
成員屬性
我們需要一個能保存成員屬性的,符合C++語法的物件。有兩種選擇:對象,類型。
對象對應著運行時,類型對應著編譯時。考慮到速度和效率,我們選擇類型。
在C++進行元編程,主要是依靠模板來實現的,首先我們聲明一個模板,用來表示成員
template <class Type ,class Struct, Type(Struct::*Field)>
struct auto_field;
這個模板有三個參數:Type表示成員的C++類型,Struct表示這個成員所屬的結構,Field是成員指針,用來記住這個成員在所屬結構中所處的位置。
光有聲明沒有什麼作用,所以我們需要一些實現(或者說模板定義):
template <class Struct, bool(Struct::*Field)>
struct auto_field<bool, Struct, Field>{
typedef tinch_pp::erl::atom field_e_type;
typedef std::string field_c_type;
static void fill(Struct* s, field_c_type& c){
s->*Field = (c == "true");
};
};
可以看出,我們通過模板特化,為bool類型的成員提供了:
C++類型
Erlang類型
填充C++類型的fill方法
這裡其實隱藏了一個問題,怎麼知道需要定義這幾個類型和靜態成員函數呢?稍後再介紹。
類似的,我們可以為更多的類型提供特化,不再重復。
至此,我們已經知道怎麼定義類型成員,並記住成員的屬性。
填充數據
有了成員的屬性,我們就可以解析消息tuple了,參考最初的代碼,填充方法的偽實現應該長這樣:
template <class Msg>
bool fill(Msg* e){
field_0_c_type field_0_c;
field_1_c_type field_1_c;
field_2_c_type field_2_c;
bool matched = p->match(make_e_tuple(
field_0_e_type(&field_0_c),
field_1_e_type(&field_1_c),
field_2_e_type(&field_2_c)
));
if(matched){
Event::fill(e,field_0_c);
Event::fill(e,field_1_c);
Event::fill(e,field_2_c);
return true;
}
return false;
};
到此,我們發現不同事件的成員數目是不同的,所以,上述偽代碼只能適應成員數為3的消息。
那麼,我們就需要提供一組fill實現,每個負責一個成員數。同樣,使用模板參數和模板特化來實現:
template <int Size,class Msg>
bool fill(Msg* e);
template <class Msg>
bool fill<1,Msg>(Msg* e){
field_0_c_type field_0_c;
bool matched = p->match(make_e_tuple(
field_0_e_type(&field_0_c)
));
if(matched){
Event::fill(e,field_0_c);
return true;
}
return false;
};
template <class Msg>
bool fill<2,Msg>(Msg* e)
field_0_c_type field_0_c;
field_1_c_type field_1_c;
......
額~ 這不是又重復了嗎?
別急,我們可以用boost::preprocess收斂這些實現,boost::preprocess用來生成重復的代碼,
使用後,我們的fill方法長這樣:
namespace aux {
template <int FieldListSize,typename FieldList>
struct fill_impl;
#define EMATCH_MAX_FIELD 8
#define BOOST_PP_ITERATION_LIMITS (1,8)
#define BOOST_PP_FILENAME_1 <e_match_impl.h>
#include BOOST_PP_ITERATE()
};
template<typename FieldList>
struct fill : aux::fill_impl<boost::mpl::size<FieldList>::type::value , FieldList>{
};
怎麼回事?fill方法消失了?
不,並沒有消失,我們把他隱藏在
e_match_impl.h
這個文件裡,通過boost::preprocess重復include這個文件8次,從而獲得1個到8個成員的fill實現。並通過集成把這個實現模板提供的功能暴露出來,同時收斂其模板參數。
至此,我們得到了一個可以根據FieldList(成員屬性的mpl::list),自動match和填充C++結構的fill方法。
使用
好了,我們來寫一段代碼,測試一下上述實現吧:
struct SimpleObject{
bool b;
std::string s;
};
typedef boost::mpl::list<
auto_field<bool, SimpleObject, &SimpleObject::b>,
auto_field<std::string, SimpleObject, &SimpleObject::s>
> SimpleObjectFields;
int _tmain(int argc, _TCHAR* argv[])
{
SimpleObject so;
const std::string remote_node_name("[email protected]");
const std::string to_name("reflect_msg");
tinch_pp::node_ptr my_node = tinch_pp::node::create("[email protected]", "abcdef");
tinch_pp::mailbox_ptr mbox = my_node->create_mailbox();
mbox->send(to_name, remote_node_name, tinch_pp::erl::make_e_tuple(tinch_pp::erl::atom("echo"), tinch_pp::erl::pid(mbox->self()), tinch_pp::erl::make_e_tuple(
tinch_pp::erl::make_atom("false"),
tinch_pp::erl::make_string("hello c++")
)));
const tinch_pp::matchable_ptr reply = mbox->receive();
bool ret = fill<SimpleObjectFields>::fill_on_match(&so,reply);
printf("ret is %s \n",(ret?"true":"false"));
printf("so.b == %s \n",(so.b?"true":"false"));
printf("so.s == %s \n",so.s.c_str());
system("pause");
return 0;
}
由於我沒有找到tinch_pp怎麼構造一個matchable_ptr,所以需要一個erlang的外部節點把我構造的tuple反射回來,tinch_pp已經提供了這樣的一個server,運行上述代碼前,需要先把他啟動起來:
view sourceprint?1 werl -pa . -sname testnode -setcookie abcdef
運行後,應該打印出:
ret is true
so.b == false
so.s == hello c++
請按任意鍵繼續. . .
至此,我們實現了想要的功能,使用同一份代碼(fill)將Erlang tuple直接填充到指定的C++結構中,而不必大量重復填充代碼。