一、问题。
这段时间考虑实现一个纯C++的分布式服务包装库,简要描述如下:
有如下类和函数:
struct Test
{
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,并让它携带多一些信息:
struct 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++增加一些简单的自省能力。上面那个服务发布接口,实际上离这个要求还有很远,再来看一下:
struct 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);
};
可以想见,它是没有一点自省能力的,我们如何向它查询,它的名字?它的方法列表?方法的类型?它如何与Test类的成员函数以及test_func函数关联?
二、方向。
要让上面那个服务具有自省能力,要做的扩充其实并不多。考虑下面的代码:
struct TestService : public Service
{
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、方法的类型?通过模板方式,把各个参数类型收集起来,给个字符串名称就可以了。
使用宏来实现,大概可以写成这样:
BEGIN_SERVICE (TestService)
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这种世界上最先进的技术原样拷贝下来,并且通过简单修改的方法实现这种世界上最先进的重用。
上面的代码经过修改,结果就成这样:
BEGIN_SERVICE (TestService)
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 ()
看上去对应得非常整齐,修改起来也比较简单。上面那部分被扩充为如下代码:
struct TestService : public 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函数我们并不打算交给客户端,所以使用函数指针会出现链接错误。
实际上客户端不需要这个,我们想办法把它拿掉就行了。客户端实际需要生成的代码如下:
struct TestService : public 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");
this->registerMethod (&test1);
test2.setName ("test2");
this->registerMethod (&test2);
test_func.setName ("test_func");
this->registerMethod (&test3);
}
};
还是上面提到的,C++给我们带来的麻烦。这次需要另一组宏来完成它:
BEGIN_SERVICE_D (TestService)
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、使用的问题。
如何才能比较方便地使用?我考虑了下面这种方式:
template <class T>
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实际上是一个继承模板,上面并没有把它指出来。它的定义是:
template <class T>
class XProxy : public T
{
//
};
所以我们的TestService其实也是模板类,它将使用XProxy中定义的序列化类。XProxy将实现Service基类中序列化虚函数以及调用虚函数。
当一个Method调用时,它会调用Service的序列化,由于被重写了,所以调用的是XProxy中的序列化方法。这个方法会把这个Method的各in/inout参数序列化,然后执行远程调用,再把调用结果反序列化给inout/out参数。
4、其它想法。
在考虑上面的定义方式时,我也考虑了其它方式,主要是返回值处理的方法,简述如下。
前面我们假设了一段将被开放为远程服务的代码:
struct Test
{
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);
在前面的做法中,我们的服务描述是放在那一组宏里面,好处是不用改这段代码,坏处就是代码定义的地方和描述不在一起,协调可能会有一些不便。
我也考虑了另一种做法:
struct Test
{
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返回值的函数设定。
这种做法修改了原有的代码,不过在定义服务时可以节省一些工作:
BEGIN_SERVICE (TestService)
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目前也不能跨编译器。
三、实现。
老实说我现在还没有一份完整的或半完整的实现,大部分想法还在头脑中,测试代码倒是写了不少,主要是用来测试上述想法能否实现,我想大部分情况都已经测试了,只需要有时间来把它实现出来。
这是我近期要做的事之一,争取月内把它做完罢。