sdbus-c++ Basic API
本章节通过一个用指定分隔符,将整型数组拼接为字符串的连接器dbus实现示例,详细介绍sdbus-c++库基础层API的使用流程。
1. sdbus-c++创建连接
在sdbus-c++中创建D-Bus连接有多种方法,可以选择给定一个name,将被作为一个连接的Bus Name:
createBusConnection()
:在用户上下文中打开与会话总线的连接,否则打开与系统总线的连接。createBusConnection(const sdbus::ServiceName& name)
:按照指定的公共名(well-known name),在用户上下文中就打开会话总线连接,否则打开系统总线连接。createSystemBusConnection()
:打开与系统总线的连接;createSystemBusConnection(const sdbus::ServiceName& name)
:按照指定名称,打开与系统总线的连接。createSessionBusConnection()
:打开与会话总线的连接。createSessionBusConnection(const sdbus::ServiceName& name)
:按照指定名称,打开与会话总线的连接。createSessionBusConnectionWithAddress(const std::string& address)
:打开与自定义地址的会话总线的连接createRemoteSystemBusConnection(const std::string& host)
:使用 ssh 打开与远程主机上的系统总线的连接createDirectBusConnection(const std::string& address)
:在自定义地址打开直接 D-Bus 连接(请参阅点对点直连(待发布))createDirectBusConnection(int fd)
:在给定的文件描述符处打开直接 D-Bus 连接(请参阅点对点直连(待发布))createServerBus(int fd)
:作为服务器在给定的文件描述符上打开直接 D-Bus 连接(请参阅点对点直连(待发布))createBusConnection(sd_bus *bus)
:直接从底层 sd_bus 连接实例创建连接(该实例已直接通过 sd-bus API 创建并预先设置)。
2. sdbus-c++连接的使用
在sdbus-c++连接的设计中,每个连接都是一个IConnection实例,每个实例都会有一个线程负责运行事件循环,来处理所有传入和传出消息以及与 D-Bus 守护进程的所有通信。简单来说,sdbus-c++的这一事件循环线程,对于同步Method调用是阻塞的,对于异步Method是非阻塞的。
那么,从技术角度来说,我们怎样从服务器和客户端的角度来使用连接?
2.1 在服务器端使用 D-Bus 连接
在服务器端,自己手动创建 D-Bus 连接,在其上请求总线名称,然后结合需要选择手动启动事件循环:
- 用于内部事件循环
- 阻塞方式:
enterEventLoop()
- 非阻塞异步方式:
enterEventLoopAsync()
- 阻塞方式:
- 用于外部事件循环:
如果在应用程序中使用自定义的事件循环实现(例如,sd-event、GLib Event Loop、boost::asio 等),并且想将sdbus-c++的D-Bus连接与该事件循环关联,可使用这种方法。有关更多信息请参阅在外部事件循环中使用 sdbus-c++(待发布)
在事件循环创建的前后,都可以创建、“挂钩”(hook)、删除该连接上的对象和代理。对象的构造函数中将 D-Bus 连接作为引用,从而实现连接和对象互相绑定,当使用B-Bus对象时,必须确保它的连接存在。
注意:对象和代理可能都挂到一个连接上。一个D-Bus 服务器也可能是另一个D-Bus服务器的客户端,并与其导出的 D-Bus 接口以及面向其他 D-Bus 接口的代理共享一个 D-Bus 连接。
2.2 在客户端使用 D-Bus 连接
在客户端,我们同样需要一个连接——只是与服务器端不同,我们不需要在其上请求唯一的总线名称。创建代理时,我们有更多选项:
- 将已经存在的连接作为引用传递:当应用程序已经维护 D-Bus 连接时(可能它在其上提供 D-Bus API,或者它已经挂接了一些代理),这是典型的方法。代理将与其他人共享连接。使用这种方法,我们必须确保只要代理存在,连接就存在。有关在该连接上运行事件循环的选项的讨论,请参阅上面的2.1 在服务器端使用 D-Bus 连接部分。
- 让代理维护自己的连接(对于简单的D-Bus客户端来说,这是更简单的方法),我们有两个选择:
- 通过连接构造代理:自己创建连接,然后用std::move构造代理,让代理成为此连接的所有者。这样做的好处是我们可以选择连接类型(system, session, remote)。此外构造函数存在一个传入
sdbus::dont_run_event_loop_thread_t
的重载,区别在于:- 当创建时没有
dont_run_event_loop_thread_t
,代理将在该连接上启动专用的事件循环线程; - 当传入
dont_run_event_loop_thread_t
创建时,代理将不会在该连接上启动事件循环线程。
- 当创建时没有
- 不通过连接构造代理:在底层,代理会创建自己的连接,连接到会话总线(在用户上下文中时)或系统总线,例如5.2 同步Client端中的代理实现。此外:
- 当创建时没有
dont_run_event_loop_thread_t
,代理将在该连接上启动专用的事件循环线程; - 或者,当传入
dont_run_event_loop_thread_t
创建时,代理将不会在该连接上启动事件循环线程。
- 当创建时没有
- 通过连接构造代理:自己创建连接,然后用std::move构造代理,让代理成为此连接的所有者。这样做的好处是我们可以选择连接类型(system, session, remote)。此外构造函数存在一个传入
3. sdbus-c++中代理的使用
如果代理是监听传入消息(如信号、异步调用回复等)的“长寿命”代理,才需要事件循环,在代理中启动专用的事件循环很简单,但每次创建/销毁代理都会带来性能和资源成本,并且会损害可扩展性。
如果我们只是偶尔需要执行一个或几个 D-Bus 调用而已,则应该使用“短寿命、轻量级”代理:通过启用dont_run_event_loop_thread_t
标签来立即创建代理、执行调用、然后立即销毁代理。这样的代理不会产生事件循环线程,它仅支持同步 D-Bus 调用(无信号、无异步调用……)。
4. 优雅地停止内部 I/O 事件循环
具有异步事件循环的连接(即通过enterEventLoopAsync()
启动的连接)将停止并在其析构函数中自动join其事件循环线程。如果在enterEventLoop()
启动的同步事件循环中发生阻塞,可以通过从不同线程或系统信号的handler,在相应的总线连接上调用leaveEventLoop()
来解除阻塞。
5. 连接器的实现示例
5.1 同步Server端
在基础层API中,我们已经得到了D-Bus总线连接、对象、代理的抽象,我们可以通过它们的接口类(IConnection
、IObject
、IProxy
)与他们进行交互,但与底层 sd-bus C 库类似,我们仍在 D-Bus 消息级别上工作。我们需要create、对参数进行序列化/反序列化、send。基于基本 sdbus-c++ API 实现的简单 Concatenator 服务如下所示:
#include <sdbus-c++/sdbus-c++.h>
#include <vector>
#include <string>
// Yeah, global variable is ugly, but this is just an example and we want to access
// the concatenator instance from within the concatenate method handler to be able
// to emit signals.
sdbus::IObject* g_concatenator{};
void concatenate(sdbus::MethodCall call)
{
// 从Message中获得反序列化的numbers集合
std::vector<int> numbers;
call >> numbers;
// 从Message中获得反序列化的分隔符
std::string separator;
call >> separator;
// 整型数组为空时抛出异常
if (numbers.empty())
throw sdbus::Error(sdbus::Error::Name{"org.sdbuscpp.Concatenator.Error"}, "No numbers provided");
std::string result;
for (auto number : numbers)
{
result += (result.empty() ? std::string() : separator) + std::to_string(number);
}
// 序列化的结果字符串赋值给reply,并将其发给caller
auto reply = call.createReply();
reply << result;
reply.send();
// 声明该连接上的接口名称 org.sdbuscpp.Concatenator
sdbus::InterfaceName interfaceName{"org.sdbuscpp.Concatenator"};
// 创建信号'concatenated'
sdbus::SignalName signalName{"concatenated"};
auto signal = g_concatenator->createSignal(interfaceName, signalName);
signal << result;
// 发出信号'concatenated'
g_concatenator->emitSignal(signal);
}
int main(int argc, char *argv[])
{
// 创建D-Bus连接并定义了公共名称
sdbus::ServiceName serviceName{"org.sdbuscpp.concatenator"};
auto connection = sdbus::createBusConnection(serviceName);
// 创建concatenator的D-Bus对象
sdbus::ObjectPath objectPath{"/org/sdbuscpp/concatenator"};
auto concatenator = sdbus::createObject(*connection, std::move(objectPath));
g_concatenator = concatenator.get();
// 注册D-Bus方法和信号到concatenator对象, 并输出对象.
sdbus::InterfaceName interfaceName{"org.sdbuscpp.Concatenator"};
concatenator->addVTable( sdbus::MethodVTableItem{sdbus::MethodName{"concatenate"}, sdbus::Signature{"ais"}, {}, sdbus::Signature{"s"}, {}, &concatenate, {}}
, sdbus::SignalVTableItem{sdbus::MethodName{"concatenated"}, sdbus::Signature{"s"}, {}, {}} )
.forInterface(interfaceName);
// 在当前总线连接上执行 I/O 事件循环.
connection->enterEventLoop();
}
Server端的代码解读:
我们建立一个 D-Bus 系统连接并在其上请求 D-Bus 名称为org.sdbuscpp.concatenator
,D-Bus 客户端将使用此名称来查找服务。然后,我们在此连接上创建一个具有路径(/org/sdbuscpp/concatenator
)的对象。我们添加一个所谓的对象 vtable,在其中声明和描述其 D-Bus API,即对象提供的接口、方法、信号、属性(如果有的话)。然后,我们需要确保在连接上运行事件循环,该循环处理所有传入、传出和其他请求。
提示:还有一个带参数return_slot_tSlot
的addVTable()
的重载方法,会返回一个Slot对象(插槽对象)。该插槽是与 vtable 注册相关的简单句柄(基于 RAII )。只要我们保留插槽对象,vtable 注册就处于活动状态。当释放插槽时,vtable 会自动从 D-Bus 对象中删除。这使我们能够实现“动态”D-Bus 对象 API,该 API 在对象生命周期内可随时添加和移除。(在sdbus-c++中vtable相关插槽均符合该定义,后文出现时仅作提示,将不再赘述。)
注意: D-Bus 对象可以附加任意数量的 vtable。甚至对象的 D-Bus 接口也可以附加多个 vtable。
这一层上任何 D-Bus 对象方法的回调都是类型为void(sdbus::MethodCall call)
的可回调函数。参数call
是传入的方法调用消息。我们需要从中反序列化我们的方法输入参数,然后可以调用方法的逻辑并获取结果,其次对于给定的call
,我们创建一条reply
消息,将结果打包进去并通过send()
将其发回给调用者。(如果我们有一个返回空的方法,我们只会发回一个空的reply
。)我们还会用结果发出一个信号,为此,需要通过对象的createSignal()
来创建一个信号消息,将序列化结果放到其中,然后通过调用对象的emitSignal()
将其发送给订阅者。
请注意,我们可以在运行时的任何时间动态地在连接上创建和销毁 D-Bus 对象,即使连接上有活动的事件循环。因此,管理 D-Bus 对象的生命周期(创建、导出和销毁 D-Bus 对象)是完全线程安全的。
5.2 同步Client端
#include <sdbus-c++/sdbus-c++.h>
#include <vector>
#include <string>
#include <iostream>
#include <unistd.h>
void onConcatenated(sdbus::Signal signal)
{
std::string concatenatedString;
signal >> concatenatedString;
std::cout << "Received signal with concatenated string " << concatenatedString << std::endl;
}
int main(int argc, char *argv[])
{
// 创建concatenator对象的Server端代理. 由于这里我们在创建代理实例时没有向其传递连接,该代理
// 将自动创建自己的连接(连接到系统总线或会话总线, 具体取决于上下文)。
sdbus::ServiceName destination{"org.sdbuscpp.concatenator"};
sdbus::ObjectPath objectPath{"/org/sdbuscpp/concatenator"};
auto concatenatorProxy = sdbus::createProxy(std::move(destination), std::move(objectPath));
// 声明该对象上存在的一个接口名 org.sdbuscpp.Concatenator
sdbus::InterfaceName interfaceName{"org.sdbuscpp.Concatenator"};
// 订阅名为'concatenated'的信号
sdbus::SignalName signalName{"concatenated"};
concatenatorProxy->registerSignalHandler(interfaceName, signalName, &onConcatenated);
std::vector<int> numbers = {1, 2, 3};
std::string separator = ":";
sdbus::MethodName concatenate{"concatenate"};
// 在对象的给定接口上调用concatenate
{
auto method = concatenatorProxy->createMethodCall(interfaceName, concatenate);
method << numbers << separator;
auto reply = concatenatorProxy->callMethod(method);
std::string result;
reply >> result;
assert(result == "1:2:3");
}
// 再次调用,这次我们传入空的整型数组,这会导致触发异常 sdbus::Error
{
auto method = concatenatorProxy->createMethodCall(interfaceName, concatenate);
method << std::vector<int>() << separator;
try
{
auto reply = concatenatorProxy->callMethod(method);
assert(false);
}
catch(const sdbus::Error& e)
{
std::cerr << "Got concatenate error " << e.getName() << " with message " << e.getMessage() << std::endl;
}
}
// sleep一会,等待第一次调用时产生的'concatenated' 信号
sleep(1);
return 0;
}
Client端的代码解读:
以上示例中,我们在总线org.sdbuscpp.concatenator
上给/org/sdbuscpp/concatenator
对象创建了一个代理,并给感兴趣的信号注册了处理程序onConcatenated
。
此级别的 D-Bus 信号处理程序的回调类型为void(sdbus::Signal signal)
,参数signal
是传入的信号消息。我们需要从中反序列化参数,然后用来执行业务逻辑。
提示:还有一个带参数return_slot_t
的registerSignalHandler()
重载,它也会返回一个Slot
插槽对象。这里只要保留槽对象,信号订阅就处于活动状态,当释放对象时,信号处理程序会自动取消注册,以此更好地控制信号订阅的生命周期。(sdbus-c++中与信号注册相关插槽均符合该定义,后文出现该类插槽时仅作提示,不再赘述。)
随后,我们对对象的concatenate()
方法进行了两次 RPC 调用。通过调用代理的createMethodCall()
方法来创建调用消息,将方法输入参数序列化到其中,并通过代理的callMethod()
进行同步调用,即可获得返回值。第二次RPC 调用concatenate()
使用无效参数完成,因此我们从服务收到 D-Bus 错误回复,我们可以看到,这是通过抛出sdbus::Error类型的异常来表现的。
5.3 异步Server端
使用 sdbus-c++ 的基本层 API,以下是concatenate 方法的异步实现:
void concatenate(sdbus::MethodCall call)
{
// Deserialize the collection of numbers from the message
std::vector<int> numbers;
call >> numbers;
// Deserialize separator from the message
std::string separator;
call >> separator;
// 启用一个线程实现异步执行...
std::thread([numbers = std::move(numbers), separator = std::move(separator), call = std::move(call)]()
{
// Return error if there are no numbers in the collection
if (numbers.empty())
{
// Let's send the error reply message back to the client
auto reply = call.createErrorReply({"org.sdbuscpp.Concatenator.Error", "No numbers provided"});
reply.send();
return;
}
std::string result;
for (auto number : numbers)
{
result += (result.empty() ? std::string() : separator) + std::to_string(number);
}
// Let's send the reply message back to the client
auto reply = call.createReply();
reply << result;
reply.send();
// Emit 'concatenated' signal (creating and emitting signals is thread-safe)
sdbus::InterfaceName interfaceName{"org.sdbuscpp.Concatenator"};
sdbus::SignalName signalName{"concatenated"};
auto signal = g_concatenator->createSignal(interfaceName, signalName);
signal << result;
g_concatenator->emitSignal(signal);
}).detach();
}
异步Server端代码解读:
与同步版本相比,有一些细微的差别。请注意,我们使用std::move
将当前消息call
发送给工作线程(顺便说一句,我们也可以选择在工作线程中执行输入参数反序列化,而不是先反序列化后移动到工作线程…)。在这个异步执行的工作线程中,我们可以选择在传入的消息call
上执行以下方法(创建和发送回复、创建和发射信号在设计上是线程安全的):
createReply()
:当我们得到执行结果result
,可以调用当前消息call
的该方法创建回复消息;createErrorReply()
:当我们执行期间出现异常,异步情况下将无法像同步那样抛出Error
类型消息,只能通过调用该方法,创建一个Error
类型的回复(这是因为现在处于工作线程的上下文中,而不是 D-Bus 调度程序线程的上下文中,这种发回错误的方式,实际上也可以在同步 D-Bus 方法中使用)。方法回调参数类型在同步和异步版本中是相同的(均为
sdbus::MethodCall call
),这意味着 sdbus-c++ 并不关心我们如何执行 D-Bus 方法,只是在运行时决定是否同步执行它,或者是否要将执行过程(如耗时更长、运行更复杂的情况)移至工作线程。
5.4 异步Client端
基于sdbus-c++基本层API实现Client端的异步调用,我们可以选择以下两种方法:
1. 基于回调(callback-based):在发出调用时将回调函数传给代理,收到回复时会自动调用该回调函数
int main(int argc, char *argv[])
{
/* ... */
auto callback = [](MethodReply reply, std::optional<sdbus::Error> error)
{
if (!error) // No error
{
std::string result;
reply >> result;
std::cout << "Got concatenate result: " << result << std::endl;
}
else // We've got a D-Bus error...
{
std::cerr << "Got concatenate error " << error->getName() << " with message " << error->getMessage() << std::endl;
}
}
// Invoke concatenate on given interface of the object
{
auto method = concatenatorProxy->createMethodCall(interfaceName, concatenate);
method << numbers << separator;
concatenatorProxy->callMethod(method, callback);
// When the reply comes, we shall get "Got concatenate result 1:2:3" on the standard output
}
// Invoke concatenate again, this time with no numbers and we shall get an error
{
auto method = concatenatorProxy->createMethodCall(interfaceName, concatenate);
method << std::vector<int>() << separator;
concatenatorProxy->callMethod(method, callback);
// When the reply comes, we shall get concatenation error message on the standard error output
}
/* ... */
return 0;
}
方法1-异步Client端的代码解读:
回调是一个返回空值的函数,它接受两个参数:一个MethodReply
类型表示回复消息的引用,以及一个指向预期sdbus::Error
实例的指针,该指针的用法如下:
- 可选参数
error
为空:表示在调用时没有发生 D-Bus 错误,并且回复消息包含有效回复。- 可选参数
error
非空:表示在调用期间发生了错误,我们可以从参数error
访问错误名称和消息内容。此外,函数
IProxy::callMethod()
还有一个重载方法,允许客户端异步执行时,设置超时参数timeout
。
2. 基于std::future:使用函数IProxy::callMethod()
基于std::future
的重载
...
// Invoke concatenate on given interface of the object
{
auto method = concatenatorProxy->createMethodCall(interfaceName, concatenate);
method << numbers << separator;
auto future = concatenatorProxy->callMethod(method, sdbus::with_future);
try
{
auto reply = future.get(); // This will throw if call ends with an error
std::string result;
reply >> result;
std::cout << "Got concatenate result: " << result << std::endl;
}
catch (const sdbus::Error& e)
{
std::cerr << "Got concatenate error " << e.getName() << " with message " << e.getMessage() << std::endl;
}
}
方法2-异步Client端的代码解读:
该重载函数调用后返回一个std::future
对象,当方法异步执行后,在没有异常发生时该对象将包含返回值对应的回复消息,在调用发生异常时返回Error
(由std::future::get()
抛出sdbus::Error
)。
DBus专栏导航:
D-Bus理论基础
Linux系统DBus工具的使用
sdbus-c++中文版使用说明(一)——概括介绍与编译
sdbus-c++中文版使用说明(二)——基础层API
sdbus-c++中文版使用说明(三)——便捷层API
sdbus-c++中文版使用说明(四)——C++绑定层API