sdbus-c++ Bindings API
本章节将继续通过连接器示例,介绍sdbus-c++库的最高层API使用流程。首先sdbus-c++中附带了xml2cpp工具sdbus-c+±xml2cpp,该工具的使用与dbus-cxx库中的dbusxx-xml2cpp类似:
将XML IDL描述作为输入,可以选择生成服务器端所需的Adaptor头文件、客户端所需的Proxy头文件,如下所示:
sdbus-c++-xml2cpp concatenator-bindings.xml --adaptor=concatenator-server-glue.h --proxy=concatenator-client-glue.h
Adaptor头文件包含可用于实现 XML中描述的接口类(即对象接口),Proxy头文件包含可用于调用远程对象的类(这些类表示远程对象接口)。
1. 连接器的实现示例
1.1 同步Server/Client端
1.1.1 XML 描述文件编写
作为示例,这里直接展示连接器的XML描述内容:
<?xml version="1.0" encoding="UTF-8"?>
<node name="/org/sdbuscpp/concatenator">
<interface name="org.sdbuscpp.Concatenator">
<method name="concatenate">
<arg type="ai" name="numbers" direction="in" />
<arg type="s" name="separator" direction="in" />
<arg type="s" name="concatenatedString" direction="out" />
</method>
<signal name="concatenated">
<arg type="s" name="concatenatedString" />
</signal>
</interface>
</node>
1.1.2 生成服务器端的Adaptor头文件
/*
* This file was automatically generated by sdbus-c++-xml2cpp; DO NOT EDIT!
*/
#ifndef __sdbuscpp__concatenator_server_glue_h__adaptor__H__
#define __sdbuscpp__concatenator_server_glue_h__adaptor__H__
#include <sdbus-c++/sdbus-c++.h>
#include <string>
#include <tuple>
namespace org {
namespace sdbuscpp {
class Concatenator_adaptor
{
public:
static constexpr const char* INTERFACE_NAME = "org.sdbuscpp.Concatenator";
protected:
Concatenator_adaptor(sdbus::IObject& object)
: m_object(object)
{
}
Concatenator_adaptor(const Concatenator_adaptor&) = delete;
Concatenator_adaptor& operator=(const Concatenator_adaptor&) = delete;
Concatenator_adaptor(Concatenator_adaptor&&) = delete;
Concatenator_adaptor& operator=(Concatenator_adaptor&&) = delete;
~Concatenator_adaptor() = default;
void registerAdaptor()
{
m_object.addVTable( sdbus::registerMethod("concatenate").withInputParamNames("numbers", "separator").withOutputParamNames("concatenatedString").implementedAs([this](const std::vector<int32_t>& numbers, const std::string& separator){ return this->concatenate(numbers, separator); })
, sdbus::registerSignal("concatenated").withParameters<std::string>("concatenatedString")
).forInterface(INTERFACE_NAME);
}
public:
void emitConcatenated(const std::string& concatenatedString)
{
m_object.emitSignal("concatenated").onInterface(INTERFACE_NAME).withArguments(concatenatedString);
}
private:
virtual std::string concatenate(const std::vector<int32_t>& numbers, const std::string& separator) = 0;
private:
sdbus::IObject& m_object;
};
}} // namespaces
#endif
生成的服务端接口类解读:
对于 XML IDL 文件中的每个接口,生成器都会创建一个表示该接口的C++接口类,应由继承它的类实现。自动生成的适配器(Adaptor)主要有以下几点:
- 该类的构造函数负责注册所有方法、信号和属性。
- 对于每个 D-Bus 方法,都有一个纯虚函数(以上示例代码中的虚函数
concatenate
)。这些纯虚函数必须在子类中实现。- 对于每个信号,都有一个发出此信号的公共函数成员(例如示例中生成的
emitConcatenated
函数)。- 生成的适配器类在设计上不可复制、不可移动。可以在堆上创建它们,并在需要移动语义的情况下使用
std::unique_ptr
管理它们(例如,当适配器类存储在容器中时)。
1.1.3 生成客户端的Proxy头文件
/*
* This file was automatically generated by sdbus-c++-xml2cpp; DO NOT EDIT!
*/
#ifndef __sdbuscpp__concatenator_client_glue_h__proxy__H__
#define __sdbuscpp__concatenator_client_glue_h__proxy__H__
#include <sdbus-c++/sdbus-c++.h>
#include <string>
#include <tuple>
namespace org {
namespace sdbuscpp {
class Concatenator_proxy
{
public:
static constexpr const char* INTERFACE_NAME = "org.sdbuscpp.Concatenator";
protected:
Concatenator_proxy(sdbus::IProxy& proxy)
: m_proxy(proxy)
{
}
Concatenator_proxy(const Concatenator_proxy&) = delete;
Concatenator_proxy& operator=(const Concatenator_proxy&) = delete;
Concatenator_proxy(Concatenator_proxy&&) = delete;
Concatenator_proxy& operator=(Concatenator_proxy&&) = delete;
~Concatenator_proxy() = default;
void registerProxy()
{
m_proxy.uponSignal("concatenated").onInterface(INTERFACE_NAME).call([this](const std::string& concatenatedString){ this->onConcatenated(concatenatedString); });
}
virtual void onConcatenated(const std::string& concatenatedString) = 0;
public:
std::string concatenate(const std::vector<int32_t>& numbers, const std::string& separator)
{
std::string result;
m_proxy.callMethod("concatenate").onInterface(INTERFACE_NAME).withArguments(numbers, separator).storeResultsTo(result);
return result;
}
private:
sdbus::IProxy& m_proxy;
};
}} // namespaces
#endif
生成的客户端接口类解读:
与上面描述的服务器端适配器类相似,根据 XML IDL 文件中的一个接口生成对应的一个代理类,该类实际上是远程对象具体单一接口的代理。
- 对于每个 D-Bus 信号,都有一个纯虚拟成员函数,其主体必须在子类中提供。
- 对于每个方法,都有一个公共函数成员可远程调用该方法。
- 生成的代理类在设计上不可复制也不可移动。可以在堆上创建它们,并在需要移动语义的情况下使用std::unique_ptr进行管理(例如,当代理类存储在容器中时)。
1.1.4 根据生成的Adaptor实现同步Server端
我们现在需要创建一个表示 D-Bus 对象的类来实现所有 D-Bus 接口,该类必须继承自所有相应的*_adaptor
类(也称为对象接口,因为这些类是 as-if 接口),并实现所有纯虚成员函数。
使用sdbus-c++的C++绑定层封装,上述过程只需要我们将对象类从AdaptorInterfaces
可变参数模板类继承,然后用所有生成的接口类的列表填充其模板参数。AdaptorInterfaces
是一个隐藏了一些样板细节的便捷类。例如,在其构造函数中,它创建一个Object
实例,并负责正确初始化所有适配器超类。
在对象类中我们需要:
- 通过重写相应的虚函数来实现 D-Bus 对象的方法;
- 在构造函数中调用
registerAdaptor()
,使适配器( D-Bus 对象)可用于远程调用; - 调用
unregisterAdaptor()
,将会从总线上取消注册适配器。
在以前的 sdbus-c++ 版本中并不需要调用registerAdaptor()
和unregisterAdaptor()
,因为它由父类处理,但存在潜在的纯虚函数调用问题。只有实现虚函数的类才能进行注册,因此给用户带来了一点不便。
#include <sdbus-c++/sdbus-c++.h>
#include "concatenator-server-glue.h"
class Concatenator : public sdbus::AdaptorInterfaces<org::sdbuscpp::Concatenator_adaptor /*, more adaptor classes if there are more interfaces*/>
{
public:
Concatenator(sdbus::IConnection& connection, sdbus::ObjectPath objectPath)
: AdaptorInterfaces(connection, std::move(objectPath))
{
registerAdaptor();
}
~Concatenator()
{
unregisterAdaptor();
}
protected:
std::string concatenate(const std::vector<int32_t>& numbers, const std::string& separator) override
{
// Return error if there are no numbers in the collection
if (numbers.empty())
throw sdbus::Error("org.sdbuscpp.Concatenator.Error", "No numbers provided");
// Concatenate the numbers
std::string result;
for (auto number : numbers)
{
result += (result.empty() ? std::string() : separator) + std::to_string(number);
}
// Emit the 'concatenated' signal with the resulting string
emitConcatenated(result);
// Return the resulting string
return result;
}
};
提示:通过继承
sdbus::AdaptorInterfaces
,我们可以访问受保护的getObject()
方法,从而在适配器实现类中调用此方法来访问底层IObject
对象。
以上就是org.sdbuscpp.Concatenator
接口的 D-Bus 对象的实现,现在创建一个发布该对象的服务:
#include "Concatenator.h"
int main(int argc, char *argv[])
{
// Create D-Bus connection to (either the system or session) bus and request a well-known name on it.
sdbus::ServiceName serviceName{"org.sdbuscpp.concatenator"};
auto connection = sdbus::createBusConnection(serviceName);
// Create concatenator D-Bus object.
sdbus::ObjectPath objectPath{"/org/sdbuscpp/concatenator"};
Concatenator concatenator(*connection, std::move(objectPath));
// Run the loop on the connection.
connection->enterEventLoop();
}
1.1.5 根据生成的Proxy实现同步Client端
要实现远程 D-Bus 对象的代理,我们需要创建一个代表代理对象的类,该类必须继承自所有相应的*_proxy
类(又称为远程对象接口,因为这些类是模拟接口),并且(如果适用)实现所有纯虚成员函数。
与前面Server端实现类似,我们的代理类只需要从可变参数模板类ProxyInterfaces
继承,然后用所有生成的接口类的列表填充其模板参数。ProxyInterfaces
是一个隐藏了一些样板细节的便捷类。例如,在其构造函数中,它可以为我们创建一个Proxy
实例,并负责正确初始化所有生成的接口超类。
在代理类中我们需要:
- 通过重写相应的虚函数来实现信号处理程序和异步方法答复处理程序(如果有);
- 在构造函数中调用
registerProxy()
,使代理( D-Bus 代理对象)准备好接收信号和异步调用回复; - 调用
unregisterProxy()
,会从总线上取消注册代理。
同样的,在 sdbus-c++ 的早期版本中,调用registerProxy()
和unregisterProxy()
不是必需的。
#include <sdbus-c++/sdbus-c++.h>
#include "concatenator-client-glue.h"
class ConcatenatorProxy : public sdbus::ProxyInterfaces<org::sdbuscpp::Concatenator_proxy /*, more proxy classes if there are more interfaces*/>
{
public:
ConcatenatorProxy(sdbus::ServiceName destination, sdbus::ObjectPath objectPath)
: ProxyInterfaces(std::move(destination), std::move(objectPath))
{
registerProxy();
}
~ConcatenatorProxy()
{
unregisterProxy();
}
protected:
void onConcatenated(const std::string& concatenatedString) override
{
std::cout << "Received signal with concatenated string " << concatenatedString << std::endl;
}
};
提示:通过继承sdbus::ProxyInterfaces,我们可以访问受保护的getProxy()方法,从而在代理实现类中调用此方法来访问底层IProxy对象。
上面的示例创建了一个代理,该代理会创建并维护自己的总线连接(总线是会话总线还是系统总线,具体取决于上下文)。但是,有些ProxyInterfaces
类模板构造函数重载也会将来自用户的连接作为第一个参数,并将该连接传递给底层代理,在ProxyInterfaces
模板参数列表中列出的所有接口都会使用这个连接实例。
但请注意,有多个ProxyInterfaces
构造函数重载,在代理对 D-Bus 连接的行为方式上有所不同。这些重载实际上是在sdbus::createProxy
之上实现的,因此与sdbus::createProxy
的重载构成了精准的映射关系。我们甚至可以自己创建一个IProxy
的实例,并将其注入到我们的代理类中——在ProxyInterfaces
中有一个这样的构造函数重载。如果我们需要在单元测试中提供模拟实现,这会有所帮助。
接下来,我们在实际应用程序中使用这个代理进行远程调用和监听信号:
#include "ConcatenatorProxy.h"
#include <unistd.h>
int main(int argc, char *argv[])
{
// Create proxy object for the concatenator object on the server side
sdbus::ServiceName destination{"org.sdbuscpp.concatenator"};
sdbus::ObjectPath objectPath{"/org/sdbuscpp/concatenator"};
ConcatenatorProxy concatenatorProxy(std::move(destination), std::move(objectPath));
std::vector<int> numbers = {1, 2, 3};
std::string separator = ":";
// Invoke concatenate with some numbers
auto concatenatedString = concatenatorProxy.concatenate(numbers, separator);
assert(concatenatedString == "1:2:3");
// Invoke concatenate again, this time with no numbers and we shall get an error
try
{
auto concatenatedString = concatenatorProxy.concatenate(std::vector<int>(), separator);
assert(false);
}
catch(const sdbus::Error& e)
{
std::cerr << "Got concatenate error " << e.getName() << " with message " << e.getMessage() << std::endl;
}
// Give sufficient time to receive 'concatenated' signal from the first concatenate invocation
sleep(1);
return 0;
}
1.1.6 如何在绑定层API访问D-Bus消息
只需组合getObject()
/getProxy()
和getCurrentlyProcessedMessage()
方法即可,在上文中已经讨论过两者:
- 本章节的1.1.4 根据生成的Adaptor实现同步Server端的提示
- 本章节的1.1.5 根据生成的Proxy实现同步Client端的提示
- sdbus-c++中文版使用说明(三)——便捷层API章节 1.2 同步Client端的提示:如何在便捷层API访问D-Bus消息?
这里再举个例子:
class Concatenator : public sdbus::AdaptorInterfaces</*...*/>
{
public:
/*...*/
protected:
std::string concatenate(const std::vector<int32_t>& numbers, const std::string& separator) override
{
const auto* methodCallMsg = getObject().getCurrentlyProcessedMessage();
std::cout << "Sender of this method call: " << methodCallMsg.getSender() << std::endl;
/*...*/
}
};
1.2 异步Server端
sdbus-c+±xml2cpp工具可以生成服务端异步方法的C++代码,我们只需要用org.freedesktop.DBus.Method.Async
注释该方法即可,注释元素值必须是server
(仅服务端异步的方法) 或client-server
(客户端和服务端均为异步的方法):
<?xml version="1.0" encoding="UTF-8"?>
<node name="/org/sdbuscpp/concatenator">
<interface name="org.sdbuscpp.Concatenator">
<method name="concatenate">
<annotation name="org.freedesktop.DBus.Method.Async" value="server" />
<arg type="ai" name="numbers" direction="in" />
<arg type="s" name="separator" direction="in" />
<arg type="s" name="concatenatedString" direction="out" />
</method>
<signal name="concatenated">
<arg type="s" name="concatenatedString" />
</signal>
</interface>
</node>
1.3 异步Client端
对于C++绑定层API的异步客户端实现,与其异步Server端一致,只需要在XML中注释该方法为org.freedesktop.DBus.Method.Async
,注释元素值必须是server
(仅服务端异步的方法) 或client-server
(客户端和服务端均为异步的方法):
<?xml version="1.0" encoding="UTF-8"?>
<node name="/org/sdbuscpp/concatenator">
<interface name="org.sdbuscpp.Concatenator">
<method name="concatenate">
<annotation name="org.freedesktop.DBus.Method.Async" value="client" />
<arg type="ai" name="numbers" direction="in" />
<arg type="s" name="separator" direction="in" />
<arg type="s" name="concatenatedString" direction="out" />
</method>
<signal name="concatenated">
<arg type="s" name="concatenatedString" />
</signal>
</interface>
</node>
在该层API中,异步方法包括:基于回调(callback-based)的方法、基于std::future(std::future-based )的方法两种生成方式,这可以通过附加注释org.freedesktop.DBus.Method.Async.ClientImpl
进行自定义。其支持的值就是callback
和std::future
(默认是基于回调的callback
)。
- 生成基于回调的异步方法
对于每个客户端异步方法,在生成的代理类中都会生成一个对应的on<MethodName>Reply
纯虚函数,其中<MethodName>
是大写的 D-Bus 方法名。此函数是 D-Bus 方法回复到达时调用的回调,必须在实现类中通过重写它来提供主体。
因此,在上面的具体示例中,该工具将生成一个类似于1.1.3 生成客户端的Proxy头文件中所示的类Concatenator_proxy
,不同之处在于它还将生成一个附加方法:virtual void onConcatenateReply(std::optional[sdbus::Error](sdbus::Error) error, const std::string& concatenatedString);
,我们要在派生类ConcatenatorProxy
中覆盖该方法。 - 生成基于 std:future 的异步方法
在这种情况下,方法将返回一个std::future
对象,当方法异步执行后,在没有异常发生时该对象将包含返回值对应的回复消息,在调用发生异常时返回Error
(由std::future::get()
抛出sdbus::Error
)。
2. 方法回调的Timeout
在前文sdbus-c++中文版使用说明(二)——基础层API 5.4 异步Client端中提到,基础层API允许我们通过IProxy::callMethod()
函数的timeout
设置超时时间,而在C++绑定层,我们可以通过XML描述文件中添加一个 Timeout 如org.freedesktop.DBus.Method.Timeout
,来给一个Method设置超时时间(可以带单位:us / ms / s / min,只有数值的话默认为微秒),这个Timeout可以与异步方法结合使用,例子如下:
<?xml version="1.0" encoding="UTF-8"?>
<node>
<interface name="org.bluez.Device1">
<method name="Connect">
<annotation name="org.freedesktop.DBus.Method.Async" value="client"/>
<annotation name="org.freedesktop.DBus.Method.Timeout" value="3000ms"/>
</method>
<method name="Disconnect">
<annotation name="org.freedesktop.DBus.Method.Async" value="client"/>
<annotation name="org.freedesktop.DBus.Method.Timeout" value="2000000"/> <!-- 2000000us -->
</method>
</interface>
</node>
3. D-Bus属性(Property)的同步/异步读写
在sdbus-c++库中,使用 XML 描述定义和使用 D-Bus 属性也变得非常容易,该层API也为同步、异步读写提供了支持。
3.1 定义和实现属性的同步读写
- 在XML IDL中定义同步属性
在XML描述文件中,property
元素只具有属性name
、type
和access
,且这些属性都是必需的。access
属性允许值为“readwrite”、“read”和“write”。此外,该属性也可以添加更多注释,除了 D-Bus 规范中定义的标准注释外,还有 sdbus-c++ 特定的注释,在下面将进一步讨论。
以下是让一个接口具有读写属性status的写法示例:
<?xml version="1.0" encoding="UTF-8"?>
<node name="/org/sdbuscpp/propertyprovider">
<interface name="org.sdbuscpp.PropertyProvider">
<!--...-->
<property name="status" type="u" access="readwrite"/>
<!--...-->
</interface>
</node>
- 生成带属性的服务端/客户端头文件
- 生成具有读写属性status的服务端Adaptor:在实现服务端时,重写
status
的 getter 和 setter 方法。
class PropertyProvider_adaptor
{
/*...*/
public:
PropertyProvider_adaptor(sdbus::IObject& object)
: m_object(object)
{
}
/*...*/
void registerAdaptor()
{
m_object.addVTable( sdbus::registerProperty("status").withGetter([this](){ return this->status(); }).withSetter([this](const uint32_t& value){ this->status(value); })
).forInterface(INTERFACE_NAME);
}
private:
// status的getter方法
virtual uint32_t status() = 0;
// status的setter方法
virtual void status(const uint32_t& value) = 0;
/*...*/
};
#endif
- 生成具有读写属性status的客户端Proxy:在客户端的代理中,我们只需调用属性对应的get、set方法即可。
class PropertyProvider_proxy
{
/*...*/
public:
// 获取属性值
uint32_t status()
{
return m_object.getProperty("status").onInterface(INTERFACE_NAME).get<uint32_t>();
}
// 修改属性值
void status(const uint32_t& value)
{
m_object.setProperty("status").onInterface(INTERFACE_NAME).toValue(value);
}
/*...*/
};
3.2 定义和实现属性的异步读写
这里给出的是客户端异步属性的定义和实现示例。
- 在XML IDL中定义客户端的异步属性
我们可以给property
标记更多属性,包括:org.freedesktop.DBus.Property.Get.Async
:生成 getter 方法的异步变体,对于此处客户端,value必须是"client"。org.freedesktop.DBus.Property.Set.Async
:生成 setter 方法的异步变体,对于此处客户端,value必须是"client"。org.freedesktop.DBus.Property.Get.Async.ClientImpl
:设置getter方法是基于回调还是基于std::future的异步变体,value可选值为:callback、std::future。(概念与本章节的1.3 异步Client端类似)org.freedesktop.DBus.Property.Set.Async.ClientImpl
:设置setter方法是基于回调还是基于std::future的异步变体,value可选值为:callback、std::future。(概念与本章节的1.3 异步Client端类似)
基于回调的方式将生成一个纯虚函数On<PropertyName>Property[Get|Set]Reply()
,该函数必须在派生类中重写。
例如我们可以在XML中定义这样的描述:
<?xml version="1.0" encoding="UTF-8"?>
<node name="/org/sdbuscpp/propertyprovider">
<interface name="org.sdbuscpp.PropertyProvider">
<!--...-->
<property name="status" type="u" access="readwrite">
<annotation name="org.freedesktop.DBus.Property.Get.Async" value="client"/>
<annotation name="org.freedesktop.DBus.Property.Get.Async.ClientImpl" value="callback"/>
</property>
<!--...-->
</interface>
</node>
- 生成异步属性的客户端 C++ 代码
class PropertyProvider_proxy
{
/*...*/
virtual void onStatusPropertyGetReply(const uint32_t& value, std::optional<sdbus::Error> error) = 0;
public:
// 获取属性值
sdbus::PendingAsyncCall status()
{
return m_object.getPropertyAsync("status").onInterface(INTERFACE_NAME).uponReplyInvoke([this](std::optional<sdbus::Error> error, const sdbus::Variant& value){ this->onStatusPropertyGetReply(value.get<uint32_t>(), std::move(error)); });
}
// 修改属性值
void status(const uint32_t& value)
{
m_object.setProperty("status").onInterface(INTERFACE_NAME).toValue(value);
}
/*...*/
};
除了属性的getter/setter自定义生成代码,sdbus-c++库通过在sdbus-c++/StandardInterfaces.h
中预定义的sdbus::Properties_proxy
实现了标准D-Bus接口(即每个D-Bus连接自带的org.freedesktop.DBus.Properties
接口),也可用于属性的读/写操作。请参阅下一章节。
DBus专栏导航:
D-Bus理论基础
Linux系统DBus工具的使用
sdbus-c++中文版使用说明(一)——概括介绍与编译
sdbus-c++中文版使用说明(二)——基础层API
sdbus-c++中文版使用说明(三)——便捷层API
sdbus-c++中文版使用说明(四)——C++绑定层API