Skip to content

DBus ​

Utilities for declaring DBus interfaces and using them as services or client proxies in a fully typed manner.

Read more about using DBus in GJS on gjs.guide.

What is DBus

DBus is a message bus for interprocess communication: processes connect to a shared bus, usually the session bus, or the system bus for system-wide services, and exchange messages.

On connection, each process is assigned a unique name such as :1.42. Since unique names are unpredictable, a process can additionally request a well-known name such as com.example.MyApplication, which is what makes IPC practical: clients address the service by its well-known name without having to know which process currently provides it. A Gio.Application instance automatically owns its application ID as a well-known name on the session bus.

Under a name, a process exports objects identified by object paths such as /com/example/MyApplication/window1, and each object can export multiple interfaces that they implement. Addressing a remote property, method or signal therefore takes the triple of name, object path and interface name.

Example usage ​

Declare an interface

ts
import { createDBusInterface, property, method, signal } from "gnim/dbus"

const MyInterface = createDBusInterface("example.gjs.MyInterface", {
  MyProperty: property("s"),
  MyMethod: method(["s"], ["s"]),
  MySignal: signal("s"),
})

Use it as a service

ts
const service = await MyInterface.serve({
  name: "example.gjs.MyInterface",
  objectPath: "/example/gjs/MyInterface",
  implementation: (emitter) => ({
    MyProperty: "initial value",
    MyMethod(str): [string] {
      emitter.MySignal(str)
      return [str]
    },
  }),
})

service.connect("my-signal", (_, str) => {
  console.log(`MySignal emitted with argument: "${str}"`)
})

service.connect("notify::my-property", () => {
  console.log(`MyProperty set to ${service.MyProperty}`)
})

Use it as a proxy

ts
const proxy = await MyInterface.proxy({
  name: "example.gjs.MyInterface",
  objectPath: "/example/gjs/MyInterface",
})

proxy.connect("my-signal", (_, str: string) => {
  console.log(`MySignal emitted with argument: "${str}"`)
})

proxy.connect("notify::my-property", () => {
  console.log(`MyProperty set to ${proxy.MyProperty}`)
})

proxy.MyProperty = "new value"

const [value] = await proxy.MyMethod("hello")
console.log(value) // "hello"

Both services and proxies are GObject.Object instances: DBus signals are also GObject signals and DBus properties are also GObject properties, using kebab-cased names.

TIP

They are also fully annotated for TypeScript and can be used with Gnim primitives.

ts
connectSignal(service, "my-signal", (str) => {
  console.log(`MySignal emitted with argument: "${str}"`)
})

const myProperty = bind(service, "my-property")

effect(() => {
  console.log(`value of MyProperty: ${myProperty()}`)
})

Synchronous instantiation ​

You can instead use the new operator, export services and init proxies.

ts
const service = new MyInterface.Service((emitter) => ({
  // impl
}))

service.export(Gio.DBus.session, "/example/gjs/MyInterface")

NOTE

To own a name other than the application ID of the main Gio.Application instance, you can use Gio.bus_own_name.

ts
const proxy = new MyInterface.Proxy({
  name: "example.gjs.MyInterface",
  objectPath: "/example/gjs/MyInterface",
})

proxy.init() // blocks IO
print(proxy.MyProperty)

// non blocking
proxy.initAsync().then(() => {
  print(proxy.MyProperty)
})

NOTE

The proxy function on MyInterface is just a wrapper over asynchronous initialization.

createDBusInterface ​

Declares a DBus interface from which both services and proxies can be created.

ts
function createDBusInterface<T extends InterfaceDeclaration>(
  name: string,
  interfaceDeclaration: T,
): DBusInterface<T>

interface DBusInterface<T extends InterfaceDeclaration> {
  Proxy: ProxyClass<T>
  Service: ServiceClass<T>
  serve(props: ServeProps<T>): Promise<ServiceInstance<T>>
  proxy(props: ProxyProps): Promise<ProxyInstance<T>>
}
Primitives it is composed of

If you only need the proxy part or the service part, you can use the underlying primitives instead.

ts
import { createInterfaceInfo, method, signal } from "gnim/dbus"

const ExampleServiceInterface = createInterfaceInfo("example.Service", {
  Method: method(),
  Signal: signal(),
})

Services only:

ts
import { createServiceClass, serve } from "gnim/dbus"

const ExampleService = createServiceClass(ExampleServiceInterface)

const exampleService = await serve(ExampleService, {
  implementation: (emitter) => ({
    Method: () => emitter.Signal(),
  }),
})

Proxies only:

ts
import { createProxyClass, proxy } from "gnim/dbus"

const ExampleProxy = createProxyClass(ExampleServiceInterface)

const exampleProxy = await proxy(ExampleProxy, {
  name: "example.Service",
  objectPath: "/example/Service",
})

property ​

Declares a DBus property with a variant type string.

ts
function property(type: string, access?: "r" | "w" | "rw")

Properties are read-write by default. Pass "r" to declare a read-only or "w" to declare a write-only property, which is reflected in the types of the service implementation and the proxy.

method ​

Declares a DBus method.

ts
type Arg = string | { name: string; type: string }

function method(...inArgs: Arg[])
function method(inArgs: Arg[], outArgs: Arg[])

Return values are declared as a list of out arguments, which implementations return and proxies resolve as a tuple.

NOTE

Optionally, you can declare the name of the arguments for DBus inspection by passing a { name: string, type: string } object instead of just the type string.

ts
method([{ name: "str", type: "s" }], ["s"])

signal ​

Declares a DBus signal.

ts
type Arg = string | { name: string; type: string }

function signal(...args: Arg[])

Unlike GObject signals, DBus signals do not have return types.

Services ​

serve ​

Instantiates an instance of service with the given implementation and attempts to own name and export it at objectPath on busType.

ts
interface DBusInterface<T extends InterfaceDeclaration> {
  serve(props: {
    busType?: Gio.BusType // default: Gio.BusType.SESSION
    name?: string // default: the interface name
    objectPath?: string // default: the interface name as a path
    flags?: Gio.BusNameOwnerFlags // default: Gio.BusNameOwnerFlags.NONE
    cancellable?: Gio.Cancellable
    implementation: (emitter: ServiceEmitter<T>) => ServiceImplementation<T>
  }): Promise<ServiceInstance<T>>
}

NOTE

It is mostly a wrapper over Gio.bus_own_name.

Implementing properties ​

Read-write properties can be implemented either as plain data properties or as getter/setter pairs. Changes to data properties are detected automatically, while getters/setters have to notify through the emitter. Read-only properties are implemented with a getter and write-only properties with a setter.

ts
const service = await MyInterface.serve({
  implementation: (emitter) => ({
    // data property: assignments automatically emit
    // PropertiesChanged and notify:: when the value changes
    MyProperty: "initial value",

    // getter/setter property: has to emit manually
    get MyOtherProperty() {
      return internalValue
    },
    set MyOtherProperty(value: string) {
      internalValue = value
      emitter.MyOtherProperty()
      emitter.ReadOnlyProperty()
    },

    // read-only property
    get ReadOnlyProperty() {
      return this.MyOtherProperty + "ReadOnly"
    },

    // write-only property
    set WriteOnlyProperty(value: number) {
      writeOnlyValue = value
    },
  }),
})

Implementing methods ​

Methods take their in arguments as parameters and return their out arguments as a tuple, either synchronously or as a Promise. Methods with no out arguments return nothing.

ts
const service = await MyInterface.serve({
  implementation: (emitter) => ({
    MySyncMethod(str): [string] {
      return [str]
    },
    async MyAsyncMethod(str): Promise<[string]> {
      await somethingAsync()
      return [str]
    },
  }),
})

Thrown errors are returned to the caller as DBus errors.

Implementation classes ​

InferImplementation and InferEmitter extract the implementation and emitter types of a declared interface, which is useful for implementing services as classes.

ts
import type { InferEmitter, InferImplementation } from "gnim/dbus"

type Emitter = InferEmitter<typeof MyInterface>
type Impl = InferImplementation<typeof MyInterface>

class Implementation implements Impl {
  private emitter: Emitter

  constructor(emitter: Emitter) {
    this.emitter = emitter
  }

  MyProperty = "initial value"

  MyMethod(str: string): [string] {
    this.emitter.MySignal(str)
    return [str]
  }
}

const service = await MyInterface.serve({
  implementation: (emitter) => new Implementation(emitter),
})

The service object ​

It is an instance of GObject.Object exposing the interface.

  • Its properties read and write through the implementation and enforce access flags: reading a write-only or assigning a read-only property throws. Assigning a data property automatically emits PropertiesChanged and notify:: just like a remote write does.
  • Its methods invoke the implementation directly.
  • DBus signals and property changes are also emitted as GObject signals, which can be connected to with their kebab-cased names, such as my-signal or notify::my-property.
ts
class ServiceInstance extends GObject.Object {
  implementation: ServiceImplementation<T>
  export(connection: Gio.DBusConnection, objectPath?: string): boolean
  unexport(): void
}

Serving stops with unexport, which also releases the owned name when the serve function was used to instantiate the service.

Proxies ​

proxy ​

Attempts to proxy name's object at objectPath on the bus connection.

ts
interface DBusInterface<T extends InterfaceDeclaration> {
  proxy(props?: {
    bus?: Gio.DBusConnection // default: Gio.DBus.session
    name?: string // default: the interface name
    objectPath?: string // default: the interface name as a path
    flags?: Gio.DBusProxyFlags // default: Gio.DBusProxyFlags.NONE
    timeout?: number // default: 10_000
  }): Promise<ProxyInstance<T>>
}

NOTE

It is a wrapper over the initAsync method of the Proxy class produced by createDBusInterface.

The proxy object ​

  • Property reads are served from a cache which is kept in sync through PropertiesChanged signals. If a value is missing from the cache, it is fetched with a blocking call.
  • Property writes update the cache optimistically and set the remote property asynchronously. If the remote write fails, the cached value is rolled back.
  • Methods are invoked asynchronously and resolve their out arguments as a tuple, or an empty tuple for methods without out arguments. They reject with a GLib.Error if the remote implementation throws.
  • DBus signals and property changes are emitted as GObject signals, which can be connected to with their kebab-cased names, such as my-signal or notify::my-property.