一、问题。
这段时间考虑实现一个纯C++的分布式服务包装库,简要描述如下:
有如下类和函数:
{
void test1 ( /* in */ int v1, /* in */ int * v2);
int test2 ( /* in */ int & v1, /* out */ int * v2);
};
int test_func ( /* in */ int * v1, /* inout */ string * v2);
想把它们作为服务发布出去,以SOAP或其它方式。发布为一个TestService,并让它携带多一些信息:
{
void test1 ( in < int > v1, in < int > v2);
int test2 ( in < int > v1, out < int > v2);
int test_func ( in < int > v1, inout < string > v2);
};
C++有许多工具、库来做到这点,但是,都需要生成一堆代码,很是不爽。
其它语言,比如python, java, c#等,都可以通过自省机制,抛开IDL在语言内实现。
C++并非不能做这个,它只是缺少足够的类型信息。比如上面的例子,如果要发布为服务,那么至少应该把它的参数、返回值搞得明确些,否则要么会造成不必要的参数传递,要么会产生错误(把OUT参数取值可不是安全的)。
比如上面出现的int, int&, int*,在作为in参数时,我们是想传递它的值,类型为int。而int*和string*作为out参数时,我们想让它传递指针或引用,当调用返回时,我们给它赋值。
C++语言的类型极为丰富,却没有描述一个参数到底是in还是out。java也没有,但它可以正常序列化一个null值,在C++中,这可能存在一些麻烦。
再考虑一下char*类型,假如它是in参数,那么它是要传递一个字符还是一个字符串?C++语言没有对它进行描述。
所以要实现一个分布式服务包装(或代理)库,必须让发布者提供这些信息。
我们知道,要查询一个远程服务,必须查询相应主机端口,获取服务信息。最简单的服务信息包括:服务列表,每个服务中的方法列表,方法的类型(包括参数和返回值类型,in/out信息等)。
实际上,我们是要为C++增加一些简单的自省能力。上面那个服务发布接口,实际上离这个要求还有很远,再来看一下:
{
void test1 ( in < int > v1, in < int > v2);
int test2 ( in < int > v1, out < int > v2);
int test_func ( in < int > v1, inout < string > v2);
};
可以想见,它是没有一点自省能力的,我们如何向它查询,它的名字?它的方法列表?方法的类型?它如何与Test类的成员函数以及test_func函数关联?
二、方向。
要让上面那个服务具有自省能力,要做的扩充其实并不多。考虑下面的代码:
{
TestService ();
Method < void ( in < int > , in < int > ) > test1;
Method < int ( in < int > , out < int > ) > test2;
Method < int ( in < int > , inout < string > ) test_func;
};
这几个Method可以用自己写的委托类来做。
1、假如我们在TestService的构造函数里给它分配一个“TestService”名字,并且Service类实现了查询名字的接口,那么它就知道它自己的名字了。
2、假如在TestService的构造函数里为各个Method分配名字,并且注册到TestService,那么它就能够查询方法列表。
3、方法的类型?通过模板方式,把各个参数类型收集起来,给个字符串名称就可以了。
使用宏来实现,大概可以写成这样:
METHOD ( void ( in < int > , in < int > ), test1, & Test::test1)
METHOD ( int ( in < int > , out < int > ), test2, & Test::test2)
METHOD ( int < in < int > , inout < string > ), test_func, test_func)
END_SERVICE ()
通过上面这几个宏,我们能够生成TestService声明。
不过,有几个问题,罗列如下,并一一解决它:
1、如何把函数指针传给它?如何把方法名称传给它?
这个只是C++语言为我们增加了一些麻烦,我们无法在定义成员的地方调用它的构造函数,不过这并不会造成多大障碍。
上面的METHOD宏如果只是生成类的声明,那么函数指针可以省略。我把它加上的原因是,它可以被我用Ctrl+C, Ctrl+V这种世界上最先进的技术原样拷贝下来,并且通过简单修改的方法实现这种世界上最先进的重用。
上面的代码经过修改,结果就成这样:
METHOD ( void ( in < int > , in < int > ), test1, & Test::test1)
METHOD ( int ( in < int > , out < int > ), test2, & Test::test2)
METHOD ( int < in < int > , inout < string > ), test_func, test_func)
BEGIN_DEFINE (TestService)
METHOD_DEFINE ( void ( in < int > , in < int > ), test1, & Test::test1)
METHOD_DEFINE( int ( in < int > , out < int > ), test2, & Test::test2)
METHOD_DEFINE( int( in < int > , inout < string > ), test_func, test_func)
END_DEFINE ()
END_SERVICE ()
看上去对应得非常整齐,修改起来也比较简单。上面那部分被扩充为如下代码:
{
Method < void ( in < int > , in < int > ) > test1;
Method < int ( in < int > , out < int > ) > test2;
Method < int ( in < int > , inout < string > ) test_func;
TestService ()
: Service ( " TestService " )
{
test1.setName ( " test1 " );
test1.setMethod ( & Test::test1);
this -> registerMethod ( & test1);
test2.setName ( " test2 " );
test2.setMethod ( & Test::test2);
this -> registerMethod ( & test2);
test_func.setName ( " test_func " );
test_func.setMethod (test_func);
this -> registerMethod ( & test3);
}
};
基本上需要的东西都在这里了。
2、客户端的问题。
上面这种映射,直接拿到客户端会有问题,Test类和test_func函数我们并不打算交给客户端,所以使用函数指针会出现链接错误。
实际上客户端不需要这个,我们想办法把它拿掉就行了。客户端实际需要生成的代码如下:
{
Method < void ( in < int > , in < int > ) > test1;
Method < int ( in < int > , out < int > ) > test2;
Method < int ( in < int > , inout < string > ) test_func;
TestService ()
: Service ( " TestService " )
{
test1.setName ( " test1 " );
this -> registerMethod ( & test1);
test2.setName ( " test2 " );
this -> registerMethod ( & test2);
test_func.setName ( " test_func " );
this -> registerMethod ( & test3);
}
};
还是上面提到的,C++给我们带来的麻烦。这次需要另一组宏来完成它:
METHOD_D ( void ( in < int > , in < int > ), test1)
METHOD_D ( int ( in < int > , out < int > ), test2)
METHOD_D ( int ( in < int > , inout < string > ), test_func)
BEGIN_DEFINE_D (TestService)
METHOD_DEFINE_D ( void ( in < int > , in < int > ), test1)
METHOD_DEFINE_D( int ( in < int > , out < int > ), test2)
METHOD_DEFINE_D( int ( in < int > , inout < string > ), test_func)
END_DEFINE_D ()
END_SERVICE_D ()
METHOD*和METHOD_DEFINE*宏的参数都有一些多余的信息,没有去掉是因为放在一起容易看到写错的地方。(这个技巧来源于前几天看的一篇BLOG,很报歉没有记下地址)
3、使用的问题。
如何才能比较方便地使用?我考虑了下面这种方式:
struct IProxy;
template < class T >
struct SOAPProxy;
SOAPProxy < TestService > service;
service.connect ( 5000 , " localhost " );
int a = 0 ;
int * n = & a;
service.test1 ( 3 , n);
service.test1 ( 3 , * n);
service.test2 ( 3 , n);
service.test2 ( 3 , * n);
service.test2 ( 3 , NONE);
//
Method::operator ()的各个参数都将可以接受相容的类型,像上面一样,因为在TestService中我们已经定义了它要传输的值的类型。
a.NONE是什么?其实是为异步调用考虑的。假如指定某个OUT参数为NONE,则这个参数的值并不真正的OUT,而是保存在Method中。实际上Method中保存每个参数的值。
b.Method与Service如何发生关系?
从TestService的定义中我们知道,Method向Service注册自己以实现自省,但它同时也会保存Service的指向。
我们的Proxy实际上是一个继承模板,上面并没有把它指出来。它的定义是:
class XProxy : public T
{
};
所以我们的TestService其实也是模板类,它将使用XProxy中定义的序列化类。XProxy将实现Service基类中序列化虚函数以及调用虚函数。
当一个Method调用时,它会调用Service的序列化,由于被重写了,所以调用的是XProxy中的序列化方法。这个方法会把这个Method的各in/inout参数序列化,然后执行远程调用,再把调用结果反序列化给inout/out参数。
4、其它想法。
在考虑上面的定义方式时,我也考虑了其它方式,主要是返回值处理的方法,简述如下。
前面我们假设了一段将被开放为远程服务的代码:
{
void test1 ( /* in */ int v1, /* in */ int * v2);
int test2 ( /* in */ int & v1, /* out */ int * v2);
};
int test_func ( /* in */ int * v1, /* inout */ string * v2);
在前面的做法中,我们的服务描述是放在那一组宏里面,好处是不用改这段代码,坏处就是代码定义的地方和描述不在一起,协调可能会有一些不便。
我也考虑了另一种做法:
{
idl < void ( in < int > , in < int > ) > test1 ( int v1, int * v2);
idl < int ( in < int > , out < int > ) > test2 ( int & v1, int * v2);
};
idl < int ( in < int > , inout < string > ) test_func int * v1, string * v2);
对于实现代码,只需要修改返回值为void的函数,把return;修改为return VOID;,并且为没有写此语句的分支加上此句。
VOID是一个特殊类型的静态变量,专为void返回值的函数设定。
这种做法修改了原有的代码,不过在定义服务时可以节省一些工作:
METHOD (test1, & Test::test1)
METHOD (test2, & Test::test2)
METHOD (test_func, test_func)
BEGIN_DEFINE (TestService)
METHOD_DEFINE (test1, & Test::test1)
METHOD_DEFINE (test2, & Test::test2)
METHOD_DEFINE (test_func, test_func)
END_DEFINE ()
END_SERVICE ()
它所需要的函数类型,将由函数指针推导。
在G++编译器下,可以使用typeof来获得函数指针的类型而不需要真得获得函数指针值,不过目前仅仅在G++下可用。(顺便说一下,typeof已经列入c++0x)
最终我放弃了这个想法,毕竟它要修改现有的代码,某些情况下这是不可能的,而且typeof目前也不能跨编译器。
三、实现。
老实说我现在还没有一份完整的或半完整的实现,大部分想法还在头脑中,测试代码倒是写了不少,主要是用来测试上述想法能否实现,我想大部分情况都已经测试了,只需要有时间来把它实现出来。
这是我近期要做的事之一,争取月内把它做完罢。