chromeos-dbus-bindings: add README

Using this library can be confusing because of the code generation,
so here's some documentation to explain what the code generator is
doing.

BUG=chromium:719077
TEST=md_browser

Change-Id: I5607360283685df694022d214253f48689c412d2
1 file changed
tree: e87a3f0631d40c4cd159ebd5a0fde500cf73c62b
  1. Android.mk
  2. MODULE_LICENSE_BSD
  3. NOTICE
  4. README.md
  5. chromeos-dbus-bindings/
README.md

chromeos-dbus-bindings

chromeos-dbus-bindings was created to supplement libbrillo and simplify the implementation of D-Bus daemons and proxies. It generates C++ classes from the XML specifications of the D-Bus interface. Instead of dealing directly with MethodCall objects and unpacking the arguments manually, the generated bindings take care of marshalling and unmarshalling D-Bus method call arguments for you.

Setting up chromeos-dbus-bindings

The XML format defining objects and interfaces is the same format used in the introspection API. Method and signal handlers are generated from this XML file. If you were using dbus-c++ before, you are probably using xml2cpp to generate C++ bindings from the XML specification. If not, you may need to write an XML specification.

After that, you will need to set up some actions in the gyp file for your service and its users. That will look something like this in your service:

{
  'target_name': 'frobinator-adaptors',
  'type': 'none',
  'variables': {
    'dbus_service_config': 'dbus_bindings/dbus-service-config.json',
    'dbus_adaptors_out_dir': 'include/frobinator/dbus_adaptors',
  },
  'sources': [
    'dbus_bindings/service.name.of.Frobinator.xml',
  ],
  'includes': ['../common-mk/generate-dbus-adaptors.gypi'],
},

and this in users of your service (or for a client library target):

{
  'target_name': 'frobinator-proxies',
  'type': 'none',
  'actions': [
    {
      'action_name': 'generate-frobinator-proxies',
      'variables': {
        'proxy_output_file': 'include/frobinator/dbus-proxies.h',
      },
      'sources': [
        'path/to/frobinator/dbus_bindings/service.name.of.Frobinator.xml',
      ],
      'includes': ['../common-mk/generate-dbus-proxies.gypi'],
    },
  ],
},

The JSON service configuration file will look like this:

{
  "service_name": "service.name.of.Frobinator",
  "object_manager": {
    "object_path": "/object/path/to/Frobinator/ObjectManager"
  }
}

Then, in your service, you can #include "frobinator/dbus_adaptors/service.name.of.Frobinator.h" to get the interface and adaptor classes for Frobinator, and users can #include <frobinator/dbus-proxies.h> to get the proxy classes. Try to follow the best practices doc and only export one object for your service. The ObjectManager here will be used to integrate with Brillo's D-Bus daemons but otherwise should not affect your implementation of the bindings.

D-Bus types vs. C++ types

D-Bus methods, signals and properties have type signatures. When generating bindings, chromeos-dbus-bindings will map D-Bus types to C++ types like so:

D-Bus type signatureC++ type
yuint8_t
bbool
nint16_t
quint16_t
iint32_t
uuint32_t
xint64_t
tuint64_t
ddouble
sstd::string
hdbus::FileDescriptor
odbus::ObjectPath
v (variant)brillo::Any
(TU...)std::tuple<T, U, ...>
aTstd::vector<T>
a{TU}std::map<T, U>
a{sv}brillo::VariantDictionary

This type mapping is also recursive, i.e. an argument of type a{s(io)} will be mapped to std::map<std::string, std::tuple<int32_t, dbus::ObjectPath>>.

Method generation

Suppose you have a service with the following XML specification:

<node name="/org/chromium/Frobinator">
  <interface name="org.chromium.Frobinator">
    <method name="Frobinate">
      <arg name="foo" type="i" direction="in" />
      <arg name="bar" type="a{sv}" direction="in" />
      <arg name="baz" type="s" direction="out" />
    </method>
  </interface>
</node>

The generator will generate a class org::chromium::FrobinatorInterface with the following C++ method signature:

bool Frobinate(brillo::ErrorPtr* error,
               int32_t foo,
               const brillo::VariantDictionary& bar,
               std::string* baz);

This method can be implemented by inheriting org::chromium::FrobinatorInterface and can be called on proxy objects of type org::chromium::FrobinatorProxy. If the method fails, it should set the error to something descriptive and return false. If an arg has direction “in” and is not a simple numeric type, it will be passed in as const &.

Annotations

The bindings generator also supports several method annotations. Marking your methods with these will change the generated bindings.

org.chromium.DBus.Method.Kind:

  • simple: This method will not fail and no brillo::ErrorPtr argument is given. If it has only one “out” argument, it is treated as a normal return value. Otherwise, the method returns void and passes “out” arguments back as pointers as usual.
  • normal: As stated above. Returns false and sets a brillo::ErrorPtr on failure.
  • async: Instead of returning “out” arguments directly, the C++ method will take a DBusMethodResponse argument templated on the types of the “out” arguments. You can pass this object around and call its methods to reply later.
  • raw: Takes a dbus::MethodCall and dbus::ExportedObject::ResponseSender object directly. Use this if you need to do your own message parsing. Protos are often passed as type ay but Chrome's D-Bus bindings have special methods to handle them, and it might make sense to take the MethodCall directly for these.

These would have the following effect on the Frobinate method above:

Kind annotationC++ method signature
simplestd::string Frobinate(int32_t foo, const brillo::VariantDictionary& bar);
normalbool Frobinate(brillo::ErrorPtr* error, int32_t foo, const brillo::VariantDictionary& bar, std::string* baz);
asyncvoid Frobinate(std::unique_ptr<DBusMethodResponse<std::string>> response, int32_t foo, const brillo::VariantDictionary& bar);
rawvoid Frobinate(dbus::MethodCall* method_call, ResponseSender sender);

org.chromium.DBus.Method.Const: “true” adds const to the method signature

org.chromium.DBus.Method.IncludeDBusMessage: passes the dbus::Message* as an argument to the generated adaptor method following the brillo::ErrorPtr* or DBusMethodResponse

org.freedesktop.DBus.GLib.Async: same as setting Kind to async

Signal generation

Unlike methods which are exported in the FrobinatorInterface class, signals are sent from the FrobinatorAdaptor class and received by the FrobinatorProxy class. Thus, they look different to the service and its users. Suppose our service has the following XML specification:

<node name="/org/chromium/Frobinator">
  <interface name="org.chromium.Frobinator">
    <signal name="FrobinationCompleted">
      <arg name="foo" type="i" direction="out" />
      <arg name="bar" type="a{sv}" direction="out" />
    </method>
  </interface>
</node>

Our adaptor class will have a method:

void SendFrobinationCompletedSignal(int32_t foo,
                                    const brillo::VariantDictionary& bar);

and our proxy class will have a method:

void RegisterFrobinationCompletedSignalHandler(
    const base::Callback<void(int32_t, const brillo::VariantDictionary&)>& signal_callback,
    dbus::ObjectProxy::OnConnectedCallback on_connected_callback);

Calling this function will call on_connected_callback with whether or not the registration succeeded, and if it did, signal_callback will be called when the service emits this signal.

On properties

As stated the best practices doc, avoid using D-Bus properties because they won't transfer well to other IPC mechanisms if we need to switch in the future. Instead, get and set attributes on your service by using methods, and if you want users to be able to listen for changes in attributes, use signals.

Integrating with DBusServiceDaemon

brillo::DBusServiceDaemon is a class which abstracts away some initialization tasks for D-Bus services and also ensures that all methods are exported before the service takes its proper name on the bus. This helps prevent races where users fail invoking methods on a service which claimed its name too early.

DBusServiceDaemon has a virtual method RegisterDBusObjectsAsync which is where the adaptor can set up its D-Bus object and export it. Your adaptor implementation can inherit from DBusServiceDaemon, but it's clearer just to use containment instead here. A simple daemon could look like this:

class DBusAdaptor : public org::chromium::FrobinatorInterface,
                    public org::chromium::FrobinatorAdaptor {
 public:
  explicit DBusAdaptor(
      brillo::dbus_utils::ExportedObjectManager* object_manager)
    : org::chromium::FrobinatorAdaptor(this),
      dbus_object_(object_manager,
                   object_manager->GetBus(),
                   dbus::ObjectPath(kFrobinatorServicePath)) {

  void RegisterAsync(
      const brillo::dbus_utils::AsyncEventSequencer::CompletionAction& cb) {
    RegisterWithDBusObject(&dbus_object_);
    dbus_object_.RegisterAsync(cb);
  }

  // org::chromium::FrobinatorAdaptor overrides.
  bool Frobinate(brillo::ErrorPtr* error,
                 int32_t foo,
                 const brillo::VariantDictionary& bar,
                 std::string* baz) override;

 private:
  brillo::dbus_utils::DBusObject dbus_object_;

  DISALLOW_COPY_AND_ASSIGN(DBusAdaptor);
};

class FrobinatorDaemon : public brillo::DBusServiceDaemon {
 public:
  FrobinatorDaemon()
    : DBusServiceDaemon(kFrobinatorServiceName,
                        dbus::ObjectPath(kObjectManagerPath)) {}

 protected:
  void RegisterDBusObjectsAsync(
      brillo::dbus_utils::AsyncEventSequencer* sequencer) override {
    adaptor_.reset(new DBusAdaptor(object_manager_.get()));
    adaptor_->RegisterAsync(sequencer->GetHandler("RegisterAsync() failed",
                                                  true));
  }

 private:
  std::unique_ptr<DBusAdaptor> adaptor_;

  DISALLOW_COPY_AND_ASSIGN(FrobinatorDaemon);
};

int main(int argc, char** argv) {
  return FrobinatorDaemon().Run();
}

When the DBusServiceDaemon is ready to register objects, it calls your RegisterDBusObjectsAsync method. Here we use the RegisterWithDBusObject method from the generated adaptor class to export the methods, and then call RegisterAsync on the DBusObject to grab the name and interfaces for the D-Bus service later. The AsyncEventSequencer that the base daemon code passes us ensures that we'll do things in the right order.

Your service should now appear on the bus and you should be able to call methods using dbus-send or create org::chromium::FrobinatorProxy objects to interact with it:

dbus::Bus::Options options;
options.bus_type = dbus::Bus::SYSTEM;
scoped_refptr<dbus::Bus> bus(new dbus::Bus(options));
auto frobinator = base::MakeUnique<org::chromium::FrobinatorProxy>(bus);
frobinator.Frobinate(42, {{ "qux", brillo::Any("squawk") }});