Skip to content

Latest commit

 

History

History
421 lines (306 loc) · 16.7 KB

README.md

File metadata and controls

421 lines (306 loc) · 16.7 KB

JNIDBus - A JNI wrapper around libdbus

JNIDbus is a Java JNI wrapper around the native library libdbus allowing an easy and straightforward use of Dbus from Java code in an object oriented way. Keep in mind that this library is a wrapper around the official library and not a protocol implementation like this one.

JNIDbus is built around a single-threaded event loop, making it lightweight and easy to use but the developer must keep in mind he must avoid having long running code in its handlers or else the event loop will stall.

This library is distributed under the AFL licence.

Message serialization

JNIdbus uses annotations and reflection to serialize POJO into DBus messages. The following types are supported:

  • Boolean, Byte, Short, Integer, Long, Double and their primitive equivalent
  • String
  • Object path (mapped on the ObjectPath class)
  • Enum (transferred either as String or Integer according to the signature of the message)
  • Nested serializable objects
  • Lists

The basics

In order to make an object serializable for JNIDBus, it must extends the Message class and have the DBusType annotation. The library will try to cache the POJO reflection information and check that it respect the signature given in the signature property of the annotation so the developer can quickly detect errors in the object mappings.

example for a basic string message:

@DBusType(
    /* please refer to the DBus documentation for more information
     * about signature format
     */
    signature = "s",
    /* fields mapped to the signature, if your signature contains multiple elements
     * the fields will be mapped from left to right to the first element found
     */
    fields = "string"
)
public class StringMessage extends Message {
    /* fields exposed to JNIDBus must have setters and getters respecting the classic
     * getXXX and setXXX format.
     */
    private String string;

    public String getString() {
        return string;
    }
    public void setString(String string) {
        this.string = string;
    }
}

Nested objects

When dealing with nested objects, the signature of the parent object must include the signature of its child object. This choice was made to avoid implicit signature modification when modifying the child object. If your child object is an inner class, don't forget to make it public and static so JNIDBus can instantiate it.

In the example below if you change the signature of SubObject you will have to explicitely update the signature of NestedObject too or else JNIDBus will throw en exception.

example for a nested object message:

@DBusType(
    /* The following signature means we have a root object containing an integer
     * and another object. The child object contain a single string. Please note that
     * you have to specify the child object signature in the parent object and in the
     * child object
     */
    signature = "i(s)",
    fields = {"integer","object"}
)
public class NestedObject extends Message {
    private int integer;
    private SubObject object;

    public int getInteger() ...
    public void setInteger(int integer) ...

    public SubObject getObject() ...
    public void setObject(SubObject object) ...
}

@DBusType(
    signature = "s",
    fields = {"string"}
)
public class SubObject extends Message{
    private String string;

    public String getString() ...
    public void setString(String string) ...
}

Arrays

The DBus array type is mapped to either the List class or to native arrays . A List/array can contain anything serializable, including other lists/arrays. As JNIDBus uses reflection to know the type of the List items, the generic type must always be explicitly used in the getters and setters

example for a nested list message:

@DBusType(
    /* This signature correspond to an array containing other arrays of integers */
    signature = "aai",
    fields = "array"
)
public class CollectionOfCollectionArray extends Message {
    private List<List<Integer>> array;

    //always explicitly give the precise generic type
    public List<List<Integer>> getArray() ...
        
	//for setters too
    public void setArray(List<List<Integer>> array) ...
}

Dictionaries (Maps)

The DBus arrays of dict_entries are mapped to the Map class, the key must be a primitive DBus type (refer to the DBus documentation for a definition of primitive) and its value can contain nested objects, arrays or maps. As for the List, the generic types must be explicitly used in the getters/setters.

example for a map message:

@DBusType(
    /* This signature correspond to an array containing dict_entries of a string and an  
     * integer. In the java world, it's a Map with a string as key and integers as values
     */
    signature = "a{si}",
    fields = "map"
)
public class MapMessage extends Message {
    private Map<String,Integer> map;

    //always explicitly give the precise generic type
    public Map<String,Integer> getMap() ...
        
	//for setters too
    public void setMap(Map<String,Integer> map) ...
}

Enums

You can use Enums in your messages, they will be transferred by name or by ordinal according to the signature you gave to the DBusType annotation.

example of an Enum message:

@DBusType(
    //"byName" will be transferred as a string and "byOrdinal" as an integer
    signature = "si",
    fields = {"byName","byOrdinal"}
)
public class EnumMessage extends Message {
    private Enum byName;
    private Enum byOrdinal;
   
    public Enum getByName() { ... }
    public void setByName(Enum byName) { ... }
    public Enum getByOrdinal() { ... }
    public void setByOrdinal(Enum byOrdinal) { ... }

    public static enum Enum{ ... }
}

The EmptyMessage

if your message does not contain any data, the Message class contains a static EMPTY property which contains an empty Message that can be used. Using this object allows some internal optimizations and it is recommended to use it whenever you can.

How to use the DBus object

As all the operation regarding DBus are processed by the event loop, most of the JNIDBus primitives are asynchronous and need an additional RequestCallback parameter. If you don't need this asynchronicity, the parameter is nullable. Most of the asynchronous DBus primitives have a blocking version, that you can use to block your program until the result arrives. Those blocking method will throw an exception if called from the main event loop.

Handlers

Handlers are classes that will be able to receive signals and expose method calls to DBus. Those classes must extends the GenericHandler class and be annotated with the Handler annotation which will give JNIDBus informations about the object path and DBus interface you want to match.

Your handler methods must be annotated with the HandlerMethod annotation which will define the member (method or signal name) and type of handler (Signal or Call). Please note that non-annotated methods will be ignored and left unprocessed.

Signal handler methods must return void and Call handler methods must return an object extending the Message class. The signatures of the handler method will be inferred from their parameter types and return type. You can have multiple methods bound to the same member as long as they have different input signature.

Handlers will be executed on the same thread as the event loop which means that the kind of code you write in your handler will have a great impact on the overall latency of the event loop. You must avoid blocking code at all cost.

example of a Signal and Call handler:

@Handler(
    /* please make sure tor espect the Dbus object path format (which is pretty much
     * the same as Java namespace format)
     */
    path = "/some/object/path",
    interfaceName = "some.dbus.interface"
)
public class SomeHandler extends GenericHandler {
    @HandlerMethod(
        //the member can be different from the java method name
        member = "someSignal",
        type = HandlerType.SIGNAL
    )
    //Here our signal do not have any data so we can use the EmptyMessage
    public void someSignal(Message.EmptyMessage emptyMessage) ...

    @HandlerMethod(
        member = "stringSignal",
        type = HandlerType.METHOD
    )
    public SomeOutput someCall(SomeInput input) ...
}

how to use register the handler to DBus:

//connect to the DBus deamon
Dbus receiver = new Dbus(BusType.SESSION,"my.bus.name");
//instantiate your handler
SomeHandler handler = new SomeHandler();
//add it to DBus, JNIDBus will automatically check your bindings and throw if something is wrong.
this.receiver.addHandlerBlocking(handler);

Asynchronous handler

As we have seen above, you must avoid blocking task on the event loop, but how to do long running task then? JNIDBus support asynchronous computation using its own Promise class. A handler method can return a Promise that will be resolved by another thread at a later time when the computation is done

example of an asynchronous Call handler:

@Handler(
    ...
)
public class CallHandler extends GenericHandler {
    @HandlerMethod(
        member = "blockingCall",
        type = HandlerType.METHOD
    )
    
    public Promise<Message.EmptyMessage> blockingCall(Message.EmptyMessage emptyMessage){
        final Promise<Message.EmptyMessage> promise = new Promise<>();
        new Thread(new Runnable() {
            @Override
            public void run() {
                //a very long computation
                ...
                //we are done, resolve the promise
                promise.resolve(Message.EMPTY);
            }
        }).start();
        return promise;
    }
}

Exception handling

Exception handling is made through the DBusException class, which contains two fields: code and message which correspond to the DBus error fields of the same name. If an error happens in your handler method call, you should catch it and throw a new DBusException with tis field correctly set so the caller can process the error.

If an unexpected exception happens in a handler, a DBusException will be automatically created with its code being the name of the exception class and its message the message of the exception. Beware, if an exception happen outside of the event loop (ie. asynchronous handler), uncaught exception won't be automatically sent back to the caller and will be lost in the current thread exception handler.

Send a call

JNIDBus provide a way to represent your DBus distant objects through Java proxies. You describe your DBus object as an interface with a @RemoteInterface annotation and annotate the methods with @RemoteMember. All of the interface methods must be annotated.

There must be only one parameter on each method which must be serializable. If your call do not have any parameter you can omit it instead of using the EmptyMessage class. Each method must return a Promise with its generic type being the expected return type.

ThePromise class allows you to bind a callback that will be notified when a result or an error is received. A callback will be notified once and further results/errors will be ignored. you can cancel a call by manually calling the fail() method on the Promise which will notify the callback and block any incoming result. you can choose the thread that will execute the callback by specifying the Executor parameter. The EventLoop itself is an executor and you can it if your callback execute DBus methods to speed up the event processing.

By default, if no Executor are specified, the callback will be executed either on the thread binding the callback, or on the event loop, depending upon which comes last.

example of a call without input that returns a string:

@RemoteInterface("call.interface.name")
public interface MyRemoteObject{
    
    @RemoteMember("someCall")
    Promise<StringMessage> call();
    //equivalent to: 
    //Promise<StringMessage> call(Message.EmptyMessage msg)
}

example of a callback for the above call:

public class StringCallback implements Promise.Callback<StringMessage>{
    @Override
    public void value(StringMessage value, Exception e) {
        //do something
    }
}

how to use all of the above:

//connect to the DBus deamon
Dbus sender = new Dbus(BusType.SESSION,"my.bus.name");

//create the proxy instance of your remote object
MyRemoteObject remote = sender.createRemoteObject("receiver.bus.name",
                                                  "/remote/object/path",
                                                  MyRemoteObject.class);
//call the method on your remote object
Promise<StringMessage> pending = remote.call();

//create the listener
StringListener l = new StringCallback();

//bind it, now the listener will be executed when a result arrives
pending.then(l);

Send a Signal

Signals are represented with inner classes of a RemoteInterface extending the Signal class. The generic type of the Signal class will be the data attached to this signal

example of a string signal and of an empty signal:

@RemoteInterface("call.interface.name")
public interface MyRemoteObject{
    
    @RemoteMember("emptySignal")
    class EmptySignal extends Signal<Message.EmptyMessage>{
        public EmptySignal() {
            //the super constructor takes as argument an instance of the Message to send
            super(Message.EMPTY);
        }
    }

    @RemoteMember("stringSignal")
    class StringSignal extends Signal<StringMessage>{
        public StringSignal(StringMessage msg) {
            super(msg);
        }
    }
}

how to send the signals:

//connect to the DBus deamon
Dbus sender = new Dbus(BusType.SESSION,"my.bus.name");

//create and fill your message
StringMessage msg = new StringMessage();
msg.setString("A string");

//simply send it
sender.sendSignal("/remote/object/path",new MyRemoteObject.StringSignal(msg));

Kotlin

A support library for Kotlin is available under the artifact jnidbus-kotlin, it provides basic support for coroutines through the await() extension on the Promsie class and it also allows for DBus handlers to declare suspending methods. In order to do so, your handler class must extends the KotlinGenericHandler class instead of the GenericHandler one.

Please note that you must explicitly register the kotlin extension to jnidbus by using the KotlinMethodInvocator.registerKotlinInvocator() before registering any kotlin handler

how to use jnidbus-kotlin:

/// How to write a suspending dbus handler ///
KotlinMethodInvocator.registerKotlinInvocator()

@Handler(path = "...", interfaceName = "...")
class CallHandler : KotlinGenericHandler() {

    @HandlerMethod(member = "suspendingCall", type = MemberType.METHOD)
    suspend fun suspendingCall(emptyMessage: Message.EmptyMessage): SingleStringMessage {
        //coroutines are awesome
        delay(2000)
        return SingleStringMessage().apply { string = "test" }
    }
}

/// How to do a suspending dbus call ///
suspend fun suspendingDBusCall() : SingleStringMessage{
    val remoteObj = dbus.createRemoteObject("...", "...",SomeRemoteInterface::class.java)
    val pending = remoteObj.suspendingCall()
    return pending.await()
}

Planned tasks

  • Refactor serialization.cpp to use proper OOP and cleanup the code
  • Add the capability to create downcasted typed array in JNI code instead of plain Object[] objects
  • Use direct ByteBuffer instead of plain Object arrays to avoid copies.

FAQ

Which java versions are compatible?

Java 8 to Java 11 are tested by the CI, Java 7 should run but is not tested. You will also need libc and libdbus-1 to run the native code

How fast is this library

I was able to get around 40k/s "complex" signals sent and received on a single event loop on my modest i3-4130 work machine. I was able to get around 95k/s empty signals with the same setup. This should satisfy most of the use cases so unless you really need to push DBus to its limit it's enough.

I found a bug

Feel free to post an issue describing precisely what behavior you got and whet result you expected. If you can provide a small reproducer we will be able to get rid of this bug even faster.

How to contribute

Fork this project and send us a merge request, we will make sure to get back to you as soon as possible. You should also open an issue beforehand so we can give you feedback before you write any code.