前言

虽然kanon早就实现了protobuf的编解码器并在此基础上实现了基于protobuf的rpc模块以便进行rpc通信。但是我一直都挺拒绝使用这玩意,因为google::protobuf::RpcChannel提供的CalllMethod()签名如下:

1
2
3
4
5
void CallMethod(MethodDescriptor const *method,
RpcController *controller,
Message const *request,
Message *response,
Closure *done);

然而,protobuf实现的Closure的派生类并不是类似std::function<>那样可以包裹任何可调用对象(callable),想必是C++11之前的产物,因为11已经有lambda expressionstd::bindstd::function可以包装任何回调了。
如果使用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
19
inline 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
28
template <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
10
class PROTOBUF_EXPORT Closure {
public:
Closure() {}
virtual ~Closure();

virtual void Run() = 0;

private:
GOOGLE_DISALLOW_EVIL_CONSTRUCTORS(Closure);
};

可见,Closure只要求你实现Run()罢了。
既然要接受任何可调用对象,那么写个std::function的wrapper就行了,即作为数据成员。
函数签名用void()即可,因为可以捕获变量,这都是无所谓的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Callable : public ::google::protobuf::Closure {
using callable_t = std::function<void()>;

public:
Callable(callable_t c, bool d)
: callable_(std::move(c))
, self_delete_(d)
{
}

virtual ~Callable() override = default;

void Run() override
{
callable_();
if (self_delete_) delete this;
}

private:
callable_t callable_;
bool self_delete_;
};

相信有cpp11基础的读者不难理解,稍微说几个点。

  • 为了兼容左右值,我采用的是值重载,因为std::function的实现用到了类型擦除,因此移动的开销就是两个指针的赋值,值重载是可以接受的。具体参考之前的博客
  • self_delete_是为了兼容protobuf的使用习惯,它们也支持自删除。

最后再包装下即可:

1
2
3
4
5
6
7
8
9
inline 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);
}

总结

在库允许实现接口类来定制化自己想要的效果的前提下,不应拘泥于原有接口。