1. 背景

在项目开发中时常会遇到需要多个进程间交互/通信的场景,进程间通信(IPC)的方式有很多,比如文件、注册表、网络、管道、共享内存等。

对于简单的交互场景,我们可以随意选择一种合适的方式,如 Google Chrome 使用的是管道的方式。

但在交互场景复杂的情况下,远程过程调用(RPC)的方式则会更新便捷。

目前开源的、功能相对完善的 C++ RPC 框架都是基于网络方式实现的,这种方式存在服务端和客户端的概念,两端相互调用需要各方都启动一个端口监听服务,既然需要监听端口,那就会存在端口被占用的问题,特别是在 Windows 上还会存在端口假可用的问题,端口虽然监听成功,但客户端仍然无法连接的情况。

我一直想找到一个基于共享内存实现的、跨平台的 C++ RPC 框架,但遗憾的是一直没能结缘,于是我决定烹饪一个。

之所以取名为“Veigar”,因为该词来源于英雄联盟里面的“邪恶小法师-维迦”,我的开源项目取名也大多来自该游戏。

项目地址:

https://github.com/winsoft666/veigar

目前该项目已经在多个 Windows 客户端产品中得到应用,Linux 平台也经过了测试。

2. 特性

  • 基于共享内存技术实现。

  • 支持 Windows、Linux 平台。

  • 可以将任何函数暴露给调用方(不限语言,只要实现 msgpack-rpc 即可)。

  • 任何语言编写的程序都可以调用被暴露的函数。

  • 不需要学习 IDL 语言。

  • 不需要添加额外的代码生成步骤,仅需要 C++ 代码。

  • 没有服务端和客户端的概念,每个 Veigar 实例间都可以相互调用。

  • 没有网络问题,如端口占用、半关闭状态等。

  • 没有诡异的端口假可用性问题(特别是在 Windows 系统上)。

3. 编译

虽然Veigar的底层是基于msgpack实现的,但已经将其包含到项目中,不需要额外编译和安装msgpack

虽然在veigar公共头文件引用了msgpack头文件,但这不会污染您的全局msgpack命名空间,因为Veigar中的msgpack命令空间为veigar_msgpack

Veigar仅支持编译为静态库。

可以使用CMake进行编译构建,也可以使用vcpkg进行安装,如:

1
vcpkg install veigar

4. 快速上手

在使用Veigar时,仅需要在项目中包含include目录,并链接静态库即可。

4.1 同步调用

下面是一个同步调用的示例:

本示例为了使代码更加简洁,没有对函数返回值进行校验,请在实际使用中不要这样做!

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
29
30
31
32
33
34
#include <iostream>
#include "veigar/veigar.h"

int main(int argc, char** argv) {
if (argc != 3) {
return 1;
}

std::string channelName = argv[1];
std::string targetChannelName = argv[2];

veigar::Veigar vg;

vg.bind("echo", [](const std::string& msg, int i, double d, std::vector<uint8_t> buf) {
std::string result;
// ...
return result;
});

vg.init(channelName);

std::vector<uint8_t> buf;
veigar::CallResult ret = vg.syncCall(targetChannelName, 100, "echo", "hello", 12, 3.14, buf);
if (ret.isSuccess()) {
std::cout << ret.obj.get().as<std::string>() << std::endl;
}
else {
std::cout << ret.errorMessage << std::endl;
}

vg.uninit();

return 0;
}

每个Veigar实例有一个在本机范围内唯一的通道名称(Channel),在调用init函数时需要为Veigar指定通道名称,Veigar不会检测通道的唯一性,需要由调用者来保证通道名称的唯一性。

在上述示例中,需要通过命令行参数指定当前实例的通道名称和目标实例的通道名称,如:

1
sample.exe myself other

每个实例都绑定了名为echo的函数,该函数简单的原样返回msg参数字符串。

通过为syncCall函数指定“目标通道名称”、“函数名称”、“函数参数”及“超时毫秒数”就可以同步调用目标函数并得到调用结果。

4.2 拒绝异常

我不喜欢异常,因此Veigar也不会通过异常的形式来抛出错误,Veigar会主动捕获所有C++标准库、msgpack、boost异常,以返回值的形式返回给调用者。当调用失败时(!ret.isSuccess()),errorMessage中存储的错误信息就可能是Veigar捕获的异常信息。

4.3 返回Promise的异步调用

使用asyncCall函数可以实现异步调用。

下面是返回Promise的异步调用示例:

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
//
// 与同步调用相同
// ...
std::vector<uint8_t> buf;
std::shared_ptr<veigar::AsyncCallResult> acr = vg.asyncCall(targetChannelName, "echo", "hello", 12, 3.14, buf);
if (acr->second.valid()) {
auto waitResult = acr->second.wait_for(std::chrono::milliseconds(100));
if (waitResult == std::future_status::timeout) {
// timeout
}
else {
veigar::CallResult ret = std::move(acr->second.get());
if(ret.isSuccess()) {
std::cout << ret.obj.get().as<std::string>() << std::endl;
}
else {
std::cout << ret.errorMessage << std::endl;
}
}
}

vg.releaseCall(acr->first);

//
// 与同步调用相同
// ...

与同步调用不同,asyncCall函数返回的是std::shared_ptr<veigar::AsyncCallResult>,而且调用者在获取到CallResult或不再关系调用结果时,需要调用releaseCall函数释放资源。

4.4 基于回调函数的异步调用

使用asyncCall函数同样可以实现基于回调函数的异步调用。

下面是基于回调函数的异步调用示例:

1
2
3
4
5
6
7
8
9
10
std::vector<uint8_t> buf;
vg.asyncCall([](const veigar::CallResult& cr) {
if(cr.isSuccess()) {
std::cout << cr.obj.get().as<std::string>() << std::endl;
}
else {
std::cout << cr.errorMessage << std::endl;
}
}, targetChannelName, "echo", "hello", 12, 3.14, buf);

该方式不需要调用releaseCall函数释放资源。

4.5 RPC函数参数类型

支持常规的 C++ 数据类型,如:

  • bool
  • char, wchar_t
  • int, unsigned int, long, unsigned long, long long, unsigned long long
  • uint8_t, int8_t, int32_t, uint32_t, int64_t, uint64_t
  • float, double
1
2
3
4
veigar::Veigar vg;
vg.bind("func", [](char c, wchar_t w, int i, int8_t j, int64_t k) {
// ......
});

也支持如下 STL 数据类型:

  • std::string
  • std::set
  • std::vector
  • std::map
  • std::string_view (C++ 17)
  • std::optional (C++ 17)
  • 不支持 std::wstring,但是我们可以使用 std::vector 来代替 std::wstring
1
2
3
4
veigar::Veigar vg;
vg.bind("func", [](std::string s, std::vector<std::string>, std::string_view v, std::map<int, bool> m) {
// ......
});

也可以支持自定义数据类型,如:

1
2
3
4
5
6
7
8
9
10
11
12
#include "veigar/msgpack/adaptor/define.hpp"

struct MyPoint {
int x;
int y;
MSGPACK_DEFINE(x, y);
};

veigar::Veigar vg;
vg1.bind("func", [](MyPoint m) {
// ......
});

详细的参数绑定方法见 tests/type_test.cpp。

5. 性能

使用 examples\performance-test 程序作为测试用例:

进程 A 使用 4 个线程同时进行同步调用进程 B,每个线程调用 25000 次,平均每次“调用 <–> 返回结果”消耗 12 微妙。

1
Used: 1s240ms721μs, Total: 100000 Success: 100000, Timeout: 0, Failed: 0, Average: 12μs/call.

虽然 Veigar 在性能上还有一定的优化提升空间,但就测试结果来看,目前已经远远超越了其他 RPC 框架。