From d9603eb87ae9608086d273f13efb642728db1e3a Mon Sep 17 00:00:00 2001 From: andreatp Date: Fri, 12 Jun 2026 12:16:31 +0100 Subject: [PATCH] Fix @HostRefParam with array/varargs parameters When JavaScript passes an array of HostRef pointers (e.g. [ptr1, ptr2]) to a Java @HostRefParam varargs parameter, Engine.invokeBuiltin() called ArrayNode.intValue() which silently returns 0, resolving the wrong object. - Engine: detect ArrayNode and resolve each element individually - BuiltinsProcessor: use TypeKind.ARRAY to detect array @HostRefParam and generate List-to-typed-array conversion in the lambda Co-Authored-By: Claude Opus 4.6 (1M context) --- .../io/roastedroot/quickjs4j/core/Engine.java | 10 ++++- .../quickjs4j/core/EngineTest.java | 41 +++++++++++++++++++ .../io/roastedroot/js/test/HelloJsTest.java | 26 ++++++++++++ .../processor/BuiltinsProcessor.java | 32 ++++++++++++++- 4 files changed, 106 insertions(+), 3 deletions(-) diff --git a/core/src/main/java/io/roastedroot/quickjs4j/core/Engine.java b/core/src/main/java/io/roastedroot/quickjs4j/core/Engine.java index 13c9bec..0095a51 100644 --- a/core/src/main/java/io/roastedroot/quickjs4j/core/Engine.java +++ b/core/src/main/java/io/roastedroot/quickjs4j/core/Engine.java @@ -252,7 +252,15 @@ private long[] invokeBuiltin(Instance instance, long[] args) { } if (clazz == HostRef.class) { - argsList.add(javaRefs.get(value.intValue())); + if (value.isArray()) { + var refs = new ArrayList<>(); + for (JsonNode elem : value) { + refs.add(javaRefs.get(elem.intValue())); + } + argsList.add(refs); + } else { + argsList.add(javaRefs.get(value.intValue())); + } } else { argsList.add(mapper.treeToValue(value, clazz)); } diff --git a/core/src/test/java/io/roastedroot/quickjs4j/core/EngineTest.java b/core/src/test/java/io/roastedroot/quickjs4j/core/EngineTest.java index 59dbdf5..42ccd5f 100644 --- a/core/src/test/java/io/roastedroot/quickjs4j/core/EngineTest.java +++ b/core/src/test/java/io/roastedroot/quickjs4j/core/EngineTest.java @@ -290,6 +290,47 @@ public void callJavaFunctionsUsingJavaRefs() { expectedUser.name, expectedUser.surname, expectedUser.age)); } + @SuppressWarnings("unchecked") + @Test + public void hostRefArray() { + var received = new AtomicReference<>(); + var builtins = + Builtins.builder("from_java") + .add( + new HostFunction( + "create", + List.of(Integer.class, Integer.class), + HostRef.class, + (args) -> { + var x = (Integer) args.get(0); + var y = (Integer) args.get(1); + return new Point(x, y); + })) + .add( + new HostFunction( + "check_refs", + List.of(HostRef.class), + Void.class, + (args) -> { + received.set(args.get(0)); + return null; + })) + .build(); + + var engine = Engine.builder().addBuiltins(builtins).build(); + + compileAndExec( + engine, + "const p1 = from_java.create(10, 20);\n" + + "const p2 = from_java.create(30, 40);\n" + + "from_java.check_refs([p1, p2]);\n"); + + var refs = (List) received.get(); + assertEquals(2, refs.size()); + assertEquals(new Point(10, 20), refs.get(0)); + assertEquals(new Point(30, 40), refs.get(1)); + } + @Test public void useBundledJS() throws Exception { var myCow = diff --git a/it/src/it/hello-js/src/test/java/io/roastedroot/js/test/HelloJsTest.java b/it/src/it/hello-js/src/test/java/io/roastedroot/js/test/HelloJsTest.java index 461bc84..88c6d68 100644 --- a/it/src/it/hello-js/src/test/java/io/roastedroot/js/test/HelloJsTest.java +++ b/it/src/it/hello-js/src/test/java/io/roastedroot/js/test/HelloJsTest.java @@ -1,5 +1,6 @@ package chicory.test; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -51,6 +52,19 @@ public void myRefCheck(@HostRefParam String value) { refInvoked = true; assertEquals("a pure java string", value); } + + @ReturnsHostRef + @HostFunction("my_java_ref_create") + public String createRef(String value) { + return value; + } + + public String[] receivedRefs; + + @HostFunction("my_java_ref_check_array") + public void myRefCheckArray(@HostRefParam String... values) { + receivedRefs = values; + } } class JsTest { @@ -113,6 +127,18 @@ public void useJavaRefs() { assertTrue(helloJs.isRefInvoked()); } + @Test + public void useJavaRefArray() { + var helloJs = new JsTest(); + + helloJs.exec( + "var r1 = from_java.my_java_ref_create('hello');\n" + + "var r2 = from_java.my_java_ref_create('world');\n" + + "from_java.my_java_ref_check_array([r1, r2]);"); + + assertArrayEquals(new String[] {"hello", "world"}, helloJs.javaApi.receivedRefs); + } + @Test public void useInvokables() { // Arrange diff --git a/processor/src/main/java/io/roastedroot/quickjs4j/processor/BuiltinsProcessor.java b/processor/src/main/java/io/roastedroot/quickjs4j/processor/BuiltinsProcessor.java index 3a05335..d58b577 100644 --- a/processor/src/main/java/io/roastedroot/quickjs4j/processor/BuiltinsProcessor.java +++ b/processor/src/main/java/io/roastedroot/quickjs4j/processor/BuiltinsProcessor.java @@ -13,6 +13,7 @@ import com.github.javaparser.ast.expr.ArrayCreationExpr; import com.github.javaparser.ast.expr.ArrayInitializerExpr; import com.github.javaparser.ast.expr.CastExpr; +import com.github.javaparser.ast.expr.EnclosedExpr; import com.github.javaparser.ast.expr.Expression; import com.github.javaparser.ast.expr.FieldAccessExpr; import com.github.javaparser.ast.expr.IntegerLiteralExpr; @@ -42,6 +43,8 @@ import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.TypeElement; import javax.lang.model.element.VariableElement; +import javax.lang.model.type.ArrayType; +import javax.lang.model.type.TypeKind; import javax.tools.StandardLocation; public final class BuiltinsProcessor extends Quickjs4jAbstractProcessor { @@ -219,12 +222,37 @@ private Expression processHostFunction( break; default: var typeLiteral = parameter.asType().toString(); - var type = parseType(parameter.asType().toString()); - arguments.add(new CastExpr(type, argExpr(paramTypes.size()))); + var argIndex = paramTypes.size(); if (annotatedWith(parameter, HostRefParam.class)) { var javaRefType = "io.roastedroot.quickjs4j.core.HostRef"; paramTypes.add(new FieldAccessExpr(new NameExpr(javaRefType), "class")); + if (parameter.asType().getKind() == TypeKind.ARRAY) { + var componentType = + ((ArrayType) parameter.asType()).getComponentType().toString(); + var castToList = + new EnclosedExpr( + new CastExpr( + parseType("java.util.List"), + argExpr(argIndex))); + var emptyArray = + new ArrayCreationExpr( + parseType(componentType), + NodeList.nodeList( + new ArrayCreationLevel() + .setDimension( + new IntegerLiteralExpr("0"))), + null); + var toArrayCall = + new MethodCallExpr( + castToList, "toArray", NodeList.nodeList(emptyArray)); + arguments.add(new CastExpr(parseType(typeLiteral), toArrayCall)); + } else { + var type = parseType(typeLiteral); + arguments.add(new CastExpr(type, argExpr(argIndex))); + } } else { + var type = parseType(typeLiteral); + arguments.add(new CastExpr(type, argExpr(argIndex))); paramTypes.add(new FieldAccessExpr(new NameExpr(typeLiteral), "class")); } }