Skip to content
This repository was archived by the owner on Jun 19, 2024. It is now read-only.

Commit c9862f1

Browse files
authored
Merge pull request #30 from LabyMod/develop
Update 0.3.5
2 parents 5bcdb3e + fb70a86 commit c9862f1

File tree

7 files changed

+149
-57
lines changed

7 files changed

+149
-57
lines changed

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
0.3.4
1+
0.3.5

example/lwjgl3-opengl/src/main/java/com/labymedia/ultralight/lwjgl3/opengl/js/JSInteraction.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
import java.util.Arrays;
2323
import java.util.List;
24+
import java.util.function.Consumer;
2425

2526
/**
2627
* Example class containing methods which Javascript can interact with.
@@ -54,4 +55,14 @@ public List<String> getMessageList() {
5455
public String[] getMessageArray() {
5556
return messages;
5657
}
58+
59+
/**
60+
* Javascript methods can be automatically convert to Java functional interfaces as long as they are annotated with
61+
* {@link FunctionalInterface}.
62+
*
63+
* @param consumer The consumer to pass the messages to
64+
*/
65+
public void useConsumer(Consumer<String[]> consumer) {
66+
consumer.accept(messages);
67+
}
5768
}

example/lwjgl3-opengl/src/main/resources/example.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,3 +97,9 @@ for(let i = 0; i < messageList.size(); i++) {
9797
for(const message of messageArray) {
9898
console.log(`messageArray[@] = ${message}`)
9999
}
100+
101+
// Methods will automatically be translated to functional interfaces if the interface is annotated
102+
// with @FunctionalInterface
103+
interaction.useConsumer((messages) => {
104+
console.log(`Messages in consumer lambda: messages = [${messages.join(", ")}]`)
105+
});

ultralight-java-databind/src/main/java/com/labymedia/ultralight/DatabindJavascriptClass.java

Lines changed: 7 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,11 @@ private void addMethods(Collection<Method> methods) {
9494
for (Method method : methods) {
9595
String name = method.getName();
9696

97+
if(method.getName().equals("valueOf") && method.getDeclaringClass().isEnum()) {
98+
// Skip the valueOf method of enums because it breaks Javascript internals
99+
continue;
100+
}
101+
97102
if (this.methods.containsKey(name)) {
98103
// Overloaded method, add it to the set of methods with the same name
99104
this.methods.get(name).add(method);
@@ -366,28 +371,8 @@ private static <T extends AccessibleObject & Member> Set<T> filterAccessible(T[]
366371
* @return {@code true} if the member is accessible using a public path, {@code false} otherwise
367372
*/
368373
private static boolean allPublic(Member member) {
369-
Class<?> classToCheck = null;
370-
371-
do {
372-
if (classToCheck == null) {
373-
if ((member.getModifiers() & Modifier.PUBLIC) == 0) {
374-
return false;
375-
}
376-
377-
classToCheck = member.getDeclaringClass();
378-
} else {
379-
if((classToCheck.getModifiers() & Modifier.PUBLIC) == 0) {
380-
return false;
381-
}
382-
383-
classToCheck = classToCheck.getSuperclass();
384-
if(classToCheck == Object.class) {
385-
return true;
386-
}
387-
}
388-
} while (classToCheck != null);
389-
390-
return true;
374+
Class<?> classToCheck = member.getDeclaringClass();
375+
return Modifier.isPublic(classToCheck.getModifiers()) && Modifier.isPublic(member.getModifiers());
391376
}
392377

393378
/**

ultralight-java-databind/src/main/java/com/labymedia/ultralight/call/HeuristicMethodChooser.java

Lines changed: 64 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import com.labymedia.ultralight.utils.JavascriptConversionUtils;
2626

2727
import java.lang.reflect.Executable;
28+
import java.lang.reflect.Modifier;
2829
import java.lang.reflect.Parameter;
2930
import java.util.*;
3031

@@ -142,14 +143,74 @@ public <T extends Executable> CallData<T> choose(
142143
}
143144

144145
if (availableMethods.size() > 1) {
145-
throw new IllegalStateException("Ambiguous argument types, could not determine methods");
146+
throw new IllegalStateException(formatErrorMessage(
147+
"Ambiguous argument types, could not determine methods",
148+
possibilities,
149+
sourceParameterTypes
150+
));
146151
} else if (availableMethods.isEmpty()) {
147-
throw new IllegalStateException("No matching method found");
152+
throw new IllegalStateException(formatErrorMessage(
153+
"No matching method found",
154+
possibilities,
155+
sourceParameterTypes
156+
));
148157
}
149158

150159
return availableMethods.iterator().next();
151160
}
152161

162+
/**
163+
* Formats the given error message so it contains all the required information for debugging.
164+
*
165+
* @param header The header of the error message
166+
* @param searched The methods which have been searched
167+
* @param types The types of the passed arguments
168+
* @param <T> The type this method chooser is operating on
169+
* @return The formatted error message
170+
*/
171+
private <T extends Executable> String formatErrorMessage(String header, Collection<T> searched, Class<?>[] types) {
172+
StringBuilder message = new StringBuilder(header);
173+
message.append('\n');
174+
message.append("Arguments: [").append(formatClasses(types)).append("]\n");
175+
message.append("Tried ").append(searched.size()).append(" methods:\n");
176+
177+
Iterator<T> it = searched.iterator();
178+
while (it.hasNext()) {
179+
T t = it.next();
180+
message
181+
.append("- ")
182+
.append(Modifier.toString(t.getModifiers()))
183+
.append(" ")
184+
.append(t.getDeclaringClass().getName())
185+
.append('#')
186+
.append(t.getName())
187+
.append('(')
188+
.append(formatClasses(t.getParameterTypes()))
189+
.append(")")
190+
.append(it.hasNext() ? "\n" : "");
191+
}
192+
193+
return message.toString();
194+
}
195+
196+
/**
197+
* Formats an array of classes nicely.
198+
*
199+
* @param classes The array of classes to format
200+
* @return The formatted string
201+
*/
202+
private String formatClasses(Class<?>[] classes) {
203+
StringBuilder builder = new StringBuilder();
204+
for (int i = 0; i < classes.length; i++) {
205+
Class<?> type = classes[i];
206+
builder.append(type.getName());
207+
if (i + 1 != classes.length) {
208+
builder.append(", ");
209+
}
210+
}
211+
return builder.toString();
212+
}
213+
153214
private int calculatePenalty(Class<?> target, Class<?> source, JavascriptValue value) {
154215
if (target == JavascriptValue.class) {
155216
// No casting required at all, as the value can be passed directly
@@ -160,7 +221,7 @@ private int calculatePenalty(Class<?> target, Class<?> source, JavascriptValue v
160221
} else if (isZeroCostConversion(target, source)) {
161222
// Special zero cost conversion
162223
return 0;
163-
} else if (target == source) {
224+
} else if (target == source) {
164225
// No casting required, fast case to not run through selection
165226
return 0;
166227
} else if (source == JavascriptObject.class &&

ultralight-java-databind/src/main/java/com/labymedia/ultralight/utils/FunctionalInvocationHandler.java

Lines changed: 59 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,15 @@
2222
import com.labymedia.ultralight.Databind;
2323
import com.labymedia.ultralight.context.ContextProvider;
2424
import com.labymedia.ultralight.ffi.gc.DeletableObject;
25-
import com.labymedia.ultralight.javascript.*;
26-
import com.labymedia.ultralight.javascript.*;
25+
import com.labymedia.ultralight.javascript.JavascriptContext;
26+
import com.labymedia.ultralight.javascript.JavascriptObject;
27+
import com.labymedia.ultralight.javascript.JavascriptProtectedValue;
28+
import com.labymedia.ultralight.javascript.JavascriptValue;
2729

2830
import java.lang.invoke.MethodHandles;
2931
import java.lang.reflect.InvocationHandler;
3032
import java.lang.reflect.Method;
3133
import java.util.concurrent.CountDownLatch;
32-
import java.util.concurrent.atomic.AtomicReference;
3334

3435
/**
3536
* Invocation handler for Javascript functions bound to functional interfaces.
@@ -90,38 +91,61 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl
9091
return method.invoke(this, args);
9192
}
9293

93-
Class<?>[] methodParameterTypes = method.getParameterTypes();
94-
9594
CountDownLatch awaiter = new CountDownLatch(1);
96-
AtomicReference<Object> out = new AtomicReference<>();
95+
GuardedInvocationResult result = new GuardedInvocationResult();
9796

9897
contextProvider.syncWithJavascript((contextLock) -> {
99-
synchronized (lock) {
100-
JavascriptContext context = contextLock.getContext();
101-
102-
// Revive the Javascript value, this will effectively invalidate the protected value
103-
JavascriptObject object = protectedValue.get().value.revive(contextLock).toObject();
104-
105-
// Convert all Java arguments to Javascript values
106-
JavascriptValue[] arguments = new JavascriptValue[args.length];
107-
for (int i = 0; i < arguments.length; i++) {
108-
arguments[i] = databind.getConversionUtils()
109-
.toJavascript(context, args[i], methodParameterTypes[i]);
98+
try {
99+
synchronized(lock) {
100+
JavascriptContext context = contextLock.getContext();
101+
102+
// Revive the Javascript value, this will effectively invalidate the protected value
103+
JavascriptObject object = protectedValue.get().value.revive(contextLock).toObject();
104+
105+
// Convert all Java arguments to Javascript values
106+
JavascriptValue[] arguments = new JavascriptValue[args.length];
107+
for (int i = 0; i < arguments.length; i++) {
108+
arguments[i] = databind.getConversionUtils().toJavascript(context, args[i]);
109+
}
110+
111+
// Protect the value again
112+
protectedValue.get().value = object.protect();
113+
114+
JavascriptValue returnValue = object.callAsFunction(null, arguments);
115+
result.returnValue = databind.getConversionUtils().fromJavascript(
116+
returnValue,
117+
returnValue != null ? returnValue.getClass() : method.getReturnType()
118+
);
110119
}
120+
} catch(Throwable t) {
121+
// Capture exceptions to prevent deadlocking
122+
result.throwable = t;
123+
}
124+
awaiter.countDown();
125+
});
126+
awaiter.await();
111127

112-
// Protect the value again
113-
protectedValue.get().value = object.protect();
128+
if (result.throwable != null) {
129+
Throwable t = result.throwable;
114130

115-
JavascriptValue returnValue = object.callAsFunction(null, arguments);
116-
Object ret = databind.getConversionUtils().fromJavascript(returnValue, method.getReturnType());
117-
out.set(ret);
118-
awaiter.countDown();
131+
if (t instanceof RuntimeException || t instanceof Error) {
132+
// Unchecked, rethrow as is
133+
throw t;
119134
}
120-
});
121135

122-
awaiter.await();
136+
Class<?> throwableClass = t.getClass();
137+
for (Class<?> exceptionType : method.getExceptionTypes()) {
138+
if (exceptionType.isAssignableFrom(throwableClass)) {
139+
// Declared to be thrown by the interface method
140+
throw t;
141+
}
142+
}
143+
144+
// Checked exception which has not been declared as thrown by the interface method
145+
throw new RuntimeException("Exception thrown while invoking Javascript method", t);
146+
}
123147

124-
return out.get();
148+
return result.returnValue;
125149
}
126150

127151
/**
@@ -143,14 +167,20 @@ private ValueWrapper(ContextProvider contextProvider, JavascriptProtectedValue v
143167
}
144168
}
145169

170+
/**
171+
* Helper class for handling the results of asynchronous method invocations including exceptions.
172+
*/
173+
private static class GuardedInvocationResult {
174+
private Throwable throwable;
175+
private Object returnValue;
176+
}
177+
146178
/**
147179
* Deletes a value wrapper when it is not required anymore.
148180
*
149181
* @param valueWrapper The wrapper to delete
150182
*/
151183
private static void delete(ValueWrapper valueWrapper) {
152-
valueWrapper.contextProvider.syncWithJavascript((contextLock) -> {
153-
valueWrapper.value.revive(contextLock);
154-
});
184+
valueWrapper.contextProvider.syncWithJavascript((contextLock) -> valueWrapper.value.revive(contextLock));
155185
}
156186
}

ultralight-java-databind/src/main/java/com/labymedia/ultralight/utils/JavascriptConversionUtils.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
import com.labymedia.ultralight.Databind;
2323
import com.labymedia.ultralight.DatabindJavascriptClass;
2424
import com.labymedia.ultralight.javascript.*;
25-
import com.labymedia.ultralight.javascript.*;
2625

2726
import java.lang.reflect.Array;
2827
import java.util.Date;
@@ -51,7 +50,7 @@ public JavascriptConversionUtils(Databind databind) {
5150
* @return The converted object as a Javascript value
5251
*/
5352
public JavascriptValue toJavascript(JavascriptContext context, Object object) {
54-
return toJavascript(context, object, object.getClass());
53+
return toJavascript(context, object, object != null ? object.getClass() : null);
5554
}
5655

5756
/**

0 commit comments

Comments
 (0)