diff --git a/README.md b/README.md index bba7d6e..304f11a 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Cairo Java bindings Java language bindings for the [cairo](https://www.cairographics.org) graphics library using the -JEP-434 Panama FFI. The bindings are based on **cairo 1.18.0** and work with **JDK 20** (with preview -features enabled). +JEP-442 Panama FFI. The bindings are based on **cairo 1.18.0** and work with **JDK 21** (with preview +features enabled). I created these language bindings primarily as a companion to the GObject-Introspection-based Java language bindings for Gtk and GStreamer generated with [Java-GI](https://github.com/jwharm/java-gi). @@ -20,16 +20,24 @@ available as Java enums. The proxy classes inherit when applicable: `RadialGradi functions and parameters follow Java (camel case) naming practices, so `cairo_move_to(*cr, x, y)` becomes `cr.moveTo(x, y)`. Out-parameters in the C API are mapped to return values in Java. Multiple out parameters (like coordinates) are mapped to a -`Point`, `Circle` or `Rectangle` return type in Java. +`Point`, `Circle`, `Rect` or `RGBA` return type in Java. ### Resource allocation and disposal -Resources are allocated and deallocated automatically, so there is no need to manually dispose -cairo resources in Java. However, please be aware that the disposal of proxy objects (like Context, -surfaces, matrices and patterns) is initiated by the Java garbage collector, which does not know -about the native resources, and might wait an indefinite amount of time before the objects are -effectively disposed. Therefore, manual calls to `destroy()` are still possible in case the -normal cleanup during GC is not sufficient to prevent resource exhaustion. +Most resources are allocated and deallocated automatically, so there is no need to manually dispose +cairo resources in Java as would be the case in C with the various `cairo_..._destroy()` functions. +However, please be aware that the disposal of these objects (like Context) is initiated by the Java +garbage collector, which does not know about the native resources, and might wait an indefinite +amount of time before the objects are effectively disposed. Therefore, manual calls to `destroy()` +are still possible in case the automatic cleanup during GC is not sufficient to prevent resource +exhaustion. + +Some data types (like Matrix and TextExtents) must be allocated by the client; the `create()` +methods for these classes have an `Arena` parameter. Consult the JDK documentation to choose the +optimal type of Arena for your use case. + +The `Surface` and `Device` classes implement `AutoCloseable` and are preferably used in +try-with-resources blocks. (The `close()` method calls the C `cairo_..._finish()` function.) ### Error handling @@ -52,20 +60,9 @@ Some other features that the language bindings offer: path operation. They can be iterated and processed with record patterns (JEP 440). See the `Path` class javadoc for example code. -* The `cairo_set_user_data()` and `cairo_get_user_data()` functions (to attach - custom data to a cairo struct) are available in Java, with a twist. You can call - `setUserData()` to attach any Java object instance, and `getUserData()` to get it - back. Objects that can be marshaled to a native memory segment (primitive types, memory segments, - and other `Proxy` objects) will be attached to the native cairo struct. Other types will only - be attached to the Java object and will not be passed to cairo itself. - * I/O operations in cairo that are designed to work with streams accept Java `InputStream` and `OutputStream` parameters. -* The `Surface` and `Device` classes implement `AutoCloseable` and can be used in - try-with-resources blocks. (The `close()` method calls the C `cairo_..._finish()` - function.) - * The cairo Script surface has been split into a `Script` class that inherits from `Device`, and a `ScriptSurface` class that inherits from `Surface`. @@ -97,8 +94,8 @@ dependencies { ``` Furthermore, you obviously need to have the cairo library version 1.18 installed on your system, -or else the Java bindings have nothing to bind to. You also need to install JDK 20 (not JDK 19 or -earlier, nor JDK 21 or later), because the Panama FFI is slightly different between JDK versions. +or else the Java bindings have nothing to bind to. You also need to install JDK 21 (not JDK 20 or +earlier, nor JDK 22 or later, because the Panama FFI is slightly different between JDK versions). Now, you can start developing with cairo in Java. Have fun! This is a simple example to get started, ported from [the first sample on this page](https://www.cairographics.org/samples/): @@ -111,35 +108,36 @@ public class CairoExample { public static void main(String[] args) throws IOException { // Create surface - var surface = ImageSurface.create(Format.ARGB32, 300, 300); - - // Create drawing context - var cr = Context.create(surface); - - double x = 128.0; - double y = 128.0; - double radius = 100.0; - double angle1 = 45.0 * (Math.PI/180.0); // angles are specified - double angle2 = 180.0 * (Math.PI/180.0); // in radians - - // Draw shapes - cr.setLineWidth(10.0) - .arc(x, y, radius, angle1, angle2) - .stroke(); - - cr.setSourceRGBA(1.0, 0.2, 0.2, 0.6) - .setLineWidth(6.0) - .arc(x, y, 10.0, 0.0, 2 * Math.PI) - .fill(); - - cr.arc(x, y, radius, angle1, angle1) - .lineTo(x, y) - .arc(x, y, radius, angle2, angle2) - .lineTo(x, y) - .stroke(); - - // Write image to png file - surface.writeToPNG("example.png"); + try (var surface = ImageSurface.create(Format.ARGB32, 300, 300)) { + + // Create drawing context + var cr = Context.create(surface); + + double x = 128.0; + double y = 128.0; + double radius = 100.0; + double angle1 = 45.0 * (Math.PI / 180.0); // angles are specified + double angle2 = 180.0 * (Math.PI / 180.0); // in radians + + // Draw shapes + cr.setLineWidth(10.0) + .arc(x, y, radius, angle1, angle2) + .stroke(); + + cr.setSourceRGBA(1.0, 0.2, 0.2, 0.6) + .setLineWidth(6.0) + .arc(x, y, 10.0, 0.0, 2 * Math.PI) + .fill(); + + cr.arc(x, y, radius, angle1, angle1) + .lineTo(x, y) + .arc(x, y, radius, angle2, angle2) + .lineTo(x, y) + .stroke(); + + // Write image to png file + surface.writeToPNG("example.png"); + } } } ``` @@ -150,4 +148,4 @@ access, you can optionally add `--enable-native-access=org.freedesktop.cairo`. ## Building and Contributing -Please contribute PRs or log issues on [Github](https://github.com/jwharm/cairo-java-bindings). +Please contribute PRs or log issues on [GitHub](https://github.com/jwharm/cairo-java-bindings). diff --git a/build.gradle b/build.gradle index 3d734f0..e06f0a8 100644 --- a/build.gradle +++ b/build.gradle @@ -7,7 +7,7 @@ plugins { } group = "io.github.jwharm.cairobindings" -version = "1.18.0" +version = "1.18.1-SNAPSHOT" repositories { mavenCentral() @@ -15,7 +15,7 @@ repositories { java { toolchain { - languageVersion = JavaLanguageVersion.of(20) + languageVersion = JavaLanguageVersion.of(21) } withSourcesJar() withJavadocJar() @@ -37,7 +37,7 @@ tasks.named('compileJava') { tasks.named('javadoc') { options.addBooleanOption("-enable-preview", true) - options.addStringOption("-release", "20") + options.addStringOption("-release", "21") } tasks.named('compileTestJava') { diff --git a/settings.gradle b/settings.gradle index 398eafe..201a6ce 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,5 +1,5 @@ plugins { - id 'org.gradle.toolchains.foojay-resolver-convention' version '0.6.0' + id 'org.gradle.toolchains.foojay-resolver-convention' version '0.7.0' } rootProject.name = 'cairo' diff --git a/src/main/java/io/github/jwharm/cairobindings/ArenaCloseAction.java b/src/main/java/io/github/jwharm/cairobindings/ArenaCloseAction.java new file mode 100644 index 0000000..34c38e5 --- /dev/null +++ b/src/main/java/io/github/jwharm/cairobindings/ArenaCloseAction.java @@ -0,0 +1,39 @@ +/* cairo-java-bindings - Java language bindings for cairo + * Copyright (C) 2023 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.cairobindings; + +import java.lang.foreign.Arena; +import java.lang.ref.Cleaner; + +/** + * Helper class to separate the cleanup logic from the object being cleaned + * + * @param arena the Arena that will be closed + * @since 1.18.1 + */ +public record ArenaCloseAction(Arena arena) implements Runnable { + + // Cleaner used to close the arena + public static final Cleaner CLEANER = Cleaner.create(); + + @Override + public void run() { + arena.close(); + } +} diff --git a/src/main/java/io/github/jwharm/cairobindings/Interop.java b/src/main/java/io/github/jwharm/cairobindings/Interop.java index 6f159fa..b938035 100644 --- a/src/main/java/io/github/jwharm/cairobindings/Interop.java +++ b/src/main/java/io/github/jwharm/cairobindings/Interop.java @@ -38,33 +38,6 @@ public final class Interop { // Prevent instantiation private Interop() {} - /** - * Reinterpret a zero-length MemorySegment to the size of - * {@code layout} - * - * @param address the address with {@code layout} memory layout - * @param layout the actual memory layout - * @return a MemorySegment with the size of {@code layout} - */ - public static MemorySegment reinterpret(MemorySegment address, MemoryLayout layout) { - if (address.byteSize() == layout.byteSize()) - return address; - return MemorySegment.ofAddress(address.address(), layout.byteSize()); - } - - /** - * Reinterpret a zero-length MemorySegment to the requested size - * - * @param address the address of a memory segment with the requested size - * @param size the requested size - * @return the memory segment with the requested size - */ - public static MemorySegment reinterpret(MemorySegment address, long size) { - if (address.byteSize() == size) - return address; - return MemorySegment.ofAddress(address.address(), size); - } - /** * Creates a method handle that is used to call the native function with * the provided name and function descriptor. The method handle is cached diff --git a/src/main/java/org/freedesktop/cairo/Cairo.java b/src/main/java/org/freedesktop/cairo/Cairo.java index c546231..428b3d5 100644 --- a/src/main/java/org/freedesktop/cairo/Cairo.java +++ b/src/main/java/org/freedesktop/cairo/Cairo.java @@ -179,12 +179,12 @@ public static String versionString() { if (MemorySegment.NULL.equals(result)) { return null; } - return result.getUtf8String(0); + return result.reinterpret(Integer.MAX_VALUE).getUtf8String(0); } catch (Throwable e) { throw new RuntimeException(e); } } private static final MethodHandle cairo_version_string = Interop.downcallHandle("cairo_version_string", - FunctionDescriptor.of(ValueLayout.ADDRESS.asUnbounded())); + FunctionDescriptor.of(ValueLayout.ADDRESS)); } diff --git a/src/main/java/org/freedesktop/cairo/Circle.java b/src/main/java/org/freedesktop/cairo/Circle.java index 67192e1..59eb5a9 100644 --- a/src/main/java/org/freedesktop/cairo/Circle.java +++ b/src/main/java/org/freedesktop/cairo/Circle.java @@ -25,6 +25,7 @@ * @param x x coordinate of the center * @param y y coordinate of the center * @param radius radius of the circle + * @since 1.16 */ public record Circle(double x, double y, double radius) { } diff --git a/src/main/java/org/freedesktop/cairo/Context.java b/src/main/java/org/freedesktop/cairo/Context.java index 8c9ebff..84b9872 100644 --- a/src/main/java/org/freedesktop/cairo/Context.java +++ b/src/main/java/org/freedesktop/cairo/Context.java @@ -118,9 +118,6 @@ public final class Context extends Proxy { */ public static final String TAG_CONTENT_REF = "cairo.content_ref"; - // Keeps user data keys and values - private final UserDataStore userDataStore; - // Keep a reference to natively allocated resources that are passed to the // Context during its lifetime. @@ -151,7 +148,6 @@ public final class Context extends Proxy { public Context(MemorySegment address) { super(address); MemoryCleaner.setFreeFunc(handle(), "cairo_destroy"); - userDataStore = new UserDataStore(address.scope()); } /** @@ -670,7 +666,7 @@ public Antialias getAntialias() { */ public Context setDash(double[] dash, double offset) throws IllegalArgumentException { try { - try (Arena arena = Arena.openConfined()) { + try (Arena arena = Arena.ofConfined()) { MemorySegment dashesPtr = (dash == null || dash.length == 0) ? MemorySegment.NULL : arena.allocateArray(ValueLayout.JAVA_DOUBLE, dash); cairo_set_dash.invoke(handle(), dashesPtr, dash == null ? 0 : dash.length, offset); @@ -717,7 +713,7 @@ public int getDashCount() { */ public double[] getDash() { try { - try (Arena arena = Arena.openConfined()) { + try (Arena arena = Arena.ofConfined()) { MemorySegment dashesPtr = arena.allocate(ValueLayout.JAVA_DOUBLE.byteSize() * getDashCount()); MemorySegment offsetPtr = arena.allocate(ValueLayout.JAVA_DOUBLE); cairo_get_dash.invoke(handle(), dashesPtr, offsetPtr); @@ -738,7 +734,7 @@ public double[] getDash() { */ public double getDashOffset() { try { - try (Arena arena = Arena.openConfined()) { + try (Arena arena = Arena.ofConfined()) { MemorySegment dashesPtr = arena.allocate(ValueLayout.JAVA_DOUBLE.byteSize() * getDashCount()); MemorySegment offsetPtr = arena.allocate(ValueLayout.JAVA_DOUBLE); cairo_get_dash.invoke(handle(), dashesPtr, offsetPtr); @@ -1151,15 +1147,15 @@ public Context clipPreserve() { * @return the resulting extents * @since 1.4 */ - public Rectangle clipExtents() { + public Rect clipExtents() { try { - try (Arena arena = Arena.openConfined()) { + try (Arena arena = Arena.ofConfined()) { MemorySegment x1Ptr = arena.allocate(ValueLayout.JAVA_DOUBLE); MemorySegment y1Ptr = arena.allocate(ValueLayout.JAVA_DOUBLE); MemorySegment x2Ptr = arena.allocate(ValueLayout.JAVA_DOUBLE); MemorySegment y2Ptr = arena.allocate(ValueLayout.JAVA_DOUBLE); cairo_clip_extents.invoke(handle(), x1Ptr, y1Ptr, x2Ptr, y2Ptr); - return Rectangle.create(x1Ptr.get(ValueLayout.JAVA_DOUBLE, 0), y1Ptr.get(ValueLayout.JAVA_DOUBLE, 0), + return new Rect(x1Ptr.get(ValueLayout.JAVA_DOUBLE, 0), y1Ptr.get(ValueLayout.JAVA_DOUBLE, 0), x2Ptr.get(ValueLayout.JAVA_DOUBLE, 0), y2Ptr.get(ValueLayout.JAVA_DOUBLE, 0)); } } catch (Throwable e) { @@ -1246,7 +1242,7 @@ public RectangleList copyClipRectangleList() throws IllegalStateException { } private static final MethodHandle cairo_copy_clip_rectangle_list = Interop.downcallHandle( - "cairo_copy_clip_rectangle_list", FunctionDescriptor.of(ValueLayout.ADDRESS.asUnbounded(), ValueLayout.ADDRESS)); + "cairo_copy_clip_rectangle_list", FunctionDescriptor.of(ValueLayout.ADDRESS, ValueLayout.ADDRESS)); /** * A drawing operator that fills the current path according to the current fill @@ -1313,15 +1309,15 @@ public Context fillPreserve() { * @see #fillPreserve() * @since 1.0 */ - public Rectangle fillExtents() { + public Rect fillExtents() { try { - try (Arena arena = Arena.openConfined()) { + try (Arena arena = Arena.ofConfined()) { MemorySegment x1Ptr = arena.allocate(ValueLayout.JAVA_DOUBLE); MemorySegment y1Ptr = arena.allocate(ValueLayout.JAVA_DOUBLE); MemorySegment x2Ptr = arena.allocate(ValueLayout.JAVA_DOUBLE); MemorySegment y2Ptr = arena.allocate(ValueLayout.JAVA_DOUBLE); cairo_fill_extents.invoke(handle(), x1Ptr, y1Ptr, x2Ptr, y2Ptr); - return Rectangle.create(x1Ptr.get(ValueLayout.JAVA_DOUBLE, 0), y1Ptr.get(ValueLayout.JAVA_DOUBLE, 0), + return new Rect(x1Ptr.get(ValueLayout.JAVA_DOUBLE, 0), y1Ptr.get(ValueLayout.JAVA_DOUBLE, 0), x2Ptr.get(ValueLayout.JAVA_DOUBLE, 0), y2Ptr.get(ValueLayout.JAVA_DOUBLE, 0)); } } catch (Throwable e) { @@ -1541,15 +1537,15 @@ public Context strokePreserve() { * @see #setDash(double[], double) * @since 1.0 */ - public Rectangle strokeExtents() { + public Rect strokeExtents() { try { - try (Arena arena = Arena.openConfined()) { + try (Arena arena = Arena.ofConfined()) { MemorySegment x1Ptr = arena.allocate(ValueLayout.JAVA_DOUBLE); MemorySegment y1Ptr = arena.allocate(ValueLayout.JAVA_DOUBLE); MemorySegment x2Ptr = arena.allocate(ValueLayout.JAVA_DOUBLE); MemorySegment y2Ptr = arena.allocate(ValueLayout.JAVA_DOUBLE); cairo_stroke_extents.invoke(handle(), x1Ptr, y1Ptr, x2Ptr, y2Ptr); - return Rectangle.create(x1Ptr.get(ValueLayout.JAVA_DOUBLE, 0), y1Ptr.get(ValueLayout.JAVA_DOUBLE, 0), + return new Rect(x1Ptr.get(ValueLayout.JAVA_DOUBLE, 0), y1Ptr.get(ValueLayout.JAVA_DOUBLE, 0), x2Ptr.get(ValueLayout.JAVA_DOUBLE, 0), y2Ptr.get(ValueLayout.JAVA_DOUBLE, 0)); } } catch (Throwable e) { @@ -1715,7 +1711,7 @@ public Path copyPath() { } private static final MethodHandle cairo_copy_path = Interop.downcallHandle("cairo_copy_path", - FunctionDescriptor.of(ValueLayout.ADDRESS.asUnbounded(), ValueLayout.ADDRESS)); + FunctionDescriptor.of(ValueLayout.ADDRESS, ValueLayout.ADDRESS)); /** * Gets a flattened copy of the current path and returns it to the user as a @@ -1747,7 +1743,7 @@ public Path copyPathFlat() { } private static final MethodHandle cairo_copy_path_flat = Interop.downcallHandle("cairo_copy_path_flat", - FunctionDescriptor.of(ValueLayout.ADDRESS.asUnbounded(), ValueLayout.ADDRESS)); + FunctionDescriptor.of(ValueLayout.ADDRESS, ValueLayout.ADDRESS)); /** * Append the path onto the current path. The path may be either the return @@ -1823,7 +1819,7 @@ public boolean hasCurrentPoint() { */ public Point getCurrentPoint() { try { - try (Arena arena = Arena.openConfined()) { + try (Arena arena = Arena.ofConfined()) { MemorySegment xPtr = arena.allocate(ValueLayout.JAVA_DOUBLE); MemorySegment yPtr = arena.allocate(ValueLayout.JAVA_DOUBLE); cairo_get_current_point.invoke(handle(), xPtr, yPtr); @@ -2170,7 +2166,7 @@ public Context glyphPath(Glyphs glyphs) { */ public Context textPath(String string) { try { - try (Arena arena = Arena.openConfined()) { + try (Arena arena = Arena.ofConfined()) { MemorySegment utf8 = Interop.allocateNativeString(string, arena); cairo_text_path.invoke(handle(), utf8); return this; @@ -2306,15 +2302,15 @@ public Context relMoveTo(double dx, double dy) throws IllegalStateException { * resulting extents * @since 1.6 */ - public Rectangle pathExtents() { + public Rect pathExtents() { try { - try (Arena arena = Arena.openConfined()) { + try (Arena arena = Arena.ofConfined()) { MemorySegment x1Ptr = arena.allocate(ValueLayout.JAVA_DOUBLE); MemorySegment y1Ptr = arena.allocate(ValueLayout.JAVA_DOUBLE); MemorySegment x2Ptr = arena.allocate(ValueLayout.JAVA_DOUBLE); MemorySegment y2Ptr = arena.allocate(ValueLayout.JAVA_DOUBLE); cairo_path_extents.invoke(handle(), x1Ptr, y1Ptr, x2Ptr, y2Ptr); - return Rectangle.create(x1Ptr.get(ValueLayout.JAVA_DOUBLE, 0), y1Ptr.get(ValueLayout.JAVA_DOUBLE, 0), + return new Rect(x1Ptr.get(ValueLayout.JAVA_DOUBLE, 0), y1Ptr.get(ValueLayout.JAVA_DOUBLE, 0), x2Ptr.get(ValueLayout.JAVA_DOUBLE, 0), y2Ptr.get(ValueLayout.JAVA_DOUBLE, 0)); } } catch (Throwable e) { @@ -2442,20 +2438,18 @@ public Context setMatrix(Matrix matrix) { FunctionDescriptor.ofVoid(ValueLayout.ADDRESS, ValueLayout.ADDRESS)); /** - * Stores the current transformation matrix (CTM) into the returned matrix. + * Stores the current transformation matrix (CTM) into {@code matrix}. *

* The CTM is a two-dimensional affine transformation that maps all coordinates * and other drawing instruments from the user space into the surface's * canonical coordinate system, also known as the device space. * - * @return a matrix with the current transformation matrix + * @param matrix return value for the matrix * @since 1.0 */ - public Matrix getMatrix() { + public void getMatrix(Matrix matrix) { try { - Matrix matrix = Matrix.create(); cairo_get_matrix.invoke(handle(), matrix.handle()); - return matrix; } catch (Throwable e) { throw new RuntimeException(e); } @@ -2498,7 +2492,7 @@ public Point userToDevice(Point point) { return null; } try { - try (Arena arena = Arena.openConfined()) { + try (Arena arena = Arena.ofConfined()) { MemorySegment xPtr = arena.allocate(ValueLayout.JAVA_DOUBLE); MemorySegment yPtr = arena.allocate(ValueLayout.JAVA_DOUBLE); xPtr.set(ValueLayout.JAVA_DOUBLE, 0, point.x()); @@ -2528,7 +2522,7 @@ public Point userToDeviceDistance(Point point) { return null; } try { - try (Arena arena = Arena.openConfined()) { + try (Arena arena = Arena.ofConfined()) { MemorySegment xPtr = arena.allocate(ValueLayout.JAVA_DOUBLE); MemorySegment yPtr = arena.allocate(ValueLayout.JAVA_DOUBLE); xPtr.set(ValueLayout.JAVA_DOUBLE, 0, point.x()); @@ -2558,7 +2552,7 @@ public Point deviceToUser(Point point) { return null; } try { - try (Arena arena = Arena.openConfined()) { + try (Arena arena = Arena.ofConfined()) { MemorySegment xPtr = arena.allocate(ValueLayout.JAVA_DOUBLE); MemorySegment yPtr = arena.allocate(ValueLayout.JAVA_DOUBLE); xPtr.set(ValueLayout.JAVA_DOUBLE, 0, point.x()); @@ -2589,7 +2583,7 @@ public Point deviceToUserDistance(Point point) { return null; } try { - try (Arena arena = Arena.openConfined()) { + try (Arena arena = Arena.ofConfined()) { MemorySegment xPtr = arena.allocate(ValueLayout.JAVA_DOUBLE); MemorySegment yPtr = arena.allocate(ValueLayout.JAVA_DOUBLE); xPtr.set(ValueLayout.JAVA_DOUBLE, 0, point.x()); @@ -2658,7 +2652,7 @@ public Point deviceToUserDistance(Point point) { */ public Context selectFontFace(String family, FontSlant slant, FontWeight weight) { try { - try (Arena arena = Arena.openConfined()) { + try (Arena arena = Arena.ofConfined()) { MemorySegment utf8 = Interop.allocateNativeString(family, arena); cairo_select_font_face.invoke(handle(), utf8, slant.getValue(), weight.getValue()); return this; @@ -2725,16 +2719,15 @@ public Context setFontMatrix(Matrix matrix) { FunctionDescriptor.ofVoid(ValueLayout.ADDRESS, ValueLayout.ADDRESS)); /** - * Returns the current font matrix. See {@link #setFontMatrix(Matrix)} - * - * @return the matrix + * Stores the current font matrix into {@code matrix}. See + * {@link #setFontMatrix} + * + * @param matrix return value for the matrix * @since 1.0 */ - public Matrix getFontMatrix() { + public void getFontMatrix(Matrix matrix) { try { - Matrix fontMatrix = Matrix.create(); cairo_get_font_matrix.invoke(handle(), fontMatrix.handle()); - return fontMatrix; } catch (Throwable e) { throw new RuntimeException(e); } @@ -2838,7 +2831,7 @@ public FontFace getFontFace() { * Replaces the current font face, font matrix, and font options in the * {@link Context} with those of the {@link ScaledFont}. Except for some * translation, the current CTM of the Context should be the same as that of the - * ScaledFont, which can be accessed using {@link ScaledFont#getCTM()}. + * ScaledFont, which can be accessed using {@link ScaledFont#getCTM}. * * @param scaledFont a ScaledFont * @return the context @@ -2911,7 +2904,7 @@ public ScaledFont getScaledFont() { */ public Context showText(String string) { try { - try (Arena arena = Arena.openConfined()) { + try (Arena arena = Arena.ofConfined()) { MemorySegment utf8 = Interop.allocateNativeString(string, arena); cairo_show_text.invoke(handle(), utf8); return this; @@ -2974,7 +2967,7 @@ public Context showGlyphs(Glyphs glyphs) { */ public Context showTextGlyphs(String string, Glyphs glyphs) { try { - try (Arena arena = Arena.openConfined()) { + try (Arena arena = Arena.ofConfined()) { MemorySegment stringPtr = Interop.allocateNativeString(string, arena); cairo_show_text_glyphs.invoke(handle(), stringPtr, string == null ? 0 : string.length(), @@ -2996,14 +2989,12 @@ public Context showTextGlyphs(String string, Glyphs glyphs) { /** * Gets the font extents for the currently selected font. * - * @return the font extents + * @param extents a FontExtents object into which the results will be stored. * @since 1.0 */ - public FontExtents fontExtents() { + public void fontExtents(FontExtents extents) { try { - FontExtents extents = FontExtents.create(); cairo_font_extents.invoke(handle(), extents.handle()); - return extents; } catch (Throwable e) { throw new RuntimeException(e); } @@ -3026,17 +3017,15 @@ public FontExtents fontExtents() { * size of the rectangle, though they will affect the {@code xAdvance} and * {@code yAdvance} values. * - * @param string a string of text, or {@code null} - * @return the text extents + * @param string a string of text, or {@code null} + * @param extents a TextExtents object into which the results will be stored * @since 1.0 */ - public TextExtents textExtents(String string) { + public void textExtents(String string, TextExtents extents) { try { - try (Arena arena = Arena.openConfined()) { - TextExtents extents = TextExtents.create(); + try (Arena arena = Arena.ofConfined()) { MemorySegment utf8 = Interop.allocateNativeString(string, arena); cairo_text_extents.invoke(handle(), utf8, extents.handle()); - return extents; } } catch (Throwable e) { throw new RuntimeException(e); @@ -3057,17 +3046,15 @@ public TextExtents textExtents(String string) { * ({@code extents.width} and {@code extents.height}). * * @param glyphs an array of Glyph objects - * @return the glyph extents + * @param extents a TextExtents object into which the results will be stored * @since 1.0 */ - public TextExtents glyphExtents(Glyphs glyphs) { + public void glyphExtents(Glyphs glyphs, TextExtents extents) { if (glyphs == null) { - return null; + return; } try { - TextExtents extents = TextExtents.create(); cairo_glyph_extents.invoke(handle(), glyphs.getGlyphsPointer(), glyphs.getNumGlyphs(), extents.handle()); - return extents; } catch (Throwable e) { throw new RuntimeException(e); } @@ -3108,7 +3095,7 @@ public TextExtents glyphExtents(Glyphs glyphs) { */ public Context tagBegin(String tagName, String attributes) throws IllegalArgumentException { try { - try (Arena arena = Arena.openConfined()) { + try (Arena arena = Arena.ofConfined()) { MemorySegment tagNamePtr = Interop.allocateNativeString(tagName, arena); MemorySegment attributesPtr = Interop.allocateNativeString(attributes, arena); cairo_tag_begin.invoke(handle(), tagNamePtr, attributesPtr); @@ -3137,7 +3124,7 @@ public Context tagBegin(String tagName, String attributes) throws IllegalArgumen */ public Context tagEnd(String tagName) throws IllegalArgumentException { try { - try (Arena arena = Arena.openConfined()) { + try (Arena arena = Arena.ofConfined()) { MemorySegment tagNamePtr = Interop.allocateNativeString(tagName, arena); cairo_tag_end.invoke(handle(), tagNamePtr); } @@ -3153,44 +3140,21 @@ public Context tagEnd(String tagName) throws IllegalArgumentException { private static final MethodHandle cairo_tag_end = Interop.downcallHandle("cairo_tag_end", FunctionDescriptor.ofVoid(ValueLayout.ADDRESS, ValueLayout.ADDRESS)); - /** - * Attach user data to the context. This method will generate and return a - * {@link UserDataKey}. To update the user data for the same key, call - * {@link #setUserData(UserDataKey, Object)}. To remove user data from a - * context, call this function with {@code null} for {@code userData}. - * - * @param userData the user data to attach to the context. {@code userData} can - * be any Java object, but if it is a primitive type, a - * {@link MemorySegment} or a {@link Proxy} instance, it will be - * stored as cairo user data in native memory as well. - * @return the key that the user data is attached to - * @since 1.4 - */ - public UserDataKey setUserData(Object userData) { - UserDataKey key = UserDataKey.create(this); - return setUserData(key, userData); - } - /** * Attach user data to the context. To remove user data from a context, call * this function with the key that was used to set it and {@code null} for * {@code userData}. - * - * @param key the key to attach the user data to - * @param userData the user data to attach to the context. {@code userData} can - * be any Java object, but if it is a primitive type, a - * {@link MemorySegment} or a {@link Proxy} instance, it will be - * stored as cairo user data in native memory as well. + * + * @param key the key to attach the user data to + * @param userData the user data to attach to the context * @return the key * @throws NullPointerException if {@code key} is {@code null} * @since 1.4 */ - public UserDataKey setUserData(UserDataKey key, Object userData) { + public UserDataKey setUserData(UserDataKey key, MemorySegment userData) { Status status; - userDataStore.set(key, userData); try { - int result = (int) cairo_set_user_data.invoke(handle(), key.handle(), userDataStore.dataSegment(userData), - MemorySegment.NULL); + int result = (int) cairo_set_user_data.invoke(handle(), key.handle(), userData, MemorySegment.NULL); status = Status.of(result); } catch (Throwable e) { throw new RuntimeException(e); @@ -3210,14 +3174,25 @@ public UserDataKey setUserData(UserDataKey key, Object userData) { * If no user data has been attached with the given key this function returns * {@code null}. * - * @param key the UserDataKey the user data was attached to + * @param key the UserDataKey the user data was attached to * @return the user data previously attached or {@code null} * @since 1.4 */ - public Object getUserData(UserDataKey key) { - return key == null ? null : userDataStore.get(key); + public MemorySegment getUserData(UserDataKey key) { + if (key == null) { + return null; + } + try { + MemorySegment result = (MemorySegment) cairo_get_user_data.invoke(handle(), key.handle()); + return MemorySegment.NULL.equals(result) ? null : result; + } catch (Throwable e) { + throw new RuntimeException(e); + } } + private static final MethodHandle cairo_get_user_data = Interop.downcallHandle("cairo_get_user_data", + FunctionDescriptor.of(ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS)); + /** * Get the CairoContext GType * @return the GType diff --git a/src/main/java/org/freedesktop/cairo/DestroyFunc.java b/src/main/java/org/freedesktop/cairo/DestroyFunc.java index 385d3d3..2d6000a 100644 --- a/src/main/java/org/freedesktop/cairo/DestroyFunc.java +++ b/src/main/java/org/freedesktop/cairo/DestroyFunc.java @@ -19,11 +19,7 @@ package org.freedesktop.cairo; -import java.lang.foreign.FunctionDescriptor; -import java.lang.foreign.Linker; -import java.lang.foreign.MemorySegment; -import java.lang.foreign.SegmentScope; -import java.lang.foreign.ValueLayout; +import java.lang.foreign.*; import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; @@ -39,35 +35,36 @@ public interface DestroyFunc { /** * The function to implement as callback in a destroy operation. - * + * + * @param data the data element being destroyed. * @since 1.0 */ - void destroy(); + void destroy(MemorySegment data); /** * The callback that is executed by native code. This method marshals the - * parameters and calls {@link #destroy()}. + * parameters and calls {@link #destroy}. * - * @param data the buffer into which to read the data + * @param data the data element being destroyed. * @since 1.0 */ default void upcall(MemorySegment data) { - destroy(); + destroy(data); } /** * Generates an upcall stub, a C function pointer that will call - * {@link #upcall(MemorySegment)}. - * - * @param scope the scope in which the upcall stub will be allocated + * {@link #upcall}. + * + * @param arena the arena in which the upcall stub will be allocated * @return the function pointer of the upcall stub * @since 1.0 */ - default MemorySegment toCallback(SegmentScope scope) { + default MemorySegment toCallback(Arena arena) { try { FunctionDescriptor fdesc = FunctionDescriptor.ofVoid(ValueLayout.ADDRESS); MethodHandle handle = MethodHandles.lookup().findVirtual(DestroyFunc.class, "upcall", fdesc.toMethodType()); - return Linker.nativeLinker().upcallStub(handle.bindTo(this), fdesc, scope); + return Linker.nativeLinker().upcallStub(handle.bindTo(this), fdesc, arena); } catch (NoSuchMethodException | IllegalAccessException e) { throw new RuntimeException(e); } diff --git a/src/main/java/org/freedesktop/cairo/Device.java b/src/main/java/org/freedesktop/cairo/Device.java index 8f50dc2..5af90a2 100644 --- a/src/main/java/org/freedesktop/cairo/Device.java +++ b/src/main/java/org/freedesktop/cairo/Device.java @@ -63,9 +63,6 @@ public class Device extends Proxy implements AutoCloseable { Cairo.ensureInitialized(); } - // Keeps user data keys and values - private final UserDataStore userDataStore; - /** * Constructor used internally to instantiate a java Device object for a native * {@code cairo_device_t} instance @@ -76,7 +73,6 @@ public class Device extends Proxy implements AutoCloseable { public Device(MemorySegment address) { super(address); MemoryCleaner.setFreeFunc(handle(), "cairo_device_destroy"); - userDataStore = new UserDataStore(address.scope()); } /** @@ -325,9 +321,9 @@ public void observerPrint(OutputStream stream) throws IOException { } Status status; try { - try (Arena arena = Arena.openConfined()) { + try (Arena arena = Arena.ofConfined()) { WriteFunc writeFunc = stream::write; - int result = (int) cairo_device_observer_print.invoke(handle(), writeFunc.toCallback(arena.scope()), + int result = (int) cairo_device_observer_print.invoke(handle(), writeFunc.toCallback(arena), MemorySegment.NULL); status = Status.of(result); } @@ -363,44 +359,21 @@ public double observerStrokeElapsed() { private static final MethodHandle cairo_device_observer_stroke_elapsed = Interop.downcallHandle( "cairo_device_observer_stroke_elapsed", FunctionDescriptor.of(ValueLayout.JAVA_DOUBLE, ValueLayout.ADDRESS)); - /** - * Attach user data to the device. This method will generate and return a - * {@link UserDataKey}. To update the user data for the same key, call - * {@link #setUserData(UserDataKey, Object)}. To remove user data from a - * device, call this function with {@code null} for {@code userData}. - * - * @param userData the user data to attach to the device. {@code userData} can - * be any Java object, but if it is a primitive type, a - * {@link MemorySegment} or a {@link Proxy} instance, it will be - * stored as cairo user data in native memory as well. - * @return the key that the user data is attached to - * @since 1.10 - */ - public UserDataKey setUserData(Object userData) { - UserDataKey key = UserDataKey.create(this); - return setUserData(key, userData); - } - /** * Attach user data to the device. To remove user data from a device, call * this function with the key that was used to set it and {@code null} for * {@code userData}. - * - * @param key the key to attach the user data to - * @param userData the user data to attach to the device. {@code userData} can - * be any Java object, but if it is a primitive type, a - * {@link MemorySegment} or a {@link Proxy} instance, it will be - * stored as cairo user data in native memory as well. + * + * @param key the key to attach the user data to + * @param userData the user data to attach to the device * @return the key * @throws NullPointerException if {@code key} is {@code null} - * @since 1.10 + * @since 1.4 */ - public UserDataKey setUserData(UserDataKey key, Object userData) { + public UserDataKey setUserData(UserDataKey key, MemorySegment userData) { Status status; - userDataStore.set(key, userData); try { - int result = (int) cairo_device_set_user_data.invoke(handle(), key.handle(), - userDataStore.dataSegment(userData), MemorySegment.NULL); + int result = (int) cairo_device_set_user_data.invoke(handle(), key.handle(), userData, MemorySegment.NULL); status = Status.of(result); } catch (Throwable e) { throw new RuntimeException(e); @@ -419,15 +392,26 @@ public UserDataKey setUserData(UserDataKey key, Object userData) { * Return user data previously attached to the device using the specified key. * If no user data has been attached with the given key this function returns * {@code null}. - * - * @param key the UserDataKey the user data was attached to + * + * @param key the UserDataKey the user data was attached to * @return the user data previously attached or {@code null} - * @since 1.10 + * @since 1.4 */ - public Object getUserData(UserDataKey key) { - return key == null ? null : userDataStore.get(key); + public MemorySegment getUserData(UserDataKey key) { + if (key == null) { + return null; + } + try { + MemorySegment result = (MemorySegment) cairo_device_get_user_data.invoke(handle(), key.handle()); + return MemorySegment.NULL.equals(result) ? null : result; + } catch (Throwable e) { + throw new RuntimeException(e); + } } - + + private static final MethodHandle cairo_device_get_user_data = Interop.downcallHandle("cairo_device_get_user_data", + FunctionDescriptor.of(ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS)); + /** * Closing a device will invoke {@link #finish()}, which will flush the * device and drop all references to external resources. A closed device diff --git a/src/main/java/org/freedesktop/cairo/FTScaledFont.java b/src/main/java/org/freedesktop/cairo/FTScaledFont.java index 9976eb7..7be3b14 100644 --- a/src/main/java/org/freedesktop/cairo/FTScaledFont.java +++ b/src/main/java/org/freedesktop/cairo/FTScaledFont.java @@ -19,7 +19,6 @@ package org.freedesktop.cairo; -import java.lang.foreign.Arena; import java.lang.foreign.FunctionDescriptor; import java.lang.foreign.MemorySegment; import java.lang.foreign.ValueLayout; @@ -74,18 +73,16 @@ public FTScaledFont(MemorySegment address) { public static FTScaledFont create(FTFontFace fontFace, Matrix fontMatrix, Matrix ctm, FontOptions options) { FTScaledFont font; try { - try (Arena arena = Arena.openConfined()) { - MemorySegment result = (MemorySegment) cairo_scaled_font_create.invoke( - fontFace == null ? MemorySegment.NULL : fontFace.handle(), - fontMatrix == null ? MemorySegment.NULL : fontMatrix.handle(), - ctm == null ? MemorySegment.NULL : ctm.handle(), - options == null ? MemorySegment.NULL : options.handle()); - font = new FTScaledFont(result); - MemoryCleaner.takeOwnership(font.handle()); - font.fontFace = fontFace; - font.fontMatrix = fontMatrix; - font.ctm = ctm; - } + MemorySegment result = (MemorySegment) cairo_scaled_font_create.invoke( + fontFace == null ? MemorySegment.NULL : fontFace.handle(), + fontMatrix == null ? MemorySegment.NULL : fontMatrix.handle(), + ctm == null ? MemorySegment.NULL : ctm.handle(), + options == null ? MemorySegment.NULL : options.handle()); + font = new FTScaledFont(result); + MemoryCleaner.takeOwnership(font.handle()); + font.fontFace = fontFace; + font.fontMatrix = fontMatrix; + font.ctm = ctm; } catch (Throwable e) { throw new RuntimeException(e); } diff --git a/src/main/java/org/freedesktop/cairo/FTSynthesize.java b/src/main/java/org/freedesktop/cairo/FTSynthesize.java index 0c806f6..7b31102 100644 --- a/src/main/java/org/freedesktop/cairo/FTSynthesize.java +++ b/src/main/java/org/freedesktop/cairo/FTSynthesize.java @@ -51,8 +51,8 @@ public record FTSynthesize(int value) { */ public FTSynthesize or(FTSynthesize... masks) { int value = this.value(); - for (FTSynthesize(int arg) : masks) { - value |= arg; + for (var arg : masks) { + value |= arg.value(); } return new FTSynthesize(value); } @@ -66,8 +66,8 @@ public FTSynthesize or(FTSynthesize... masks) { */ public static FTSynthesize combined(FTSynthesize mask, FTSynthesize... masks) { int value = mask.value(); - for (FTSynthesize(int arg) : masks) { - value |= arg; + for (var arg : masks) { + value |= arg.value(); } return new FTSynthesize(value); } diff --git a/src/main/java/org/freedesktop/cairo/FontExtents.java b/src/main/java/org/freedesktop/cairo/FontExtents.java index c33db34..1590c25 100644 --- a/src/main/java/org/freedesktop/cairo/FontExtents.java +++ b/src/main/java/org/freedesktop/cairo/FontExtents.java @@ -21,10 +21,9 @@ import io.github.jwharm.cairobindings.Proxy; +import java.lang.foreign.Arena; import java.lang.foreign.MemoryLayout; import java.lang.foreign.MemorySegment; -import java.lang.foreign.SegmentAllocator; -import java.lang.foreign.SegmentScope; import java.lang.foreign.ValueLayout; import java.lang.invoke.VarHandle; @@ -65,10 +64,13 @@ static MemoryLayout getMemoryLayout() { private static final VarHandle MAX_Y_ADVANCE = getMemoryLayout().varHandle(MemoryLayout.PathElement.groupElement("max_y_advance")); /** - * Allocate a new {@code cairo_font_extents_t} + * Allocate a new, uninitialized {@code cairo_font_extents_t} + * + * @param arena the arena in which the FontExtents will be allocated + * @return a newly allocated, uninitialized FontExtents */ - static FontExtents create() { - return new FontExtents(SegmentAllocator.nativeAllocator(SegmentScope.auto()).allocate(getMemoryLayout())); + public static FontExtents create(Arena arena) { + return new FontExtents(arena.allocate(getMemoryLayout())); } /** diff --git a/src/main/java/org/freedesktop/cairo/FontFace.java b/src/main/java/org/freedesktop/cairo/FontFace.java index c08a29e..89da0b0 100644 --- a/src/main/java/org/freedesktop/cairo/FontFace.java +++ b/src/main/java/org/freedesktop/cairo/FontFace.java @@ -58,9 +58,6 @@ public sealed class FontFace extends Proxy permits ToyFontFace, FTFontFace, User Cairo.ensureInitialized(); } - // Keeps user data keys and values - private final UserDataStore userDataStore; - /** * Constructor used internally to instantiate a java FontFace object for a * native {@code cairo_font_face_t} instance @@ -71,7 +68,6 @@ public sealed class FontFace extends Proxy permits ToyFontFace, FTFontFace, User public FontFace(MemorySegment address) { super(address); MemoryCleaner.setFreeFunc(handle(), "cairo_font_face_destroy"); - userDataStore = new UserDataStore(address.scope()); } /** @@ -139,44 +135,21 @@ void reference() { private static final MethodHandle cairo_font_face_reference = Interop.downcallHandle("cairo_font_face_reference", FunctionDescriptor.of(ValueLayout.ADDRESS, ValueLayout.ADDRESS)); - /** - * Attach user data to the font face. This method will generate and return a - * {@link UserDataKey}. To update the user data for the same key, call - * {@link #setUserData(UserDataKey, Object)}. To remove user data from a font - * face, call this function with {@code null} for {@code userData}. - * - * @param userData the user data to attach to the font face. {@code userData} - * can be any Java object, but if it is a primitive type, a - * {@link MemorySegment} or a {@link Proxy} instance, it will be - * stored as cairo user data in native memory as well. - * @return the key that the user data is attached to - * @since 1.4 - */ - public UserDataKey setUserData(Object userData) { - UserDataKey key = UserDataKey.create(this); - return setUserData(key, userData); - } - /** * Attach user data to the font face. To remove user data from a font face, call * this function with the key that was used to set it and {@code null} for * {@code userData}. - * - * @param key the key to attach the user data to - * @param userData the user data to attach to the font face. {@code userData} - * can be any Java object, but if it is a primitive type, a - * {@link MemorySegment} or a {@link Proxy} instance, it will be - * stored as cairo user data in native memory as well. + * + * @param key the key to attach the user data to + * @param userData the user data to attach to the font face * @return the key * @throws NullPointerException if {@code key} is {@code null} * @since 1.4 */ - public UserDataKey setUserData(UserDataKey key, Object userData) { + public UserDataKey setUserData(UserDataKey key, MemorySegment userData) { Status status; - userDataStore.set(key, userData); try { - int result = (int) cairo_font_face_set_user_data.invoke(handle(), key.handle(), - userDataStore.dataSegment(userData), MemorySegment.NULL); + int result = (int) cairo_font_face_set_user_data.invoke(handle(), key.handle(), userData, MemorySegment.NULL); status = Status.of(result); } catch (Throwable e) { throw new RuntimeException(e); @@ -195,15 +168,26 @@ public UserDataKey setUserData(UserDataKey key, Object userData) { * Return user data previously attached to the font face using the specified * key. If no user data has been attached with the given key this function * returns {@code null}. - * + * * @param key the UserDataKey the user data was attached to * @return the user data previously attached or {@code null} * @since 1.4 */ - public Object getUserData(UserDataKey key) { - return key == null ? null : userDataStore.get(key); + public MemorySegment getUserData(UserDataKey key) { + if (key == null) { + return null; + } + try { + MemorySegment result = (MemorySegment) cairo_font_face_get_user_data.invoke(handle(), key.handle()); + return MemorySegment.NULL.equals(result) ? null : result; + } catch (Throwable e) { + throw new RuntimeException(e); + } } + private static final MethodHandle cairo_font_face_get_user_data = Interop.downcallHandle("cairo_get_user_data", + FunctionDescriptor.of(ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS)); + /** * Get the CairoFontFace GType * @return the GType diff --git a/src/main/java/org/freedesktop/cairo/FontOptions.java b/src/main/java/org/freedesktop/cairo/FontOptions.java index b6a92a4..b3111dd 100644 --- a/src/main/java/org/freedesktop/cairo/FontOptions.java +++ b/src/main/java/org/freedesktop/cairo/FontOptions.java @@ -391,7 +391,7 @@ public String getVariations() { if (MemorySegment.NULL.equals(result)) { return null; } - return result.getUtf8String(0); + return result.reinterpret(Integer.MAX_VALUE).getUtf8String(0); } catch (Throwable e) { throw new RuntimeException(e); } @@ -399,7 +399,7 @@ public String getVariations() { private static final MethodHandle cairo_font_options_get_variations = Interop.downcallHandle( "cairo_font_options_get_variations", - FunctionDescriptor.of(ValueLayout.ADDRESS.asUnbounded(), ValueLayout.ADDRESS)); + FunctionDescriptor.of(ValueLayout.ADDRESS, ValueLayout.ADDRESS)); /** * Sets the OpenType font variations for the font options object. Font @@ -420,7 +420,7 @@ public String getVariations() { */ public void setVariations(String variations) { try { - try (Arena arena = Arena.openConfined()) { + try (Arena arena = Arena.ofConfined()) { MemorySegment utf8 = Interop.allocateNativeString(variations, arena); cairo_font_options_set_variations.invoke(handle(), utf8); } @@ -549,7 +549,7 @@ public RGBA getCustomPaletteColor(int index) throws IndexOutOfBoundsException { Status status; RGBA rgba; try { - try (Arena arena = Arena.openConfined()) { + try (Arena arena = Arena.ofConfined()) { MemorySegment x1Ptr = arena.allocate(ValueLayout.JAVA_DOUBLE); MemorySegment y1Ptr = arena.allocate(ValueLayout.JAVA_DOUBLE); MemorySegment x2Ptr = arena.allocate(ValueLayout.JAVA_DOUBLE); diff --git a/src/main/java/org/freedesktop/cairo/Glyph.java b/src/main/java/org/freedesktop/cairo/Glyph.java index 7da879a..5f2543d 100644 --- a/src/main/java/org/freedesktop/cairo/Glyph.java +++ b/src/main/java/org/freedesktop/cairo/Glyph.java @@ -100,6 +100,6 @@ public double y() { * instance */ public Glyph(MemorySegment address) { - super(Interop.reinterpret(address, getMemoryLayout())); + super(address.reinterpret(getMemoryLayout().byteSize())); } } diff --git a/src/main/java/org/freedesktop/cairo/Gradient.java b/src/main/java/org/freedesktop/cairo/Gradient.java index 71451b7..1dc2ace 100644 --- a/src/main/java/org/freedesktop/cairo/Gradient.java +++ b/src/main/java/org/freedesktop/cairo/Gradient.java @@ -122,7 +122,7 @@ public void addColorStopRGBA(double offset, double red, double green, double blu */ public int getColorStopCount() { try { - try (Arena arena = Arena.openConfined()) { + try (Arena arena = Arena.ofConfined()) { MemorySegment countPtr = arena.allocate(ValueLayout.JAVA_INT); cairo_pattern_get_color_stop_count.invoke(handle(), countPtr); return countPtr.get(ValueLayout.JAVA_INT, 0); @@ -153,7 +153,7 @@ public int getColorStopCount() { public double[] getColorStopRGBA(int index) throws IndexOutOfBoundsException { double[] values; try { - try (Arena arena = Arena.openConfined()) { + try (Arena arena = Arena.ofConfined()) { MemorySegment ptrs = arena.allocateArray(ValueLayout.JAVA_DOUBLE, 5); long size = ValueLayout.JAVA_DOUBLE.byteSize(); int result = (int) cairo_pattern_get_color_stop_rgba.invoke(handle(), index, ptrs, ptrs.asSlice(size), diff --git a/src/main/java/org/freedesktop/cairo/ImageSurface.java b/src/main/java/org/freedesktop/cairo/ImageSurface.java index e080efc..0d3b6e6 100644 --- a/src/main/java/org/freedesktop/cairo/ImageSurface.java +++ b/src/main/java/org/freedesktop/cairo/ImageSurface.java @@ -205,7 +205,7 @@ public static ImageSurface create(MemorySegment data, Format format, int width, public static ImageSurface createFromPNG(String filename) throws IOException { ImageSurface surface; try { - try (Arena arena = Arena.openConfined()) { + try (Arena arena = Arena.ofConfined()) { MemorySegment filenamePtr = Interop.allocateNativeString(filename, arena); MemorySegment result = (MemorySegment) cairo_image_surface_create_from_png.invoke(filenamePtr); surface = new ImageSurface(result); @@ -244,10 +244,10 @@ public static ImageSurface createFromPNG(InputStream stream) throws IOException } ImageSurface surface; try { - try (Arena arena = Arena.openConfined()) { + try (Arena arena = Arena.ofConfined()) { ReadFunc readFunc = stream::readNBytes; MemorySegment result = (MemorySegment) cairo_image_surface_create_from_png_stream - .invoke(readFunc.toCallback(arena.scope()), MemorySegment.NULL); + .invoke(readFunc.toCallback(arena), MemorySegment.NULL); surface = new ImageSurface(result); MemoryCleaner.takeOwnership(surface.handle()); } @@ -281,9 +281,9 @@ public static ImageSurface createFromPNG(InputStream stream) throws IOException * @since 1.0 */ public void writeToPNG(String filename) throws IOException { - Status status = null; + Status status; try { - try (Arena arena = Arena.openConfined()) { + try (Arena arena = Arena.ofConfined()) { MemorySegment filenamePtr = Interop.allocateNativeString(filename, arena); int result = (int) cairo_surface_write_to_png.invoke(handle(), filenamePtr); status = Status.of(result); @@ -319,10 +319,10 @@ public void writeToPNG(OutputStream stream) throws IOException { } Status status; try { - try (Arena arena = Arena.openConfined()) { + try (Arena arena = Arena.ofConfined()) { WriteFunc writeFunc = stream::write; int result = (int) cairo_surface_write_to_png_stream.invoke(handle(), - writeFunc.toCallback(arena.scope()), MemorySegment.NULL); + writeFunc.toCallback(arena), MemorySegment.NULL); status = Status.of(result); } } catch (Throwable e) { diff --git a/src/main/java/org/freedesktop/cairo/LinearGradient.java b/src/main/java/org/freedesktop/cairo/LinearGradient.java index 0c3afd7..aaec6c4 100644 --- a/src/main/java/org/freedesktop/cairo/LinearGradient.java +++ b/src/main/java/org/freedesktop/cairo/LinearGradient.java @@ -90,7 +90,7 @@ public static LinearGradient create(double x0, double y0, double x1, double y1) */ public Point[] getLinearPoints() { try { - try (Arena arena = Arena.openConfined()) { + try (Arena arena = Arena.ofConfined()) { MemorySegment ptrs = arena.allocateArray(ValueLayout.JAVA_DOUBLE, 4); long size = ValueLayout.JAVA_DOUBLE.byteSize(); cairo_pattern_get_linear_points.invoke(handle(), ptrs, ptrs.asSlice(size), ptrs.asSlice(2 * size), diff --git a/src/main/java/org/freedesktop/cairo/Matrix.java b/src/main/java/org/freedesktop/cairo/Matrix.java index 7a21e4d..b7529e6 100644 --- a/src/main/java/org/freedesktop/cairo/Matrix.java +++ b/src/main/java/org/freedesktop/cairo/Matrix.java @@ -26,10 +26,9 @@ import java.lang.foreign.FunctionDescriptor; import java.lang.foreign.MemoryLayout; import java.lang.foreign.MemorySegment; -import java.lang.foreign.SegmentAllocator; -import java.lang.foreign.SegmentScope; import java.lang.foreign.ValueLayout; import java.lang.invoke.MethodHandle; +import java.lang.invoke.VarHandle; /** * Generic matrix operations. @@ -46,7 +45,7 @@ * * The current transformation matrix of a {@link Context}, represented as a * Matrix, defines the transformation from user-space coordinates to - * device-space coordinates. See {@link Context#getMatrix()} and + * device-space coordinates. See {@link Context#getMatrix} and * {@link Context#setMatrix(Matrix)}. * * @since 1.0 @@ -68,41 +67,75 @@ static MemoryLayout getMemoryLayout() { .withName("cairo_matrix_t"); } + private static final VarHandle XX = getMemoryLayout().varHandle(MemoryLayout.PathElement.groupElement("xx")); + private static final VarHandle YX = getMemoryLayout().varHandle(MemoryLayout.PathElement.groupElement("yx")); + private static final VarHandle XY = getMemoryLayout().varHandle(MemoryLayout.PathElement.groupElement("xy")); + private static final VarHandle YY = getMemoryLayout().varHandle(MemoryLayout.PathElement.groupElement("yy")); + private static final VarHandle X0 = getMemoryLayout().varHandle(MemoryLayout.PathElement.groupElement("x0")); + private static final VarHandle Y0 = getMemoryLayout().varHandle(MemoryLayout.PathElement.groupElement("y0")); + /** - * Allocate a new {@code caio_matrix_t} + * Get the xx value of the Matrix + * + * @return the xx value */ - static Matrix create() { - return new Matrix(SegmentAllocator.nativeAllocator(SegmentScope.auto()).allocate(getMemoryLayout())); + public double xx() { + return (double) XX.get(handle()); } /** - * Sets the matrix to be the affine transformation given by {@code xx}, - * {@code yx}, {@code xy}, {@code yy}, {@code x0}, {@code y0}. The - * transformation is given by: - * - *

-     * xNew = xx * x + xy * y + x0;
-     * yNew = yx * x + yy * y + y0;
-     * 
- * - * @param xx xx component of the affine transformation - * @param yx yx component of the affine transformation - * @param xy xy component of the affine transformation - * @param yy yy component of the affine transformation - * @param x0 X translation component of the affine transformation - * @param y0 Y translation component of the affine transformation - * @return the resulting Matrix - * @see Matrix#init(double, double, double, double, double, double) - * @since 1.0 + * Get the yx value of the Matrix + * + * @return the yx value */ - public static Matrix create(double xx, double yx, double xy, double yy, double x0, double y0) { - try { - Matrix matrix = create(); - cairo_matrix_init.invoke(matrix.handle(), xx, yx, xy, yy, x0, y0); - return matrix; - } catch (Throwable e) { - throw new RuntimeException(e); - } + public double yx() { + return (double) YX.get(handle()); + } + + /** + * Get the xy value of the Matrix + * + * @return the xy value + */ + public double xy() { + return (double) XY.get(handle()); + } + + /** + * Get the yy value of the Matrix + * + * @return the yy value + */ + public double yy() { + return (double) YY.get(handle()); + } + + /** + * Get the x0 value of the Matrix + * + * @return the x0 value + */ + public double x0() { + return (double) X0.get(handle()); + } + + /** + * Get the y0 value of the Matrix + * + * @return the y0 value + */ + public double y0() { + return (double) Y0.get(handle()); + } + + /** + * Allocate a new, uninitialized {@code caio_matrix_t} + * + * @param arena the arena in which the Matrix will be allocated + * @return the newly allocated, uninitialized Matrix object + */ + public static Matrix create(Arena arena) { + return new Matrix(arena.allocate(getMemoryLayout())); } /** @@ -139,15 +172,14 @@ public Matrix init(double xx, double yx, double xy, double yy, double x0, double /** * Modifies the matrix to be an identity transformation. - * - * @return the resulting Matrix + * + * @return the matrix * @since 1.0 */ - public static Matrix createIdentity() { + public Matrix initIdentity() { try { - Matrix matrix = create(); - cairo_matrix_init_identity.invoke(matrix.handle()); - return matrix; + cairo_matrix_init_identity.invoke(handle()); + return this; } catch (Throwable e) { throw new RuntimeException(e); } @@ -162,14 +194,13 @@ public static Matrix createIdentity() { * * @param tx amount to translate in the X direction * @param ty amount to translate in the Y direction - * @return the resulting Matrix + * @return the matrix * @since 1.0 */ - public static Matrix createTranslate(double tx, double ty) { + public Matrix initTranslate(double tx, double ty) { try { - Matrix matrix = create(); - cairo_matrix_init_translate.invoke(matrix.handle(), tx, ty); - return matrix; + cairo_matrix_init_translate.invoke(handle(), tx, ty); + return this; } catch (Throwable e) { throw new RuntimeException(e); } @@ -179,52 +210,6 @@ public static Matrix createTranslate(double tx, double ty) { "cairo_matrix_init_translate", FunctionDescriptor.ofVoid(ValueLayout.ADDRESS, ValueLayout.JAVA_DOUBLE, ValueLayout.JAVA_DOUBLE)); - /** - * Initializes the matrix to a transformation that scales by {@code sx} and - * {@code sy} in the X and Y dimensions, respectively. - * - * @param sx scale factor in the X direction - * @param sy scale factor in the Y direction - * @return the resulting Matrix - * @since 1.0 - */ - public static Matrix createScale(double sx, double sy) { - try { - Matrix matrix = create(); - cairo_matrix_init_scale.invoke(matrix.handle(), sx, sy); - return matrix; - } catch (Throwable e) { - throw new RuntimeException(e); - } - } - - private static final MethodHandle cairo_matrix_init_scale = Interop.downcallHandle("cairo_matrix_init_scale", - FunctionDescriptor.ofVoid(ValueLayout.ADDRESS, ValueLayout.JAVA_DOUBLE, ValueLayout.JAVA_DOUBLE)); - - /** - * Initializes the matrix to a transformation that rotates by {@code radians}. - * - * @param radians angle of rotation, in radians. The direction of rotation is - * defined such that positive angles rotate in the direction from - * the positive X axis toward the positive Y axis. With the - * default axis orientation of cairo, positive angles rotate in a - * clockwise direction. - * @return the resulting Matrix - * @since 1.0 - */ - public static Matrix createRotate(double radians) { - try { - Matrix matrix = create(); - cairo_matrix_init_rotate.invoke(matrix.handle(), radians); - return matrix; - } catch (Throwable e) { - throw new RuntimeException(e); - } - } - - private static final MethodHandle cairo_matrix_init_rotate = Interop.downcallHandle("cairo_matrix_init_rotate", - FunctionDescriptor.ofVoid(ValueLayout.ADDRESS, ValueLayout.JAVA_DOUBLE)); - /** * Applies a translation by {@code tx, ty} to the transformation in the matrix. * The effect of the new transformation is to first translate the coordinates by @@ -333,22 +318,22 @@ public void invert() throws IllegalArgumentException { /** * Multiplies the affine transformations in this matrix and the provided matrix * together and stores the result in the resulting matrix. The effect of the - * resulting transformation is to first apply the transformation in this matrix - * to the coordinates and then apply the transformation in the provided matrix - * to the coordinates. + * resulting transformation is to first apply the transformation in {@code a} + * to the coordinates and then apply the transformation in {@code b} to the + * coordinates. *

- * It is allowable for the resulting matrix to be identical to either this - * matrix or the provided matrix. + * It is allowable for the resulting matrix to be identical to either {@code a} + * or {@code b}. * - * @param other a matrix - * @return the resulting matrix + * @param a a matrix + * @param b a matrix * @since 1.0 */ - public Matrix multiply(Matrix other) { + public void multiply(Matrix a, Matrix b) { try { - Matrix result = create(); - cairo_matrix_multiply.invoke(result.handle(), this.handle(), other == null ? MemorySegment.NULL : other.handle()); - return result; + cairo_matrix_multiply.invoke(this.handle(), + a == null ? MemorySegment.NULL : a.handle(), + b == null ? MemorySegment.NULL : b.handle()); } catch (Throwable e) { throw new RuntimeException(e); } @@ -377,11 +362,9 @@ public Point transformDistance(Point distanceVector) { return null; } try { - try (Arena arena = Arena.openConfined()) { - MemorySegment dxPtr = arena.allocate(ValueLayout.JAVA_DOUBLE); - MemorySegment dyPtr = arena.allocate(ValueLayout.JAVA_DOUBLE); - dxPtr.set(ValueLayout.JAVA_DOUBLE, 0, distanceVector.x()); - dyPtr.set(ValueLayout.JAVA_DOUBLE, 0, distanceVector.y()); + try (Arena arena = Arena.ofConfined()) { + MemorySegment dxPtr = arena.allocate(ValueLayout.JAVA_DOUBLE, distanceVector.x()); + MemorySegment dyPtr = arena.allocate(ValueLayout.JAVA_DOUBLE, distanceVector.y()); cairo_matrix_transform_distance.invoke(handle(), dxPtr, dyPtr); return new Point(dxPtr.get(ValueLayout.JAVA_DOUBLE, 0), dyPtr.get(ValueLayout.JAVA_DOUBLE, 0)); } @@ -406,11 +389,9 @@ public Point transformPoint(Point point) { return null; } try { - try (Arena arena = Arena.openConfined()) { - MemorySegment dxPtr = arena.allocate(ValueLayout.JAVA_DOUBLE); - MemorySegment dyPtr = arena.allocate(ValueLayout.JAVA_DOUBLE); - dxPtr.set(ValueLayout.JAVA_DOUBLE, 0, point.x()); - dyPtr.set(ValueLayout.JAVA_DOUBLE, 0, point.y()); + try (Arena arena = Arena.ofConfined()) { + MemorySegment dxPtr = arena.allocate(ValueLayout.JAVA_DOUBLE, point.x()); + MemorySegment dyPtr = arena.allocate(ValueLayout.JAVA_DOUBLE, point.y()); cairo_matrix_transform_point.invoke(handle(), dxPtr, dyPtr); return new Point(dxPtr.get(ValueLayout.JAVA_DOUBLE, 0), dyPtr.get(ValueLayout.JAVA_DOUBLE, 0)); } @@ -433,4 +414,15 @@ public Point transformPoint(Point point) { public Matrix(MemorySegment address) { super(address); } + + /** + * String representation of this Matrix + * + * @return a String representation of this Matrix + */ + @Override + public String toString() { + return String.format("Matrix address=%d xx=%f yx=%f xy=%f yy=%f x0=%f y0=%f", + handle().address(), xx(), yx(), xy(), yy(), x0(), y0()); + } } diff --git a/src/main/java/org/freedesktop/cairo/Mesh.java b/src/main/java/org/freedesktop/cairo/Mesh.java index a4fdd34..2d4049e 100644 --- a/src/main/java/org/freedesktop/cairo/Mesh.java +++ b/src/main/java/org/freedesktop/cairo/Mesh.java @@ -461,7 +461,7 @@ public Mesh setCornerColorRGBA(int cornerNum, double red, double green, double b */ public int getPatchCount() { try { - try (Arena arena = Arena.openConfined()) { + try (Arena arena = Arena.ofConfined()) { MemorySegment count = arena.allocate(ValueLayout.JAVA_INT); cairo_mesh_pattern_get_patch_count.invoke(handle(), count); return count.get(ValueLayout.JAVA_INT, 0); @@ -505,7 +505,7 @@ public Path getPath(int patchNum) throws IndexOutOfBoundsException { private static final MethodHandle cairo_mesh_pattern_get_path = Interop.downcallHandle( "cairo_mesh_pattern_get_path", - FunctionDescriptor.of(ValueLayout.ADDRESS.asUnbounded(), ValueLayout.ADDRESS, ValueLayout.JAVA_INT)); + FunctionDescriptor.of(ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.JAVA_INT)); /** * Gets the control point {@code pointNum} of patch {@code patchNum} for a mesh @@ -527,7 +527,7 @@ public Path getPath(int patchNum) throws IndexOutOfBoundsException { */ public Point getControlPoint(int patchNum, int pointNum) throws IndexOutOfBoundsException { try { - try (Arena arena = Arena.openConfined()) { + try (Arena arena = Arena.ofConfined()) { MemorySegment xPtr = arena.allocate(ValueLayout.JAVA_DOUBLE); MemorySegment yPtr = arena.allocate(ValueLayout.JAVA_DOUBLE); int result = (int) cairo_mesh_pattern_get_control_point.invoke(handle(), patchNum, pointNum, xPtr, @@ -566,7 +566,7 @@ public Point getControlPoint(int patchNum, int pointNum) throws IndexOutOfBounds */ public double[] getCornerColorRGBA(int patchNum, int cornerNum) throws IndexOutOfBoundsException { try { - try (Arena arena = Arena.openConfined()) { + try (Arena arena = Arena.ofConfined()) { MemorySegment redPtr = arena.allocate(ValueLayout.JAVA_DOUBLE); MemorySegment greenPtr = arena.allocate(ValueLayout.JAVA_DOUBLE); MemorySegment bluePtr = arena.allocate(ValueLayout.JAVA_DOUBLE); diff --git a/src/main/java/org/freedesktop/cairo/PDFSurface.java b/src/main/java/org/freedesktop/cairo/PDFSurface.java index 4b9ad6d..b2dfbe1 100644 --- a/src/main/java/org/freedesktop/cairo/PDFSurface.java +++ b/src/main/java/org/freedesktop/cairo/PDFSurface.java @@ -19,6 +19,7 @@ package org.freedesktop.cairo; +import io.github.jwharm.cairobindings.ArenaCloseAction; import io.github.jwharm.cairobindings.Interop; import io.github.jwharm.cairobindings.MemoryCleaner; @@ -26,9 +27,9 @@ import java.lang.foreign.Arena; import java.lang.foreign.FunctionDescriptor; import java.lang.foreign.MemorySegment; -import java.lang.foreign.SegmentScope; import java.lang.foreign.ValueLayout; import java.lang.invoke.MethodHandle; +import java.lang.ref.Cleaner; /** * The PDF surface is used to render cairo graphics to Adobe PDF files and is a @@ -109,14 +110,6 @@ public final class PDFSurface extends Surface { Cairo.ensureInitialized(); } - /* - * Initialized by {@link #create(OutputStream, int, int)} to keep a reference to - * the memory segment for the upcall stub alive during the lifetime of the - * PDFSurface instance. - */ - @SuppressWarnings("unused") - private MemorySegment callbackAllocation; - /** * The root outline item in cairo_pdf_surface_add_outline(). * @@ -153,7 +146,7 @@ public PDFSurface(MemorySegment address) { public static PDFSurface create(String filename, int widthInPoints, int heightInPoints) { PDFSurface surface; try { - try (Arena arena = Arena.openConfined()) { + try (Arena arena = Arena.ofConfined()) { MemorySegment filenamePtr = Interop.allocateNativeString(filename, arena); MemorySegment result = (MemorySegment) cairo_pdf_surface_create.invoke(filenamePtr, widthInPoints, heightInPoints); @@ -189,11 +182,12 @@ public static PDFSurface create(String filename, int widthInPoints, int heightIn */ public static PDFSurface create(OutputStream stream, int widthInPoints, int heightInPoints) { PDFSurface surface; + Arena arena = Arena.ofConfined(); try { MemorySegment writeFuncPtr; if (stream != null) { WriteFunc writeFunc = stream::write; - writeFuncPtr = writeFunc.toCallback(SegmentScope.auto()); + writeFuncPtr = writeFunc.toCallback(arena); } else { writeFuncPtr = MemorySegment.NULL; } @@ -202,7 +196,7 @@ public static PDFSurface create(OutputStream stream, int widthInPoints, int heig surface = new PDFSurface(result); MemoryCleaner.takeOwnership(surface.handle()); if (stream != null) { - surface.callbackAllocation = writeFuncPtr; // Keep the memory segment of the upcall stub alive + ArenaCloseAction.CLEANER.register(surface, new ArenaCloseAction(arena)); } } catch (Throwable e) { throw new RuntimeException(e); @@ -289,7 +283,7 @@ public PDFSurface setSize(double widthInPoints, double heightInPoints) { */ public int addOutline(int parentId, String string, String linkAttribs, PDFOutlineFlags flags) { try { - try (Arena arena = Arena.openConfined()) { + try (Arena arena = Arena.ofConfined()) { MemorySegment utf8 = Interop.allocateNativeString(string, arena); MemorySegment linkAttribsPtr = linkAttribs == null ? MemorySegment.NULL : arena.allocateUtf8String(linkAttribs); @@ -326,7 +320,7 @@ public int addOutline(int parentId, String string, String linkAttribs, PDFOutlin */ public PDFSurface setMetadata(PDFMetadata metadata, String string) { try { - try (Arena arena = Arena.openConfined()) { + try (Arena arena = Arena.ofConfined()) { MemorySegment utf8 = Interop.allocateNativeString(string, arena); cairo_pdf_surface_set_metadata.invoke(handle(), metadata.getValue(), utf8); return this; @@ -357,7 +351,7 @@ public PDFSurface setMetadata(PDFMetadata metadata, String string) { */ public PDFSurface setCustomMetadata(String name, String value) { try { - try (Arena arena = Arena.openConfined()) { + try (Arena arena = Arena.ofConfined()) { MemorySegment namePtr = Interop.allocateNativeString(name, arena); MemorySegment valuePtr = Interop.allocateNativeString(value, arena); cairo_pdf_surface_set_custom_metadata.invoke(handle(), namePtr, valuePtr); @@ -381,7 +375,7 @@ public PDFSurface setCustomMetadata(String name, String value) { */ public PDFSurface setPageLabel(String string) { try { - try (Arena arena = Arena.openConfined()) { + try (Arena arena = Arena.ofConfined()) { MemorySegment utf8 = Interop.allocateNativeString(string, arena); cairo_pdf_surface_set_page_label.invoke(handle(), utf8); return this; diff --git a/src/main/java/org/freedesktop/cairo/PDFVersion.java b/src/main/java/org/freedesktop/cairo/PDFVersion.java index a79a6a5..15659ed 100644 --- a/src/main/java/org/freedesktop/cairo/PDFVersion.java +++ b/src/main/java/org/freedesktop/cairo/PDFVersion.java @@ -95,13 +95,12 @@ public static PDFVersion of(int ordinal) { */ public static PDFVersion[] getVersions() { try { - try (Arena arena = Arena.openConfined()) { + try (Arena arena = Arena.ofConfined()) { MemorySegment versionsPtr = arena.allocate(ValueLayout.ADDRESS); MemorySegment numVersionsPtr = arena.allocate(ValueLayout.JAVA_INT); cairo_pdf_get_versions.invoke(versionsPtr, numVersionsPtr); int numVersions = numVersionsPtr.get(ValueLayout.JAVA_INT, 0); - int[] versionInts = MemorySegment.ofAddress(versionsPtr.address(), numVersions, arena.scope()) - .toArray(ValueLayout.JAVA_INT); + int[] versionInts = versionsPtr.reinterpret(ValueLayout.JAVA_INT.byteSize() * numVersions).toArray(ValueLayout.JAVA_INT); PDFVersion[] versions = new PDFVersion[numVersions]; for (int i = 0; i < versionInts.length; i++) { versions[i] = PDFVersion.of(versionInts[i]); @@ -131,7 +130,7 @@ public String toString() { if (MemorySegment.NULL.equals(result)) { return null; } - return result.getUtf8String(0); + return result.reinterpret(Integer.MAX_VALUE).getUtf8String(0); } catch (Throwable e) { throw new RuntimeException(e); } @@ -139,5 +138,5 @@ public String toString() { private static final MethodHandle cairo_pdf_version_to_string = Interop.downcallHandle( "cairo_pdf_version_to_string", - FunctionDescriptor.of(ValueLayout.ADDRESS.asUnbounded(), ValueLayout.JAVA_INT)); + FunctionDescriptor.of(ValueLayout.ADDRESS, ValueLayout.JAVA_INT)); } diff --git a/src/main/java/org/freedesktop/cairo/PSLevel.java b/src/main/java/org/freedesktop/cairo/PSLevel.java index c582a2f..94cbbaa 100644 --- a/src/main/java/org/freedesktop/cairo/PSLevel.java +++ b/src/main/java/org/freedesktop/cairo/PSLevel.java @@ -81,13 +81,12 @@ public static PSLevel of(int ordinal) { */ public static PSLevel[] getLevels() { try { - try (Arena arena = Arena.openConfined()) { + try (Arena arena = Arena.ofConfined()) { MemorySegment levelsPtr = arena.allocate(ValueLayout.ADDRESS); MemorySegment numLevelsPtr = arena.allocate(ValueLayout.JAVA_INT); cairo_ps_get_levels.invoke(levelsPtr, numLevelsPtr); int numLevels = numLevelsPtr.get(ValueLayout.JAVA_INT, 0); - int[] levelInts = MemorySegment.ofAddress(levelsPtr.address(), numLevels, arena.scope()) - .toArray(ValueLayout.JAVA_INT); + int[] levelInts = levelsPtr.reinterpret(ValueLayout.JAVA_INT.byteSize() * numLevels).toArray(ValueLayout.JAVA_INT); PSLevel[] levels = new PSLevel[numLevels]; for (int i = 0; i < levelInts.length; i++) { levels[i] = PSLevel.of(levelInts[i]); @@ -117,12 +116,12 @@ public String toString() { if (MemorySegment.NULL.equals(result)) { return null; } - return result.getUtf8String(0); + return result.reinterpret(Integer.MAX_VALUE).getUtf8String(0); } catch (Throwable e) { throw new RuntimeException(e); } } private static final MethodHandle cairo_ps_level_to_string = Interop.downcallHandle("cairo_ps_level_to_string", - FunctionDescriptor.of(ValueLayout.ADDRESS.asUnbounded(), ValueLayout.JAVA_INT)); + FunctionDescriptor.of(ValueLayout.ADDRESS, ValueLayout.JAVA_INT)); } diff --git a/src/main/java/org/freedesktop/cairo/PSSurface.java b/src/main/java/org/freedesktop/cairo/PSSurface.java index 74f0238..947ac36 100644 --- a/src/main/java/org/freedesktop/cairo/PSSurface.java +++ b/src/main/java/org/freedesktop/cairo/PSSurface.java @@ -19,6 +19,7 @@ package org.freedesktop.cairo; +import io.github.jwharm.cairobindings.ArenaCloseAction; import io.github.jwharm.cairobindings.Interop; import io.github.jwharm.cairobindings.MemoryCleaner; @@ -26,9 +27,9 @@ import java.lang.foreign.Arena; import java.lang.foreign.FunctionDescriptor; import java.lang.foreign.MemorySegment; -import java.lang.foreign.SegmentScope; import java.lang.foreign.ValueLayout; import java.lang.invoke.MethodHandle; +import java.lang.ref.Cleaner; /** * The PostScript surface is used to render cairo graphics to Adobe PostScript @@ -71,14 +72,6 @@ public final class PSSurface extends Surface { Cairo.ensureInitialized(); } - /* - * Initialized by {@link #create(OutputStream, int, int)} to keep a reference to - * the memory segment for the upcall stub alive during the lifetime of the - * PSSurface instance. - */ - @SuppressWarnings("unused") - private MemorySegment callbackAllocation; - /** * Constructor used internally to instantiate a java PSSurface object for a * native {@code cairo_surface_t} instance @@ -113,7 +106,7 @@ public PSSurface(MemorySegment address) { public static PSSurface create(String filename, int widthInPoints, int heightInPoints) { PSSurface surface; try { - try (Arena arena = Arena.openConfined()) { + try (Arena arena = Arena.ofConfined()) { MemorySegment filenamePtr = Interop.allocateNativeString(filename, arena); MemorySegment result = (MemorySegment) cairo_ps_surface_create.invoke(filenamePtr, widthInPoints, heightInPoints); @@ -154,11 +147,12 @@ public static PSSurface create(String filename, int widthInPoints, int heightInP */ public static PSSurface create(OutputStream stream, int widthInPoints, int heightInPoints) { PSSurface surface; + Arena arena = Arena.ofConfined(); try { MemorySegment writeFuncPtr; if (stream != null) { WriteFunc writeFunc = stream::write; - writeFuncPtr = writeFunc.toCallback(SegmentScope.auto()); + writeFuncPtr = writeFunc.toCallback(arena); } else { writeFuncPtr = MemorySegment.NULL; } @@ -167,7 +161,7 @@ public static PSSurface create(OutputStream stream, int widthInPoints, int heigh surface = new PSSurface(result); MemoryCleaner.takeOwnership(surface.handle()); if (stream != null) { - surface.callbackAllocation = writeFuncPtr; // Keep the memory segment of the upcall stub alive + ArenaCloseAction.CLEANER.register(surface, new ArenaCloseAction(arena)); } } catch (Throwable e) { throw new RuntimeException(e); @@ -418,7 +412,7 @@ public PSSurface dscBeginPageSetup() { */ public PSSurface dscComment(String comment) throws IllegalArgumentException { try { - try (Arena arena = Arena.openConfined()) { + try (Arena arena = Arena.ofConfined()) { MemorySegment commentPtr = Interop.allocateNativeString(comment, arena); cairo_ps_surface_dsc_comment.invoke(handle(), commentPtr); } diff --git a/src/main/java/org/freedesktop/cairo/Path.java b/src/main/java/org/freedesktop/cairo/Path.java index c3879ed..7d7ba0a 100644 --- a/src/main/java/org/freedesktop/cairo/Path.java +++ b/src/main/java/org/freedesktop/cairo/Path.java @@ -59,10 +59,10 @@ public class Path extends Proxy implements Iterable { static MemoryLayout getMemoryLayout() { return MemoryLayout.structLayout( ValueLayout.JAVA_INT.withName("status"), - MemoryLayout.paddingLayout(32), - ValueLayout.ADDRESS.asUnbounded().withName("data"), + MemoryLayout.paddingLayout(4), + ValueLayout.ADDRESS.withName("data"), ValueLayout.JAVA_INT.withName("num_data"), - MemoryLayout.paddingLayout(32) + MemoryLayout.paddingLayout(4) ).withName("cairo_path_t"); } @@ -87,13 +87,13 @@ public Status status() { * @return a stream of data segments */ private Stream data() { - // Read the `data` field (a pointer with unbounded size) + // Read the `data` pointer MemorySegment dataSegment = (MemorySegment) DATA.get(handle()); // Construct a SequenceLayout, based on the size that is specified in the // `num_data` field MemoryLayout sequenceLayout = MemoryLayout.sequenceLayout(numData(), PathData.getMemoryLayout()); - // Create a slice that fits the exact size of the SequenceLayout - MemorySegment slice = dataSegment.asSlice(0, sequenceLayout.byteSize()); + // Reinterpret the data field to the exact size of the SequenceLayout + MemorySegment slice = dataSegment.reinterpret(sequenceLayout.byteSize()); // Split the slice into PathData blocks return slice.elements(PathData.getMemoryLayout()); } @@ -114,7 +114,7 @@ private int numData() { * @param address the memory address of the native {@code cairo_path_t} instance */ public Path(MemorySegment address) { - super(address); + super(address.reinterpret(getMemoryLayout().byteSize())); MemoryCleaner.setFreeFunc(handle(), "cairo_path_destroy"); } diff --git a/src/main/java/org/freedesktop/cairo/Pattern.java b/src/main/java/org/freedesktop/cairo/Pattern.java index 00db4f3..2e20122 100644 --- a/src/main/java/org/freedesktop/cairo/Pattern.java +++ b/src/main/java/org/freedesktop/cairo/Pattern.java @@ -52,9 +52,6 @@ public abstract class Pattern extends Proxy { Cairo.ensureInitialized(); } - // Keeps user data keys and values - private final UserDataStore userDataStore; - // Keep a reference to the matrix during the lifetime of the Pattern. Matrix matrix; @@ -68,7 +65,6 @@ public abstract class Pattern extends Proxy { public Pattern(MemorySegment address) { super(address); MemoryCleaner.setFreeFunc(handle(), "cairo_pattern_destroy"); - userDataStore = new UserDataStore(address.scope()); } /** @@ -267,44 +263,21 @@ public PatternType getPatternType() { private static final MethodHandle cairo_pattern_get_type = Interop.downcallHandle("cairo_pattern_get_type", FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.ADDRESS)); - /** - * Attach user data to the pattern. This method will generate and return a - * {@link UserDataKey}. To update the user data for the same key, call - * {@link #setUserData(UserDataKey, Object)}. To remove user data from a - * pattern, call this function with {@code null} for {@code userData}. - * - * @param userData the user data to attach to the pattern. {@code userData} can - * be any Java object, but if it is a primitive type, a - * {@link MemorySegment} or a {@link Proxy} instance, it will be - * stored as cairo user data in native memory as well. - * @return the key that the user data is attached to - * @since 1.4 - */ - public UserDataKey setUserData(Object userData) { - UserDataKey key = UserDataKey.create(this); - return setUserData(key, userData); - } - /** * Attach user data to the pattern. To remove user data from a pattern, call * this function with the key that was used to set it and {@code null} for * {@code userData}. - * - * @param key the key to attach the user data to - * @param userData the user data to attach to the pattern. {@code userData} can - * be any Java object, but if it is a primitive type, a - * {@link MemorySegment} or a {@link Proxy} instance, it will be - * stored as cairo user data in native memory as well. + * + * @param key the key to attach the user data to + * @param userData the user data to attach to the pattern * @return the key * @throws NullPointerException if {@code key} is {@code null} * @since 1.4 */ - public UserDataKey setUserData(UserDataKey key, Object userData) { + public UserDataKey setUserData(UserDataKey key, MemorySegment userData) { Status status; - userDataStore.set(key, userData); try { - int result = (int) cairo_pattern_set_user_data.invoke(handle(), key.handle(), - userDataStore.dataSegment(userData), MemorySegment.NULL); + int result = (int) cairo_pattern_set_user_data.invoke(handle(), key.handle(), userData, MemorySegment.NULL); status = Status.of(result); } catch (Throwable e) { throw new RuntimeException(e); @@ -315,23 +288,34 @@ public UserDataKey setUserData(UserDataKey key, Object userData) { return key; } - private static final MethodHandle cairo_pattern_set_user_data = Interop - .downcallHandle("cairo_pattern_set_user_data", FunctionDescriptor.of(ValueLayout.JAVA_INT, - ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS)); + private static final MethodHandle cairo_pattern_set_user_data = Interop.downcallHandle("cairo_pattern_set_user_data", + FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS, + ValueLayout.ADDRESS)); /** * Return user data previously attached to the pattern using the specified key. * If no user data has been attached with the given key this function returns * {@code null}. - * - * @param key the UserDataKey the user data was attached to + * + * @param key the UserDataKey the user data was attached to * @return the user data previously attached or {@code null} * @since 1.4 */ - public Object getUserData(UserDataKey key) { - return key == null ? null : userDataStore.get(key); + public MemorySegment getUserData(UserDataKey key) { + if (key == null) { + return null; + } + try { + MemorySegment result = (MemorySegment) cairo_pattern_get_user_data.invoke(handle(), key.handle()); + return MemorySegment.NULL.equals(result) ? null : result; + } catch (Throwable e) { + throw new RuntimeException(e); + } } + private static final MethodHandle cairo_pattern_get_user_data = Interop.downcallHandle("cairo_pattern_get_user_data", + FunctionDescriptor.of(ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS)); + /** * Set the dithering mode of the rasterizer used for drawing shapes. This value * is a hint, and a particular backend may or may not support a particular diff --git a/src/main/java/org/freedesktop/cairo/Point.java b/src/main/java/org/freedesktop/cairo/Point.java index e5c8137..03ad7fe 100644 --- a/src/main/java/org/freedesktop/cairo/Point.java +++ b/src/main/java/org/freedesktop/cairo/Point.java @@ -24,6 +24,7 @@ * * @param x the X coordinate of the point * @param y the Y coordinate of the point + * @since 1.16 */ public record Point(double x, double y) { } diff --git a/src/main/java/org/freedesktop/cairo/RadialGradient.java b/src/main/java/org/freedesktop/cairo/RadialGradient.java index 443ecde..913bbaa 100644 --- a/src/main/java/org/freedesktop/cairo/RadialGradient.java +++ b/src/main/java/org/freedesktop/cairo/RadialGradient.java @@ -95,7 +95,7 @@ public static RadialGradient create(double cx0, double cy0, double radius0, doub */ public Circle[] getRadialCircles() { try { - try (Arena arena = Arena.openConfined()) { + try (Arena arena = Arena.ofConfined()) { MemorySegment ptrs = arena.allocateArray(ValueLayout.JAVA_DOUBLE, 6); long size = ValueLayout.JAVA_DOUBLE.byteSize(); cairo_pattern_get_radial_circles.invoke(handle(), ptrs, ptrs.asSlice(size), ptrs.asSlice(2 * size), diff --git a/src/main/java/org/freedesktop/cairo/RasterSource.java b/src/main/java/org/freedesktop/cairo/RasterSource.java index 66d6a7c..ba6bca8 100644 --- a/src/main/java/org/freedesktop/cairo/RasterSource.java +++ b/src/main/java/org/freedesktop/cairo/RasterSource.java @@ -22,10 +22,12 @@ import io.github.jwharm.cairobindings.Interop; import io.github.jwharm.cairobindings.MemoryCleaner; +import java.lang.foreign.Arena; import java.lang.foreign.FunctionDescriptor; import java.lang.foreign.MemorySegment; import java.lang.foreign.ValueLayout; import java.lang.invoke.MethodHandle; +import java.lang.ref.Cleaner; /** * A user pattern providing raster data. @@ -50,6 +52,12 @@ public class RasterSource extends Pattern { Cairo.ensureInitialized(); } + // Cleaner used to close the arena + private static final Cleaner CLEANER = Cleaner.create(); + + // Arena used to allocate the upcall stubs for the callback functions + private final Arena arena = Arena.ofShared(); + // Callback functions private RasterSourceAcquireFunc acquire = null; private RasterSourceReleaseFunc release = null; @@ -107,8 +115,8 @@ public static RasterSource create(Content content, int width, int height) { public void setAcquire(RasterSourceAcquireFunc acquire, RasterSourceReleaseFunc release) { try { cairo_raster_source_pattern_set_acquire.invoke(handle(), - acquire == null ? MemorySegment.NULL : acquire.toCallback(handle().scope()), - release == null ? MemorySegment.NULL : release.toCallback(handle().scope())); + acquire == null ? MemorySegment.NULL : acquire.toCallback(arena), + release == null ? MemorySegment.NULL : release.toCallback(arena)); this.acquire = acquire; this.release = release; } catch (Throwable e) { @@ -151,7 +159,7 @@ public RasterSourceReleaseFunc getRelease() { public void setSnapshot(RasterSourceSnapshotFunc snapshot) { try { cairo_raster_source_pattern_set_snapshot.invoke(handle(), - snapshot == null ? MemorySegment.NULL : snapshot.toCallback(handle().scope())); + snapshot == null ? MemorySegment.NULL : snapshot.toCallback(arena)); this.snapshot = snapshot; } catch (Throwable e) { throw new RuntimeException(e); @@ -182,7 +190,7 @@ public RasterSourceSnapshotFunc getSnapshot() { public void setCopy(RasterSourceCopyFunc copy) { try { cairo_raster_source_pattern_set_copy.invoke(handle(), - copy == null ? MemorySegment.NULL : copy.toCallback(handle().scope())); + copy == null ? MemorySegment.NULL : copy.toCallback(arena)); this.copy = copy; } catch (Throwable e) { throw new RuntimeException(e); @@ -212,7 +220,7 @@ public RasterSourceCopyFunc getCopy() { public void setFinish(RasterSourceFinishFunc finish) { try { cairo_raster_source_pattern_set_finish.invoke(handle(), - finish == null ? MemorySegment.NULL : finish.toCallback(handle().scope())); + finish == null ? MemorySegment.NULL : finish.toCallback(arena)); this.finish = finish; } catch (Throwable e) { throw new RuntimeException(e); @@ -234,14 +242,25 @@ public RasterSourceFinishFunc getFinish() { } /** - * Constructor used internally to instantiate a java RasterSourcePattern object - * for a native {@code cairo_pattern_t} instance - * + * Constructor used internally to instantiate a java RasterSource object for a + * native {@code cairo_pattern_t} instance + * * @param address the memory address of the native {@code cairo_pattern_t} * instance */ public RasterSource(MemorySegment address) { super(address); + + // Setup a Cleaner to close the Arena and release the allocated memory for + // the callback functions + CleanupAction cleanup = new CleanupAction(arena); + CLEANER.register(this, cleanup); } + // Static class to separate the cleanup logic from the object being cleaned + private record CleanupAction(Arena arena) implements Runnable { + @Override public void run() { + arena.close(); + } + } } diff --git a/src/main/java/org/freedesktop/cairo/RasterSourceAcquireFunc.java b/src/main/java/org/freedesktop/cairo/RasterSourceAcquireFunc.java index 972209b..f7e98fd 100644 --- a/src/main/java/org/freedesktop/cairo/RasterSourceAcquireFunc.java +++ b/src/main/java/org/freedesktop/cairo/RasterSourceAcquireFunc.java @@ -19,11 +19,7 @@ package org.freedesktop.cairo; -import java.lang.foreign.FunctionDescriptor; -import java.lang.foreign.Linker; -import java.lang.foreign.MemorySegment; -import java.lang.foreign.SegmentScope; -import java.lang.foreign.ValueLayout; +import java.lang.foreign.*; import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; @@ -86,19 +82,19 @@ default MemorySegment upcall(MemorySegment pattern, MemorySegment callbackData, /** * Generates an upcall stub, a C function pointer that will call - * {@link #upcall(MemorySegment, MemorySegment, MemorySegment, MemorySegment)}. - * - * @param scope the scope in which the upcall stub will be allocated + * {@link #upcall}. + * + * @param arena the arena in which the upcall stub will be allocated * @return the function pointer of the upcall stub * @since 1.12 */ - default MemorySegment toCallback(SegmentScope scope) { + default MemorySegment toCallback(Arena arena) { try { FunctionDescriptor fdesc = FunctionDescriptor.of(ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS); MethodHandle handle = MethodHandles.lookup().findVirtual(RasterSourceAcquireFunc.class, "upcall", fdesc.toMethodType()); - return Linker.nativeLinker().upcallStub(handle.bindTo(this), fdesc, scope); + return Linker.nativeLinker().upcallStub(handle.bindTo(this), fdesc, arena); } catch (NoSuchMethodException | IllegalAccessException e) { throw new RuntimeException(e); } diff --git a/src/main/java/org/freedesktop/cairo/RasterSourceCopyFunc.java b/src/main/java/org/freedesktop/cairo/RasterSourceCopyFunc.java index 5ff773e..a8d66ec 100644 --- a/src/main/java/org/freedesktop/cairo/RasterSourceCopyFunc.java +++ b/src/main/java/org/freedesktop/cairo/RasterSourceCopyFunc.java @@ -19,11 +19,7 @@ package org.freedesktop.cairo; -import java.lang.foreign.FunctionDescriptor; -import java.lang.foreign.Linker; -import java.lang.foreign.MemorySegment; -import java.lang.foreign.SegmentScope; -import java.lang.foreign.ValueLayout; +import java.lang.foreign.*; import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; @@ -70,19 +66,19 @@ default int upcall(MemorySegment pattern, MemorySegment callbackData, MemorySegm /** * Generates an upcall stub, a C function pointer that will call - * {@link #upcall(MemorySegment, MemorySegment, MemorySegment)}. - * - * @param scope the scope in which the upcall stub will be allocated + * {@link #upcall}. + * + * @param arena the arena in which the upcall stub will be allocated * @return the function pointer of the upcall stub * @since 1.12 */ - default MemorySegment toCallback(SegmentScope scope) { + default MemorySegment toCallback(Arena arena) { try { FunctionDescriptor fdesc = FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS); MethodHandle handle = MethodHandles.lookup().findVirtual(RasterSourceCopyFunc.class, "upcall", fdesc.toMethodType()); - return Linker.nativeLinker().upcallStub(handle.bindTo(this), fdesc, scope); + return Linker.nativeLinker().upcallStub(handle.bindTo(this), fdesc, arena); } catch (NoSuchMethodException | IllegalAccessException e) { throw new RuntimeException(e); } diff --git a/src/main/java/org/freedesktop/cairo/RasterSourceFinishFunc.java b/src/main/java/org/freedesktop/cairo/RasterSourceFinishFunc.java index ac8bc15..54959f5 100644 --- a/src/main/java/org/freedesktop/cairo/RasterSourceFinishFunc.java +++ b/src/main/java/org/freedesktop/cairo/RasterSourceFinishFunc.java @@ -19,11 +19,7 @@ package org.freedesktop.cairo; -import java.lang.foreign.FunctionDescriptor; -import java.lang.foreign.Linker; -import java.lang.foreign.MemorySegment; -import java.lang.foreign.SegmentScope; -import java.lang.foreign.ValueLayout; +import java.lang.foreign.*; import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; @@ -58,18 +54,18 @@ default void upcall(MemorySegment pattern, MemorySegment callbackData) { /** * Generates an upcall stub, a C function pointer that will call - * {@link #upcall(MemorySegment, MemorySegment)}. - * - * @param scope the scope in which the upcall stub will be allocated + * {@link #upcall}. + * + * @param arena the arena in which the upcall stub will be allocated * @return the function pointer of the upcall stub * @since 1.12 */ - default MemorySegment toCallback(SegmentScope scope) { + default MemorySegment toCallback(Arena arena) { try { FunctionDescriptor fdesc = FunctionDescriptor.ofVoid(ValueLayout.ADDRESS, ValueLayout.ADDRESS); MethodHandle handle = MethodHandles.lookup().findVirtual(RasterSourceFinishFunc.class, "upcall", fdesc.toMethodType()); - return Linker.nativeLinker().upcallStub(handle.bindTo(this), fdesc, scope); + return Linker.nativeLinker().upcallStub(handle.bindTo(this), fdesc, arena); } catch (NoSuchMethodException | IllegalAccessException e) { throw new RuntimeException(e); } diff --git a/src/main/java/org/freedesktop/cairo/RasterSourceReleaseFunc.java b/src/main/java/org/freedesktop/cairo/RasterSourceReleaseFunc.java index 0c9dfdc..719f362 100644 --- a/src/main/java/org/freedesktop/cairo/RasterSourceReleaseFunc.java +++ b/src/main/java/org/freedesktop/cairo/RasterSourceReleaseFunc.java @@ -19,11 +19,7 @@ package org.freedesktop.cairo; -import java.lang.foreign.FunctionDescriptor; -import java.lang.foreign.Linker; -import java.lang.foreign.MemorySegment; -import java.lang.foreign.SegmentScope; -import java.lang.foreign.ValueLayout; +import java.lang.foreign.*; import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; @@ -64,19 +60,19 @@ default void upcall(MemorySegment pattern, MemorySegment callbackData, MemorySeg /** * Generates an upcall stub, a C function pointer that will call - * {@link #upcall(MemorySegment, MemorySegment, MemorySegment)}. - * - * @param scope the scope in which the upcall stub will be allocated + * {@link #upcall}. + * + * @param arena the arena in which the upcall stub will be allocated * @return the function pointer of the upcall stub * @since 1.12 */ - default MemorySegment toCallback(SegmentScope scope) { + default MemorySegment toCallback(Arena arena) { try { FunctionDescriptor fdesc = FunctionDescriptor.ofVoid(ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS); MethodHandle handle = MethodHandles.lookup().findVirtual(RasterSourceReleaseFunc.class, "upcall", fdesc.toMethodType()); - return Linker.nativeLinker().upcallStub(handle.bindTo(this), fdesc, scope); + return Linker.nativeLinker().upcallStub(handle.bindTo(this), fdesc, arena); } catch (NoSuchMethodException | IllegalAccessException e) { throw new RuntimeException(e); } diff --git a/src/main/java/org/freedesktop/cairo/RasterSourceSnapshotFunc.java b/src/main/java/org/freedesktop/cairo/RasterSourceSnapshotFunc.java index c7e2d14..7ea239d 100644 --- a/src/main/java/org/freedesktop/cairo/RasterSourceSnapshotFunc.java +++ b/src/main/java/org/freedesktop/cairo/RasterSourceSnapshotFunc.java @@ -19,11 +19,7 @@ package org.freedesktop.cairo; -import java.lang.foreign.FunctionDescriptor; -import java.lang.foreign.Linker; -import java.lang.foreign.MemorySegment; -import java.lang.foreign.SegmentScope; -import java.lang.foreign.ValueLayout; +import java.lang.foreign.*; import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; @@ -72,19 +68,19 @@ default int upcall(MemorySegment pattern, MemorySegment callbackData) { /** * Generates an upcall stub, a C function pointer that will call - * {@link #upcall(MemorySegment, MemorySegment)}. - * - * @param scope the scope in which the upcall stub will be allocated + * {@link #upcall}. + * + * @param arena the arena in which the upcall stub will be allocated * @return the function pointer of the upcall stub * @since 1.12 */ - default MemorySegment toCallback(SegmentScope scope) { + default MemorySegment toCallback(Arena arena) { try { FunctionDescriptor fdesc = FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.ADDRESS, ValueLayout.ADDRESS); MethodHandle handle = MethodHandles.lookup().findVirtual(RasterSourceSnapshotFunc.class, "upcall", fdesc.toMethodType()); - return Linker.nativeLinker().upcallStub(handle.bindTo(this), fdesc, scope); + return Linker.nativeLinker().upcallStub(handle.bindTo(this), fdesc, arena); } catch (NoSuchMethodException | IllegalAccessException e) { throw new RuntimeException(e); } diff --git a/src/main/java/org/freedesktop/cairo/ReadFunc.java b/src/main/java/org/freedesktop/cairo/ReadFunc.java index da09756..f3f9e84 100644 --- a/src/main/java/org/freedesktop/cairo/ReadFunc.java +++ b/src/main/java/org/freedesktop/cairo/ReadFunc.java @@ -24,7 +24,6 @@ import java.lang.foreign.FunctionDescriptor; import java.lang.foreign.Linker; import java.lang.foreign.MemorySegment; -import java.lang.foreign.SegmentScope; import java.lang.foreign.ValueLayout; import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; @@ -69,34 +68,32 @@ default int upcall(MemorySegment closure, MemorySegment data, int length) { if (length <= 0) { return Status.SUCCESS.getValue(); } - try (Arena arena = Arena.openConfined()) { - try { - byte[] bytes = read(length); - if (bytes == null || bytes.length == 0) { - return Status.READ_ERROR.getValue(); - } - MemorySegment.ofAddress(data.address(), length).asByteBuffer().put(bytes); - return Status.SUCCESS.getValue(); - } catch (IOException ioe) { + try { + byte[] bytes = read(length); + if (bytes == null || bytes.length == 0) { return Status.READ_ERROR.getValue(); } + data.reinterpret(length).asByteBuffer().put(bytes); + return Status.SUCCESS.getValue(); + } catch (IOException ioe) { + return Status.READ_ERROR.getValue(); } } /** * Generates an upcall stub, a C function pointer that will call - * {@link #upcall(MemorySegment, MemorySegment, int)}. - * - * @param scope the scope in which the upcall stub will be allocated + * {@link #upcall}. + * + * @param arena the arena in which the upcall stub will be allocated * @return the function pointer of the upcall stub * @since 1.0 */ - default MemorySegment toCallback(SegmentScope scope) { + default MemorySegment toCallback(Arena arena) { try { FunctionDescriptor fdesc = FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.JAVA_INT); MethodHandle handle = MethodHandles.lookup().findVirtual(ReadFunc.class, "upcall", fdesc.toMethodType()); - return Linker.nativeLinker().upcallStub(handle.bindTo(this), fdesc, scope); + return Linker.nativeLinker().upcallStub(handle.bindTo(this), fdesc, arena); } catch (NoSuchMethodException | IllegalAccessException e) { throw new RuntimeException(e); } diff --git a/src/main/java/org/freedesktop/cairo/RecordingSurface.java b/src/main/java/org/freedesktop/cairo/RecordingSurface.java index 1b6d0d2..e801d25 100644 --- a/src/main/java/org/freedesktop/cairo/RecordingSurface.java +++ b/src/main/java/org/freedesktop/cairo/RecordingSurface.java @@ -125,15 +125,15 @@ public static RecordingSurface create(Content content, Rectangle extents) { * and the height of the ink bounding box * @since 1.10 */ - public Rectangle inkExtents() { + public Rect inkExtents() { try { - try (Arena arena = Arena.openConfined()) { + try (Arena arena = Arena.ofConfined()) { MemorySegment ptrs = arena.allocateArray(ValueLayout.JAVA_DOUBLE, 4); long size = ValueLayout.JAVA_DOUBLE.byteSize(); cairo_recording_surface_ink_extents.invoke(handle(), ptrs, ptrs.asSlice(size), ptrs.asSlice(2 * size), ptrs.asSlice(3 * size)); double[] values = ptrs.toArray(ValueLayout.JAVA_DOUBLE); - return Rectangle.create(values[0], values[1], values[2], values[3]); + return new Rect(values[0], values[1], values[2], values[3]); } } catch (Throwable e) { throw new RuntimeException(e); @@ -147,23 +147,21 @@ public Rectangle inkExtents() { /** * Get the extents of the recording-surface. * - * @return a rectangle with the extents + * @param extents the Rectangle to be assigned the extents * @throws IllegalStateException if the surface is not bounded, or in an error * state * @since 1.12 */ - public Rectangle getExtents() throws IllegalStateException { + public void getExtents(Rectangle extents) throws IllegalStateException { int result; - Rectangle rectangle = Rectangle.create(0, 0, 0, 0); try { - result = (int) cairo_recording_surface_get_extents.invoke(handle(), rectangle.handle()); + result = (int) cairo_recording_surface_get_extents.invoke(handle(), extents.handle()); } catch (Throwable e) { throw new RuntimeException(e); } if (result == 0) { throw new IllegalStateException(); } - return rectangle; } private static final MethodHandle cairo_recording_surface_get_extents = Interop.downcallHandle( diff --git a/src/main/java/org/freedesktop/cairo/Rect.java b/src/main/java/org/freedesktop/cairo/Rect.java new file mode 100644 index 0000000..2956c23 --- /dev/null +++ b/src/main/java/org/freedesktop/cairo/Rect.java @@ -0,0 +1,38 @@ +/* cairo-java-bindings - Java language bindings for cairo + * Copyright (C) 2023 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 org.freedesktop.cairo; + +/** + * A rectangle. + *

+ * This is one of several helper classes in Java (see also {@link RGBA} and + * {@link Point}), that do not exist in the native cairo API. The difference between + * {@code Rect} and {@link Rectangle} is that the latter class is part of the native + * cairo API and stores its values in native memory, while {@code Rect} instances + * only exist in the JVM. + * + * @param x X coordinate of the left side of the rectangle + * @param y Y coordinate of the top side of the rectangle + * @param width width of the rectangle + * @param height height of the rectangle + * @since 1.18.1 + */ +public record Rect(double x, double y, double width, double height) { +} diff --git a/src/main/java/org/freedesktop/cairo/Rectangle.java b/src/main/java/org/freedesktop/cairo/Rectangle.java index 8c9fa5f..e27bba7 100644 --- a/src/main/java/org/freedesktop/cairo/Rectangle.java +++ b/src/main/java/org/freedesktop/cairo/Rectangle.java @@ -96,20 +96,21 @@ public double height() { * instance */ public Rectangle(MemorySegment address) { - super(Interop.reinterpret(address, getMemoryLayout())); + super(address.reinterpret(getMemoryLayout().byteSize())); } /** * A data structure for holding a rectangle. - * + * + * @param arena the arena in which memory for the Rectangle is allocated * @param x X coordinate of the left side of the rectangle * @param y Y coordinate of the top side of the rectangle * @param width width of the rectangle * @param height height of the rectangle * @return the newly created rectangle */ - public static Rectangle create(double x, double y, double width, double height) { - Rectangle rect = new Rectangle(SegmentAllocator.nativeAllocator(SegmentScope.auto()).allocate(getMemoryLayout())); + public static Rectangle create(Arena arena, double x, double y, double width, double height) { + Rectangle rect = new Rectangle(arena.allocate(getMemoryLayout())); X.set(rect.handle(), x); Y.set(rect.handle(), y); WIDTH.set(rect.handle(), width); @@ -124,7 +125,8 @@ public static Rectangle create(double x, double y, double width, double height) */ @Override public String toString() { - return String.format("Rectangle address=%d x=%f y=%f width=%f height=%f", handle().address(), x(), y(), width(), height()); + return String.format("Rectangle address=%d x=%f y=%f width=%f height=%f", + handle().address(), x(), y(), width(), height()); } /** diff --git a/src/main/java/org/freedesktop/cairo/RectangleInt.java b/src/main/java/org/freedesktop/cairo/RectangleInt.java index f494f0b..8311a42 100644 --- a/src/main/java/org/freedesktop/cairo/RectangleInt.java +++ b/src/main/java/org/freedesktop/cairo/RectangleInt.java @@ -101,15 +101,16 @@ public RectangleInt(MemorySegment address) { /** * Create a rectangle with integer coordinates. - * + * + * @param arena the arena in which memory for the RectangleInt is allocated * @param x X coordinate of the left side of the rectangle * @param y Y coordinate of the the top side of the rectangle * @param width width of the rectangle * @param height height of the rectangle * @return the newly created rectangle */ - public static RectangleInt create(int x, int y, int width, int height) { - RectangleInt rect = new RectangleInt(SegmentAllocator.nativeAllocator(SegmentScope.auto()).allocate(getMemoryLayout())); + public static RectangleInt create(Arena arena, int x, int y, int width, int height) { + RectangleInt rect = new RectangleInt(arena.allocate(getMemoryLayout())); X.set(rect.handle(), x); Y.set(rect.handle(), y); WIDTH.set(rect.handle(), width); @@ -124,7 +125,8 @@ public static RectangleInt create(int x, int y, int width, int height) { */ @Override public String toString() { - return String.format("RectangleInt address=%d x=%d y=%d width=%d height=%d", handle().address(), x(), y(), width(), height()); + return String.format("RectangleInt address=%d x=%d y=%d width=%d height=%d", + handle().address(), x(), y(), width(), height()); } /** diff --git a/src/main/java/org/freedesktop/cairo/RectangleList.java b/src/main/java/org/freedesktop/cairo/RectangleList.java index 91f93a5..6223d07 100644 --- a/src/main/java/org/freedesktop/cairo/RectangleList.java +++ b/src/main/java/org/freedesktop/cairo/RectangleList.java @@ -22,11 +22,7 @@ import io.github.jwharm.cairobindings.Proxy; import io.github.jwharm.cairobindings.MemoryCleaner; -import java.lang.foreign.MemoryLayout; -import java.lang.foreign.MemorySegment; -import java.lang.foreign.SegmentAllocator; -import java.lang.foreign.SegmentScope; -import java.lang.foreign.ValueLayout; +import java.lang.foreign.*; import java.lang.invoke.VarHandle; import java.util.List; @@ -49,10 +45,10 @@ public class RectangleList extends Proxy { static MemoryLayout getMemoryLayout() { return MemoryLayout.structLayout( ValueLayout.JAVA_INT.withName("status"), - MemoryLayout.paddingLayout(32), + MemoryLayout.paddingLayout(4), ValueLayout.ADDRESS.withName("rectangles"), ValueLayout.JAVA_INT.withName("num_rectangles"), - MemoryLayout.paddingLayout(32)) + MemoryLayout.paddingLayout(4)) .withName("cairo_rectangle_list_t"); } @@ -77,10 +73,9 @@ public Status status() { * @return the list of rectangles */ public List rectangles() { - MemorySegment address = (MemorySegment) RECTANGLES.get(handle()); int length = (int) NUM_RECTANGLES.get(handle()); long segmentSize = Rectangle.getMemoryLayout().byteSize() * length; - MemorySegment array = MemorySegment.ofAddress(address.address(), segmentSize, handle().scope()); + MemorySegment array = ((MemorySegment) RECTANGLES.get(handle())).reinterpret(segmentSize); return array.elements(Rectangle.getMemoryLayout()).map(Rectangle::new).toList(); } @@ -92,7 +87,7 @@ public List rectangles() { * {@code cairo_rectangle_list_t} instance */ public RectangleList(MemorySegment address) { - super(address); + super(address.reinterpret(getMemoryLayout().byteSize())); MemoryCleaner.setFreeFunc(handle(), "cairo_rectangle_list_destroy"); } @@ -107,24 +102,22 @@ public void destroy() { /** * A data structure for holding a dynamically allocated array of rectangles. - * - * @param status Error status of the rectangle list - * @param rectangles List containing the rectangles + * + * @param arena the arena in which memory for the Rectangle is allocated + * @param status error status of the rectangle list + * @param rectangles list containing the rectangles * @return the newly created RectangleList */ - public static RectangleList create(Status status, List rectangles) { - RectangleList rectangleList = new RectangleList(SegmentAllocator.nativeAllocator(SegmentScope.auto()).allocate(getMemoryLayout())); + public static RectangleList create(Arena arena, Status status, List rectangles) { + RectangleList rectangleList = new RectangleList(arena.allocate(getMemoryLayout())); STATUS.set(rectangleList.handle(), status.getValue()); NUM_RECTANGLES.set(rectangleList.handle(), rectangles == null ? 0 : rectangles.size()); if (rectangles == null || rectangles.isEmpty()) { return rectangleList; } - MemorySegment array = SegmentAllocator.nativeAllocator(rectangleList.handle().scope()) - .allocateArray(Rectangle.getMemoryLayout(), rectangles.size()); + MemorySegment array = arena.allocateArray(Rectangle.getMemoryLayout(), rectangles.size()); for (int i = 0; i < rectangles.size(); i++) { - MemorySegment rectangle = rectangles.get(i).handle(); - MemorySegment src = MemorySegment.ofAddress(rectangle.address(), Rectangle.getMemoryLayout().byteSize(), - rectangle.scope()); + MemorySegment src = rectangles.get(i).handle().reinterpret(Rectangle.getMemoryLayout().byteSize(), arena, null); MemorySegment dst = array.asSlice(i * Rectangle.getMemoryLayout().byteSize()); dst.copyFrom(src); } diff --git a/src/main/java/org/freedesktop/cairo/Region.java b/src/main/java/org/freedesktop/cairo/Region.java index f57ba65..27a8cd2 100644 --- a/src/main/java/org/freedesktop/cairo/Region.java +++ b/src/main/java/org/freedesktop/cairo/Region.java @@ -129,7 +129,7 @@ public static Region create(RectangleInt rectangle) { public static Region create(RectangleInt[] rects) { Region region; try { - try (Arena arena = Arena.openConfined()) { + try (Arena arena = Arena.ofConfined()) { MemorySegment rectsPtr = MemorySegment.NULL; if (rects != null) { rectsPtr = arena.allocateArray(ValueLayout.ADDRESS, rects.length); @@ -201,14 +201,12 @@ public Status status() { /** * Gets the bounding rectangle of region as a {@link RectangleInt} * - * @return rectangle into which the extents are stored + * @param extents rectangle into which to store the extents * @since 1.10 */ - public RectangleInt getExtents() { + public void getExtents(RectangleInt extents) { try { - RectangleInt extents = RectangleInt.create(0, 0, 0, 0); cairo_region_get_extents.invoke(handle(), extents.handle()); - return extents; } catch (Throwable e) { throw new RuntimeException(e); } @@ -237,15 +235,13 @@ public int numRectangles() { /** * Returns the {@code nth} rectangle from this region. * - * @param nth a number indicating which rectangle should be returned - * @return the rectangle + * @param nth a number indicating which rectangle should be returned + * @param rectangle a RectangleInt * @since 1.10 */ - public RectangleInt getRectangle(int nth) { + public void getRectangle(int nth, RectangleInt rectangle) { try { - RectangleInt rectangle = RectangleInt.create(0, 0, 0, 0); cairo_region_get_rectangle.invoke(handle(), nth, rectangle.handle()); - return rectangle; } catch (Throwable e) { throw new RuntimeException(e); } diff --git a/src/main/java/org/freedesktop/cairo/SVGSurface.java b/src/main/java/org/freedesktop/cairo/SVGSurface.java index 4989445..4eaa6b8 100644 --- a/src/main/java/org/freedesktop/cairo/SVGSurface.java +++ b/src/main/java/org/freedesktop/cairo/SVGSurface.java @@ -19,6 +19,7 @@ package org.freedesktop.cairo; +import io.github.jwharm.cairobindings.ArenaCloseAction; import io.github.jwharm.cairobindings.Interop; import io.github.jwharm.cairobindings.MemoryCleaner; @@ -26,7 +27,6 @@ import java.lang.foreign.Arena; import java.lang.foreign.FunctionDescriptor; import java.lang.foreign.MemorySegment; -import java.lang.foreign.SegmentScope; import java.lang.foreign.ValueLayout; import java.lang.invoke.MethodHandle; @@ -43,14 +43,6 @@ public final class SVGSurface extends Surface { Cairo.ensureInitialized(); } - /* - * Initialized by {@link #create(OutputStream, int, int)} to keep a reference to - * the memory segment for the upcall stub alive during the lifetime of the - * SVGSurface instance. - */ - @SuppressWarnings("unused") - private MemorySegment callbackAllocation; - /** * Constructor used internally to instantiate a java SVGSurface object for a * native {@code cairo_surface_t} instance @@ -99,7 +91,7 @@ public SVGSurface(MemorySegment address) { public static SVGSurface create(String filename, int widthInPoints, int heightInPoints) { SVGSurface surface; try { - try (Arena arena = Arena.openConfined()) { + try (Arena arena = Arena.ofConfined()) { MemorySegment filenamePtr = Interop.allocateNativeString(filename, arena); MemorySegment result = (MemorySegment) cairo_svg_surface_create.invoke(filenamePtr, widthInPoints, heightInPoints); @@ -135,11 +127,12 @@ public static SVGSurface create(String filename, int widthInPoints, int heightIn */ public static SVGSurface create(OutputStream stream, int widthInPoints, int heightInPoints) { SVGSurface surface; + Arena arena = Arena.ofConfined(); try { MemorySegment writeFuncPtr; if (stream != null) { WriteFunc writeFunc = stream::write; - writeFuncPtr = writeFunc.toCallback(SegmentScope.auto()); + writeFuncPtr = writeFunc.toCallback(arena); } else { writeFuncPtr = MemorySegment.NULL; } @@ -148,7 +141,7 @@ public static SVGSurface create(OutputStream stream, int widthInPoints, int heig surface = new SVGSurface(result); MemoryCleaner.takeOwnership(surface.handle()); if (stream != null) { - surface.callbackAllocation = writeFuncPtr; // Keep the memory segment of the upcall stub alive + ArenaCloseAction.CLEANER.register(surface, new ArenaCloseAction(arena)); } } catch (Throwable e) { throw new RuntimeException(e); diff --git a/src/main/java/org/freedesktop/cairo/SVGVersion.java b/src/main/java/org/freedesktop/cairo/SVGVersion.java index a9259bc..c0ea620 100644 --- a/src/main/java/org/freedesktop/cairo/SVGVersion.java +++ b/src/main/java/org/freedesktop/cairo/SVGVersion.java @@ -81,13 +81,12 @@ public static SVGVersion of(int ordinal) { */ public static SVGVersion[] getVersions() { try { - try (Arena arena = Arena.openConfined()) { + try (Arena arena = Arena.ofConfined()) { MemorySegment versionsPtr = arena.allocate(ValueLayout.ADDRESS); MemorySegment numVersionsPtr = arena.allocate(ValueLayout.JAVA_INT); cairo_svg_get_versions.invoke(versionsPtr, numVersionsPtr); int numVersions = numVersionsPtr.get(ValueLayout.JAVA_INT, 0); - int[] versionInts = MemorySegment.ofAddress(versionsPtr.address(), numVersions, arena.scope()) - .toArray(ValueLayout.JAVA_INT); + int[] versionInts = versionsPtr.reinterpret(ValueLayout.JAVA_INT.byteSize() * numVersions).toArray(ValueLayout.JAVA_INT); SVGVersion[] versions = new SVGVersion[numVersions]; for (int i = 0; i < versionInts.length; i++) { versions[i] = SVGVersion.of(versionInts[i]); @@ -117,7 +116,7 @@ public String toString() { if (MemorySegment.NULL.equals(result)) { return null; } - return result.getUtf8String(0); + return result.reinterpret(Integer.MAX_VALUE).getUtf8String(0); } catch (Throwable e) { throw new RuntimeException(e); } @@ -125,5 +124,5 @@ public String toString() { private static final MethodHandle cairo_svg_version_to_string = Interop.downcallHandle( "cairo_svg_version_to_string", - FunctionDescriptor.of(ValueLayout.ADDRESS.asUnbounded(), ValueLayout.JAVA_INT)); + FunctionDescriptor.of(ValueLayout.ADDRESS, ValueLayout.JAVA_INT)); } diff --git a/src/main/java/org/freedesktop/cairo/ScaledFont.java b/src/main/java/org/freedesktop/cairo/ScaledFont.java index 30997d6..7e0f02c 100644 --- a/src/main/java/org/freedesktop/cairo/ScaledFont.java +++ b/src/main/java/org/freedesktop/cairo/ScaledFont.java @@ -27,7 +27,6 @@ import java.lang.foreign.FunctionDescriptor; import java.lang.foreign.MemorySegment; import java.lang.foreign.SegmentAllocator; -import java.lang.foreign.SegmentScope; import java.lang.foreign.ValueLayout; import java.lang.invoke.MethodHandle; @@ -54,9 +53,6 @@ public class ScaledFont extends Proxy { Cairo.ensureInitialized(); } - // Keeps user data keys and values - private final UserDataStore userDataStore; - // Keep a reference to natively allocated resources that are passed to the // ScaledFont during its lifetime. @@ -74,7 +70,6 @@ public class ScaledFont extends Proxy { public ScaledFont(MemorySegment address) { super(address); MemoryCleaner.setFreeFunc(handle(), "cairo_scaled_font_destroy"); - userDataStore = new UserDataStore(address.scope()); } /** @@ -169,15 +164,12 @@ public Status status() { /** * Gets the metrics for a ScaledFont. * - * @return the retrieved extents + * @param extents a FontExtents in which to store the retrieved extents * @since 1.0 */ - public FontExtents extents() { + public void extents(FontExtents extents) { try { - FontExtents extents = new FontExtents( - SegmentAllocator.nativeAllocator(SegmentScope.auto()).allocate(FontExtents.getMemoryLayout())); cairo_scaled_font_extents.invoke(handle(), extents.handle()); - return extents; } catch (Throwable e) { throw new RuntimeException(e); } @@ -203,20 +195,17 @@ public FontExtents extents() { * {@code yAdvance()} values. * * @param string a string of text - * @return the retrieved extents + * @param extents a TextExtents in which to store the retrieved extents. * @since 1.2 */ - public TextExtents textExtents(String string) { + public void textExtents(String string, TextExtents extents) { if (string == null) { - return null; + return; } try { - try (Arena arena = Arena.openConfined()) { + try (Arena arena = Arena.ofConfined()) { MemorySegment utf8 = Interop.allocateNativeString(string, arena); - TextExtents extents = new TextExtents( - SegmentAllocator.nativeAllocator(SegmentScope.auto()).allocate(TextExtents.getMemoryLayout())); cairo_scaled_font_text_extents.invoke(handle(), utf8, extents.handle()); - return extents; } } catch (Throwable e) { throw new RuntimeException(e); @@ -240,18 +229,15 @@ public TextExtents textExtents(String string) { * ({@code extents.width()} and {@code extents.height()}). * * @param glyphs an array of glyph IDs with X and Y offsets. - * @return the retrieved extents + * @param extents a TextExtents in which to store the retrieved extents. * @since 1.0 */ - public TextExtents glyphExtents(Glyphs glyphs) { + public void glyphExtents(Glyphs glyphs, TextExtents extents) { if (glyphs == null) { - return null; + return; } try { - TextExtents extents = new TextExtents( - SegmentAllocator.nativeAllocator(SegmentScope.auto()).allocate(TextExtents.getMemoryLayout())); cairo_scaled_font_glyph_extents.invoke(handle(), glyphs.getGlyphsPointer(), glyphs.getNumGlyphs(), extents.handle()); - return extents; } catch (Throwable e) { throw new RuntimeException(e); } @@ -308,14 +294,19 @@ public Glyphs textToGlyphs(double x, double y, String string) throws IllegalStat if (string == null) { return null; } + + // Define shorthands for void* and int* memory layouts + final ValueLayout VOID_POINTER = ValueLayout.ADDRESS.withTargetLayout(ValueLayout.ADDRESS); + final ValueLayout INT_POINTER = ValueLayout.ADDRESS.withTargetLayout(ValueLayout.JAVA_INT); + try { - try (Arena arena = Arena.openConfined()) { + try (Arena arena = Arena.ofConfined()) { MemorySegment stringPtr = Interop.allocateNativeString(string, arena); - MemorySegment glyphsPtr = arena.allocate(ValueLayout.ADDRESS.asUnbounded()); - MemorySegment numGlyphsPtr = arena.allocate(ValueLayout.ADDRESS.asUnbounded()); - MemorySegment clustersPtr = arena.allocate(ValueLayout.ADDRESS.asUnbounded()); - MemorySegment numClustersPtr = arena.allocate(ValueLayout.ADDRESS.asUnbounded()); - MemorySegment clusterFlagsPtr = arena.allocate(ValueLayout.ADDRESS.asUnbounded()); + MemorySegment glyphsPtr = arena.allocate(VOID_POINTER); + MemorySegment numGlyphsPtr = arena.allocate(INT_POINTER); + MemorySegment clustersPtr = arena.allocate(VOID_POINTER); + MemorySegment numClustersPtr = arena.allocate(INT_POINTER); + MemorySegment clusterFlagsPtr = arena.allocate(INT_POINTER); int result = (int) cairo_scaled_font_text_to_glyphs.invoke(handle(), x, y, stringPtr, string.length(), glyphsPtr, numGlyphsPtr, clustersPtr, numClustersPtr, clusterFlagsPtr); @@ -349,9 +340,8 @@ public Glyphs textToGlyphs(double x, double y, String string) throws IllegalStat "cairo_scaled_font_text_to_glyphs", FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.ADDRESS, ValueLayout.JAVA_DOUBLE, ValueLayout.JAVA_DOUBLE, ValueLayout.ADDRESS, ValueLayout.JAVA_INT, - ValueLayout.ADDRESS.asUnbounded(), ValueLayout.ADDRESS.asUnbounded(), - ValueLayout.ADDRESS.asUnbounded(), ValueLayout.ADDRESS.asUnbounded(), - ValueLayout.ADDRESS.asUnbounded())); + ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS, + ValueLayout.ADDRESS, ValueLayout.ADDRESS)); private static final MethodHandle cairo_glyph_free = Interop.downcallHandle( "cairo_glyph_free", FunctionDescriptor.ofVoid(ValueLayout.ADDRESS)); @@ -408,16 +398,15 @@ public void getFontOptions(FontOptions options) { "cairo_scaled_font_get_font_options", FunctionDescriptor.ofVoid(ValueLayout.ADDRESS, ValueLayout.ADDRESS)); /** - * Returns the font matrix with which the ScaledFont was created. - * - * @return the matrix + * Stores the font matrix with which the ScaledFont was created into + * {@code fontMatrix}. + * + * @param fontMatrix return value for the matrix * @since 1.2 */ - public Matrix getFontMatrix() { + public void getFontMatrix(Matrix fontMatrix) { try { - Matrix fontMatrix = Matrix.create(); cairo_scaled_font_get_font_matrix.invoke(handle(), fontMatrix.handle()); - return fontMatrix; } catch (Throwable e) { throw new RuntimeException(e); } @@ -432,14 +421,12 @@ public Matrix getFontMatrix() { * {@link #create(FontFace, Matrix, Matrix, FontOptions)}. So, the matrix this * function returns always has 0,0 as x0,y0. * - * @return the CTM + * @param ctm return value for the CTM * @since 1.2 */ - public Matrix getCTM() { + public void getCTM(Matrix ctm) { try { - Matrix ctm = Matrix.create(); cairo_scaled_font_get_ctm.invoke(handle(), ctm.handle()); - return ctm; } catch (Throwable e) { throw new RuntimeException(e); } @@ -453,14 +440,12 @@ public Matrix getCTM() { * the font matrix and the ctm associated with the scaled font, and hence is the * matrix mapping from font space to device space. * - * @return the matrix + * @param scaleMatrix return value for the matrix * @since 1.8 */ - public Matrix getScaleMatrix() { + public void getScaleMatrix(Matrix scaleMatrix) { try { - Matrix scaleMatrix = Matrix.create(); cairo_scaled_font_get_scale_matrix.invoke(handle(), scaleMatrix.handle()); - return scaleMatrix; } catch (Throwable e) { throw new RuntimeException(e); } @@ -489,44 +474,21 @@ public FontType getFontType() { private static final MethodHandle cairo_scaled_font_get_type = Interop.downcallHandle( "cairo_scaled_font_get_type", FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.ADDRESS)); - /** - * Attach user data to the scaled font. This method will generate and return a - * {@link UserDataKey}. To update the user data for the same key, call - * {@link #setUserData(UserDataKey, Object)}. To remove user data from a scaled - * font, call this function with {@code null} for {@code userData}. - * - * @param userData the user data to attach to the scaled font. {@code userData} - * can be any Java object, but if it is a primitive type, a - * {@link MemorySegment} or a {@link Proxy} instance, it will be - * stored as cairo user data in native memory as well. - * @return the key that the user data is attached to - * @since 1.4 - */ - public UserDataKey setUserData(Object userData) { - UserDataKey key = UserDataKey.create(this); - return setUserData(key, userData); - } - /** * Attach user data to the scaled font. To remove user data from a scaled font, * call this function with the key that was used to set it and {@code null} for * {@code userData}. - * + * * @param key the key to attach the user data to - * @param userData the user data to attach to the scaled font. {@code userData} - * can be any Java object, but if it is a primitive type, a - * {@link MemorySegment} or a {@link Proxy} instance, it will be - * stored as cairo user data in native memory as well. + * @param userData the user data to attach to the scaled font * @return the key * @throws NullPointerException if {@code key} is {@code null} * @since 1.4 */ - public UserDataKey setUserData(UserDataKey key, Object userData) { + public UserDataKey setUserData(UserDataKey key, MemorySegment userData) { Status status; - userDataStore.set(key, userData); try { - int result = (int) cairo_scaled_font_set_user_data.invoke( - handle(), key.handle(), userDataStore.dataSegment(userData), MemorySegment.NULL); + int result = (int) cairo_scaled_font_set_user_data.invoke(handle(), key.handle(), userData, MemorySegment.NULL); status = Status.of(result); } catch (Throwable e) { throw new RuntimeException(e); @@ -537,23 +499,34 @@ public UserDataKey setUserData(UserDataKey key, Object userData) { return key; } - private static final MethodHandle cairo_scaled_font_set_user_data = Interop.downcallHandle( - "cairo_scaled_font_set_user_data", FunctionDescriptor.of(ValueLayout.JAVA_INT, - ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS)); + private static final MethodHandle cairo_scaled_font_set_user_data = Interop.downcallHandle("cairo_scaled_font_set_user_data", + FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS, + ValueLayout.ADDRESS)); /** * Return user data previously attached to the scaled font using the specified * key. If no user data has been attached with the given key this function * returns {@code null}. - * + * * @param key the UserDataKey the user data was attached to * @return the user data previously attached or {@code null} * @since 1.4 */ - public Object getUserData(UserDataKey key) { - return key == null ? null : userDataStore.get(key); + public MemorySegment getUserData(UserDataKey key) { + if (key == null) { + return null; + } + try { + MemorySegment result = (MemorySegment) cairo_scaled_font_get_user_data.invoke(handle(), key.handle()); + return MemorySegment.NULL.equals(result) ? null : result; + } catch (Throwable e) { + throw new RuntimeException(e); + } } + private static final MethodHandle cairo_scaled_font_get_user_data = Interop.downcallHandle("cairo_scaled_font_get_user_data", + FunctionDescriptor.of(ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS)); + /** * Get the CairoScaledFont GType * @return the GType diff --git a/src/main/java/org/freedesktop/cairo/Script.java b/src/main/java/org/freedesktop/cairo/Script.java index 5b3208a..f4168d4 100644 --- a/src/main/java/org/freedesktop/cairo/Script.java +++ b/src/main/java/org/freedesktop/cairo/Script.java @@ -19,6 +19,7 @@ package org.freedesktop.cairo; +import io.github.jwharm.cairobindings.ArenaCloseAction; import io.github.jwharm.cairobindings.Interop; import io.github.jwharm.cairobindings.MemoryCleaner; @@ -26,7 +27,6 @@ import java.lang.foreign.Arena; import java.lang.foreign.FunctionDescriptor; import java.lang.foreign.MemorySegment; -import java.lang.foreign.SegmentScope; import java.lang.foreign.ValueLayout; import java.lang.invoke.MethodHandle; @@ -48,14 +48,6 @@ public class Script extends Device { Cairo.ensureInitialized(); } - /* - * Initialized by {@link #create(OutputStream)} to keep a reference to the - * memory segment for the upcall stub alive during the lifetime of the - * ScriptSurface instance. - */ - @SuppressWarnings("unused") - private MemorySegment callbackAllocation; - /** * Constructor used internally to instantiate a java ScriptSurface object for a * native {@code cairo_device_t} instance @@ -78,7 +70,7 @@ public Script(MemorySegment address) { public static Script create(String filename) { Script script; try { - try (Arena arena = Arena.openConfined()) { + try (Arena arena = Arena.ofConfined()) { MemorySegment filenamePtr = Interop.allocateNativeString(filename, arena); MemorySegment result = (MemorySegment) cairo_script_create.invoke(filenamePtr); script = new Script(result); @@ -106,20 +98,20 @@ public static Script create(String filename) { */ public static Script create(OutputStream stream) { Script script; + Arena arena = Arena.ofConfined(); try { MemorySegment writeFuncPtr; if (stream != null) { WriteFunc writeFunc = stream::write; - writeFuncPtr = writeFunc.toCallback(SegmentScope.auto()); + writeFuncPtr = writeFunc.toCallback(arena); } else { writeFuncPtr = MemorySegment.NULL; } - MemorySegment result = (MemorySegment) cairo_script_create_for_stream.invoke(writeFuncPtr, - MemorySegment.NULL); + var result = (MemorySegment) cairo_script_create_for_stream.invoke(writeFuncPtr, MemorySegment.NULL); script = new Script(result); MemoryCleaner.takeOwnership(script.handle()); if (stream != null) { - script.callbackAllocation = writeFuncPtr; // Keep the memory segment of the upcall stub alive + ArenaCloseAction.CLEANER.register(script, new ArenaCloseAction(arena)); } } catch (Throwable e) { throw new RuntimeException(e); @@ -259,7 +251,7 @@ public ScriptSurface createScriptSurfaceForTarget(Surface target) { */ public void writeComment(String comment) { try { - try (Arena arena = Arena.openConfined()) { + try (Arena arena = Arena.ofConfined()) { MemorySegment commentPtr = Interop.allocateNativeString(comment, arena); cairo_script_write_comment.invoke(handle(), commentPtr, comment == null ? 0 : comment.length()); } diff --git a/src/main/java/org/freedesktop/cairo/SolidPattern.java b/src/main/java/org/freedesktop/cairo/SolidPattern.java index 36fe36c..68c4e3f 100644 --- a/src/main/java/org/freedesktop/cairo/SolidPattern.java +++ b/src/main/java/org/freedesktop/cairo/SolidPattern.java @@ -113,7 +113,7 @@ public static SolidPattern createRGBA(double red, double green, double blue, dou */ public double[] getRGBA() { try { - try (Arena arena = Arena.openConfined()) { + try (Arena arena = Arena.ofConfined()) { MemorySegment ptrs = arena.allocateArray(ValueLayout.JAVA_DOUBLE, 4); long size = ValueLayout.JAVA_DOUBLE.byteSize(); cairo_pattern_get_rgba.invoke(handle(), ptrs, ptrs.asSlice(size), ptrs.asSlice(2 * size), diff --git a/src/main/java/org/freedesktop/cairo/Status.java b/src/main/java/org/freedesktop/cairo/Status.java index 4c8fc7d..c6a248e 100644 --- a/src/main/java/org/freedesktop/cairo/Status.java +++ b/src/main/java/org/freedesktop/cairo/Status.java @@ -365,14 +365,14 @@ public String toString() { if (MemorySegment.NULL.equals(result)) { return null; } - return result.getUtf8String(0); + return result.reinterpret(Integer.MAX_VALUE).getUtf8String(0); } catch (Throwable e) { throw new RuntimeException(e); } } private static final MethodHandle cairo_status_to_string = Interop.downcallHandle("cairo_status_to_string", - FunctionDescriptor.of(ValueLayout.ADDRESS.asUnbounded(), ValueLayout.JAVA_INT)); + FunctionDescriptor.of(ValueLayout.ADDRESS, ValueLayout.JAVA_INT)); /** * Returns the enum constant for the given ordinal (its position in the enum diff --git a/src/main/java/org/freedesktop/cairo/Surface.java b/src/main/java/org/freedesktop/cairo/Surface.java index 4085039..32b826e 100644 --- a/src/main/java/org/freedesktop/cairo/Surface.java +++ b/src/main/java/org/freedesktop/cairo/Surface.java @@ -55,9 +55,6 @@ public sealed class Surface extends Proxy implements AutoCloseable Cairo.ensureInitialized(); } - // Keeps user data keys and values - private final UserDataStore userDataStore; - // Keep a reference to the target surface during the lifetime of this surface @SuppressWarnings("unused") private Surface target; @@ -72,7 +69,6 @@ public sealed class Surface extends Proxy implements AutoCloseable public Surface(MemorySegment address) { super(address); MemoryCleaner.setFreeFunc(handle(), "cairo_surface_destroy"); - userDataStore = new UserDataStore(address.scope()); } /** @@ -117,15 +113,15 @@ public static T createSimilar(T other, Content content, int // Cast to T is safe, because we construct the exact same class in all cases @SuppressWarnings("unchecked") T surface = (T) switch (other) { - case ImageSurface s -> new ImageSurface(result); - case PDFSurface s -> new PDFSurface(result); - case PSSurface s -> new PSSurface(result); - case RecordingSurface s -> new RecordingSurface(result); - case SVGSurface s -> new SVGSurface(result); - case ScriptSurface s -> new ScriptSurface(result); - case TeeSurface s -> new TeeSurface(result); - case SurfaceObserver s -> new SurfaceObserver(result); - case Surface s -> new Surface(result); + case ImageSurface _ -> new ImageSurface(result); + case PDFSurface _ -> new PDFSurface(result); + case PSSurface _ -> new PSSurface(result); + case RecordingSurface _ -> new RecordingSurface(result); + case SVGSurface _ -> new SVGSurface(result); + case ScriptSurface _ -> new ScriptSurface(result); + case TeeSurface _ -> new TeeSurface(result); + case SurfaceObserver _ -> new SurfaceObserver(result); + case Surface _ -> new Surface(result); }; MemoryCleaner.takeOwnership(surface.handle()); return surface; @@ -440,7 +436,7 @@ public Surface setDeviceOffset(double xOffset, double yOffset) { */ public Point getDeviceOffset() { try { - try (Arena arena = Arena.openConfined()) { + try (Arena arena = Arena.ofConfined()) { MemorySegment xPtr = arena.allocate(ValueLayout.JAVA_DOUBLE); MemorySegment yPtr = arena.allocate(ValueLayout.JAVA_DOUBLE); cairo_surface_get_device_offset.invoke(handle(), xPtr, yPtr); @@ -467,7 +463,7 @@ public Point getDeviceOffset() { */ public Point getDeviceScale() { try { - try (Arena arena = Arena.openConfined()) { + try (Arena arena = Arena.ofConfined()) { MemorySegment xPtr = arena.allocate(ValueLayout.JAVA_DOUBLE); MemorySegment yPtr = arena.allocate(ValueLayout.JAVA_DOUBLE); cairo_surface_get_device_scale.invoke(handle(), xPtr, yPtr); @@ -567,7 +563,7 @@ public Surface setFallbackResolution(double xPixelsPerInch, double yPixelsPerInc */ public Point getFallbackResolution() { try { - try (Arena arena = Arena.openConfined()) { + try (Arena arena = Arena.ofConfined()) { MemorySegment xPtr = arena.allocate(ValueLayout.JAVA_DOUBLE); MemorySegment yPtr = arena.allocate(ValueLayout.JAVA_DOUBLE); cairo_surface_get_fallback_resolution.invoke(handle(), xPtr, yPtr); @@ -716,7 +712,7 @@ public boolean hasShowTextGlyphs() { public Surface setMimeData(MimeType mimeType, byte[] data) { Status status; try { - try (Arena arena = Arena.openConfined()) { + try (Arena arena = Arena.ofConfined()) { MemorySegment mimeTypePtr = Interop.allocateNativeString(mimeType.toString(), arena); MemorySegment dataPtr = data == null ? MemorySegment.NULL : arena.allocateArray(ValueLayout.JAVA_BYTE, data); @@ -752,10 +748,14 @@ public Surface setMimeData(MimeType mimeType, byte[] data) { */ public byte[] getMimeData(MimeType mimeType) { try { - try (Arena arena = Arena.openConfined()) { + // Define shorthands for void* and long* memory layouts + final ValueLayout VOID_POINTER = ValueLayout.ADDRESS.withTargetLayout(ValueLayout.ADDRESS); + final ValueLayout LONG_POINTER = ValueLayout.ADDRESS.withTargetLayout(ValueLayout.JAVA_LONG); + + try (Arena arena = Arena.ofConfined()) { MemorySegment mimeTypePtr = Interop.allocateNativeString(mimeType.toString(), arena); - MemorySegment dataPtr = arena.allocate(ValueLayout.ADDRESS); - MemorySegment lengthPtr = arena.allocate(ValueLayout.ADDRESS); + MemorySegment dataPtr = arena.allocate(VOID_POINTER); + MemorySegment lengthPtr = arena.allocate(LONG_POINTER); cairo_surface_get_mime_data.invoke(handle(), mimeTypePtr, dataPtr, lengthPtr); long length = lengthPtr.get(ValueLayout.JAVA_LONG, 0); if (length <= 0) { @@ -765,8 +765,7 @@ public byte[] getMimeData(MimeType mimeType) { if (MemorySegment.NULL.equals(data)) { return null; } - MemorySegment array = MemorySegment.ofAddress(data.address(), length); - return array.toArray(ValueLayout.JAVA_BYTE); + return data.reinterpret(length).toArray(ValueLayout.JAVA_BYTE); } } catch (Throwable e) { throw new RuntimeException(e); @@ -775,7 +774,7 @@ public byte[] getMimeData(MimeType mimeType) { private static final MethodHandle cairo_surface_get_mime_data = Interop .downcallHandle("cairo_surface_get_mime_data", FunctionDescriptor.ofVoid(ValueLayout.ADDRESS, - ValueLayout.ADDRESS, ValueLayout.ADDRESS.asUnbounded(), ValueLayout.ADDRESS.asUnbounded())); + ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS)); /** * Return whether this surface supports {@code mime_type}. @@ -787,7 +786,7 @@ public byte[] getMimeData(MimeType mimeType) { */ public boolean supportsMimeType(MimeType mimeType) { try { - try (Arena arena = Arena.openConfined()) { + try (Arena arena = Arena.ofConfined()) { MemorySegment mimeTypePtr = Interop.allocateNativeString(mimeType.toString(), arena); int result = (int) cairo_surface_supports_mime_type.invoke(handle(), mimeTypePtr); return result != 0; @@ -873,44 +872,21 @@ public void close() { finish(); } - /** - * Attach user data to the surface. This method will generate and return a - * {@link UserDataKey}. To update the user data for the same key, call - * {@link #setUserData(UserDataKey, Object)}. To remove user data from a - * surface, call this function with {@code null} for {@code userData}. - * - * @param userData the user data to attach to the surface. {@code userData} can - * be any Java object, but if it is a primitive type, a - * {@link MemorySegment} or a {@link Proxy} instance, it will be - * stored as cairo user data in native memory as well. - * @return the key that the user data is attached to - * @since 1.0 - */ - public UserDataKey setUserData(Object userData) { - UserDataKey key = UserDataKey.create(this); - return setUserData(key, userData); - } - /** * Attach user data to the surface. To remove user data from a surface, call * this function with the key that was used to set it and {@code null} for * {@code userData}. - * - * @param key the key to attach the user data to - * @param userData the user data to attach to the surface. {@code userData} can - * be any Java object, but if it is a primitive type, a - * {@link MemorySegment} or a {@link Proxy} instance, it will be - * stored as cairo user data in native memory as well. + * + * @param key the key to attach the user data to + * @param userData the user data to attach to the surface * @return the key * @throws NullPointerException if {@code key} is {@code null} - * @since 1.0 + * @since 1.4 */ - public UserDataKey setUserData(UserDataKey key, Object userData) { + public UserDataKey setUserData(UserDataKey key, MemorySegment userData) { Status status; - userDataStore.set(key, userData); try { - int result = (int) cairo_surface_set_user_data.invoke(handle(), key.handle(), userDataStore.dataSegment(userData), - MemorySegment.NULL); + int result = (int) cairo_surface_set_user_data.invoke(handle(), key.handle(), userData, MemorySegment.NULL); status = Status.of(result); } catch (Throwable e) { throw new RuntimeException(e); @@ -929,15 +905,26 @@ public UserDataKey setUserData(UserDataKey key, Object userData) { * Return user data previously attached to the surface using the specified key. * If no user data has been attached with the given key this function returns * {@code null}. - * - * @param key the UserDataKey the user data was attached to + * + * @param key the UserDataKey the user data was attached to * @return the user data previously attached or {@code null} - * @since 1.0 + * @since 1.4 */ - public Object getUserData(UserDataKey key) { - return key == null ? null : userDataStore.get(key); + public MemorySegment getUserData(UserDataKey key) { + if (key == null) { + return null; + } + try { + MemorySegment result = (MemorySegment) cairo_surface_get_user_data.invoke(handle(), key.handle()); + return MemorySegment.NULL.equals(result) ? null : result; + } catch (Throwable e) { + throw new RuntimeException(e); + } } + private static final MethodHandle cairo_surface_get_user_data = Interop.downcallHandle("cairo_surface_get_user_data", + FunctionDescriptor.of(ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS)); + /** * Get the CairoSurface GType * @return the GType diff --git a/src/main/java/org/freedesktop/cairo/SurfaceObserver.java b/src/main/java/org/freedesktop/cairo/SurfaceObserver.java index e05788b..3b21591 100644 --- a/src/main/java/org/freedesktop/cairo/SurfaceObserver.java +++ b/src/main/java/org/freedesktop/cairo/SurfaceObserver.java @@ -22,10 +22,12 @@ import io.github.jwharm.cairobindings.Interop; import io.github.jwharm.cairobindings.MemoryCleaner; +import java.lang.foreign.Arena; import java.lang.foreign.FunctionDescriptor; import java.lang.foreign.MemorySegment; import java.lang.foreign.ValueLayout; import java.lang.invoke.MethodHandle; +import java.lang.ref.Cleaner; import java.util.ArrayList; import java.util.List; @@ -40,6 +42,12 @@ public final class SurfaceObserver extends Surface { Cairo.ensureInitialized(); } + // Cleaner used to close the arena + private static final Cleaner CLEANER = Cleaner.create(); + + // Arena used to allocate the upcall stubs for the callback functions + private final Arena arena = Arena.ofShared(); + // Keep a reference to the callback functions that are passed to the // Observer during its lifetime. @SuppressWarnings("unused") @@ -54,6 +62,11 @@ public final class SurfaceObserver extends Surface { */ public SurfaceObserver(MemorySegment address) { super(address); + + // Setup a Cleaner to close the Arena and release the allocated memory for + // the callback functions + CleanupAction cleanup = new CleanupAction(arena); + CLEANER.register(this, cleanup); } /** @@ -95,7 +108,7 @@ public static SurfaceObserver create(Surface target, SurfaceObserverMode mode) { public SurfaceObserver addFillCallback(SurfaceObserverCallback func) { try { int ignored = (int) cairo_surface_observer_add_fill_callback.invoke(handle(), - func == null ? MemorySegment.NULL : func.toCallback(handle().scope()), MemorySegment.NULL); + func == null ? MemorySegment.NULL : func.toCallback(arena), MemorySegment.NULL); callbacks.add(func); } catch (Throwable e) { throw new RuntimeException(e); @@ -117,7 +130,7 @@ public SurfaceObserver addFillCallback(SurfaceObserverCallback func) { public SurfaceObserver addFinishCallback(SurfaceObserverCallback func) { try { int ignored = (int) cairo_surface_observer_add_finish_callback.invoke(handle(), - func == null ? MemorySegment.NULL : func.toCallback(handle().scope()), MemorySegment.NULL); + func == null ? MemorySegment.NULL : func.toCallback(arena), MemorySegment.NULL); callbacks.add(func); } catch (Throwable e) { throw new RuntimeException(e); @@ -139,7 +152,7 @@ public SurfaceObserver addFinishCallback(SurfaceObserverCallback func) { public SurfaceObserver addFlushCallback(SurfaceObserverCallback func) { try { int ignored = (int) cairo_surface_observer_add_flush_callback.invoke(handle(), - func == null ? MemorySegment.NULL : func.toCallback(handle().scope()), MemorySegment.NULL); + func == null ? MemorySegment.NULL : func.toCallback(arena), MemorySegment.NULL); callbacks.add(func); } catch (Throwable e) { throw new RuntimeException(e); @@ -161,7 +174,7 @@ public SurfaceObserver addFlushCallback(SurfaceObserverCallback func) { public SurfaceObserver addGlyphsCallback(SurfaceObserverCallback func) { try { int ignored = (int) cairo_surface_observer_add_glyphs_callback.invoke(handle(), - func == null ? MemorySegment.NULL : func.toCallback(handle().scope()), MemorySegment.NULL); + func == null ? MemorySegment.NULL : func.toCallback(arena), MemorySegment.NULL); callbacks.add(func); } catch (Throwable e) { throw new RuntimeException(e); @@ -183,7 +196,7 @@ public SurfaceObserver addGlyphsCallback(SurfaceObserverCallback func) { public SurfaceObserver addMaskCallback(SurfaceObserverCallback func) { try { int ignored = (int) cairo_surface_observer_add_mask_callback.invoke(handle(), - func == null ? MemorySegment.NULL : func.toCallback(handle().scope()), MemorySegment.NULL); + func == null ? MemorySegment.NULL : func.toCallback(arena), MemorySegment.NULL); callbacks.add(func); } catch (Throwable e) { throw new RuntimeException(e); @@ -205,7 +218,7 @@ public SurfaceObserver addMaskCallback(SurfaceObserverCallback func) { public SurfaceObserver addPaintCallback(SurfaceObserverCallback func) { try { int ignored = (int) cairo_surface_observer_add_paint_callback.invoke(handle(), - func == null ? MemorySegment.NULL : func.toCallback(handle().scope()), MemorySegment.NULL); + func == null ? MemorySegment.NULL : func.toCallback(arena), MemorySegment.NULL); callbacks.add(func); } catch (Throwable e) { throw new RuntimeException(e); @@ -227,7 +240,7 @@ public SurfaceObserver addPaintCallback(SurfaceObserverCallback func) { public SurfaceObserver addStrokeCallback(SurfaceObserverCallback func) { try { int ignored = (int) cairo_surface_observer_add_stroke_callback.invoke(handle(), - func == null ? MemorySegment.NULL : func.toCallback(handle().scope()), MemorySegment.NULL); + func == null ? MemorySegment.NULL : func.toCallback(arena), MemorySegment.NULL); callbacks.add(func); } catch (Throwable e) { throw new RuntimeException(e); @@ -265,7 +278,7 @@ public double elapsed() { public void print(WriteFunc writeFunc) { try { int ignored = (int) cairo_surface_observer_print.invoke(handle(), - writeFunc == null ? MemorySegment.NULL : writeFunc.toCallback(handle().scope()), MemorySegment.NULL); + writeFunc == null ? MemorySegment.NULL : writeFunc.toCallback(arena), MemorySegment.NULL); } catch (Throwable e) { throw new RuntimeException(e); } @@ -274,4 +287,11 @@ public void print(WriteFunc writeFunc) { private static final MethodHandle cairo_surface_observer_print = Interop.downcallHandle( "cairo_surface_observer_print", FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS)); + + // Static class to separate the cleanup logic from the object being cleaned + private record CleanupAction(Arena arena) implements Runnable { + @Override public void run() { + arena.close(); + } + } } diff --git a/src/main/java/org/freedesktop/cairo/SurfaceObserverCallback.java b/src/main/java/org/freedesktop/cairo/SurfaceObserverCallback.java index da3b2c7..ab9d2e5 100644 --- a/src/main/java/org/freedesktop/cairo/SurfaceObserverCallback.java +++ b/src/main/java/org/freedesktop/cairo/SurfaceObserverCallback.java @@ -56,17 +56,17 @@ default void upcall(MemorySegment observer, MemorySegment target, MemorySegment * Generates an upcall stub, a C function pointer that will call * {@link #upcall}. * - * @param scope the scope in which the upcall stub will be allocated + * @param arena the arena in which the upcall stub will be allocated * @return the function pointer of the upcall stub * @since 1.12 */ - default MemorySegment toCallback(SegmentScope scope) { + default MemorySegment toCallback(Arena arena) { try { FunctionDescriptor fdesc = FunctionDescriptor.ofVoid(ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS); MethodHandle handle = MethodHandles.lookup().findVirtual( SurfaceObserverCallback.class, "upcall", fdesc.toMethodType()); - return Linker.nativeLinker().upcallStub(handle.bindTo(this), fdesc, scope); + return Linker.nativeLinker().upcallStub(handle.bindTo(this), fdesc, arena); } catch (NoSuchMethodException | IllegalAccessException e) { throw new RuntimeException(e); } diff --git a/src/main/java/org/freedesktop/cairo/SurfacePattern.java b/src/main/java/org/freedesktop/cairo/SurfacePattern.java index 4db71ed..24dc25a 100644 --- a/src/main/java/org/freedesktop/cairo/SurfacePattern.java +++ b/src/main/java/org/freedesktop/cairo/SurfacePattern.java @@ -82,7 +82,7 @@ public static SurfacePattern create(Surface surface) { */ public Surface getSurface() { try { - try (Arena arena = Arena.openConfined()) { + try (Arena arena = Arena.ofConfined()) { MemorySegment surfacePtr = arena.allocate(ValueLayout.ADDRESS); cairo_pattern_get_surface.invoke(handle(), surfacePtr); return new Surface(surfacePtr.get(ValueLayout.ADDRESS, 0)); diff --git a/src/main/java/org/freedesktop/cairo/TextExtents.java b/src/main/java/org/freedesktop/cairo/TextExtents.java index fce611b..3296111 100644 --- a/src/main/java/org/freedesktop/cairo/TextExtents.java +++ b/src/main/java/org/freedesktop/cairo/TextExtents.java @@ -21,11 +21,7 @@ import io.github.jwharm.cairobindings.Proxy; -import java.lang.foreign.MemoryLayout; -import java.lang.foreign.MemorySegment; -import java.lang.foreign.SegmentAllocator; -import java.lang.foreign.SegmentScope; -import java.lang.foreign.ValueLayout; +import java.lang.foreign.*; import java.lang.invoke.VarHandle; /** @@ -66,10 +62,13 @@ static MemoryLayout getMemoryLayout() { private static final VarHandle Y_ADVANCE = getMemoryLayout().varHandle(MemoryLayout.PathElement.groupElement("y_advance")); /** - * Allocate a new {@code cairo_text_extents_t} + * Allocate a new, uninitialized {@code cairo_text_extents_t} + * + * @param arena the arena in which the TextExtents will be allocated + * @return a newly allocated, uninitialized TextExtents */ - static TextExtents create() { - return new TextExtents(SegmentAllocator.nativeAllocator(SegmentScope.auto()).allocate(getMemoryLayout())); + public static TextExtents create(Arena arena) { + return new TextExtents(arena.allocate(getMemoryLayout())); } /** diff --git a/src/main/java/org/freedesktop/cairo/ToyFontFace.java b/src/main/java/org/freedesktop/cairo/ToyFontFace.java index a72715b..a71c167 100644 --- a/src/main/java/org/freedesktop/cairo/ToyFontFace.java +++ b/src/main/java/org/freedesktop/cairo/ToyFontFace.java @@ -91,7 +91,7 @@ public static ToyFontFace create() { */ public static ToyFontFace create(String family, FontSlant slant, FontWeight weight) { try { - try (Arena arena = Arena.openConfined()) { + try (Arena arena = Arena.ofConfined()) { MemorySegment familyPtr = Interop.allocateNativeString(family, arena); MemorySegment result = (MemorySegment) cairo_toy_font_face_create.invoke(familyPtr, slant.getValue(), weight.getValue()); @@ -116,7 +116,7 @@ public static ToyFontFace create(String family, FontSlant slant, FontWeight weig public String getFamily() { try { MemorySegment result = (MemorySegment) cairo_toy_font_face_get_family.invoke(handle()); - return result.getUtf8String(0); + return result.reinterpret(Integer.MAX_VALUE).getUtf8String(0); } catch (Throwable e) { throw new RuntimeException(e); } @@ -124,7 +124,7 @@ public String getFamily() { private static final MethodHandle cairo_toy_font_face_get_family = Interop.downcallHandle( "cairo_toy_font_face_get_family", - FunctionDescriptor.of(ValueLayout.ADDRESS.asUnbounded(), ValueLayout.ADDRESS)); + FunctionDescriptor.of(ValueLayout.ADDRESS, ValueLayout.ADDRESS)); /** * Gets the slant of a toy font. diff --git a/src/main/java/org/freedesktop/cairo/UserDataKey.java b/src/main/java/org/freedesktop/cairo/UserDataKey.java index 1cff99e..c1c4dd4 100644 --- a/src/main/java/org/freedesktop/cairo/UserDataKey.java +++ b/src/main/java/org/freedesktop/cairo/UserDataKey.java @@ -21,17 +21,11 @@ import io.github.jwharm.cairobindings.Proxy; -import java.lang.foreign.MemoryLayout; -import java.lang.foreign.MemorySegment; -import java.lang.foreign.SegmentAllocator; -import java.lang.foreign.ValueLayout; +import java.lang.foreign.*; /** * UserDataKey is used for attaching user data to cairo data structures. - *

- * The lifetime of a UserDataKey is connected to the lifetime of the Proxy object - * that is passed in the {@link #create(Proxy)} method. - * + * * @since 1.0 */ public final class UserDataKey extends Proxy { @@ -55,11 +49,10 @@ public UserDataKey(MemorySegment address) { /** * Create a new UserDataKey * - * @param proxy the ProxyInstace object whose memory scope (lifetime) will be - * associated with the returned UserDataKey + * @param arena the arena in which the returned UserDataKey will be allocated * @return the newly created UserDataKey */ - static UserDataKey create(Proxy proxy) { - return new UserDataKey(SegmentAllocator.nativeAllocator(proxy.handle().scope()).allocate(getMemoryLayout())); + public static UserDataKey create(Arena arena) { + return new UserDataKey(arena.allocate(getMemoryLayout())); } } diff --git a/src/main/java/org/freedesktop/cairo/UserDataStore.java b/src/main/java/org/freedesktop/cairo/UserDataStore.java deleted file mode 100644 index acb5367..0000000 --- a/src/main/java/org/freedesktop/cairo/UserDataStore.java +++ /dev/null @@ -1,123 +0,0 @@ -/* cairo-java-bindings - Java language bindings for cairo - * Copyright (C) 2023 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 org.freedesktop.cairo; - -import io.github.jwharm.cairobindings.Proxy; - -import java.lang.foreign.MemorySegment; -import java.lang.foreign.SegmentAllocator; -import java.lang.foreign.SegmentScope; -import java.lang.foreign.ValueLayout; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - -/** - * UserDataStore is a utility class that is used to get and set user data on cairo types that - * support it. The store internally uses a {@link ConcurrentHashMap}. It is safe - * to use in a multi-threaded environment, but {@code null} keys are not - * supported. - */ -class UserDataStore { - - private final Map userData = new ConcurrentHashMap<>(); - private final SegmentScope scope; - - /** - * Create a new UserDataStore. - * - * @param scope The memory scope that will be used to create native data - * segments (see {@link #dataSegment(Object)}). - */ - UserDataStore(SegmentScope scope) { - this.scope = scope; - } - - /** - * Set the key to the provided value in the store - * @param key the key - * @param data the value - * @throws NullPointerException if the key is {@code null} - */ - void set(UserDataKey key, Object data) { - userData.put(key, data); - } - - /** - * Get the user data from the store that was set with the provided key - * @param key the key - * @return the user data, or {@code null} if the key is null - */ - Object get(UserDataKey key) { - return key == null ? null : userData.get(key); - } - - /** - * Allocate a native memory segment that contains the value of this key. This - * will work for primitive types, {@link MemorySegment} and {@link Proxy} - * instances. For all other classes, this will return - * {@link MemorySegment#NULL}. - * - * @param value the value to put in a newly allocated memory segment - * @return the newly allocated memory segment - */ - MemorySegment dataSegment(Object value) { - MemorySegment data; - switch (value) { - case MemorySegment m -> data = m; - case Proxy p -> data = p.handle(); - case Boolean b -> { - data = SegmentAllocator.nativeAllocator(scope).allocate(ValueLayout.JAVA_BOOLEAN); - data.set(ValueLayout.JAVA_BOOLEAN, 0, b); - } - case Byte b -> { - data = SegmentAllocator.nativeAllocator(scope).allocate(ValueLayout.JAVA_BYTE); - data.set(ValueLayout.JAVA_BYTE, 0, b); - } - case Character c -> { - data = SegmentAllocator.nativeAllocator(scope).allocate(ValueLayout.JAVA_CHAR); - data.set(ValueLayout.JAVA_CHAR, 0, c); - } - case Double d -> { - data = SegmentAllocator.nativeAllocator(scope).allocate(ValueLayout.JAVA_DOUBLE); - data.set(ValueLayout.JAVA_DOUBLE, 0, d); - } - case Float f -> { - data = SegmentAllocator.nativeAllocator(scope).allocate(ValueLayout.JAVA_FLOAT); - data.set(ValueLayout.JAVA_FLOAT, 0, f); - } - case Integer i -> { - data = SegmentAllocator.nativeAllocator(scope).allocate(ValueLayout.JAVA_INT); - data.set(ValueLayout.JAVA_INT, 0, i); - } - case Long l -> { - data = SegmentAllocator.nativeAllocator(scope).allocate(ValueLayout.JAVA_LONG); - data.set(ValueLayout.JAVA_LONG, 0, l); - } - case Short s -> { - data = SegmentAllocator.nativeAllocator(scope).allocate(ValueLayout.JAVA_SHORT); - data.set(ValueLayout.JAVA_SHORT, 0, s); - } - default -> { - data = MemorySegment.NULL; - } - } - return data; - } -} diff --git a/src/main/java/org/freedesktop/cairo/UserFontFace.java b/src/main/java/org/freedesktop/cairo/UserFontFace.java index 25801e1..1d918de 100644 --- a/src/main/java/org/freedesktop/cairo/UserFontFace.java +++ b/src/main/java/org/freedesktop/cairo/UserFontFace.java @@ -21,10 +21,12 @@ import io.github.jwharm.cairobindings.Interop; +import java.lang.foreign.Arena; import java.lang.foreign.FunctionDescriptor; import java.lang.foreign.MemorySegment; import java.lang.foreign.ValueLayout; import java.lang.invoke.MethodHandle; +import java.lang.ref.Cleaner; /** * Font support with font data provided by the user. @@ -42,6 +44,14 @@ public final class UserFontFace extends FontFace { Cairo.ensureInitialized(); } + // Cleaner used to close the arena + private static final Cleaner CLEANER = Cleaner.create(); + + // Arena used to allocate the upcall stubs for the callback functions + private final Arena arena = Arena.ofShared(); + + // Keep a reference to the callback functions that are passed to the + // UserScaledFont during its lifetime. private UserScaledFontInitFunc initFunc = null; private UserScaledFontRenderGlyphFunc renderGlyphFunc = null; private UserScaledFontRenderGlyphFunc renderColorGlyphFunc = null; @@ -57,6 +67,11 @@ public final class UserFontFace extends FontFace { */ public UserFontFace(MemorySegment address) { super(address); + + // Setup a Cleaner to close the Arena and release the allocated memory for + // the callback functions + CleanupAction cleanup = new CleanupAction(arena); + CLEANER.register(this, cleanup); } /** @@ -99,7 +114,7 @@ public static UserFontFace create() { public void setInitFunc(UserScaledFontInitFunc initFunc) throws IllegalStateException { try { cairo_user_font_face_set_init_func.invoke(handle(), - initFunc == null ? MemorySegment.NULL : initFunc.toCallback(handle().scope())); + initFunc == null ? MemorySegment.NULL : initFunc.toCallback(arena)); this.initFunc = initFunc; } catch (Throwable e) { throw new RuntimeException(e); @@ -151,7 +166,7 @@ public UserScaledFontInitFunc getInitFunc() { public void setRenderGlyphFunc(UserScaledFontRenderGlyphFunc renderGlyphFunc) throws IllegalStateException { try { cairo_user_font_face_set_render_glyph_func.invoke(handle(), - renderGlyphFunc == null ? MemorySegment.NULL : renderGlyphFunc.toCallback(handle().scope())); + renderGlyphFunc == null ? MemorySegment.NULL : renderGlyphFunc.toCallback(arena)); this.renderGlyphFunc = renderGlyphFunc; } catch (Throwable e) { throw new RuntimeException(e); @@ -203,7 +218,7 @@ public UserScaledFontRenderGlyphFunc getRenderGlyphFunc() { public void setRenderColorGlyphFunc(UserScaledFontRenderGlyphFunc renderGlyphFunc) throws IllegalStateException { try { cairo_user_font_face_set_render_color_glyph_func.invoke(handle(), - renderGlyphFunc == null ? MemorySegment.NULL : renderGlyphFunc.toCallback(handle().scope())); + renderGlyphFunc == null ? MemorySegment.NULL : renderGlyphFunc.toCallback(arena)); this.renderColorGlyphFunc = renderGlyphFunc; } catch (Throwable e) { throw new RuntimeException(e); @@ -244,7 +259,7 @@ public UserScaledFontRenderGlyphFunc getRenderColorGlyphFunc() { public void setUnicodeToGlyphFunc(UserScaledFontUnicodeToGlyphFunc unicodeToGlyphFunc) throws IllegalStateException { try { cairo_user_font_face_set_unicode_to_glyph_func.invoke(handle(), - unicodeToGlyphFunc == null ? MemorySegment.NULL : unicodeToGlyphFunc.toCallback(handle().scope())); + unicodeToGlyphFunc == null ? MemorySegment.NULL : unicodeToGlyphFunc.toCallback(arena)); this.unicodeToGlyphFunc = unicodeToGlyphFunc; } catch (Throwable e) { throw new RuntimeException(e); @@ -285,7 +300,7 @@ public UserScaledFontUnicodeToGlyphFunc getUnicodeToGlyphFunc() { public void setTextToGlyphsFunc(UserScaledFontTextToGlyphsFunc textToGlyphsFunc) throws IllegalStateException { try { cairo_user_font_face_set_text_to_glyphs_func.invoke(handle(), - textToGlyphsFunc == null ? MemorySegment.NULL : textToGlyphsFunc.toCallback(handle().scope())); + textToGlyphsFunc == null ? MemorySegment.NULL : textToGlyphsFunc.toCallback(arena)); this.textToGlyphsFunc = textToGlyphsFunc; } catch (Throwable e) { throw new RuntimeException(e); @@ -309,4 +324,11 @@ public void setTextToGlyphsFunc(UserScaledFontTextToGlyphsFunc textToGlyphsFunc) public UserScaledFontTextToGlyphsFunc getTextToGlyphsFunc() { return this.textToGlyphsFunc; } + + // Static class to separate the cleanup logic from the object being cleaned + private record CleanupAction(Arena arena) implements Runnable { + @Override public void run() { + arena.close(); + } + } } diff --git a/src/main/java/org/freedesktop/cairo/UserScaledFontInitFunc.java b/src/main/java/org/freedesktop/cairo/UserScaledFontInitFunc.java index 1f856b5..15e6693 100644 --- a/src/main/java/org/freedesktop/cairo/UserScaledFontInitFunc.java +++ b/src/main/java/org/freedesktop/cairo/UserScaledFontInitFunc.java @@ -84,19 +84,19 @@ default int upcall(MemorySegment scaledFont, MemorySegment cr, MemorySegment ext /** * Generates an upcall stub, a C function pointer that will call - * {@link #upcall(MemorySegment, MemorySegment, MemorySegment)}. + * {@link #upcall}. * - * @param scope the scope in which the upcall stub will be allocated + * @param arena the arena in which the upcall stub will be allocated * @return the function pointer of the upcall stub * @since 1.8 */ - default MemorySegment toCallback(SegmentScope scope) { + default MemorySegment toCallback(Arena arena) { try { FunctionDescriptor fdesc = FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS); MethodHandle handle = MethodHandles.lookup().findVirtual( UserScaledFontInitFunc.class, "upcall", fdesc.toMethodType()); - return Linker.nativeLinker().upcallStub(handle.bindTo(this), fdesc, scope); + return Linker.nativeLinker().upcallStub(handle.bindTo(this), fdesc, arena); } catch (NoSuchMethodException | IllegalAccessException e) { throw new RuntimeException(e); } diff --git a/src/main/java/org/freedesktop/cairo/UserScaledFontRenderGlyphFunc.java b/src/main/java/org/freedesktop/cairo/UserScaledFontRenderGlyphFunc.java index b3763f9..ddab4dc 100644 --- a/src/main/java/org/freedesktop/cairo/UserScaledFontRenderGlyphFunc.java +++ b/src/main/java/org/freedesktop/cairo/UserScaledFontRenderGlyphFunc.java @@ -118,19 +118,19 @@ default int upcall(MemorySegment scaledFont, long glyph, MemorySegment cr, Memor /** * Generates an upcall stub, a C function pointer that will call - * {@link #upcall(MemorySegment, long, MemorySegment, MemorySegment)}. + * {@link #upcall}. * - * @param scope the scope in which the upcall stub will be allocated + * @param arena the arena in which the upcall stub will be allocated * @return the function pointer of the upcall stub * @since 1.8 */ - default MemorySegment toCallback(SegmentScope scope) { + default MemorySegment toCallback(Arena arena) { try { FunctionDescriptor fdesc = FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.ADDRESS, ValueLayout.JAVA_LONG, ValueLayout.ADDRESS, ValueLayout.ADDRESS); MethodHandle handle = MethodHandles.lookup().findVirtual( UserScaledFontRenderGlyphFunc.class, "upcall", fdesc.toMethodType()); - return Linker.nativeLinker().upcallStub(handle.bindTo(this), fdesc, scope); + return Linker.nativeLinker().upcallStub(handle.bindTo(this), fdesc, arena); } catch (NoSuchMethodException | IllegalAccessException e) { throw new RuntimeException(e); } diff --git a/src/main/java/org/freedesktop/cairo/UserScaledFontTextToGlyphsFunc.java b/src/main/java/org/freedesktop/cairo/UserScaledFontTextToGlyphsFunc.java index 6fb078a..e59e2d5 100644 --- a/src/main/java/org/freedesktop/cairo/UserScaledFontTextToGlyphsFunc.java +++ b/src/main/java/org/freedesktop/cairo/UserScaledFontTextToGlyphsFunc.java @@ -127,15 +127,15 @@ default int upcall(MemorySegment scaledFont, MemorySegment utf8, int utf8Len, MemorySegment numClustersPtr, MemorySegment clusterFlagsPtr) { // Read the string byte[] utf8Bytes = new byte[utf8Len]; - Interop.reinterpret(utf8, utf8Len).asByteBuffer().get(utf8Bytes); + utf8.reinterpret(utf8Len).asByteBuffer().get(utf8Bytes); // Read the memory segments int numGlyphs = numGlyphsPtr.equals(MemorySegment.NULL) ? 0 - : Interop.reinterpret(numGlyphsPtr, ValueLayout.JAVA_INT).get(ValueLayout.JAVA_INT, 0); + : numGlyphsPtr.reinterpret(ValueLayout.JAVA_INT.byteSize()).get(ValueLayout.JAVA_INT, 0); int numClusters = numClustersPtr.equals(MemorySegment.NULL) ? 0 - : Interop.reinterpret(numClustersPtr, ValueLayout.JAVA_INT).get(ValueLayout.JAVA_INT, 0); + : numClustersPtr.reinterpret(ValueLayout.JAVA_INT.byteSize()).get(ValueLayout.JAVA_INT, 0); TextClusterFlags clusterFlags = clusterFlagsPtr.equals(MemorySegment.NULL) ? null - : TextClusterFlags.of(Interop.reinterpret(clusterFlagsPtr, ValueLayout.JAVA_INT).get(ValueLayout.JAVA_INT, 0)); + : TextClusterFlags.of(clusterFlagsPtr.reinterpret(ValueLayout.JAVA_INT.byteSize()).get(ValueLayout.JAVA_INT, 0)); // Run the callback try { @@ -144,11 +144,11 @@ default int upcall(MemorySegment scaledFont, MemorySegment utf8, int utf8Len, // Write the results back into the memory segments if (! numGlyphsPtr.equals(MemorySegment.NULL)) - Interop.reinterpret(numGlyphsPtr, ValueLayout.JAVA_INT).set(ValueLayout.JAVA_INT, 0, glyphs.getNumGlyphs()); + numGlyphsPtr.reinterpret(ValueLayout.JAVA_INT.byteSize()).set(ValueLayout.JAVA_INT, 0, glyphs.getNumGlyphs()); if (! numClustersPtr.equals(MemorySegment.NULL)) - Interop.reinterpret(numClustersPtr, ValueLayout.JAVA_INT).set(ValueLayout.JAVA_INT, 0, glyphs.getNumClusters()); + numClustersPtr.reinterpret(ValueLayout.JAVA_INT.byteSize()).set(ValueLayout.JAVA_INT, 0, glyphs.getNumClusters()); if (! clusterFlagsPtr.equals(MemorySegment.NULL)) - Interop.reinterpret(clusterFlagsPtr, ValueLayout.JAVA_INT).set(ValueLayout.JAVA_INT, 0, glyphs.getClusterFlags().getValue()); + clusterFlagsPtr.reinterpret(ValueLayout.JAVA_INT.byteSize()).set(ValueLayout.JAVA_INT, 0, glyphs.getClusterFlags().getValue()); return Status.SUCCESS.getValue(); } catch (UnsupportedOperationException uoe) { @@ -160,21 +160,20 @@ default int upcall(MemorySegment scaledFont, MemorySegment utf8, int utf8Len, /** * Generates an upcall stub, a C function pointer that will call - * {@link #upcall(MemorySegment, MemorySegment, int, MemorySegment, - * MemorySegment, MemorySegment, MemorySegment, MemorySegment)}. + * {@link #upcall}. * - * @param scope the scope in which the upcall stub will be allocated + * @param arena the arena in which the upcall stub will be allocated * @return the function pointer of the upcall stub * @since 1.8 */ - default MemorySegment toCallback(SegmentScope scope) { + default MemorySegment toCallback(Arena arena) { try { FunctionDescriptor fdesc = FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.JAVA_INT, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS); MethodHandle handle = MethodHandles.lookup().findVirtual( UserScaledFontTextToGlyphsFunc.class, "upcall", fdesc.toMethodType()); - return Linker.nativeLinker().upcallStub(handle.bindTo(this), fdesc, scope); + return Linker.nativeLinker().upcallStub(handle.bindTo(this), fdesc, arena); } catch (NoSuchMethodException | IllegalAccessException e) { throw new RuntimeException(e); } diff --git a/src/main/java/org/freedesktop/cairo/UserScaledFontUnicodeToGlyphFunc.java b/src/main/java/org/freedesktop/cairo/UserScaledFontUnicodeToGlyphFunc.java index 65e79e9..8378f49 100644 --- a/src/main/java/org/freedesktop/cairo/UserScaledFontUnicodeToGlyphFunc.java +++ b/src/main/java/org/freedesktop/cairo/UserScaledFontUnicodeToGlyphFunc.java @@ -19,8 +19,6 @@ package org.freedesktop.cairo; -import io.github.jwharm.cairobindings.Interop; - import java.lang.foreign.*; import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; @@ -86,7 +84,7 @@ long unicodeToGlyph(UserScaledFont scaledFont, long unicode) default int upcall(MemorySegment scaledFont, long unicode, MemorySegment glyphIndex) { try { long result = unicodeToGlyph(new UserScaledFont(scaledFont), unicode); - Interop.reinterpret(glyphIndex, ValueLayout.JAVA_LONG).set(ValueLayout.JAVA_LONG, 0, result); + glyphIndex.reinterpret(ValueLayout.JAVA_LONG.byteSize()).set(ValueLayout.JAVA_LONG, 0, result); return Status.SUCCESS.getValue(); } catch (UnsupportedOperationException uoe) { return Status.USER_FONT_NOT_IMPLEMENTED.getValue(); @@ -97,19 +95,19 @@ default int upcall(MemorySegment scaledFont, long unicode, MemorySegment glyphIn /** * Generates an upcall stub, a C function pointer that will call - * {@link #upcall(MemorySegment, long, MemorySegment)}. + * {@link #upcall}. * - * @param scope the scope in which the upcall stub will be allocated + * @param arena the arena in which the upcall stub will be allocated * @return the function pointer of the upcall stub * @since 1.8 */ - default MemorySegment toCallback(SegmentScope scope) { + default MemorySegment toCallback(Arena arena) { try { FunctionDescriptor fdesc = FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.ADDRESS, ValueLayout.JAVA_LONG, ValueLayout.ADDRESS); MethodHandle handle = MethodHandles.lookup().findVirtual( UserScaledFontUnicodeToGlyphFunc.class, "upcall", fdesc.toMethodType()); - return Linker.nativeLinker().upcallStub(handle.bindTo(this), fdesc, scope); + return Linker.nativeLinker().upcallStub(handle.bindTo(this), fdesc, arena); } catch (NoSuchMethodException | IllegalAccessException e) { throw new RuntimeException(e); } diff --git a/src/main/java/org/freedesktop/cairo/WriteFunc.java b/src/main/java/org/freedesktop/cairo/WriteFunc.java index c6c6f9d..3347c89 100644 --- a/src/main/java/org/freedesktop/cairo/WriteFunc.java +++ b/src/main/java/org/freedesktop/cairo/WriteFunc.java @@ -24,7 +24,6 @@ import java.lang.foreign.FunctionDescriptor; import java.lang.foreign.Linker; import java.lang.foreign.MemorySegment; -import java.lang.foreign.SegmentScope; import java.lang.foreign.ValueLayout; import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; @@ -67,32 +66,29 @@ default int upcall(MemorySegment closure, MemorySegment data, int length) { if (length <= 0) { return Status.SUCCESS.getValue(); } - try (Arena arena = Arena.openConfined()) { - byte[] bytes = MemorySegment.ofAddress(data.address(), length, arena.scope()) - .toArray(ValueLayout.JAVA_BYTE); - try { - write(bytes); - return Status.SUCCESS.getValue(); - } catch (IOException ioe) { - return Status.WRITE_ERROR.getValue(); - } + byte[] bytes = data.reinterpret(length).toArray(ValueLayout.JAVA_BYTE); + try { + write(bytes); + return Status.SUCCESS.getValue(); + } catch (IOException ioe) { + return Status.WRITE_ERROR.getValue(); } } /** * Generates an upcall stub, a C function pointer that will call - * {@link #upcall(MemorySegment, MemorySegment, int)}. - * - * @param scope the scope in which the upcall stub will be allocated + * {@link #upcall}. + * + * @param arena the arena in which the upcall stub will be allocated * @return the function pointer of the upcall stub * @since 1.0 */ - default MemorySegment toCallback(SegmentScope scope) { + default MemorySegment toCallback(Arena arena) { try { FunctionDescriptor fdesc = FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.JAVA_INT); MethodHandle handle = MethodHandles.lookup().findVirtual(WriteFunc.class, "upcall", fdesc.toMethodType()); - return Linker.nativeLinker().upcallStub(handle.bindTo(this), fdesc, scope); + return Linker.nativeLinker().upcallStub(handle.bindTo(this), fdesc, arena); } catch (NoSuchMethodException | IllegalAccessException e) { throw new RuntimeException(e); } diff --git a/src/main/java/org/freedesktop/freetype/Face.java b/src/main/java/org/freedesktop/freetype/Face.java index 7173b8f..2bc15ee 100644 --- a/src/main/java/org/freedesktop/freetype/Face.java +++ b/src/main/java/org/freedesktop/freetype/Face.java @@ -21,13 +21,10 @@ import io.github.jwharm.cairobindings.Proxy; import io.github.jwharm.cairobindings.Interop; -import io.github.jwharm.cairobindings.MemoryCleaner; import java.lang.foreign.Arena; import java.lang.foreign.FunctionDescriptor; import java.lang.foreign.MemorySegment; -import java.lang.foreign.SegmentAllocator; -import java.lang.foreign.SegmentScope; import java.lang.foreign.ValueLayout; import java.lang.invoke.MethodHandle; @@ -41,6 +38,9 @@ public class Face extends Proxy { FreeType2.ensureInitialized(); } + // The Arena in which the handle to the Face object was allocated. + private Arena allocator = null; + /** * Constructor used internally to instantiate a java Face object for a native * {@code FT_Face} instance @@ -49,48 +49,35 @@ public class Face extends Proxy { */ public Face(MemorySegment address) { super(address); - MemoryCleaner.setFreeFunc(handle(), "FT_Done_Face"); - } - - /** - * Invokes the cleanup action that is normally invoked during garbage collection. - * If the instance is "owned" by the user, the {@code destroy()} function is run - * to dispose the native instance. - */ - public void destroy() { - MemoryCleaner.free(handle()); } /** * Call {@code FT_Open_Face} to open a font by its pathname. - * + *

+ * The resulting Face instance must be cleaned up manually with a call to + * {@link #doneFace()}. + * * @param library the library resource * @param filepathname path to the font file * @param faceIndex See FT_Open_Face * for a detailed description of this parameter. */ - public Face(Library library, String filepathname, long faceIndex) { - super(constructNew(library, filepathname, faceIndex)); - MemoryCleaner.takeOwnership(handle()); - } - - /* - * Helper function for the Face(library, filepathname, faceIndex) constructor - */ - private static MemorySegment constructNew(Library library, String filepathname, long faceIndex) { + public static Face newFace(Library library, String filepathname, long faceIndex) { + Arena allocator = Arena.ofConfined(); try { - MemorySegment pointer = SegmentAllocator.nativeAllocator(SegmentScope.auto()) - .allocate(ValueLayout.ADDRESS.asUnbounded()); - try (Arena arena = Arena.openConfined()) { + MemorySegment pointer = allocator.allocate(ValueLayout.ADDRESS.withTargetLayout(ValueLayout.ADDRESS)); + try (Arena arena = Arena.ofConfined()) { MemorySegment utf8 = Interop.allocateNativeString(filepathname, arena); int result = (int) FT_New_Face.invoke(library.handle(), utf8, faceIndex, pointer); if (result != 0) { throw new UnsupportedOperationException( "Error " + result + " occurred during FreeType FT_Face initialization"); } + Face face = new Face(pointer.get(ValueLayout.ADDRESS, 0)); + face.allocator = allocator; + return face; } - return pointer.get(ValueLayout.ADDRESS, 0); } catch (Throwable e) { throw new RuntimeException(e); } @@ -99,4 +86,22 @@ private static MemorySegment constructNew(Library library, String filepathname, private static final MethodHandle FT_New_Face = Interop.downcallHandle("FT_New_Face", FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.JAVA_LONG, ValueLayout.ADDRESS)); + + /** + * Discard a given face object, as well as all of its child slots and sizes. + */ + public void doneFace() { + try { + int ignored = (int) FT_Done_Face.invoke(handle()); + } catch (Throwable e) { + throw new RuntimeException(e); + } + if (allocator != null) { + allocator.close(); + allocator = null; + } + } + + private static final MethodHandle FT_Done_Face = Interop.downcallHandle("FT_Done_Face", + FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.ADDRESS)); } diff --git a/src/main/java/org/freedesktop/freetype/Library.java b/src/main/java/org/freedesktop/freetype/Library.java index 8e4906f..452a3af 100644 --- a/src/main/java/org/freedesktop/freetype/Library.java +++ b/src/main/java/org/freedesktop/freetype/Library.java @@ -21,13 +21,10 @@ import io.github.jwharm.cairobindings.Proxy; import io.github.jwharm.cairobindings.Interop; -import io.github.jwharm.cairobindings.MemoryCleaner; import java.lang.foreign.Arena; import java.lang.foreign.FunctionDescriptor; import java.lang.foreign.MemorySegment; -import java.lang.foreign.SegmentAllocator; -import java.lang.foreign.SegmentScope; import java.lang.foreign.ValueLayout; import java.lang.invoke.MethodHandle; @@ -43,6 +40,9 @@ public class Library extends Proxy { FreeType2.ensureInitialized(); } + // The Arena in which the handle to the Library object was allocated. + private Arena allocator = null; + /** * Constructor used internally to instantiate a java Library object for a native * {@code FT_Library} instance @@ -51,36 +51,29 @@ public class Library extends Proxy { */ public Library(MemorySegment address) { super(address); - MemoryCleaner.setFreeFunc(handle(), "FT_Done_FreeType"); - } - - /** - * Invokes the cleanup action that is normally invoked during garbage collection. - * If the instance is "owned" by the user, the {@code destroy()} function is run - * to dispose the native instance. - */ - public void destroy() { - MemoryCleaner.free(handle()); } /** * Initialize a new FreeType library object. - * + *

+ * The resulting Library instance must be cleaned up manually with a call to + * {@link #doneFreeType()}. + * * @return a new library object * @throws UnsupportedOperationException when {@code FT_Init_FreeType} returns a * non-zero error code */ public static Library initFreeType() throws UnsupportedOperationException { try { - MemorySegment pointer = SegmentAllocator.nativeAllocator(SegmentScope.auto()) - .allocate(ValueLayout.ADDRESS.asUnbounded()); + Arena allocator = Arena.ofConfined(); + MemorySegment pointer = allocator.allocate(ValueLayout.ADDRESS.withTargetLayout(ValueLayout.ADDRESS)); int result = (int) FT_Init_FreeType.invoke(pointer); if (result != 0) { throw new UnsupportedOperationException( "Error " + result + " occurred during FreeType library initialization"); } Library library = new Library(pointer.get(ValueLayout.ADDRESS, 0)); - MemoryCleaner.takeOwnership(library.handle()); + library.allocator = allocator; return library; } catch (Throwable e) { throw new RuntimeException(e); @@ -98,7 +91,7 @@ public static Library initFreeType() throws UnsupportedOperationException { */ public String version() { try { - try (Arena arena = Arena.openConfined()) { + try (Arena arena = Arena.ofConfined()) { MemorySegment amajor = arena.allocate(ValueLayout.JAVA_INT); MemorySegment aminor = arena.allocate(ValueLayout.JAVA_INT); MemorySegment apatch = arena.allocate(ValueLayout.JAVA_INT); @@ -116,4 +109,23 @@ public String version() { private static final MethodHandle FT_Library_Version = Interop.downcallHandle( "FT_Library_Version", FunctionDescriptor.ofVoid(ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS)); + + /** + * Destroy a given FreeType library object and all of its children, including + * resources, drivers, faces, sizes, etc. + */ + public void doneFreeType() { + try { + int ignored = (int) FT_Done_FreeType.invoke(handle()); + } catch (Throwable e) { + throw new RuntimeException(e); + } + if (allocator != null) { + allocator.close(); + allocator = null; + } + } + + private static final MethodHandle FT_Done_FreeType = Interop.downcallHandle("FT_Done_FreeType", + FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.ADDRESS)); } diff --git a/src/test/java/org/freedesktop/cairo/test/ContextTest.java b/src/test/java/org/freedesktop/cairo/test/ContextTest.java index 6fcdef5..ecb1c4b 100644 --- a/src/test/java/org/freedesktop/cairo/test/ContextTest.java +++ b/src/test/java/org/freedesktop/cairo/test/ContextTest.java @@ -1,6 +1,9 @@ package org.freedesktop.cairo.test; import java.io.IOException; +import java.lang.foreign.Arena; +import java.lang.foreign.MemorySegment; +import java.lang.foreign.ValueLayout; import org.freedesktop.cairo.*; import org.junit.jupiter.api.Test; @@ -355,7 +358,7 @@ void testStrokeExtents() { Context cr = createContext(); cr.moveTo(20d, 10d); cr.lineTo(30d, 40d); - Rectangle r = cr.strokeExtents(); + Rect r = cr.strokeExtents(); assertTrue(r.x() > 19d && r.x() < 20d); assertTrue(r.y() > 9d && r.y() < 10d); assertTrue(r.width() > 30d && r.width() < 31d); @@ -514,18 +517,20 @@ void testRectangle() { @Test void testGlyphPath() { - Context cr = createContext(); - ScaledFont font = ScaledFont.create(ToyFontFace.create("Arial", FontSlant.NORMAL, FontWeight.NORMAL), - Matrix.createIdentity(), Matrix.createIdentity(), FontOptions.create()); - Glyphs glyphs = font.textToGlyphs(0, 0, "test"); - assertEquals(4, glyphs.getNumGlyphs()); - var path = cr.glyphPath(glyphs).copyPath(); - assertNotNull(path); - // I'm not sure how many path elements there are in the glyphs; for me it reports 103, - // but I'm not sure if that will be the same on all systems and platforms. So let's - // just check if there are more than 10 - assertTrue(getPathLength(path) > 10); - assertEquals(Status.SUCCESS, cr.status()); + try (Arena arena = Arena.ofConfined()) { + Context cr = createContext(); + ScaledFont font = ScaledFont.create(ToyFontFace.create("Arial", FontSlant.NORMAL, FontWeight.NORMAL), + Matrix.create(arena).initIdentity(), Matrix.create(arena).initIdentity(), FontOptions.create()); + Glyphs glyphs = font.textToGlyphs(0, 0, "test"); + assertEquals(4, glyphs.getNumGlyphs()); + var path = cr.glyphPath(glyphs).copyPath(); + assertNotNull(path); + // I'm not sure how many path elements there are in the glyphs; for me it reports 103, + // but I'm not sure if that will be the same on all systems and platforms. So let's + // just check if there are more than 10 + assertTrue(getPathLength(path) > 10); + assertEquals(Status.SUCCESS, cr.status()); + } } @Test @@ -563,7 +568,7 @@ void testRelMoveTo() { void testPathExtents() { Context cr = createContext(); cr.rectangle(10, 20, 30, 40); - Rectangle rect = cr.pathExtents(); + Rect rect = cr.pathExtents(); assertEquals(10, rect.x()); assertEquals(20, rect.y()); assertEquals(40, rect.width()); @@ -594,17 +599,29 @@ void testRotate() { @Test void testTransform() { - Context cr = createContext(); - cr.transform(Matrix.createIdentity()); - assertEquals(Status.SUCCESS, cr.status()); + try (Arena arena = Arena.ofConfined()) { + Context cr = createContext(); + cr.transform(Matrix.create(arena).initIdentity()); + assertEquals(Status.SUCCESS, cr.status()); + } } @Test void testMatrix() { - Context cr = createContext(); - cr.setMatrix(Matrix.createIdentity()); - cr.getMatrix(); - assertEquals(Status.SUCCESS, cr.status()); + try (Arena arena = Arena.ofConfined()) { + Context cr = createContext(); + Matrix m1 = Matrix.create(arena).initTranslate(2.5, 3.0); + Matrix m2 = Matrix.create(arena); + cr.setMatrix(m1); + cr.getMatrix(m2); + assertEquals(m1.xx(), m2.xx()); + assertEquals(m1.yx(), m2.yx()); + assertEquals(m1.xy(), m2.xy()); + assertEquals(m1.yy(), m2.yy()); + assertEquals(m1.x0(), m2.x0()); + assertEquals(m1.y0(), m2.y0()); + assertEquals(Status.SUCCESS, cr.status()); + } } @Test @@ -645,11 +662,21 @@ void testDeviceToUserDistance() { @Test void testUserData() { - Context cr = createContext(); - int input = 12345; - UserDataKey key = cr.setUserData(input); - int output = (int) cr.getUserData(key); - assertEquals(input, output); - assertEquals(cr.status(), Status.SUCCESS); + try (Arena arena = Arena.ofConfined()) { + var segmentIn = arena.allocate(ValueLayout.JAVA_INT); + Context cr = createContext(); + + int input = 12345; + segmentIn.set(ValueLayout.JAVA_INT, 0, input); + + UserDataKey key = UserDataKey.create(arena); + cr.setUserData(key, segmentIn); + + var segmentOut = cr.getUserData(key).reinterpret(ValueLayout.JAVA_INT.byteSize()); + int output = segmentOut.get(ValueLayout.JAVA_INT, 0); + + assertEquals(input, output); + assertEquals(cr.status(), Status.SUCCESS); + } } } diff --git a/src/test/java/org/freedesktop/cairo/test/DeviceTest.java b/src/test/java/org/freedesktop/cairo/test/DeviceTest.java index f07aa3a..3c3d848 100644 --- a/src/test/java/org/freedesktop/cairo/test/DeviceTest.java +++ b/src/test/java/org/freedesktop/cairo/test/DeviceTest.java @@ -4,6 +4,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; +import java.lang.foreign.Arena; import java.nio.file.Path; import static org.junit.jupiter.api.Assertions.*; @@ -56,12 +57,14 @@ void acquireAndRelease() { @Test void testUserData() { - try (Device d = Script.create(tempDir.resolve("test.script").toString())) { - Rectangle input = Rectangle.create(0, 0, 10, 10); - UserDataKey key = d.setUserData(input); - Rectangle output = (Rectangle) d.getUserData(key); + try (Arena arena = Arena.ofConfined(); + Device device = Script.create(tempDir.resolve("test.script").toString())) { + Rectangle input = Rectangle.create(arena, 0, 0, 10, 10); + UserDataKey key = UserDataKey.create(arena); + device.setUserData(key, input.handle()); + Rectangle output = new Rectangle(device.getUserData(key)); assertEquals(input, output); - assertEquals(d.status(), Status.SUCCESS); + assertEquals(device.status(), Status.SUCCESS); } } -} \ No newline at end of file +} diff --git a/src/test/java/org/freedesktop/cairo/test/FTFontFaceTest.java b/src/test/java/org/freedesktop/cairo/test/FTFontFaceTest.java index dd1510d..909cad0 100644 --- a/src/test/java/org/freedesktop/cairo/test/FTFontFaceTest.java +++ b/src/test/java/org/freedesktop/cairo/test/FTFontFaceTest.java @@ -1,7 +1,5 @@ -package org.freedesktop.cairo.test; + package org.freedesktop.cairo.test; -import static org.freedesktop.cairo.FTSynthesize.BOLD; -import static org.freedesktop.cairo.FTSynthesize.OBLIQUE; import static org.junit.jupiter.api.Assertions.assertEquals; import org.freedesktop.cairo.FTFontFace; @@ -19,7 +17,7 @@ class FTFontFaceTest { private static String TTF_FILE; - + @BeforeAll static void setup() { // These files are going to be in different locations depending on your system. @@ -36,23 +34,27 @@ static void setup() { case "macos" -> TTF_FILE = "/Library/Fonts/Arial Unicode.ttf"; } } - + @Test void testCreate() { Library ftLib = Library.initFreeType(); - Face ftFace = new Face(ftLib, TTF_FILE, 0); + Face ftFace = Face.newFace(ftLib, TTF_FILE, 0); FTFontFace face = FTFontFace.create(ftFace, 0); assertEquals(Status.SUCCESS, face.status()); + ftFace.doneFace(); + ftLib.doneFreeType(); } @Test void testSynthesize() { Library ftLib = Library.initFreeType(); - Face ftFace = new Face(ftLib, TTF_FILE, 0); + Face ftFace = Face.newFace(ftLib, TTF_FILE, 0); FTFontFace face = FTFontFace.create(ftFace, 0); face.setSynthesize(FTSynthesize.BOLD.or(FTSynthesize.OBLIQUE)); face.unsetSynthesize(FTSynthesize.BOLD); assertEquals(FTSynthesize.OBLIQUE, face.getSynthesize()); assertEquals(Status.SUCCESS, face.status()); + ftFace.doneFace(); + ftLib.doneFreeType(); } } diff --git a/src/test/java/org/freedesktop/cairo/test/FTScaledFontTest.java b/src/test/java/org/freedesktop/cairo/test/FTScaledFontTest.java index 909e9ac..d9c8deb 100644 --- a/src/test/java/org/freedesktop/cairo/test/FTScaledFontTest.java +++ b/src/test/java/org/freedesktop/cairo/test/FTScaledFontTest.java @@ -1,7 +1,6 @@ package org.freedesktop.cairo.test; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; import io.github.jwharm.cairobindings.Platform; import org.freedesktop.cairo.FTFontFace; @@ -12,10 +11,10 @@ import org.freedesktop.freetype.Face; import org.freedesktop.freetype.Library; import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import java.io.File; +import java.lang.foreign.Arena; class FTScaledFontTest { @@ -40,21 +39,29 @@ static void setup() { @Test void testCreate() { - Library ftLib = Library.initFreeType(); - Face ftFace = new Face(ftLib, TTF_FILE, 0); - FTFontFace face = FTFontFace.create(ftFace, 0); - FTScaledFont scaledFont = FTScaledFont.create(face, Matrix.createIdentity(), Matrix.createIdentity(), FontOptions.create()); - assertEquals(Status.SUCCESS, scaledFont.status()); + try (Arena arena = Arena.ofConfined()) { + Library ftLib = Library.initFreeType(); + Face ftFace = Face.newFace(ftLib, TTF_FILE, 0); + FTFontFace face = FTFontFace.create(ftFace, 0); + FTScaledFont scaledFont = FTScaledFont.create(face, Matrix.create(arena).initIdentity(), Matrix.create(arena).initIdentity(), FontOptions.create()); + assertEquals(Status.SUCCESS, scaledFont.status()); + ftFace.doneFace(); + ftLib.doneFreeType(); + } } @Test void testLockAndUnlockFace() { - Library ftLib = Library.initFreeType(); - Face ftFace = new Face(ftLib, TTF_FILE, 0); - FTFontFace face = FTFontFace.create(ftFace, 0); - FTScaledFont scaledFont = FTScaledFont.create(face, Matrix.createIdentity(), Matrix.createIdentity(), FontOptions.create()); - scaledFont.lockFace(); - scaledFont.unlockFace(); - assertEquals(Status.SUCCESS, scaledFont.status()); + try (Arena arena = Arena.ofConfined()) { + Library ftLib = Library.initFreeType(); + Face ftFace = Face.newFace(ftLib, TTF_FILE, 0); + FTFontFace face = FTFontFace.create(ftFace, 0); + FTScaledFont scaledFont = FTScaledFont.create(face, Matrix.create(arena).initIdentity(), Matrix.create(arena).initIdentity(), FontOptions.create()); + scaledFont.lockFace(); + scaledFont.unlockFace(); + assertEquals(Status.SUCCESS, scaledFont.status()); + ftFace.doneFace(); + ftLib.doneFreeType(); + } } } diff --git a/src/test/java/org/freedesktop/cairo/test/ImageSurfaceTest.java b/src/test/java/org/freedesktop/cairo/test/ImageSurfaceTest.java index 9c810eb..e56e8e8 100644 --- a/src/test/java/org/freedesktop/cairo/test/ImageSurfaceTest.java +++ b/src/test/java/org/freedesktop/cairo/test/ImageSurfaceTest.java @@ -1,9 +1,8 @@ package org.freedesktop.cairo.test; import java.io.*; +import java.lang.foreign.Arena; import java.lang.foreign.MemorySegment; -import java.lang.foreign.SegmentAllocator; -import java.lang.foreign.SegmentScope; import java.nio.file.Files; import java.nio.file.Path; @@ -53,10 +52,12 @@ void testCreateFormatIntInt() { @Test void testCreateMemorySegmentFormatIntIntInt() { - int stride = ImageSurface.formatStrideForWidth(Format.ARGB32, 120); - MemorySegment data = SegmentAllocator.nativeAllocator(SegmentScope.auto()).allocate(120L * 120 * stride); - try (ImageSurface s = ImageSurface.create(data, Format.ARGB32, 120, 120, stride)) { - assertEquals(Status.SUCCESS, s.status()); + try (Arena arena = Arena.ofConfined()) { + int stride = ImageSurface.formatStrideForWidth(Format.ARGB32, 120); + MemorySegment data = arena.allocate(120L * 120 * stride); + try (ImageSurface s = ImageSurface.create(data, Format.ARGB32, 120, 120, stride)) { + assertEquals(Status.SUCCESS, s.status()); + } } } diff --git a/src/test/java/org/freedesktop/cairo/test/MatrixTest.java b/src/test/java/org/freedesktop/cairo/test/MatrixTest.java index 834a76f..708ba7b 100644 --- a/src/test/java/org/freedesktop/cairo/test/MatrixTest.java +++ b/src/test/java/org/freedesktop/cairo/test/MatrixTest.java @@ -7,88 +7,113 @@ import org.freedesktop.cairo.Point; import org.junit.jupiter.api.Test; -class MatrixTest { +import java.lang.foreign.Arena; - @Test - void testCreateDoubleDoubleDoubleDoubleDoubleDouble() { - Matrix matrix = Matrix.create(1, 0, 0, 1, 0, 0); - assertNotNull(matrix); - } +class MatrixTest { @Test void testInit() { - Matrix matrix = Matrix.create(1, 0, 0, 1, 0, 0); - matrix.init(2, 0, 0, 2, 0, 0); + try (Arena arena = Arena.ofConfined()) { + Matrix matrix = Matrix.create(arena).init(1, 0, 0, 1, 0, 0); + matrix.init(2, 0, 0, 2, 0, 0); + } } @Test void testCreateIdentity() { - Matrix matrix = Matrix.createIdentity(); - assertNotNull(matrix); + try (Arena arena = Arena.ofConfined()) { + Matrix matrix = Matrix.create(arena).initIdentity(); + assertNotNull(matrix); + } } @Test void testCreateTranslate() { - Matrix matrix = Matrix.createTranslate(20, 20); - assertNotNull(matrix); + try (Arena arena = Arena.ofConfined()) { + Matrix matrix = Matrix.create(arena).initTranslate(20, 20); + assertNotNull(matrix); + } } @Test void testCreateScale() { - Matrix matrix = Matrix.createScale(2, 2); - assertNotNull(matrix); + try (Arena arena = Arena.ofConfined()) { + Matrix matrix = Matrix.create(arena).initIdentity(); + matrix.scale(2, 2); + assertNotNull(matrix); + } } @Test void testCreateRotate() { - Matrix matrix = Matrix.createRotate(90); - assertNotNull(matrix); + try (Arena arena = Arena.ofConfined()) { + Matrix matrix = Matrix.create(arena).initIdentity(); + matrix.rotate(90); + assertNotNull(matrix); + } } @Test void testTranslate() { - Matrix matrix = Matrix.createIdentity(); - matrix.translate(10, 10); + try (Arena arena = Arena.ofConfined()) { + Matrix matrix = Matrix.create(arena).initIdentity(); + matrix.translate(10, 10); + } } @Test void testScale() { - Matrix matrix = Matrix.createTranslate(20, 20); - matrix.scale(0.5, 0.5); + try (Arena arena = Arena.ofConfined()) { + Matrix matrix = Matrix.create(arena).initTranslate(20, 20); + matrix.scale(0.5, 0.5); + } } @Test void testRotate() { - Matrix matrix = Matrix.createTranslate(20, 20); - matrix.rotate(180); + try (Arena arena = Arena.ofConfined()) { + Matrix matrix = Matrix.create(arena).initTranslate(20, 20); + matrix.rotate(180); + } } @Test void testInvert() { - Matrix matrix = Matrix.createTranslate(20, 20); - matrix.invert(); + try (Arena arena = Arena.ofConfined()) { + Matrix matrix = Matrix.create(arena).initTranslate(20, 20); + matrix.invert(); + } } @Test void testMultiply() { - Matrix matrix1 = Matrix.createRotate(90); - Matrix matrix2 = Matrix.createTranslate(20, 20); - matrix1.multiply(matrix2); + try (Arena arena = Arena.ofConfined()) { + Matrix matrix1 = Matrix.create(arena).initIdentity(); + matrix1.rotate(90); + Matrix matrix2 = Matrix.create(arena).initTranslate(20, 20); + Matrix matrix3 = Matrix.create(arena).initIdentity(); + matrix3.multiply(matrix1, matrix2); + } } @Test void testTransformDistance() { - Matrix matrix = Matrix.createScale(3, 2); - Point point = matrix.transformDistance(new Point(10, 10)); - assertEquals(30, point.x()); - assertEquals(20, point.y()); + try (Arena arena = Arena.ofConfined()) { + Matrix matrix = Matrix.create(arena).initIdentity(); + matrix.scale(3, 2); + Point point = matrix.transformDistance(new Point(10, 10)); + assertEquals(30, point.x()); + assertEquals(20, point.y()); + } } @Test void testTransformPoint() { - Matrix matrix = Matrix.createTranslate(20, 30); - Point point = matrix.transformPoint(new Point(30, 40)); - assertEquals(50, point.x()); - assertEquals(70, point.y()); + try (Arena arena = Arena.ofConfined()) { + Matrix matrix = Matrix.create(arena).initTranslate(20, 30); + Point point = matrix.transformPoint(new Point(30, 40)); + assertEquals(50, point.x()); + assertEquals(70, point.y()); + } } } diff --git a/src/test/java/org/freedesktop/cairo/test/PatternTest.java b/src/test/java/org/freedesktop/cairo/test/PatternTest.java index 2c32a95..52c1c56 100644 --- a/src/test/java/org/freedesktop/cairo/test/PatternTest.java +++ b/src/test/java/org/freedesktop/cairo/test/PatternTest.java @@ -6,6 +6,8 @@ import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; +import java.lang.foreign.Arena; + class PatternTest { @Test @@ -34,11 +36,15 @@ void testFilter() { @Test void testMatrix() { - Gradient pattern = LinearGradient.create(0, 0, 10, 10); - pattern.setMatrix(Matrix.createScale(2, 2)); - Matrix m = Matrix.create(0, 0, 0, 0, 0, 0); - pattern.getMatrix(m); - assertEquals(Status.SUCCESS, pattern.status()); + try (Arena arena = Arena.ofConfined()) { + Gradient pattern = LinearGradient.create(0, 0, 10, 10); + Matrix scale = Matrix.create(arena).initIdentity(); + scale.scale(2, 2); + pattern.setMatrix(scale); + Matrix m = Matrix.create(arena).init(0, 0, 0, 0, 0, 0); + pattern.getMatrix(m); + assertEquals(Status.SUCCESS, pattern.status()); + } } @Test diff --git a/src/test/java/org/freedesktop/cairo/test/RecordingSurfaceTest.java b/src/test/java/org/freedesktop/cairo/test/RecordingSurfaceTest.java index cb9d5e9..08f8313 100644 --- a/src/test/java/org/freedesktop/cairo/test/RecordingSurfaceTest.java +++ b/src/test/java/org/freedesktop/cairo/test/RecordingSurfaceTest.java @@ -4,6 +4,7 @@ import org.junit.jupiter.api.Test; import java.io.IOException; +import java.lang.foreign.Arena; import static org.junit.jupiter.api.Assertions.*; @@ -11,8 +12,9 @@ class RecordingSurfaceTest { @Test void testCreate() throws IOException { - try (Surface s = ImageSurface.create(Format.ARGB32, 120, 120); - RecordingSurface r = RecordingSurface.create(Content.COLOR_ALPHA, Rectangle.create(20, 20, 50, 50))) { + try (Arena arena = Arena.ofConfined(); + Surface s = ImageSurface.create(Format.ARGB32, 120, 120); + RecordingSurface r = RecordingSurface.create(Content.COLOR_ALPHA, Rectangle.create(arena, 20, 20, 50, 50))) { assertEquals(Status.SUCCESS, s.status()); assertEquals(Status.SUCCESS, r.status()); } @@ -25,7 +27,7 @@ void testInkExtents() throws IOException { Context.create(r) .rectangle(12, 14, 16, 18) .fill(); - Rectangle rect = r.inkExtents(); + Rect rect = r.inkExtents(); assertEquals(12, rect.x()); assertEquals(14, rect.y()); assertEquals(16, rect.width()); @@ -37,9 +39,11 @@ void testInkExtents() throws IOException { @Test void testGetExtents() { - try (Surface s = ImageSurface.create(Format.ARGB32, 120, 120); - RecordingSurface r = RecordingSurface.create(Content.COLOR_ALPHA, Rectangle.create(20, 30, 50, 60))) { - Rectangle rect = r.getExtents(); + try (Arena arena = Arena.ofConfined(); + Surface s = ImageSurface.create(Format.ARGB32, 120, 120); + RecordingSurface r = RecordingSurface.create(Content.COLOR_ALPHA, Rectangle.create(arena, 20, 30, 50, 60))) { + Rectangle rect = Rectangle.create(arena, 0, 0, 0, 0); + r.getExtents(rect); assertEquals(20, rect.x()); assertEquals(30, rect.y()); assertEquals(50, rect.width()); diff --git a/src/test/java/org/freedesktop/cairo/test/RegionTest.java b/src/test/java/org/freedesktop/cairo/test/RegionTest.java index 7db5510..dc1ebf5 100644 --- a/src/test/java/org/freedesktop/cairo/test/RegionTest.java +++ b/src/test/java/org/freedesktop/cairo/test/RegionTest.java @@ -10,6 +10,8 @@ import org.freedesktop.cairo.Status; import org.junit.jupiter.api.Test; +import java.lang.foreign.Arena; + class RegionTest { @Test @@ -20,173 +22,249 @@ void testCreate() { @Test void testCreateRectangleInt() { - Region region = Region.create(RectangleInt.create(10, 20, 100, 200)); - assertEquals(Status.SUCCESS, region.status()); + try (Arena arena = Arena.ofConfined()) { + Region region = Region.create(RectangleInt.create(arena, 10, 20, 100, 200)); + assertEquals(Status.SUCCESS, region.status()); + } } @Test void testCreateRectangleIntArray() { - Region region = Region.create( - new RectangleInt[] { RectangleInt.create(10, 20, 100, 200), RectangleInt.create(20, 30, 100, 200) }); - assertEquals(Status.SUCCESS, region.status()); + try (Arena arena = Arena.ofConfined()) { + Region region = Region.create( + new RectangleInt[]{ + RectangleInt.create(arena, 10, 20, 100, 200), + RectangleInt.create(arena, 20, 30, 100, 200) + }); + assertEquals(Status.SUCCESS, region.status()); + } } @Test void testCopy() { - Region region1 = Region.create(RectangleInt.create(10, 20, 100, 200)); - Region region2 = region1.copy(); - assertTrue(region1.equal(region2)); - assertEquals(Status.SUCCESS, region1.status()); + try (Arena arena = Arena.ofConfined()) { + Region region1 = Region.create(RectangleInt.create(arena, 10, 20, 100, 200)); + Region region2 = region1.copy(); + assertTrue(region1.equal(region2)); + assertEquals(Status.SUCCESS, region1.status()); + } } @Test void testGetExtents() { - Region region = Region.create(RectangleInt.create(10, 20, 100, 200)); - RectangleInt r = region.getExtents(); - assertTrue(r.toString().endsWith("x=10 y=20 width=100 height=200")); - assertEquals(Status.SUCCESS, region.status()); + try (Arena arena = Arena.ofConfined()) { + Region region = Region.create(RectangleInt.create(arena, 10, 20, 100, 200)); + RectangleInt r = RectangleInt.create(arena, 0, 0, 0, 0); + region.getExtents(r); + assertTrue(r.toString().endsWith("x=10 y=20 width=100 height=200")); + assertEquals(Status.SUCCESS, region.status()); + } } @Test void testNumRectangles() { - Region region = Region.create(RectangleInt.create(10, 20, 100, 200)); - int num = region.numRectangles(); - assertEquals(1, num); - assertEquals(Status.SUCCESS, region.status()); + try (Arena arena = Arena.ofConfined()) { + Region region = Region.create(RectangleInt.create(arena, 10, 20, 100, 200)); + int num = region.numRectangles(); + assertEquals(1, num); + assertEquals(Status.SUCCESS, region.status()); + } } @Test void testGetRectangle() { - Region region = Region.create(RectangleInt.create(10, 20, 100, 200)); - RectangleInt r = region.getRectangle(0); - assertTrue(r.toString().endsWith("x=10 y=20 width=100 height=200")); - assertEquals(Status.SUCCESS, region.status()); + try (Arena arena = Arena.ofConfined()) { + Region region = Region.create(RectangleInt.create(arena, 10, 20, 100, 200)); + RectangleInt r = RectangleInt.create(arena, 0, 0, 0, 0); + region.getRectangle(0, r); + assertTrue(r.toString().endsWith("x=10 y=20 width=100 height=200")); + assertEquals(Status.SUCCESS, region.status()); + } } @Test void testIsEmpty() { - Region region1 = Region.create(); - Region region2 = Region.create(RectangleInt.create(10, 20, 100, 200)); - assertTrue(region1.isEmpty()); - assertFalse(region2.isEmpty()); - assertEquals(Status.SUCCESS, region1.status()); - assertEquals(Status.SUCCESS, region2.status()); + try (Arena arena = Arena.ofConfined()) { + Region region1 = Region.create(); + Region region2 = Region.create(RectangleInt.create(arena, 10, 20, 100, 200)); + assertTrue(region1.isEmpty()); + assertFalse(region2.isEmpty()); + assertEquals(Status.SUCCESS, region1.status()); + assertEquals(Status.SUCCESS, region2.status()); + } } @Test void testContainsPoint() { - Region region = Region.create(RectangleInt.create(10, 20, 100, 200)); - assertTrue(region.containsPoint(10, 20)); - assertFalse(region.containsPoint(9, 20)); - assertEquals(Status.SUCCESS, region.status()); + try (Arena arena = Arena.ofConfined()) { + Region region = Region.create(RectangleInt.create(arena, 10, 20, 100, 200)); + assertTrue(region.containsPoint(10, 20)); + assertFalse(region.containsPoint(9, 20)); + assertEquals(Status.SUCCESS, region.status()); + } } @Test void testContainsRectangle() { - Region region = Region.create(RectangleInt.create(10, 20, 100, 200)); - assertEquals(RegionOverlap.IN, region.containsRectangle(RectangleInt.create(10, 20, 100, 200))); - assertEquals(RegionOverlap.PART, region.containsRectangle(RectangleInt.create(11, 21, 100, 200))); - assertEquals(RegionOverlap.OUT, region.containsRectangle(RectangleInt.create(111, 221, 100, 200))); - assertEquals(Status.SUCCESS, region.status()); + try (Arena arena = Arena.ofConfined()) { + Region region = Region.create(RectangleInt.create(arena, 10, 20, 100, 200)); + assertEquals(RegionOverlap.IN, region.containsRectangle(RectangleInt.create(arena, 10, 20, 100, 200))); + assertEquals(RegionOverlap.PART, region.containsRectangle(RectangleInt.create(arena, 11, 21, 100, 200))); + assertEquals(RegionOverlap.OUT, region.containsRectangle(RectangleInt.create(arena, 111, 221, 100, 200))); + assertEquals(Status.SUCCESS, region.status()); + } } @Test void testEqual() { - Region region1 = Region.create(RectangleInt.create(10, 20, 100, 200)); - Region region2 = Region.create(RectangleInt.create(10, 20, 100, 200)); - Region region3 = Region.create(RectangleInt.create(11, 20, 100, 200)); - assertTrue(region1.equal(region2)); - assertFalse(region1.equal(region3)); - assertEquals(Status.SUCCESS, region1.status()); + try (Arena arena = Arena.ofConfined()) { + Region region1 = Region.create(RectangleInt.create(arena, 10, 20, 100, 200)); + Region region2 = Region.create(RectangleInt.create(arena, 10, 20, 100, 200)); + Region region3 = Region.create(RectangleInt.create(arena, 11, 20, 100, 200)); + assertTrue(region1.equal(region2)); + assertFalse(region1.equal(region3)); + assertEquals(Status.SUCCESS, region1.status()); + } } @Test void testTranslate() { - Region region = Region.create(RectangleInt.create(10, 20, 100, 200)); - region.translate(2, -2); - assertEquals(12, region.getExtents().x()); - assertEquals(18, region.getExtents().y()); - assertEquals(Status.SUCCESS, region.status()); + try (Arena arena = Arena.ofConfined()) { + Region region = Region.create(RectangleInt.create(arena, 10, 20, 100, 200)); + region.translate(2, -2); + var extents = RectangleInt.create(arena, 0, 0, 0, 0); + region.getExtents(extents); + assertEquals(12, extents.x()); + assertEquals(18, extents.y()); + assertEquals(Status.SUCCESS, region.status()); + } } @Test void testIntersectRegion() { - Region region1 = Region.create(RectangleInt.create(10, 20, 100, 200)); - Region region2 = Region.create(RectangleInt.create(50, 80, 100, 200)); - region1.intersect(region2); - assertTrue(region1.getExtents().toString().endsWith("x=50 y=80 width=60 height=140")); - assertTrue(region2.getExtents().toString().endsWith("x=50 y=80 width=100 height=200")); - assertEquals(Status.SUCCESS, region1.status()); - assertEquals(Status.SUCCESS, region2.status()); + try (Arena arena = Arena.ofConfined()) { + Region region1 = Region.create(RectangleInt.create(arena, 10, 20, 100, 200)); + Region region2 = Region.create(RectangleInt.create(arena, 50, 80, 100, 200)); + region1.intersect(region2); + + var extents1 = RectangleInt.create(arena, 0, 0, 0, 0); + var extents2 = RectangleInt.create(arena, 0, 0, 0, 0); + region1.getExtents(extents1); + region2.getExtents(extents2); + + assertTrue(extents1.toString().endsWith("x=50 y=80 width=60 height=140")); + assertTrue(extents2.toString().endsWith("x=50 y=80 width=100 height=200")); + assertEquals(Status.SUCCESS, region1.status()); + assertEquals(Status.SUCCESS, region2.status()); + } } @Test void testIntersectRectangleInt() { - Region region = Region.create(RectangleInt.create(10, 20, 100, 200)); - RectangleInt rect = RectangleInt.create(50, 80, 100, 200); - region.intersect(rect); - assertTrue(region.getExtents().toString().endsWith("x=50 y=80 width=60 height=140")); - assertEquals(Status.SUCCESS, region.status()); + try (Arena arena = Arena.ofConfined()) { + Region region = Region.create(RectangleInt.create(arena, 10, 20, 100, 200)); + RectangleInt rect = RectangleInt.create(arena, 50, 80, 100, 200); + region.intersect(rect); + RectangleInt extents = RectangleInt.create(arena, 0, 0, 0, 0); + region.getExtents(extents); + assertTrue(extents.toString().endsWith("x=50 y=80 width=60 height=140")); + assertEquals(Status.SUCCESS, region.status()); + } } @Test void testSubtractRegion() { - Region region1 = Region.create(RectangleInt.create(10, 20, 100, 200)); - Region region2 = Region.create(RectangleInt.create(20, 20, 100, 200)); - region1.subtract(region2); - assertTrue(region1.getExtents().toString().endsWith("x=10 y=20 width=10 height=200")); - assertTrue(region2.getExtents().toString().endsWith("x=20 y=20 width=100 height=200")); - assertEquals(Status.SUCCESS, region1.status()); - assertEquals(Status.SUCCESS, region2.status()); + try (Arena arena = Arena.ofConfined()) { + Region region1 = Region.create(RectangleInt.create(arena, 10, 20, 100, 200)); + Region region2 = Region.create(RectangleInt.create(arena, 20, 20, 100, 200)); + region1.subtract(region2); + + var extents1 = RectangleInt.create(arena, 0, 0, 0, 0); + var extents2 = RectangleInt.create(arena, 0, 0, 0, 0); + region1.getExtents(extents1); + region2.getExtents(extents2); + + assertTrue(extents1.toString().endsWith("x=10 y=20 width=10 height=200")); + assertTrue(extents2.toString().endsWith("x=20 y=20 width=100 height=200")); + assertEquals(Status.SUCCESS, region1.status()); + assertEquals(Status.SUCCESS, region2.status()); + } } @Test void testSubtractRectangleInt() { - Region region = Region.create(RectangleInt.create(10, 20, 100, 200)); - RectangleInt rect = RectangleInt.create(20, 20, 100, 200); - region.subtract(rect); - assertTrue(region.getExtents().toString().endsWith("x=10 y=20 width=10 height=200")); - assertEquals(Status.SUCCESS, region.status()); + try (Arena arena = Arena.ofConfined()) { + Region region = Region.create(RectangleInt.create(arena, 10, 20, 100, 200)); + RectangleInt rect = RectangleInt.create(arena, 20, 20, 100, 200); + region.subtract(rect); + RectangleInt extents = RectangleInt.create(arena, 0, 0, 0, 0); + region.getExtents(extents); + assertTrue(extents.toString().endsWith("x=10 y=20 width=10 height=200")); + assertEquals(Status.SUCCESS, region.status()); + } } @Test void testUnionRegion() { - Region region1 = Region.create(RectangleInt.create(10, 20, 100, 200)); - Region region2 = Region.create(RectangleInt.create(20, 20, 100, 200)); - region1.union(region2); - assertTrue(region1.getExtents().toString().endsWith("x=10 y=20 width=110 height=200")); - assertTrue(region2.getExtents().toString().endsWith("x=20 y=20 width=100 height=200")); - assertEquals(Status.SUCCESS, region1.status()); - assertEquals(Status.SUCCESS, region2.status()); + try (Arena arena = Arena.ofConfined()) { + Region region1 = Region.create(RectangleInt.create(arena, 10, 20, 100, 200)); + Region region2 = Region.create(RectangleInt.create(arena, 20, 20, 100, 200)); + region1.union(region2); + + var extents1 = RectangleInt.create(arena, 0, 0, 0, 0); + var extents2 = RectangleInt.create(arena, 0, 0, 0, 0); + region1.getExtents(extents1); + region2.getExtents(extents2); + + assertTrue(extents1.toString().endsWith("x=10 y=20 width=110 height=200")); + assertTrue(extents2.toString().endsWith("x=20 y=20 width=100 height=200")); + assertEquals(Status.SUCCESS, region1.status()); + assertEquals(Status.SUCCESS, region2.status()); + } } @Test void testUnionRectangleInt() { - Region region = Region.create(RectangleInt.create(10, 20, 100, 200)); - RectangleInt rect = RectangleInt.create(20, 20, 100, 200); - region.union(rect); - assertTrue(region.getExtents().toString().endsWith("x=10 y=20 width=110 height=200")); - assertEquals(Status.SUCCESS, region.status()); + try (Arena arena = Arena.ofConfined()) { + Region region = Region.create(RectangleInt.create(arena, 10, 20, 100, 200)); + RectangleInt rect = RectangleInt.create(arena, 20, 20, 100, 200); + region.union(rect); + RectangleInt extents = RectangleInt.create(arena, 0, 0, 0, 0); + region.getExtents(extents); + assertTrue(extents.toString().endsWith("x=10 y=20 width=110 height=200")); + assertEquals(Status.SUCCESS, region.status()); + } } @Test void testXorRegion() { - Region region1 = Region.create(RectangleInt.create(0, 0, 50, 50)); - Region region2 = Region.create(RectangleInt.create(0, 0, 50, 20)); - region1.xor(region2); - assertTrue(region1.getExtents().toString().endsWith("x=0 y=20 width=50 height=30")); - assertTrue(region2.getExtents().toString().endsWith("x=0 y=0 width=50 height=20")); - assertEquals(Status.SUCCESS, region1.status()); - assertEquals(Status.SUCCESS, region2.status()); + try (Arena arena = Arena.ofConfined()) { + Region region1 = Region.create(RectangleInt.create(arena, 0, 0, 50, 50)); + Region region2 = Region.create(RectangleInt.create(arena, 0, 0, 50, 20)); + region1.xor(region2); + + var extents1 = RectangleInt.create(arena, 0, 0, 0, 0); + var extents2 = RectangleInt.create(arena, 0, 0, 0, 0); + region1.getExtents(extents1); + region2.getExtents(extents2); + + assertTrue(extents1.toString().endsWith("x=0 y=20 width=50 height=30")); + assertTrue(extents2.toString().endsWith("x=0 y=0 width=50 height=20")); + assertEquals(Status.SUCCESS, region1.status()); + assertEquals(Status.SUCCESS, region2.status()); + } } @Test void testXorRectangleInt() { - Region region = Region.create(RectangleInt.create(0, 0, 50, 50)); - RectangleInt rect = RectangleInt.create(0, 0, 50, 20); - region.xor(rect); - assertTrue(region.getExtents().toString().endsWith("x=0 y=20 width=50 height=30")); - assertEquals(Status.SUCCESS, region.status()); + try (Arena arena = Arena.ofConfined()) { + Region region = Region.create(RectangleInt.create(arena, 0, 0, 50, 50)); + RectangleInt rect = RectangleInt.create(arena, 0, 0, 50, 20); + region.xor(rect); + RectangleInt extents = RectangleInt.create(arena, 0, 0, 0, 0); + region.getExtents(extents); + assertTrue(extents.toString().endsWith("x=0 y=20 width=50 height=30")); + assertEquals(Status.SUCCESS, region.status()); + } } - } diff --git a/src/test/java/org/freedesktop/cairo/test/ScaledFontTest.java b/src/test/java/org/freedesktop/cairo/test/ScaledFontTest.java index b6ca767..227cf65 100644 --- a/src/test/java/org/freedesktop/cairo/test/ScaledFontTest.java +++ b/src/test/java/org/freedesktop/cairo/test/ScaledFontTest.java @@ -7,42 +7,40 @@ import org.freedesktop.cairo.*; import org.junit.jupiter.api.Test; +import java.lang.foreign.Arena; + class ScaledFontTest { - private ScaledFont create() { + private ScaledFont create(Arena arena) { return ScaledFont.create(ToyFontFace.create("Arial", FontSlant.NORMAL, FontWeight.NORMAL), - Matrix.createIdentity(), Matrix.createIdentity(), FontOptions.create()); + Matrix.create(arena).initIdentity(), Matrix.create(arena).initIdentity(), FontOptions.create()); } @Test void testCreate() { - ScaledFont sf = create(); - assertEquals(Status.SUCCESS, sf.status()); + try (Arena arena = Arena.ofConfined()) { + ScaledFont sf = create(arena); + assertEquals(Status.SUCCESS, sf.status()); + } } @Test void testExtents() { - ScaledFont sf = create(); - FontExtents e = sf.extents(); - assertNotNull(e); - assertEquals(Status.SUCCESS, sf.status()); + try (Arena arena = Arena.ofConfined()) { + ScaledFont sf = create(arena); + FontExtents e = FontExtents.create(arena); + sf.extents(e); + assertNotNull(e); + assertEquals(Status.SUCCESS, sf.status()); + } } @Test void testTextExtents() { - ScaledFont sf = create(); - TextExtents extents = sf.textExtents("test"); - // I'm not sure if I can hard-code the expected height and width, so - // we will only test if the extents are wider than they are high. - assertTrue(extents.width() > extents.height()); - assertEquals(Status.SUCCESS, sf.status()); - } - - @Test - void testGlyphExtents() { - ScaledFont sf = create(); - try (Glyphs glyphs = sf.textToGlyphs(0, 0, "test")) { - TextExtents extents = sf.glyphExtents(glyphs); + try (Arena arena = Arena.ofConfined()) { + ScaledFont sf = create(arena); + TextExtents extents = TextExtents.create(arena); + sf.textExtents("test", extents); // I'm not sure if I can hard-code the expected height and width, so // we will only test if the extents are wider than they are high. assertTrue(extents.width() > extents.height()); @@ -50,61 +48,94 @@ void testGlyphExtents() { } } + @Test + void testGlyphExtents() { + try (Arena arena = Arena.ofConfined()) { + ScaledFont sf = create(arena); + try (Glyphs glyphs = sf.textToGlyphs(0, 0, "test")) { + TextExtents extents = TextExtents.create(arena); + sf.glyphExtents(glyphs, extents); + // I'm not sure if I can hard-code the expected height and width, so + // we will only test if the extents are wider than they are high. + assertTrue(extents.width() > extents.height()); + assertEquals(Status.SUCCESS, sf.status()); + } + } + } + @Test void testTextToGlyphs() { - ScaledFont sf = create(); - try (Glyphs glyphs = sf.textToGlyphs(0, 0, "test")) { - assertEquals(4, glyphs.getNumGlyphs()); - assertEquals(Status.SUCCESS, sf.status()); + try (Arena arena = Arena.ofConfined()) { + ScaledFont sf = create(arena); + try (Glyphs glyphs = sf.textToGlyphs(0, 0, "test")) { + assertEquals(4, glyphs.getNumGlyphs()); + assertEquals(Status.SUCCESS, sf.status()); + } } } @Test void testGetFontFace() { - ScaledFont sf = create(); - FontFace f = sf.getFontFace(); - assertEquals(FontType.TOY, f.getFontType()); - assertEquals(Status.SUCCESS, f.status()); - assertEquals(Status.SUCCESS, sf.status()); + try (Arena arena = Arena.ofConfined()) { + ScaledFont sf = create(arena); + FontFace f = sf.getFontFace(); + assertEquals(FontType.TOY, f.getFontType()); + assertEquals(Status.SUCCESS, f.status()); + assertEquals(Status.SUCCESS, sf.status()); + } } @Test void testGetFontOptions() { - ScaledFont sf = create(); - FontOptions options = FontOptions.create(); - sf.getFontOptions(options); - assertEquals(Status.SUCCESS, options.status()); - assertEquals(Status.SUCCESS, sf.status()); + try (Arena arena = Arena.ofConfined()) { + ScaledFont sf = create(arena); + FontOptions options = FontOptions.create(); + sf.getFontOptions(options); + assertEquals(Status.SUCCESS, options.status()); + assertEquals(Status.SUCCESS, sf.status()); + } } @Test void testGetFontMatrix() { - ScaledFont sf = create(); - Matrix m = sf.getFontMatrix(); - assertNotNull(m); - assertEquals(Status.SUCCESS, sf.status()); + try (Arena arena = Arena.ofConfined()) { + ScaledFont sf = create(arena); + Matrix m = Matrix.create(arena); + sf.getFontMatrix(m); + assertNotNull(m); + assertEquals(Status.SUCCESS, sf.status()); + } } @Test void testGetCTM() { - ScaledFont sf = create(); - Matrix ctm = sf.getCTM(); - assertNotNull(ctm); - assertEquals(Status.SUCCESS, sf.status()); + try (Arena arena = Arena.ofConfined()) { + ScaledFont sf = create(arena); + Matrix ctm = Matrix.create(arena); + sf.getCTM(ctm); + assertNotNull(ctm); + assertEquals(Status.SUCCESS, sf.status()); + } } @Test void testGetScaleMatrix() { - ScaledFont sf = create(); - Matrix m = sf.getScaleMatrix(); - assertNotNull(m); - assertEquals(Status.SUCCESS, sf.status()); + try (Arena arena = Arena.ofConfined()) { + ScaledFont sf = create(arena); + Matrix m = Matrix.create(arena); + sf.getScaleMatrix(m); + assertNotNull(m); + assertEquals(Status.SUCCESS, sf.status()); + } } @Test void testGetFontType() { - ScaledFont sf = create(); - FontType t = sf.getFontType(); - assertEquals(Status.SUCCESS, sf.status()); + try (Arena arena = Arena.ofConfined()) { + ScaledFont sf = create(arena); + FontType t = sf.getFontType(); + assertNotNull(t); + assertEquals(Status.SUCCESS, sf.status()); + } } } diff --git a/src/test/java/org/freedesktop/cairo/test/SurfaceTest.java b/src/test/java/org/freedesktop/cairo/test/SurfaceTest.java index f1963ec..cc9f0be 100644 --- a/src/test/java/org/freedesktop/cairo/test/SurfaceTest.java +++ b/src/test/java/org/freedesktop/cairo/test/SurfaceTest.java @@ -2,7 +2,9 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.lang.foreign.Arena; +import io.github.jwharm.cairobindings.Interop; import org.freedesktop.cairo.*; import org.junit.jupiter.api.Test; @@ -194,8 +196,9 @@ void testSupportsMimeType() { @Test void testMapToImage() { - try (Surface s1 = ImageSurface.create(Format.ARGB32, 120, 120); - ImageSurface s2 = s1.mapToImage(RectangleInt.create(0, 0, 120, 120))) { + try (Arena arena = Arena.ofConfined(); + Surface s1 = ImageSurface.create(Format.ARGB32, 120, 120); + ImageSurface s2 = s1.mapToImage(RectangleInt.create(arena, 0, 0, 120, 120))) { s1.unmapImage(s2); assertEquals(Status.SUCCESS, s1.status()); assertEquals(Status.SUCCESS, s2.status()); @@ -204,10 +207,17 @@ void testMapToImage() { @Test void testUserData() { - try (Surface s = ImageSurface.create(Format.ARGB32, 120, 120)) { + try (Arena arena = Arena.ofConfined(); + Surface s = ImageSurface.create(Format.ARGB32, 120, 120)) { String input = "test"; - UserDataKey key = s.setUserData(input); - String output = (String) s.getUserData(key); + var segmentIn = Interop.allocateNativeString(input, arena); + + UserDataKey key = UserDataKey.create(arena); + s.setUserData(key, segmentIn); + + var segmentOut = s.getUserData(key).reinterpret(Integer.MAX_VALUE); + String output = segmentOut.getUtf8String(0); + assertEquals(input, output); assertEquals(Status.SUCCESS, s.status()); } diff --git a/src/test/java/org/freedesktop/cairo/test/ToyFontFaceTest.java b/src/test/java/org/freedesktop/cairo/test/ToyFontFaceTest.java index 50c91b4..d70cd90 100644 --- a/src/test/java/org/freedesktop/cairo/test/ToyFontFaceTest.java +++ b/src/test/java/org/freedesktop/cairo/test/ToyFontFaceTest.java @@ -6,7 +6,6 @@ import org.freedesktop.cairo.FontWeight; import org.freedesktop.cairo.Status; import org.freedesktop.cairo.ToyFontFace; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; class ToyFontFaceTest {