实现自己的Closure以使rpc的回调兼容所有可调用对象
前言
虽然kanon
早就实现了protobuf
的编解码器并在此基础上实现了基于protobuf的rpc模块以便进行rpc通信。但是我一直都挺拒绝使用这玩意,因为google::protobuf::RpcChannel
提供的CalllMethod()
签名如下:1
2
3
4
5void CallMethod(MethodDescriptor const *method,
RpcController *controller,
Message const *request,
Message *response,
Closure *done);
然而,protobuf实现的Closure
的派生类并不是类似std::function<>
那样可以包裹任何可调用对象(callable),想必是C++11
之前的产物,因为11已经有lambda expression
,std::bind
,std::functio
n可以包装任何回调了。
如果使用protobuf::protobuf::NewCallback()
(一般使用这个就够了),我们必须要另写一个函数,这对于使用和可读性都不好。
我的目的是让Closure
接受任何可调用对象,所幸Closure是个抽象基类,即接口类,所以我们可以在不修改protobuf源码的情况定制化自己想要的行为而不破坏任何兼容性。
(最艹的一点,我最开始没有注意这点结果修改的protobuf源码,重新编译是真的坐牢)
尝试
首先我们在不扩展原有接口的前提下,考虑用NewCallback()
包装std::function以支持各种可调用对象:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19inline void ProtobufRpcCallbackWrapper(std::function<void()> *fn)
{
(*fn)();
}
inline void ProtobufRpcCallbackWrapper2(std::function<void()> fn)
{
fn();
}
inline PROTOBUF::Closure *NewRpcCallback(std::function<void()> *fn)
{
return PROTOBUF::NewCallback(&ProtobufRpcCallbackWrapper, fn);
}
inline PROTOBUF::Closure *NewRpcCallback(std::function<void()> fn)
{
return PROTOBUF::NewCallback(&ProtobufRpcCallbackWrapper2, std::move(fn));
}
测试了一下,工作正常,粗看感觉符合要求。但是这个问题其实挺大的。
首先第一,需要调用两次,一次是wrapper的函数指针,另一次是包裹的std::function,这个会造成一定的性能受损(不然你以为为什么虚函数遭人指点性能)。
另一个问题我们得先看下NewCallback()的源码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28template <typename Arg1>
inline Closure* NewCallback(void (*function)(Arg1),
Arg1 arg1) {
return new internal::FunctionClosure1<Arg1>(function, true, arg1);
}
template <typename Arg1>
class FunctionClosure1 : public Closure {
public:
typedef void (*FunctionType)(Arg1 arg1);
FunctionClosure1(FunctionType function, bool self_deleting,
Arg1 arg1)
: function_(function), self_deleting_(self_deleting),
arg1_(arg1) {}
~FunctionClosure1() {}
void Run() override {
bool needs_delete = self_deleting_; // read in case callback deletes
function_(arg1_);
if (needs_delete) delete this;
}
private:
FunctionType function_;
bool self_deleting_;
Arg1 arg1_;
};
一眼就能看出问题,一个引用都没有,而Arg1一般不会是引用,意味着构造基本都是拷贝构造。这对于包裹不可拷贝的可调用对象的std::function是不友好的。
对于可拷贝的可调用对象也是没必要的,因为可能我在外面是std::move()
移动进来的,然而里面又给我拷贝,这就太绝望了。
这也是为什么上面我还重载了std::function*
,但是即便如此,我们将面临两种选择:
- 先定义一个局部变量
std::function<void()> f = ...
,然后再引用过去 - 动态分配
而且如果该函数需要跨栈的话,那么动态分配是不可避的,但是我有必要动态分配两个吗?(internal::FunctionClosure1
和std::function)
各种不合理让我只想远离这玩意,因此只能另起灶炉。
实现Closure接口
我们先来看下Closure接口有哪些:1
2
3
4
5
6
7
8
9
10class PROTOBUF_EXPORT Closure {
public:
Closure() {}
virtual ~Closure();
virtual void Run() = 0;
private:
GOOGLE_DISALLOW_EVIL_CONSTRUCTORS(Closure);
};
可见,Closure只要求你实现Run()
罢了。
既然要接受任何可调用对象,那么写个std::function的wrapper就行了,即作为数据成员。
函数签名用void()
即可,因为可以捕获变量,这都是无所谓的。
1 | class Callable : public ::google::protobuf::Closure { |
相信有cpp11基础的读者不难理解,稍微说几个点。
- 为了兼容左右值,我采用的是值重载,因为std::function的实现用到了类型擦除,因此移动的开销就是两个指针的赋值,值重载是可以接受的。具体参考之前的博客。
self_delete_
是为了兼容protobuf的使用习惯,它们也支持自删除。
最后再包装下即可:1
2
3
4
5
6
7
8
9inline Closure *NewCallable(std::function<void()> callable)
{
return new internal::Callable(std::move(callable), true);
}
inline Closure *NewPermanentCallable(std::function<void()> callable)
{
return new internal::Callable(std::move(callable), false);
}
总结
在库允许实现接口类来定制化自己想要的效果的前提下,不应拘泥于原有接口。