From 74795daeffe3846f0f8d054038475fefb68ef58b Mon Sep 17 00:00:00 2001 From: Jan-Willem Harmannij Date: Sun, 14 Jul 2024 15:10:35 +0200 Subject: [PATCH] Handle glong/gulong fields on windows (fixes #106) --- .../groovy/java-gi.library-conventions.gradle | 2 +- .../javagi/generators/AliasGenerator.java | 18 +++- .../javagi/generators/CallableGenerator.java | 48 ++++++---- .../javagi/generators/ClosureGenerator.java | 6 +- .../javagi/generators/FieldGenerator.java | 15 ++- .../generators/MemoryLayoutGenerator.java | 65 +++++++++---- .../javagi/generators/MethodGenerator.java | 67 ++++++++++--- .../generators/PostprocessingGenerator.java | 48 +++++----- .../generators/PreprocessingGenerator.java | 96 ++++++++++--------- .../javagi/generators/RecordGenerator.java | 21 +++- .../javagi/generators/SignalGenerator.java | 24 +++-- .../generators/TypedValueGenerator.java | 10 ++ .../io/github/jwharm/javagi/gir/Field.java | 19 ++-- .../io/github/jwharm/javagi/gir/Node.java | 19 ++++ .../io/github/jwharm/javagi/gir/Type.java | 5 + .../jwharm/javagi/patches/GLibPatch.java | 10 ++ .../jwharm/javagi/util/Conversions.java | 17 +++- .../jwharm/javagi/test/gio/StrvArrayTest.java | 5 + .../github/jwharm/javagi/interop/Interop.java | 14 ++- 19 files changed, 355 insertions(+), 154 deletions(-) diff --git a/buildSrc/src/main/groovy/java-gi.library-conventions.gradle b/buildSrc/src/main/groovy/java-gi.library-conventions.gradle index 92198475..e023098a 100644 --- a/buildSrc/src/main/groovy/java-gi.library-conventions.gradle +++ b/buildSrc/src/main/groovy/java-gi.library-conventions.gradle @@ -96,7 +96,7 @@ tasks.named('test', Test) { // Configure library path for Windows (MSYS2) else if (Os.isFamily(Os.FAMILY_WINDOWS)) { - jvmArgs += '-Djava.library.path=C:/msys64/mingw64/bin' + jvmArgs += '-Djava.library.path=C:/dev/msys64/mingw64/bin' } useJUnitPlatform() diff --git a/buildSrc/src/main/java/io/github/jwharm/javagi/generators/AliasGenerator.java b/buildSrc/src/main/java/io/github/jwharm/javagi/generators/AliasGenerator.java index 600db077..90d7aa93 100644 --- a/buildSrc/src/main/java/io/github/jwharm/javagi/generators/AliasGenerator.java +++ b/buildSrc/src/main/java/io/github/jwharm/javagi/generators/AliasGenerator.java @@ -134,7 +134,8 @@ private MethodSpec fromTargetType() { } private MethodSpec arrayConstructor(Type primitiveType) { - String layout = getValueLayoutPlain(primitiveType); + String layout = getValueLayoutPlain(primitiveType, false); + var spec = MethodSpec.methodBuilder("fromNativeArray") .addModifiers(Modifier.PUBLIC, Modifier.STATIC) .returns(ArrayTypeName.of(alias.typeName())) @@ -142,10 +143,17 @@ private MethodSpec arrayConstructor(Type primitiveType) { .addParameter(long.class, "length") .addParameter(boolean.class, "free") .addStatement("$T array = new $T[(int) length]", - ArrayTypeName.of(alias.typeName()), alias.typeName()) - .addStatement("long byteSize = $T.$L.byteSize()", - ValueLayout.class, layout) - .addStatement("$T segment = address.reinterpret(byteSize * length)", + ArrayTypeName.of(alias.typeName()), alias.typeName()); + + if (primitiveType.isLong()) { + spec.addStatement("long byteSize = $1T.longAsInt() ? $2T.JAVA_INT.byteSize() : $2T.JAVA_LONG.byteSize()", + ClassNames.INTEROP, ValueLayout.class); + } else { + spec.addStatement("long byteSize = $T.$L.byteSize()", + ValueLayout.class, layout); + } + + spec.addStatement("$T segment = address.reinterpret(byteSize * length)", MemorySegment.class) .beginControlFlow("for (int i = 0; i < length; i++)"); diff --git a/buildSrc/src/main/java/io/github/jwharm/javagi/generators/CallableGenerator.java b/buildSrc/src/main/java/io/github/jwharm/javagi/generators/CallableGenerator.java index 1719335d..01f8204e 100644 --- a/buildSrc/src/main/java/io/github/jwharm/javagi/generators/CallableGenerator.java +++ b/buildSrc/src/main/java/io/github/jwharm/javagi/generators/CallableGenerator.java @@ -22,7 +22,6 @@ import com.squareup.javapoet.*; import io.github.jwharm.javagi.configuration.ClassNames; import io.github.jwharm.javagi.gir.*; -import io.github.jwharm.javagi.util.Conversions; import io.github.jwharm.javagi.util.PartialStatement; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -32,11 +31,11 @@ import java.lang.foreign.MemorySegment; import java.lang.foreign.ValueLayout; import java.util.*; -import java.util.stream.Collectors; import static io.github.jwharm.javagi.util.Conversions.getValueLayout; import static io.github.jwharm.javagi.util.Conversions.toJavaIdentifier; import static java.util.function.Predicate.not; +import static java.util.stream.Collectors.joining; public class CallableGenerator { @@ -55,42 +54,47 @@ CodeBlock generateFunctionDescriptorDeclaration() { } CodeBlock generateFunctionDescriptor() { - List valueLayouts = new ArrayList<>(); + List layouts = new ArrayList<>(); var returnType = callable.returnValue().anyType(); boolean isVoid = returnType instanceof Type t && t.isVoid(); if (!isVoid) - valueLayouts.add(getValueLayout(returnType)); + layouts.add(generateValueLayout(returnType)); if (callable instanceof Signal) - valueLayouts.add("ADDRESS"); + layouts.add("$valueLayout:T.ADDRESS"); if (callable.parameters() != null) { var iParam = callable.parameters().instanceParameter(); if (iParam != null) - valueLayouts.add(getValueLayout(iParam.anyType())); - valueLayouts.addAll( + layouts.add(generateValueLayout(iParam.anyType())); + layouts.addAll( callable.parameters().parameters().stream() .filter(not(Parameter::varargs)) .map(Parameter::anyType) - .map(Conversions::getValueLayout) + .map(this::generateValueLayout) .toList()); } if (callable.throws_()) - valueLayouts.add("ADDRESS"); + layouts.add("$valueLayout:T.ADDRESS"); - if (valueLayouts.isEmpty()) { + if (layouts.isEmpty()) return CodeBlock.of("$T.ofVoid()", FunctionDescriptor.class); - } else { - String layouts = valueLayouts.stream() - .map(s -> "$2T." + s) - .collect(Collectors.joining(",$W", "(", ")")); - return CodeBlock.of("$1T.$3L" + layouts, - FunctionDescriptor.class, - ValueLayout.class, - isVoid ? "ofVoid" : "of"); - } + + return PartialStatement.of("$functionDescriptor:T." + + (isVoid ? "ofVoid" : "of")) + .add(layouts.stream().collect(joining(",$W", "(", ")")), + "functionDescriptor", FunctionDescriptor.class, + "valueLayout", ValueLayout.class, + "interop", ClassNames.INTEROP) + .toCodeBlock(); + } + + String generateValueLayout(AnyType anyType) { + return anyType instanceof Type type && type.isLong() + ? "$interop:T.longAsInt() ? $valueLayout:T.JAVA_INT : $valueLayout:T.JAVA_LONG" + : "$valueLayout:T." + getValueLayout(anyType, false); } void generateMethodParameters(MethodSpec.Builder builder, @@ -125,7 +129,7 @@ else if (p.notNull()) } } - PartialStatement marshalParameters() { + PartialStatement marshalParameters(boolean intAsLong) { var parameters = callable.parameters(); if (parameters == null) return callable.throws_() @@ -155,6 +159,10 @@ PartialStatement marshalParameters() { if (generator.checkNull()) stmt.add("($memorySegment:T) (" + generator.getName() + " == null ? $memorySegment:T.NULL : "); + // cast int parameter to a long + if (intAsLong && p.anyType() instanceof Type t && t.isLong()) + stmt.add("(long) "); + // Callback destroy if (p.isDestroyNotifyParameter()) { var notify = parameters.parameters().stream() diff --git a/buildSrc/src/main/java/io/github/jwharm/javagi/generators/ClosureGenerator.java b/buildSrc/src/main/java/io/github/jwharm/javagi/generators/ClosureGenerator.java index 163d0583..2b6a1686 100644 --- a/buildSrc/src/main/java/io/github/jwharm/javagi/generators/ClosureGenerator.java +++ b/buildSrc/src/main/java/io/github/jwharm/javagi/generators/ClosureGenerator.java @@ -103,7 +103,7 @@ MethodSpec generateUpcallMethod(String methodName, String name, String methodToI MethodSpec.Builder upcall = MethodSpec.methodBuilder(name) .returns(returnsVoid ? TypeName.VOID - : getCarrierTypeName(returnValue.anyType())); + : getCarrierTypeName(returnValue.anyType(), false)); // Javadoc if (methodToInvoke.equals("run")) @@ -127,7 +127,7 @@ MethodSpec generateUpcallMethod(String methodName, String name, String methodToI // Add parameters (native carrier types) if (closure.parameters() != null) for (Parameter p : closure.parameters().parameters()) - upcall.addParameter(getCarrierTypeName(p.anyType()), + upcall.addParameter(getCarrierTypeName(p.anyType(), false), toJavaIdentifier(p.name())); // GError** parameter @@ -182,7 +182,7 @@ MethodSpec generateUpcallMethod(String methodName, String name, String methodToI // Null-check the return value if ((!returnsVoid) - && getCarrierTypeName(returnValue.anyType()).equals(TypeName.get(MemorySegment.class)) + && getCarrierTypeName(returnValue.anyType(), false).equals(TypeName.get(MemorySegment.class)) && (!returnValue.notNull())) upcall.addStatement("if (_result == null) return $T.NULL", MemorySegment.class); diff --git a/buildSrc/src/main/java/io/github/jwharm/javagi/generators/FieldGenerator.java b/buildSrc/src/main/java/io/github/jwharm/javagi/generators/FieldGenerator.java index f9bcefa6..232789aa 100644 --- a/buildSrc/src/main/java/io/github/jwharm/javagi/generators/FieldGenerator.java +++ b/buildSrc/src/main/java/io/github/jwharm/javagi/generators/FieldGenerator.java @@ -20,6 +20,7 @@ package io.github.jwharm.javagi.generators; import com.squareup.javapoet.MethodSpec; +import com.squareup.javapoet.TypeName; import io.github.jwharm.javagi.configuration.ClassNames; import io.github.jwharm.javagi.gir.*; import io.github.jwharm.javagi.gir.Class; @@ -66,9 +67,14 @@ private String methodName(String prefix) { public MethodSpec generateReadMethod() { // To read from ...** fields, you must provide the length of the array. boolean isArray = type != null && type.isActuallyAnArray(); + + // Override the type of long values + boolean isLong = type != null && type.isLong(); + TypeName typeName = isLong ? TypeName.INT : getType(); + var spec = MethodSpec.methodBuilder(methodName(READ_PREFIX)) .addModifiers(Modifier.PUBLIC) - .returns(getType()) + .returns(typeName) .addJavadoc("Read the value of the field {@code $L}.\n\n", f.name()); if (isArray) spec.addJavadoc("@param length the number of {@code $L} to read", f.name()); @@ -94,7 +100,7 @@ public MethodSpec generateReadMethod() { } // Read a pointer or primitive value from the struct - var carrierType = getCarrierTypeName(f.anyType()); + var carrierType = getCarrierTypeName(f.anyType(), true); var getResult = "var _result = ($T) getMemoryLayout()$Z.varHandle($T.PathElement.groupElement($S)).get(handle(), 0)"; var returnResult = PartialStatement.of("return ") .add(marshalNativeToJava("_result", false)) @@ -114,7 +120,10 @@ public MethodSpec generateWriteMethod() { @param $2L The new value for the field {@code $1L} """, f.name(), getName()); - spec.addParameter(getType(), getName()); + // Override the type of long values + boolean isLong = type != null && type.isLong(); + TypeName typeName = isLong ? TypeName.INT : getType(); + spec.addParameter(typeName, getName()); if (f.allocatesMemory()) spec.addJavadoc("@param _arena to control the memory allocation scope\n") diff --git a/buildSrc/src/main/java/io/github/jwharm/javagi/generators/MemoryLayoutGenerator.java b/buildSrc/src/main/java/io/github/jwharm/javagi/generators/MemoryLayoutGenerator.java index ac0e50e9..0d5d4522 100644 --- a/buildSrc/src/main/java/io/github/jwharm/javagi/generators/MemoryLayoutGenerator.java +++ b/buildSrc/src/main/java/io/github/jwharm/javagi/generators/MemoryLayoutGenerator.java @@ -20,6 +20,7 @@ package io.github.jwharm.javagi.generators; import com.squareup.javapoet.MethodSpec; +import io.github.jwharm.javagi.configuration.ClassNames; import io.github.jwharm.javagi.gir.Class; import io.github.jwharm.javagi.gir.*; import io.github.jwharm.javagi.gir.Record; @@ -63,23 +64,46 @@ MethodSpec generateMemoryLayout(RegisteredType rt) { boolean isUnion = rt instanceof Union || !unionList.isEmpty(); - // The $> and $< in the statement increase and decrease indentation - var layout = PartialStatement.of("return $memoryLayout:T." - + (isUnion ? "union" : "struct") + "Layout(\n$>") - .add(generateFieldLayouts(fieldList, isUnion)) - .add("$<\n).withName(\"" + rt.cType() + "\");\n"); + boolean hasLongFields = rt.deepMatch( + n -> n instanceof Type t && t.isLong(), Callback.class); - return MethodSpec.methodBuilder("getMemoryLayout") + var method = MethodSpec.methodBuilder("getMemoryLayout") .addJavadoc("The memory layout of the native struct.\n") .addJavadoc("@return the memory layout\n") .addModifiers(Modifier.PUBLIC, Modifier.STATIC) - .returns(MemoryLayout.class) - .addNamedCode(layout.format(), layout.arguments()) - .build(); + .returns(MemoryLayout.class); + + // When there are `long` fields, generate 32-bit and 64-bit layout + if (hasLongFields) { + method.beginControlFlow("if ($T.longAsInt())", ClassNames.INTEROP); + var layout = generateMemoryLayout(rt.cType(), fieldList, isUnion, true); + method.addNamedCode(layout.format(), layout.arguments()) + .nextControlFlow("else"); + layout = generateMemoryLayout(rt.cType(), fieldList, isUnion, false); + method.addNamedCode(layout.format(), layout.arguments()) + .endControlFlow(); + } else { + var layout = generateMemoryLayout(rt.cType(), fieldList, isUnion, false); + method.addNamedCode(layout.format(), layout.arguments()); + } + + return method.build(); + } + + private PartialStatement generateMemoryLayout(String name, + List fieldList, + boolean isUnion, + boolean longAsInt) { + // The $> and $< in the statement increase and decrease indentation + return PartialStatement.of("return $memoryLayout:T." + + (isUnion ? "union" : "struct") + "Layout(\n$>") + .add(generateFieldLayouts(fieldList, isUnion, longAsInt)) + .add("$<\n).withName(\"" + name + "\");\n"); } private PartialStatement generateFieldLayouts(List fieldList, - boolean isUnion) { + boolean isUnion, + boolean longAsInt) { var stmt = PartialStatement.of(null, "memoryLayout", MemoryLayout.class, "valueLayout", ValueLayout.class @@ -89,7 +113,7 @@ private PartialStatement generateFieldLayouts(List fieldList, if (size > 0) stmt.add(",\n"); // Get the byte size of the field, in bytes - int s = field.getSize(); + int s = field.getSize(longAsInt); // Calculate padding (except for union layouts) if (!isUnion) { @@ -103,22 +127,23 @@ private PartialStatement generateFieldLayouts(List fieldList, } // Write the memory layout declaration - stmt.add(getFieldLayout(field)) + stmt.add(getFieldLayout(field, longAsInt)) .add(".withName(\"" + field.name() + "\")"); size += s; } return stmt; } - private PartialStatement getFieldLayout(Field f) { + private PartialStatement getFieldLayout(Field f, boolean longAsInt) { return switch (f.anyType()) { case null -> PartialStatement.of("$valueLayout:T.ADDRESS"); // callback - case Type type -> layoutForType(type); + case Type type -> layoutForType(type, longAsInt); case Array array -> { if (array.fixedSize() > 0) { + var type = (Type) array.anyType(); yield PartialStatement.of("$memoryLayout:T.sequenceLayout(" + array.fixedSize() + ", ") - .add(layoutForType((Type) array.anyType())) + .add(layoutForType(type, longAsInt)) .add(")"); } else { yield PartialStatement.of("$valueLayout:T.ADDRESS"); @@ -127,22 +152,22 @@ private PartialStatement getFieldLayout(Field f) { }; } - private PartialStatement layoutForType(Type type) { + private PartialStatement layoutForType(Type type, boolean longAsInt) { RegisteredType target = type.get(); // Recursive lookup for aliases if (target instanceof Alias alias) - return layoutForType(alias.type()); + return layoutForType(alias.type(), longAsInt); // Proxy objects with a known memory layout - if (!type.isPointer() - && new MemoryLayoutGenerator().canGenerate(target)) { + if (!type.isPointer() && canGenerate(target)) { String tag = type.toTypeTag(); return PartialStatement.of("$" + tag + ":T.getMemoryLayout()", tag, type.typeName()); } // Plain value layout - return PartialStatement.of("$valueLayout:T." + getValueLayout(type)); + String layout = getValueLayout(type, longAsInt); + return PartialStatement.of("$valueLayout:T." + layout); } } diff --git a/buildSrc/src/main/java/io/github/jwharm/javagi/generators/MethodGenerator.java b/buildSrc/src/main/java/io/github/jwharm/javagi/generators/MethodGenerator.java index d2a9a94d..d399d13c 100644 --- a/buildSrc/src/main/java/io/github/jwharm/javagi/generators/MethodGenerator.java +++ b/buildSrc/src/main/java/io/github/jwharm/javagi/generators/MethodGenerator.java @@ -34,6 +34,7 @@ import java.lang.foreign.ValueLayout; import java.lang.invoke.MethodHandle; import java.util.List; +import java.util.function.Predicate; import static io.github.jwharm.javagi.util.Conversions.*; import static java.util.Comparator.comparing; @@ -181,7 +182,7 @@ else if (func instanceof Constructor) // Declare return value if (!returnValue.anyType().isVoid()) builder.addStatement("$T _result", - Conversions.getCarrierTypeName(returnValue.anyType())); + Conversions.getCarrierTypeName(returnValue.anyType(), true)); // Try-catch for function invocation builder.beginControlFlow("try"); @@ -291,19 +292,40 @@ else if (((target instanceof Record record && !record.foreign()) } private void functionNameInvocation() { + Predicate predicate = n -> n instanceof Type t && t.isLong(); + if (func.deepMatch(predicate, Callback.class)) { + builder.beginControlFlow("if ($T.longAsInt())", ClassNames.INTEROP); + functionNameInvocation(false); + builder.nextControlFlow("else"); + functionNameInvocation(true); + builder.endControlFlow(); + } else { + functionNameInvocation(false); + } + } + + private void functionNameInvocation(boolean longAsInt) { // Result assignment PartialStatement invoke = new PartialStatement(); - if (!func.returnValue().anyType().isVoid()) { - String typeTag = getCarrierTypeTag(func.returnValue().anyType()); - TypeName typeName = getCarrierTypeName(func.returnValue().anyType()); - invoke.add("_result = ($" + typeTag + ":T) ", typeTag, typeName); + var returnType = func.returnValue().anyType(); + if (!returnType.isVoid()) { + if (longAsInt && returnType instanceof Type t && t.isLong()) { + // First cast to long, this is used by the MethodHandle to + // determine the return type. Then cast to int, because that is + // returned to the caller. + invoke.add("_result = (int) (long) "); + } else { + String typeTag = getCarrierTypeTag(func.returnValue().anyType()); + TypeName typeName = getCarrierTypeName(func.returnValue().anyType(), false); + invoke.add("_result = ($" + typeTag + ":T) ", typeTag, typeName); + } } // Function invocation invoke.add("$helperClass:T.$cIdentifier:L.invokeExact($Z", "helperClass", ((RegisteredType) func.parent()).helperClass(), "cIdentifier", func.callableAttrs().cIdentifier()) - .add(generator.marshalParameters()) + .add(generator.marshalParameters(longAsInt)) .add(");\n"); builder.addNamedCode(invoke.format(), invoke.arguments()); @@ -318,6 +340,19 @@ private void functionNameInvocation() { } private void functionPointerInvocation() { + Predicate predicate = n -> n instanceof Type t && t.isLong(); + if (func.deepMatch(predicate, Callback.class)) { + builder.beginControlFlow("if ($T.longAsInt())", ClassNames.INTEROP); + functionPointerInvocation(true); + builder.nextControlFlow("else"); + functionPointerInvocation(false); + builder.endControlFlow(); + } else { + functionPointerInvocation(false); + } + } + + private void functionPointerInvocation(boolean longAsInt) { // Function descriptor var generator = new CallableGenerator(vm); builder.addCode(generator.generateFunctionDescriptorDeclaration()); @@ -347,16 +382,26 @@ private void functionPointerInvocation() { // Result assignment PartialStatement invoke = new PartialStatement(); - if (!returnValue.anyType().isVoid()) { - String typeTag = getCarrierTypeTag(returnValue.anyType()); - TypeName typeName = getCarrierTypeName(returnValue.anyType()); - invoke.add("_result = ($" + typeTag + ":T) ", typeTag, typeName); + var returnType = returnValue.anyType(); + if (!returnType.isVoid()) { + if (longAsInt && returnType instanceof Type t && t.isLong()) { + // First cast to long, this is used by the MethodHandle to + // determine the return type. Then cast to int, because that is + // returned to the caller. + invoke.add("_result = (int) (long) "); + } else { + String typeTag = getCarrierTypeTag(returnValue.anyType()); + TypeName typeName = getCarrierTypeName(returnValue.anyType(), + false); + invoke.add("_result = ($" + typeTag + ":T) ", typeTag, + typeName); + } } // Function pointer invocation invoke.add("$interop:T.downcallHandle(_func, _fdesc)$Z.invokeExact($Z", "interop", ClassNames.INTEROP) - .add(generator.marshalParameters()) + .add(generator.marshalParameters(longAsInt)) .add(");\n"); builder.addNamedCode(invoke.format(), invoke.arguments()); diff --git a/buildSrc/src/main/java/io/github/jwharm/javagi/generators/PostprocessingGenerator.java b/buildSrc/src/main/java/io/github/jwharm/javagi/generators/PostprocessingGenerator.java index 1de7d021..a5de0688 100644 --- a/buildSrc/src/main/java/io/github/jwharm/javagi/generators/PostprocessingGenerator.java +++ b/buildSrc/src/main/java/io/github/jwharm/javagi/generators/PostprocessingGenerator.java @@ -61,8 +61,9 @@ private void readPointer(MethodSpec.Builder builder) { ? ".setValue(" : ".set("); - String identifier = "_%sPointer.get($valueLayout:T.%s, 0)" - .formatted(getName(), getValueLayoutPlain(type)); + var layout = generateValueLayoutPlain(type); + String identifier = "_%sPointer.get(%s, 0)" + .formatted(getName(), layout.format()); if ((target instanceof Alias a && a.type().isPrimitive()) || (type.isPrimitive() && type.isPointer())) { @@ -88,26 +89,27 @@ private void readPointer(MethodSpec.Builder builder) { String len = array.sizeExpression(false); PartialStatement payload; Type arrayType = (Type) array.anyType(); - String valueLayout = getValueLayoutPlain(arrayType); if (p.isOutParameter() && len != null && p.callerAllocates()) { // Out-parameter array with known length payload = arrayType.isPrimitive() - ? PartialStatement.of("_%sPointer.toArray($valueLayout:T.%s)" - .formatted(getName(), valueLayout), - "valueLayout", ValueLayout.class) + ? PartialStatement.of("_$name:LPointer.toArray(", + "name", getName()) + .add(generateValueLayoutPlain(arrayType)) + .add(")") : marshalNativeToJava("_%sPointer" .formatted(getName()), false); } else if (len != null && arrayType.isPrimitive() && !arrayType.isBoolean()) { // Arrays with primitive values and known length - payload = PartialStatement.of(("_%sPointer$Z" - + ".get(ValueLayout.ADDRESS, 0)$Z" - + ".reinterpret(%s * $valueLayout:T.%s.byteSize(), _arena, null)$Z" - + ".toArray($valueLayout:T.%s)") - .formatted(getName(), len, valueLayout, valueLayout), - "valueLayout", ValueLayout.class); + payload = PartialStatement.of("_$name:LPointer$Z.get($valueLayout:T.ADDRESS, 0)$Z.reinterpret(" + len + " * ", + "name", getName(), + "valueLayout", ValueLayout.class) + .add(generateValueLayoutPlain(arrayType)) + .add(".byteSize(), _arena, null)$Z.toArray(") + .add(generateValueLayoutPlain(arrayType)) + .add(")"); } else { // Other arrays payload = marshalNativeToJava("_" + getName() + "Pointer", false); @@ -129,11 +131,13 @@ private void readPointer(MethodSpec.Builder builder) { private void writePrimitiveAliasPointer(MethodSpec.Builder builder) { if (target instanceof Alias a && a.type().isPrimitive() - && type.isPointer()) - builder.addStatement("$1LParam.set($2T.$3L, 0, _$1LAlias.getValue())", - getName(), - ValueLayout.class, - getValueLayoutPlain(type)); + && type.isPointer()) { + var stmt = PartialStatement.of("$name:LParam.set(") + .add(generateValueLayoutPlain(type)) + .add(", 0, _$name:LAlias.getValue());\n", + "name", getName()); + builder.addNamedCode(stmt.format(), stmt.arguments()); + } } private void writeOutParameter(MethodSpec.Builder builder) { @@ -149,14 +153,14 @@ private void writeOutParameter(MethodSpec.Builder builder) { payload.add(" ? 1 : 0"); } else if (target instanceof FlaggedType) - payload = PartialStatement.of("_" + getName() + "Out.get().getValue()"); + payload = PartialStatement.of("_$name:LOut.get().getValue()", + "name", getName()); else payload = marshalJavaToNative("_" + getName() + "Out.get()"); - var stmt = PartialStatement.of(getName()) - .add("Param.set($valueLayout:T.", - "valueLayout", ValueLayout.class) - .add(getValueLayoutPlain(type)) + var stmt = PartialStatement.of("$name:LParam.set(", + "name", getName()) + .add(generateValueLayoutPlain(type)) .add(", 0, ") .add(payload) .add(");\n"); diff --git a/buildSrc/src/main/java/io/github/jwharm/javagi/generators/PreprocessingGenerator.java b/buildSrc/src/main/java/io/github/jwharm/javagi/generators/PreprocessingGenerator.java index de0b33a3..403f04cc 100644 --- a/buildSrc/src/main/java/io/github/jwharm/javagi/generators/PreprocessingGenerator.java +++ b/buildSrc/src/main/java/io/github/jwharm/javagi/generators/PreprocessingGenerator.java @@ -86,29 +86,29 @@ private void pointerAllocation(MethodSpec.Builder builder) { * copy the contents into the native memory buffer. If it is null, * allocate a pointer. */ - var stmt = PartialStatement - .of("$memorySegment:T _$name:LPointer = ($name:L == null || $name:L.get() == null)$W? ") - .add("_arena.allocate($valueLayout:T.$layout:L)") - .add("$W: ") + var stmt = PartialStatement.of( + "$memorySegment:T _$name:LPointer = ($name:L == null || $name:L.get() == null)$W? ", + "memorySegment", MemorySegment.class, + "name", getName()) + .add("_arena.allocate(") + .add(generateValueLayoutPlain(type)) + .add(")$W: ") .add("($memorySegment:T) ") .add(marshalJavaToNative(getName() + ".get()")) - .add(";\n", - "memorySegment", MemorySegment.class, - "name", getName(), - "valueLayout", ValueLayout.class, - "layout", getValueLayoutPlain(type) - ); + .add(";\n"); builder.addNamedCode(stmt.format(), stmt.arguments()); } else if (p.isOutParameter() || (type != null && type.isPointer() && target instanceof Alias a && a.type().isPrimitive())) { - builder.addStatement("$T _$LPointer = _arena.allocate($T.$L)", - MemorySegment.class, - getName(), - ValueLayout.class, - getValueLayoutPlain(type)); + var stmt = PartialStatement.of( + "$memorySegment:T _$name:LPointer = _arena.allocate(", + "memorySegment", MemorySegment.class, + "name", getName()) + .add(generateValueLayoutPlain(type)) + .add(");\n"); + builder.addNamedCode(stmt.format(), stmt.arguments()); } } @@ -122,10 +122,10 @@ private void arrayLength(MethodSpec.Builder builder) { // Set the initial value of the allocated pointer to the length // of the input array if (p.isArrayLengthParameter() && p.isOutParameter()) { - var stmt = PartialStatement.of("_$name:LPointer.set($valueLayout:T.$layout:L, 0L,$W", - "name", getName(), - "valueLayout", ValueLayout.class, - "layout", getValueLayoutPlain(type)) + var stmt = PartialStatement.of("_$name:LPointer.set(", + "name", getName()) + .add(generateValueLayoutPlain(type)) + .add(", 0L,$W") .add(arrayLengthStatement()) .add(");\n"); builder.addNamedCode(stmt.format(), stmt.arguments()); @@ -228,17 +228,21 @@ private void readPrimitiveAliasPointer(MethodSpec.Builder builder) { if (target instanceof Alias a && a.type().isPrimitive() && type.isPointer()) { - String layout = getValueLayoutPlain(type); - builder.addStatement("$1T $2LParam = $2L.reinterpret($3T.$4L.byteSize(), _arena, null)", - MemorySegment.class, - getName(), - ValueLayout.class, - layout); - builder.addStatement("$1T _$2LAlias = new $1T($2LParam.get($3T.$4L, 0))", - type.typeName(), - getName(), - ValueLayout.class, - layout); + var stmt = PartialStatement.of( + "$memorySegment:T $name:LParam = $name:L.reinterpret(", + "memorySegment", MemorySegment.class, + "name", getName()) + .add(generateValueLayoutPlain(type)) + .add(".byteSize(), _arena, null);\n"); + builder.addNamedCode(stmt.format(), stmt.arguments()); + + stmt = PartialStatement.of( + "$aliasType:T _$name:LAlias = new $aliasType:T($name:LParam.get(", + "aliasType", type.typeName(), + "name", getName()) + .add(generateValueLayoutPlain(type)) + .add(", 0));\n"); + builder.addNamedCode(stmt.format(), stmt.arguments()); } } @@ -250,28 +254,32 @@ private void readOutParameter(MethodSpec.Builder builder) { // Pointer to a single value if (type != null) { - String layout = getValueLayoutPlain(type); - builder.addStatement("$1T $2LParam = $2L.reinterpret($3T.$4L.byteSize(), _arena, null)", - MemorySegment.class, - getName(), - ValueLayout.class, - layout); + var stmt = PartialStatement.of( + "$memorySegment:T $name:LParam = $name:L.reinterpret(", + "memorySegment", MemorySegment.class, + "name", getName()) + .add(generateValueLayoutPlain(type)) + .add(".byteSize(), _arena, null);\n"); + builder.addNamedCode(stmt.format(), stmt.arguments()); if (type.isPrimitive() || target instanceof Alias a && a.type().isPrimitive()) { - builder.addStatement("$1T _$2LOut = new $3T<>($2LParam.get($4T.$5L, 0)$6L)", - getType(), - getName(), - ClassNames.OUT, - ValueLayout.class, - layout, - type.isBoolean() ? " != 0" : ""); + stmt = PartialStatement.of( + "$outType:T _$name:LOut = new $out:T<>($name:LParam.get(", + "outType", getType(), + "name", getName(), + "out", ClassNames.OUT) + .add(generateValueLayoutPlain(type)) + .add(", 0)") + .add(type.isBoolean() ? " != 0" : "") + .add(");\n"); + builder.addNamedCode(stmt.format(), stmt.arguments()); } else { String identifier = getName() + "Param"; if (target instanceof FlaggedType) identifier += ".get($valueLayout:T.JAVA_INT, 0)"; - var stmt = PartialStatement.of("$outType:T _" + getName() + "Out = new $out:T<>(", + stmt = PartialStatement.of("$outType:T _" + getName() + "Out = new $out:T<>(", "valueLayout", ValueLayout.class, "outType", getType(), "out", ClassNames.OUT) diff --git a/buildSrc/src/main/java/io/github/jwharm/javagi/generators/RecordGenerator.java b/buildSrc/src/main/java/io/github/jwharm/javagi/generators/RecordGenerator.java index b716488c..425dbb99 100644 --- a/buildSrc/src/main/java/io/github/jwharm/javagi/generators/RecordGenerator.java +++ b/buildSrc/src/main/java/io/github/jwharm/javagi/generators/RecordGenerator.java @@ -28,6 +28,7 @@ import io.github.jwharm.javagi.gir.Class; import io.github.jwharm.javagi.gir.Record; import io.github.jwharm.javagi.util.GeneratedAnnotationBuilder; +import io.github.jwharm.javagi.util.Platform; import javax.lang.model.element.Modifier; @@ -208,6 +209,11 @@ private MethodSpec constructor(boolean arenaParameter) { .addStatement("super($T.ofAuto().allocate(getMemoryLayout()))", Arena.class); + // Platform check + if (rec.doPlatformCheck()) + spec.addStatement("$T.checkSupportedPlatform($L)", + ClassNames.PLATFORM, Platform.toStringLiterals(rec.platforms())); + return spec.build(); } @@ -227,7 +233,10 @@ private MethodSpec constructorWithParameters(boolean arenaParameter) { toJavaIdentifier(f.name()), f.callback() == null ? "value" : "callback function") .addParameter( - new TypedValueGenerator(f).getType(), + // Override the type of long values + f.anyType() instanceof Type t && t.isLong() + ? TypeName.INT + : new TypedValueGenerator(f).getType(), toJavaIdentifier(f.name())) ); @@ -261,6 +270,11 @@ else if (f.allocatesMemory()) toJavaIdentifier(f.name())); }); + // Platform check + if (rec.doPlatformCheck()) + spec.addStatement("$T.checkSupportedPlatform($L)", + ClassNames.PLATFORM, Platform.toStringLiterals(rec.platforms())); + return spec.build(); } @@ -325,7 +339,10 @@ private MethodSpec allocateWithParameters() { .addParameter(Arena.class, "arena"); rec.fields().stream().filter(not(Field::isDisguised)).forEach(f -> spec.addParameter( - new TypedValueGenerator(f).getType(), + // Override the type of long values + f.anyType() instanceof Type t && t.isLong() + ? TypeName.INT + : new TypedValueGenerator(f).getType(), toJavaIdentifier(f.name())) ); diff --git a/buildSrc/src/main/java/io/github/jwharm/javagi/generators/SignalGenerator.java b/buildSrc/src/main/java/io/github/jwharm/javagi/generators/SignalGenerator.java index a1bbf94a..144853ea 100644 --- a/buildSrc/src/main/java/io/github/jwharm/javagi/generators/SignalGenerator.java +++ b/buildSrc/src/main/java/io/github/jwharm/javagi/generators/SignalGenerator.java @@ -161,12 +161,20 @@ public MethodSpec generateEmitMethod() { .map(PreprocessingGenerator::new) .forEach(p -> p.generate(builder)); + // Determine ValueLayout for return value + var returnLayout = PartialStatement.of( + generator.generateValueLayout(returnValue.anyType()), + "interop", ClassNames.INTEROP, + "valueLayout", ValueLayout.class); + // Allocate memory for return value - if (!returnValue.anyType().isVoid()) - builder.addStatement("$T _result = _arena.allocate($T.$L)", - MemorySegment.class, - ValueLayout.class, - getValueLayout(returnValue.anyType())); + if (!returnValue.anyType().isVoid()) { + var stmt = PartialStatement.of("$[$memorySegment:T _result = _arena.allocate(", + "memorySegment", MemorySegment.class) + .add(returnLayout) + .add(");\n$]"); + builder.addNamedCode(stmt.format(), stmt.arguments()); + } // Allocate memory for signal name if (signal.detailed()) @@ -195,7 +203,7 @@ public MethodSpec generateEmitMethod() { else { varargs.add("new Object[] {"); if (signal.parameters() != null) { - varargs.add(generator.marshalParameters()); + varargs.add(generator.marshalParameters(true)); } if (!returnValue.anyType().isVoid()) { if (! varargs.format().endsWith("{")) @@ -218,12 +226,12 @@ public MethodSpec generateEmitMethod() { // Marshal the return value if (!returnValue.anyType().isVoid()) { var generator = new TypedValueGenerator(returnValue); - var layout = getValueLayout(returnValue.anyType()); - var identifier = "_result.get($valueLayout:T." + layout + ", 0)"; + var identifier = "_result.get(" + returnLayout.format() + ", 0)"; var stmt = PartialStatement.of("return ", "valueLayout", ValueLayout.class) .add(generator.marshalNativeToJava(identifier, false)) .add(";\n"); + stmt.arguments().putAll(returnLayout.arguments()); builder.addNamedCode(stmt.format(), stmt.arguments()); } diff --git a/buildSrc/src/main/java/io/github/jwharm/javagi/generators/TypedValueGenerator.java b/buildSrc/src/main/java/io/github/jwharm/javagi/generators/TypedValueGenerator.java index 88944f0e..636fa123 100644 --- a/buildSrc/src/main/java/io/github/jwharm/javagi/generators/TypedValueGenerator.java +++ b/buildSrc/src/main/java/io/github/jwharm/javagi/generators/TypedValueGenerator.java @@ -29,6 +29,7 @@ import javax.lang.model.element.Modifier; import java.lang.foreign.Arena; import java.lang.foreign.MemorySegment; +import java.lang.foreign.ValueLayout; import java.util.List; import java.util.NoSuchElementException; import java.util.Set; @@ -661,4 +662,13 @@ else if (target instanceof FlaggedType) return null; } } + + PartialStatement generateValueLayoutPlain(Type type) { + String stmt = (type != null && type.isLong()) + ? "($interop:T.longAsInt() ? $valueLayout:T.JAVA_INT : $valueLayout:T.JAVA_LONG)" + : "$valueLayout:T." + getValueLayoutPlain(type, false); + return PartialStatement.of(stmt, + "interop", ClassNames.INTEROP, + "valueLayout", ValueLayout.class); + } } diff --git a/buildSrc/src/main/java/io/github/jwharm/javagi/gir/Field.java b/buildSrc/src/main/java/io/github/jwharm/javagi/gir/Field.java index d2a2482e..5e62af71 100644 --- a/buildSrc/src/main/java/io/github/jwharm/javagi/gir/Field.java +++ b/buildSrc/src/main/java/io/github/jwharm/javagi/gir/Field.java @@ -48,28 +48,29 @@ public boolean allocatesMemory() { /** * Get the memory size of this field, in bytes + * @param longAsInt when true, long is 4 bytes, else long is 8 bytes */ - public int getSize() { + public int getSize(boolean longAsInt) { return switch(anyType()) { case null -> 8; // callback - case Array array -> getSize(array); - case Type type -> getSize(type); + case Array array -> getSize(array, longAsInt); + case Type type -> getSize(type, longAsInt); }; } - private int getSize(Array array) { + private int getSize(Array array, boolean longAsInt) { int fixedSize = array.fixedSize(); if (fixedSize == -1) return 8; return fixedSize * switch(array.anyType()) { - case Array nested -> getSize(nested); - case Type type -> getSize(type); + case Array nested -> getSize(nested, longAsInt); + case Type type -> getSize(type, longAsInt); }; } - private int getSize(Type type) { + private int getSize(Type type, boolean longAsInt) { if (type.get() instanceof Alias alias) - return getSize(alias.type()); + return getSize(alias.type(), longAsInt); var typeName = type.typeName(); if (List.of(BYTE, CHAR).contains(typeName)) @@ -80,6 +81,8 @@ private int getSize(Type type) { return 4; if (type.get() instanceof FlaggedType) return 4; + if (type.isLong() && longAsInt) + return 4; else return 8; } diff --git a/buildSrc/src/main/java/io/github/jwharm/javagi/gir/Node.java b/buildSrc/src/main/java/io/github/jwharm/javagi/gir/Node.java index 1edc3de6..d2960a87 100644 --- a/buildSrc/src/main/java/io/github/jwharm/javagi/gir/Node.java +++ b/buildSrc/src/main/java/io/github/jwharm/javagi/gir/Node.java @@ -21,6 +21,7 @@ import java.util.List; import java.util.Map; +import java.util.function.Predicate; public interface Node { Namespace namespace(); @@ -37,4 +38,22 @@ public interface Node { T withAttribute(String attrName, String newValue); T withChildren(GirElement... newChildren); T withChildren(List newChildren); + + /** + * Apply the predicate on this node and its children recursively, and + * return whether a match was found. + * + * @param predicate the predicate to apply + * @param skip skip nodes of this type and their children + * @return whether the predicate matched for this node or one of its + * children (recursively) + */ + default boolean deepMatch(Predicate predicate, + java.lang.Class skip) { + if (skip.isAssignableFrom(this.getClass())) + return false; + if (predicate.test(this)) + return true; + return children().stream().anyMatch(c -> c.deepMatch(predicate, skip)); + } } diff --git a/buildSrc/src/main/java/io/github/jwharm/javagi/gir/Type.java b/buildSrc/src/main/java/io/github/jwharm/javagi/gir/Type.java index dfbcd710..7b723544 100644 --- a/buildSrc/src/main/java/io/github/jwharm/javagi/gir/Type.java +++ b/buildSrc/src/main/java/io/github/jwharm/javagi/gir/Type.java @@ -113,6 +113,11 @@ public boolean isBoolean() { return "gboolean".equals(name()) && (!"_Bool".equals(cType())); } + public boolean isLong() { + String cType = cType(); + return "glong".equals(cType) || "gulong".equals(cType); + } + public boolean isGList() { return name() != null && List.of("GLib.List", "GLib.SList").contains(name()); diff --git a/buildSrc/src/main/java/io/github/jwharm/javagi/patches/GLibPatch.java b/buildSrc/src/main/java/io/github/jwharm/javagi/patches/GLibPatch.java index 6f10fd96..2fbbfae4 100644 --- a/buildSrc/src/main/java/io/github/jwharm/javagi/patches/GLibPatch.java +++ b/buildSrc/src/main/java/io/github/jwharm/javagi/patches/GLibPatch.java @@ -65,6 +65,16 @@ public GirElement patch(GirElement element, String namespace) { return a; } + /* + * GThreadFunctions contains a virtual function pointer with a "gulong" + * parameter, which can cause problems on Windows. GThreadFunctions is + * deprecated, so we can safely remove the Windows support. + */ + if (element instanceof Record r && "ThreadFunctions".equals(r.name())) { + r.setPlatforms(Platform.LINUX | Platform.MACOS); + return r; + } + /* * GVariant has a method "get_type" that clashes with the "getType()" * method that is generated in Java. Therefore, it is renamed to diff --git a/buildSrc/src/main/java/io/github/jwharm/javagi/util/Conversions.java b/buildSrc/src/main/java/io/github/jwharm/javagi/util/Conversions.java index 2d8746f6..920a235d 100644 --- a/buildSrc/src/main/java/io/github/jwharm/javagi/util/Conversions.java +++ b/buildSrc/src/main/java/io/github/jwharm/javagi/util/Conversions.java @@ -228,7 +228,7 @@ public static String primitiveClassName(String primitive) { /** * Return the TypeName of the carrier type. */ - public static TypeName getCarrierTypeName(AnyType t) { + public static TypeName getCarrierTypeName(AnyType t, boolean longAsInt) { if (t == null || t instanceof Array) return TypeName.get(MemorySegment.class); @@ -240,6 +240,9 @@ public static TypeName getCarrierTypeName(AnyType t) { if (type.isBoolean()) return TypeName.INT; + if (type.isLong() && longAsInt) + return TypeName.INT; + if (type.isPrimitive() || "none".equals(t.cType())) return type.typeName(); @@ -287,11 +290,12 @@ public static String getCarrierTypeTag(AnyType t) { * Get the memory layout of this type. Pointer types are returned as * "ADDRESS". */ - public static String getValueLayout(AnyType anyType) { + public static String getValueLayout(AnyType anyType, boolean longAsInt) { return switch (anyType) { case null -> "ADDRESS"; case Array _ -> "ADDRESS"; - case Type t -> t.isPointer() ? "ADDRESS" : getValueLayoutPlain(t); + case Type t -> t.isPointer() ? "ADDRESS" + : getValueLayoutPlain(t, longAsInt); }; } @@ -299,7 +303,7 @@ public static String getValueLayout(AnyType anyType) { * Get the memory layout of this type. Pointers to primitive types are * treated as the actual type. */ - public static String getValueLayoutPlain(Type t) { + public static String getValueLayoutPlain(Type t, boolean longAsInt) { if (t == null) { return "ADDRESS"; } @@ -307,11 +311,14 @@ public static String getValueLayoutPlain(Type t) { if (target instanceof FlaggedType || t.isBoolean()) { return "JAVA_INT"; } + if (t.isLong() && longAsInt) { + return "JAVA_INT"; + } if (t.isPrimitive()) { return "JAVA_" + t.javaType().toUpperCase(); } if (target instanceof Alias a && a.type().isPrimitive()) { - return getValueLayoutPlain(a.type()); + return getValueLayoutPlain(a.type(), longAsInt); } return "ADDRESS"; } diff --git a/modules/gio/src/test/java/io/github/jwharm/javagi/test/gio/StrvArrayTest.java b/modules/gio/src/test/java/io/github/jwharm/javagi/test/gio/StrvArrayTest.java index 02bbd428..2ed28310 100644 --- a/modules/gio/src/test/java/io/github/jwharm/javagi/test/gio/StrvArrayTest.java +++ b/modules/gio/src/test/java/io/github/jwharm/javagi/test/gio/StrvArrayTest.java @@ -1,9 +1,11 @@ package io.github.jwharm.javagi.test.gio; +import io.github.jwharm.javagi.interop.Platform; import org.gnome.gio.DesktopAppInfo; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assumptions.assumeTrue; /** * Get a String[][] array from a Gio function, and check that it contains @@ -13,6 +15,9 @@ public class StrvArrayTest { @Test public void testStrvArrayToJava() { + // DesktopAppInfo is only available on Linux + assumeTrue("linux".equals(Platform.getRuntimePlatform())); + String[][] array = DesktopAppInfo.search("gnome"); assertNotNull(array); String result = ""; diff --git a/modules/glib/src/main/java/io/github/jwharm/javagi/interop/Interop.java b/modules/glib/src/main/java/io/github/jwharm/javagi/interop/Interop.java index 7afb2924..5f64f0a9 100644 --- a/modules/glib/src/main/java/io/github/jwharm/javagi/interop/Interop.java +++ b/modules/glib/src/main/java/io/github/jwharm/javagi/interop/Interop.java @@ -42,18 +42,28 @@ public class Interop { private final static int INT_UNBOUNDED = Integer.MAX_VALUE; + private final static long LONG_UNBOUNDED = Long.MAX_VALUE; + + private final static boolean LONG_AS_INT = Linker.nativeLinker() + .canonicalLayouts().get("long").equals(ValueLayout.JAVA_INT); + private final static Linker LINKER = Linker.nativeLinker(); - public static SymbolLookup symbolLookup = SymbolLookup.loaderLookup() + + private static SymbolLookup symbolLookup = SymbolLookup.loaderLookup() .or(Linker.nativeLinker().defaultLookup()); + public static boolean longAsInt() { + return LONG_AS_INT; + } + /** * Load the specified library using * {@link SymbolLookup#libraryLookup(String, Arena)}. * * @param name the name of the library */ - public static void loadLibrary(String name) { + public static synchronized void loadLibrary(String name) { try { symbolLookup = SymbolLookup.libraryLookup(name, Arena.global()) .or(Interop.symbolLookup);