Skip to content

Commit db5dea8

Browse files
authored
Implement dict entry (#3)
* Move packages around to be able to execute single tests + fix signature parsing for nested objects * move kotlin test file to correct package + write more tests for the signature parser * Add dict_entry signature parsing and validation * Refactor whole java serialization process (+13% performance) * Refactor serialization test (WIP) + fix primitive array support + add test for them * Fix class cache * Finish test refactor + optimize imports * Implement dict_entry serialization + small JNI code refactor/optimization * Fix nested array serialization in JNI code * Set dict_entry signature to NULL(refer to dbus doc for mroe info) * Comment code + small JNI refactor + fix some bug about complex arrays * Get ready for release * Fix merge conflicts
1 parent b1d71c1 commit db5dea8

File tree

64 files changed

+1824
-718
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

64 files changed

+1824
-718
lines changed

README.md

+34-5
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ public class SubObject extends Message{
9595

9696
### Arrays
9797

98-
DBus array type is mapped to the `List` collection. A List can contain anything serializable, including other lists. 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
98+
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
9999

100100
*<u>example for a nested list message:</u>*
101101

@@ -116,6 +116,32 @@ public class CollectionOfCollectionArray extends Message {
116116
}
117117
```
118118

119+
### Dictionaries (`Maps`)
120+
121+
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.
122+
123+
*<u>example for a map message:</u>*
124+
125+
```java
126+
@DBusType(
127+
/* 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
128+
*/
129+
signature = "a{si}",
130+
fields = "map"
131+
)
132+
public class MapMessage extends Message {
133+
private Map<String,Integer> map;
134+
135+
//always explicitly give the precise generic type
136+
public Map<String,Integer> getMap() ...
137+
138+
//for setters too
139+
public void setMap(Map<String,Integer> map) ...
140+
}
141+
```
142+
143+
144+
119145
### The EmptyMessage
120146

121147
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.
@@ -344,19 +370,22 @@ suspend fun suspendingDBusCall() : SingleStringMessage{
344370
345371
346372
347-
## Planned features
373+
## Planned tasks
348374
349-
- Support `DICT_ENTRY` type
375+
- Support `OBJECT_PATH` type
376+
- Refactor `serialization.cpp` to use proper OOP and cleanup the code
377+
- Add the capability to create downcasted typed array in JNI code instead of plain `Object[]` objects
378+
- Use direct `ByteBuffer` instead of plain `Object` arrays to avoid copies.
350379
351380
## FAQ
352381
353382
##### Which java versions are compatible?
354383
355-
Java 7 and Java 11 where tested. You will also need `libc` and `libdbus-1` to run the native code
384+
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
356385
357386
##### How fast is this library
358387
359-
I was able to get around 35k complex signals (nested lists and objects) sent and received on a single event loop on my modest i3-4130 work machine. I was able to get around 58k 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.
388+
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.
360389

361390
##### I found a bug
362391

build.gradle

+4
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,14 @@ allprojects{
3333
//add JNIDBus JNI library to path
3434
jvmArgs "-Djava.library.path=${root}/src/main/resources"
3535

36+
//set JNI in berserk mode
37+
//jvmArgs "-Xcheck:jni"
38+
3639
//show only failed test and their full exception
3740
testLogging {
3841
events "failed"
3942
exceptionFormat "full"
43+
//showStandardStreams = true
4044
}
4145

4246
//always execute tests

gradle.properties

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
jnidbus_version 0.3.0
1+
jnidbus_version 0.4.0
22

33
java_version 1.7
44
kotlin_version 1.3.50

jnidbus-kotlin/src/main/kotlin/fr/viveris/jnidbus/dispatching/KotlinGenericHandler.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ package fr.viveris.jnidbus.dispatching
33
import fr.viveris.jnidbus.message.Promise
44
import fr.viveris.jnidbus.serialization.DBusType
55
import fr.viveris.jnidbus.serialization.Serializable
6-
import java.util.HashMap
6+
import java.util.*
77
import kotlin.reflect.KClass
88
import kotlin.reflect.full.createType
99
import kotlin.reflect.full.declaredMemberFunctions

jnidbus-kotlin/src/test/kotlin/fr/viveris/jnidbus/test/PendingCallExtensionTest.kt

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
package fr.viveris.jnidbus.test
22

3-
import fr.viveris.jnidbus.test.common.DBusObjects.SingleStringMessage
4-
import fr.viveris.jnidbus.test.common.DBusTestCase
53
import fr.viveris.jnidbus.await
64
import fr.viveris.jnidbus.dispatching.GenericHandler
75
import fr.viveris.jnidbus.dispatching.MemberType
@@ -13,6 +11,8 @@ import fr.viveris.jnidbus.message.PendingCall
1311
import fr.viveris.jnidbus.message.Promise
1412
import fr.viveris.jnidbus.remote.RemoteInterface
1513
import fr.viveris.jnidbus.remote.RemoteMember
14+
import fr.viveris.jnidbus.test.common.DBusObjects.SingleStringMessage
15+
import fr.viveris.jnidbus.test.common.DBusTestCase
1616
import kotlinx.coroutines.*
1717
import kotlin.test.Test
1818
import kotlin.test.assertEquals

jnidbus-kotlin/src/test/kotlin/fr/viveris/jnidbus/test/SuspendingHandlerTest.kt

+5-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
package fr.viveris.jnidbus.test
22

3-
import fr.viveris.jnidbus.test.common.DBusObjects.SingleStringMessage
4-
import fr.viveris.jnidbus.test.common.DBusTestCase
53
import fr.viveris.jnidbus.await
64
import fr.viveris.jnidbus.dispatching.KotlinGenericHandler
75
import fr.viveris.jnidbus.dispatching.KotlinMethodInvocator
@@ -12,7 +10,11 @@ import fr.viveris.jnidbus.message.Message
1210
import fr.viveris.jnidbus.message.PendingCall
1311
import fr.viveris.jnidbus.remote.RemoteInterface
1412
import fr.viveris.jnidbus.remote.RemoteMember
15-
import kotlinx.coroutines.*
13+
import fr.viveris.jnidbus.test.common.DBusObjects.SingleStringMessage
14+
import fr.viveris.jnidbus.test.common.DBusTestCase
15+
import kotlinx.coroutines.delay
16+
import kotlinx.coroutines.runBlocking
17+
import kotlinx.coroutines.withTimeout
1618
import kotlin.test.BeforeTest
1719
import kotlin.test.Test
1820
import kotlin.test.assertEquals

src/jmh/java/fr/viveris/SameThreadSignals.java

+3-3
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,6 @@
33
*/
44
package fr.viveris;
55

6-
import fr.viveris.jnidbus.test.common.DBusObjects.ArrayRecursiveObject;
7-
import fr.viveris.jnidbus.test.common.DBusObjects.SimpleMessage;
8-
import fr.viveris.jnidbus.test.common.DBusTestCase;
96
import fr.viveris.jnidbus.BusType;
107
import fr.viveris.jnidbus.Dbus;
118
import fr.viveris.jnidbus.bindings.bus.EventLoop;
@@ -18,6 +15,9 @@
1815
import fr.viveris.jnidbus.remote.RemoteInterface;
1916
import fr.viveris.jnidbus.remote.RemoteMember;
2017
import fr.viveris.jnidbus.remote.Signal;
18+
import fr.viveris.jnidbus.test.common.DBusObjects.ArrayRecursiveObject;
19+
import fr.viveris.jnidbus.test.common.DBusObjects.SimpleMessage;
20+
import fr.viveris.jnidbus.test.common.DBusTestCase;
2121
import org.openjdk.jmh.annotations.*;
2222

2323
import java.util.concurrent.CountDownLatch;

src/main/java/fr/viveris/jnidbus/Dbus.java

+3-3
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ public Dbus(BusType type, String busName) throws ConnectionException {
8888
public void addHandler(GenericHandler handler){
8989
LOG.info("Adding DBus handler {}",handler.getClass().getName());
9090
//get annotation
91-
Handler handlerAnnotation = handler.getClass().getAnnotation(Handler.class);
91+
Handler handlerAnnotation = handler.getHandlerAnnotation();
9292
if(handlerAnnotation == null) throw new IllegalStateException("The given handler does not have the Handler annotation");
9393

9494
//get all criteria provided by this handler
@@ -124,7 +124,7 @@ public void addHandler(GenericHandler handler){
124124

125125
public void removeHandler(GenericHandler handler){
126126
//get annotation
127-
Handler handlerAnnotation = handler.getClass().getAnnotation(Handler.class);
127+
Handler handlerAnnotation = handler.getHandlerAnnotation();
128128
if(handlerAnnotation == null) throw new IllegalStateException("The given handler does not have the Handler annotation");
129129

130130
Dispatcher dispatcher = this.dispatchers.get(handlerAnnotation.path());
@@ -170,7 +170,7 @@ public <T> T createRemoteObject(String destinationBus, String objectPath, Class<
170170
* @param signal
171171
*/
172172
public void sendSignal(String objectPath, Signal signal){
173-
SignalMetadata meta = RemoteObjectInterceptor.getFromCache(signal.getClass());
173+
SignalMetadata meta = RemoteObjectInterceptor.getFromCache(signal);
174174
this.eventLoop.send(new SignalSendingRequest(signal.getParam().serialize(),objectPath,meta.getInterfaceName(),meta.getMember()));
175175
}
176176

src/main/java/fr/viveris/jnidbus/cache/MessageMetadata.java

+78-47
Original file line numberDiff line numberDiff line change
@@ -4,23 +4,21 @@
44
package fr.viveris.jnidbus.cache;
55

66
import fr.viveris.jnidbus.exception.MessageCheckException;
7-
import fr.viveris.jnidbus.message.Message;
87
import fr.viveris.jnidbus.serialization.DBusType;
98
import fr.viveris.jnidbus.serialization.Serializable;
9+
import fr.viveris.jnidbus.serialization.serializers.*;
1010
import fr.viveris.jnidbus.serialization.signature.Signature;
1111
import fr.viveris.jnidbus.serialization.signature.SignatureElement;
1212
import fr.viveris.jnidbus.serialization.signature.SupportedTypes;
1313

1414
import java.lang.reflect.*;
1515
import java.util.HashMap;
16-
import java.util.List;
1716

1817
/**
19-
* A MessageMetadata contains all the data available through reflection that are used for the serialization and unserialization
20-
* process. This optimizes the process and allows the library to do a lot of checks on the Messages classes without any
21-
* cost at runtime.
22-
*
23-
* The class caches: getters, setters, annotation data and constructor instance for faster instantiation
18+
* A MessageMetadata contains the list of fields managed by JNIDBus, their setters and getters. In order lower the cost of
19+
* the heavy use of Reflection, the fields are mapped to a pair of Serializer/deserializer which will analyze the signature
20+
* and type of the field, save those information and reuse them when doing the actual serialization. This mechanism speeds
21+
* up a lot the serialization process.
2422
*/
2523
public class MessageMetadata {
2624
/**
@@ -44,6 +42,11 @@ public class MessageMetadata {
4442
private HashMap<String, Method> setters = new HashMap<>();
4543
private HashMap<String, Method> getters = new HashMap<>();
4644

45+
/**
46+
* Serializer and deserializers by Methods
47+
*/
48+
private HashMap<String, Serializer> fieldSerializers = new HashMap<>();
49+
4750
/**
4851
* Message class object
4952
*/
@@ -62,14 +65,14 @@ public MessageMetadata(Class<? extends Serializable> clazz) throws MessageCheckE
6265

6366
//check annotation
6467
DBusType type = clazz.getAnnotation(DBusType.class);
65-
if(type == null) throw new IllegalStateException("No DBusType annotation found");
68+
if(type == null) throw new MessageCheckException("no DBusType annotation found",this.clazz);
6669

6770
//check constructor
6871
Constructor<? extends Serializable> constructor;
6972
try {
7073
constructor = clazz.getConstructor();
7174
} catch (NoSuchMethodException e) {
72-
throw new IllegalStateException("No public empty constructor found");
75+
throw new MessageCheckException("No public empty constructor found",this.clazz);
7376
}
7477

7578
//create a new cache entity
@@ -89,34 +92,52 @@ public MessageMetadata(Class<? extends Serializable> clazz) throws MessageCheckE
8992
try{
9093
field = clazz.getDeclaredField(fieldName);
9194
}catch (Exception e){
92-
throw new MessageCheckException("Could not find the field "+fieldName);
95+
throw new MessageCheckException("Could not find field",this.clazz,fieldName);
9396
}
9497

95-
//check the field against the annotation signature
96-
MessageMetadata.checkField(field,field.getGenericType(),element);
98+
Type fieldType = field.getGenericType();
9799

98100
try {
101+
//get the getter and create its serializer, the serializer will check signature and type validity
99102
Method getter = clazz.getDeclaredMethod(getterName);
100103
this.getters.put(fieldName,getter);
104+
if(!fieldType.equals(getter.getGenericReturnType())){
105+
throw new MessageCheckException("The getter return type is not the same as the field",this.clazz,fieldName);
106+
}
101107
} catch (NoSuchMethodException e) {
102-
throw new MessageCheckException("Could not find the getter for the field "+fieldName);
108+
throw new MessageCheckException("Could not find getter",this.clazz,fieldName);
103109
}
104110

105111
try {
112+
//get the setter and create its deserializer, the deserializer will check signature and type validity
106113
Method setter = clazz.getDeclaredMethod(setterName,field.getType());
107114
this.setters.put(fieldName,setter);
115+
if(setter.getGenericParameterTypes().length != 1){
116+
throw new MessageCheckException("The setter must only have one parameter",this.clazz,fieldName);
117+
}
118+
if(!fieldType.equals(setter.getGenericParameterTypes()[0])){
119+
throw new MessageCheckException("The setter return type is not the same as the field",this.clazz,fieldName);
120+
}
108121
} catch (NoSuchMethodException e) {
109-
throw new MessageCheckException("Could not find the setter for the field "+fieldName);
122+
throw new MessageCheckException("Could not find setter",this.clazz,fieldName);
110123
}
111124

125+
this.fieldSerializers.put(fieldName,this.generateSerializerForSignature(fieldType,element,fieldName));
112126
i++;
113127
}
128+
129+
if(i != this.fields.length){
130+
throw new MessageCheckException("Incomplete signature, there is too much fields",this.clazz);
131+
}
114132
}
115133

116134
public Method getGetter(String fieldName){
117135
return this.getters.get(fieldName);
118136
}
119137

138+
public Serializer getFieldSerializer(String fieldName){
139+
return this.fieldSerializers.get(fieldName);
140+
}
120141
public Method getSetter(String fieldName){
121142
return this.setters.get(fieldName);
122143
}
@@ -132,7 +153,7 @@ public String[] getFields() {
132153
public Class<? extends Serializable> getMessageClass(){ return this.clazz; }
133154

134155
/**
135-
* Create a new empty instances of the Message object (for unserialization).
156+
* Create a new empty instances of the Message object (for deserialization).
136157
*
137158
* @return new empty instance of the message
138159
*/
@@ -145,42 +166,52 @@ public Serializable newInstance(){
145166
}
146167

147168
/**
148-
* Check if the given field is valid according to the given signature element
149-
*
150-
* @param field field to check
151-
* @param element signature element used to check
169+
* This method will try to find a serializer matching a generic type and a signature, the created serializer will
170+
* perform all the necessary checks and try to find incoherence.
171+
* @param genericType the Type of the method, could be a parametrized type or standard one
172+
* @param element
173+
* @return
152174
* @throws MessageCheckException
153175
*/
154-
private static void checkField(Field field, Type fieldType, SignatureElement element) throws MessageCheckException{
155-
//get primitive will catch primitive fields and primitive arrays
156-
if(element.getPrimitive() != null){
157-
boolean isList = fieldType instanceof ParameterizedType;
158-
//check the generic is a List and if it matched the signature
159-
if(isList && (!List.class.isAssignableFrom((Class)((ParameterizedType)fieldType).getRawType()) || element.getContainerType() != SupportedTypes.ARRAY))
160-
throw new MessageCheckException("the field "+field.getName()+" is not of a List or the signature is not expecting a list");
161-
162-
//check if the generic type contains the correct type
163-
SupportedTypes type = element.getPrimitive();
164-
if(isList){
165-
ParameterizedType paramType = (ParameterizedType) fieldType;
166-
if(!paramType.getActualTypeArguments()[0].equals(type.getBoxedType()) && !paramType.getActualTypeArguments()[0].equals(type.getPrimitiveType()))
167-
throw new MessageCheckException("the field "+field.getName()+" is not of a List of Integer");
168-
}else {
169-
if (!fieldType.equals(type.getBoxedType()) && !fieldType.equals(type.getPrimitiveType())) throw new MessageCheckException("the field " + field.getName() + " is not of type "+type);
170-
}
171-
}else if(element.isArray()){
172-
//recursively check the content of the nested list
173-
MessageMetadata.checkField(field,((ParameterizedType)fieldType).getActualTypeArguments()[0],element.getSignature().getFirst());
174-
175-
}else if(element.isObject()){
176-
if(!Serializable.class.isAssignableFrom((Class)fieldType)) throw new MessageCheckException("the field "+field.getName()+" does not contain a serializable type");
176+
private Serializer generateSerializerForSignature(Type genericType, SignatureElement element, String fieldName) throws MessageCheckException {
177+
//TypeVariables are not usable and should not be allowed
178+
if(genericType instanceof TypeVariable) throw new MessageCheckException("Unspecified generic type found",this.clazz,fieldName);
179+
180+
//if the method returns a generic type, extract it, this allows us to support generic messages and nested lists
181+
Class<?> clazz;
182+
if(genericType instanceof ParameterizedType){
183+
clazz = (Class<?>) ((ParameterizedType)genericType).getRawType();
184+
}else{
185+
clazz = (Class<?>) genericType;
186+
}
177187

178-
Class<? extends Serializable> clazz = ((Class)fieldType).asSubclass(Serializable.class);
179-
MessageMetadata testedEntity = new MessageMetadata(clazz);
180-
Message.addToCache(clazz,testedEntity);
188+
//if we have an array container type and a primitive type, it means we have a primitive array
189+
if(element.getContainerType() == SupportedTypes.ARRAY && element.getPrimitive() != null){
190+
return new PrimitiveArraySerializer(clazz,element,this.clazz,fieldName);
191+
192+
// if there is a primitive type but no container, we have a primitive type
193+
}else if(element.getContainerType() == null && element.getPrimitive() != null){
194+
return new PrimitiveSerializer(clazz,element,this.clazz,fieldName);
195+
196+
//as we checked for primitive arrays before, if there is an array container type it means we have a complex array or a map
197+
}else if(element.getContainerType() == SupportedTypes.ARRAY){
198+
//check first element type to se if we have a Map
199+
if(element.getSignature().getFirst().getContainerType() == SupportedTypes.DICT_ENTRY_BEGIN){
200+
return new MapSerializer(genericType,element,this.clazz,fieldName);
201+
}else{
202+
return new ComplexArraySerializer(genericType,element,this.clazz,fieldName);
203+
}
181204

182-
if(!testedEntity.getSignature().equals(element.getSignatureString()))
183-
throw new MessageCheckException("the field "+field.getName()+" signature do not match its type signature");
205+
//if there is no primitive type and an object container type, we have an object
206+
}else if(element.getContainerType() == SupportedTypes.OBJECT_BEGIN) {
207+
if (!Serializable.class.isAssignableFrom(clazz)) {
208+
throw new MessageCheckException("Field not Serializable", this.clazz, fieldName);
209+
}
210+
return new ObjectSerializer(clazz.asSubclass(Serializable.class), element, this.clazz, fieldName);
211+
}else if( element.getContainerType() == SupportedTypes.DICT_ENTRY_BEGIN){
212+
throw new MessageCheckException("A dict_entry can not be placed outside of an array", this.clazz, fieldName);
213+
}else{
214+
throw new MessageCheckException("The given type could not be mapped to any serializer",this.clazz,fieldName);
184215
}
185216
}
186217
}

0 commit comments

Comments
 (0)