From 2f896ea654b396c1bfdbd832a026035bbb448b63 Mon Sep 17 00:00:00 2001 From: Jan-Willem Harmannij Date: Thu, 9 Jan 2025 22:17:55 +0100 Subject: [PATCH] Generate a GObject constructor that takes a Java class as first argument instead of a GType, followed by varargs for (optionally) specifying property names and values. A new utility class `VarargsUtil` was created that simplifies splitting the varargs into a "first" and "rest" component like `g_object_new` expects, and also adds a `null` at the end, in case the user forgets it. --- .../javagi/configuration/ClassNames.java | 1 + .../javagi/generators/ClassGenerator.java | 46 +++++++++++--- .../jwharm/javagi/interop/VarargsUtil.java | 60 +++++++++++++++++++ .../javagi/test/gobject/DerivedClassTest.java | 22 +++++++ 4 files changed, 121 insertions(+), 8 deletions(-) create mode 100644 modules/glib/src/main/java/io/github/jwharm/javagi/interop/VarargsUtil.java diff --git a/generator/src/main/java/io/github/jwharm/javagi/configuration/ClassNames.java b/generator/src/main/java/io/github/jwharm/javagi/configuration/ClassNames.java index 8675625d..21e8a8a3 100644 --- a/generator/src/main/java/io/github/jwharm/javagi/configuration/ClassNames.java +++ b/generator/src/main/java/io/github/jwharm/javagi/configuration/ClassNames.java @@ -50,6 +50,7 @@ public final class ClassNames { public static final ClassName MEMORY_CLEANER = get(PKG_INTEROP, "MemoryCleaner"); public static final ClassName INTEROP = get(PKG_INTEROP, "Interop"); public static final ClassName PLATFORM = get(PKG_INTEROP, "Platform"); + public static final ClassName VARARGS_UTIL = get(PKG_INTEROP, "VarargsUtil"); public static final ClassName AUTO_CLOSEABLE = get(PKG_GIO, "AutoCloseable"); public static final ClassName LIST_MODEL_JAVA_LIST = get(PKG_GIO, "ListModelJavaList"); diff --git a/generator/src/main/java/io/github/jwharm/javagi/generators/ClassGenerator.java b/generator/src/main/java/io/github/jwharm/javagi/generators/ClassGenerator.java index 559901ed..eec11332 100644 --- a/generator/src/main/java/io/github/jwharm/javagi/generators/ClassGenerator.java +++ b/generator/src/main/java/io/github/jwharm/javagi/generators/ClassGenerator.java @@ -142,10 +142,11 @@ else if (cls.isInstanceOf("GObject", "ParamSpec")) } if ("GObject".equals(cls.cType())) - builder.addMethod(gobjectConstructor()) - .addMethod(gobjectConstructorVarargs()) - .addMethod(gobjectClassConstructor()) - .addMethod(gobjectClassConstructorVarargs()) + builder.addMethod(gobjectClassConstructor()) + .addMethod(gobjectFactory()) + .addMethod(gobjectFactoryVarargs()) + .addMethod(gobjectClassFactory()) + .addMethod(gobjectClassFactoryVarargs()) .addMethod(gobjectGetProperty()) .addMethod(gobjectSetProperty()) .addMethod(gobjectBindProperty()) @@ -224,7 +225,7 @@ protected MethodSpec paramSpecGetTypeMethod() { .build(); } - private MethodSpec gobjectConstructor() { + private MethodSpec gobjectFactory() { return MethodSpec.methodBuilder("newInstance") .addJavadoc(""" Creates a new GObject instance of the provided GType. @@ -243,7 +244,7 @@ private MethodSpec gobjectConstructor() { .build(); } - private MethodSpec gobjectConstructorVarargs() { + private MethodSpec gobjectFactoryVarargs() { return MethodSpec.methodBuilder("newInstance") .addJavadoc(""" Creates a new GObject instance of the provided GType and with the @@ -266,7 +267,7 @@ private MethodSpec gobjectConstructorVarargs() { .build(); } - private MethodSpec gobjectClassConstructor() { + private MethodSpec gobjectClassFactory() { var paramType = ParameterizedTypeName.get( ClassName.get(java.lang.Class.class), TypeVariableName.get("T")); @@ -292,7 +293,7 @@ private MethodSpec gobjectClassConstructor() { .build(); } - private MethodSpec gobjectClassConstructorVarargs() { + private MethodSpec gobjectClassFactoryVarargs() { var paramType = ParameterizedTypeName.get( ClassName.get(java.lang.Class.class), TypeVariableName.get("T")); @@ -322,6 +323,35 @@ private MethodSpec gobjectClassConstructorVarargs() { .build(); } + private MethodSpec gobjectClassConstructor() { + var paramType = ParameterizedTypeName.get( + ClassName.get(java.lang.Class.class), TypeVariableName.get("?")); + + return MethodSpec.constructorBuilder() + .addJavadoc(""" + Creates a new instance of a GObject-derived class with the provided + property values. For your own GObject-derived Java classes, a GType + must have been registered using {@code Types.register(Class)} + + @param objectClass the Java class of the new GObject + @param propertyNamesAndValues pairs of property names and values + (Strings and Objects). Does not need to be null-terminated. + @return the newly created GObject instance + @throws ClassCastException invalid property name + """) + .addModifiers(Modifier.PUBLIC) + .addParameter(paramType, "objectClass") + .addParameter(Object[].class, "propertyNamesAndValues") + .varargs(true) + .addStatement("this(constructNew($1T.getType(objectClass),$W" + + "(String) $2T.first(propertyNamesAndValues),$W" + + "$2T.rest(propertyNamesAndValues)))", + ClassNames.TYPE_CACHE, ClassNames.VARARGS_UTIL) + .addStatement("$T.put(handle(), this)", + ClassNames.INSTANCE_CACHE) + .build(); + } + private MethodSpec gobjectGetProperty() { return MethodSpec.methodBuilder("getProperty") .addJavadoc(""" diff --git a/modules/glib/src/main/java/io/github/jwharm/javagi/interop/VarargsUtil.java b/modules/glib/src/main/java/io/github/jwharm/javagi/interop/VarargsUtil.java new file mode 100644 index 00000000..5a1174be --- /dev/null +++ b/modules/glib/src/main/java/io/github/jwharm/javagi/interop/VarargsUtil.java @@ -0,0 +1,60 @@ +/* Java-GI - Java language bindings for GObject-Introspection-based libraries + * Copyright (C) 2022-2025 Jan-Willem Harmannij + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, see . + */ + +package io.github.jwharm.javagi.interop; + +import org.jetbrains.annotations.Nullable; + +import java.util.Arrays; + +/** + * Utility functions to split an array of variadic arguments into a first + * argument and a null-terminated array of remaining arguments. + */ +public class VarargsUtil { + + /** + * Return the first array element. + * + * @param array input array, can be {@code null} + * @param array element type + * @return the first element, or {@code null} if the input array is + * {@code null} or empty + */ + public static @Nullable T first(@Nullable T @Nullable[] array) { + return array == null || array.length == 0 ? null : array[0]; + } + + /** + * Return all but the first array elements, terminated with a {@code null}. + * For example, {@code [1, 2, 3]} returns {@code [2, 3, null]}. + * + * @param array input array, can be {@code null} + * @param array element type + * @return a new array of all elements except the first, terminated with a + * {@code null}, or {@code null} if the input array is {@code null} + * or empty + */ + @SuppressWarnings("unchecked") // cast is safe because the array is empty + public static @Nullable T @Nullable[] rest(@Nullable T @Nullable[] array) { + return array == null ? null + : array.length == 0 ? (T[]) new Object[] {} + : Arrays.copyOfRange(array, 1, array.length + 1); + } +} diff --git a/modules/gobject/src/test/java/io/github/jwharm/javagi/test/gobject/DerivedClassTest.java b/modules/gobject/src/test/java/io/github/jwharm/javagi/test/gobject/DerivedClassTest.java index e63f3d39..51c2cb81 100644 --- a/modules/gobject/src/test/java/io/github/jwharm/javagi/test/gobject/DerivedClassTest.java +++ b/modules/gobject/src/test/java/io/github/jwharm/javagi/test/gobject/DerivedClassTest.java @@ -112,6 +112,18 @@ public void writeAndReadBooleanProperty() { assertEquals(input2, object.getProperty("bool-property")); } + @Test + public void constructWithClassParameter() { + TestObject object = new TestObject(); + assertEquals(object.readGClass().readGType(), TestObject.gtype); + + String input1 = "abc"; + boolean input2 = true; + TestObject object2 = new TestObject(input1, input2); + assertEquals(input1, object2.getProperty("string-property")); + assertEquals(input2, object2.getProperty("bool-property")); + } + /** * Simple GObject-derived class used in the above tests */ @@ -122,6 +134,16 @@ public TestObject(MemorySegment address) { super(address); } + public TestObject() { + super(TestObject.class); + } + + public TestObject(String stringValue, boolean booleanValue) { + super(TestObject.class, + "string-property", stringValue, + "bool-property", booleanValue); + } + @ClassInit public static void construct(GObject.ObjectClass typeClass) { classInitHasRun = true;