Skip to content

Commit

Permalink
Port to JDK 21
Browse files Browse the repository at this point in the history
  • Loading branch information
jwharm committed Nov 5, 2023
1 parent 67e50a6 commit 70a4840
Show file tree
Hide file tree
Showing 76 changed files with 1,300 additions and 1,304 deletions.
102 changes: 50 additions & 52 deletions README.md
Original file line number Diff line number Diff line change
@@ -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).
Expand All @@ -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

Expand All @@ -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`.

Expand Down Expand Up @@ -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/):
Expand All @@ -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");
}
}
}
```
Expand All @@ -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).
6 changes: 3 additions & 3 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,15 @@ plugins {
}

group = "io.github.jwharm.cairobindings"
version = "1.18.0"
version = "1.18.1-SNAPSHOT"

repositories {
mavenCentral()
}

java {
toolchain {
languageVersion = JavaLanguageVersion.of(20)
languageVersion = JavaLanguageVersion.of(21)
}
withSourcesJar()
withJavadocJar()
Expand All @@ -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') {
Expand Down
2 changes: 1 addition & 1 deletion settings.gradle
Original file line number Diff line number Diff line change
@@ -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'
39 changes: 39 additions & 0 deletions src/main/java/io/github/jwharm/cairobindings/ArenaCloseAction.java
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.
*/
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();
}
}
27 changes: 0 additions & 27 deletions src/main/java/io/github/jwharm/cairobindings/Interop.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions src/main/java/org/freedesktop/cairo/Cairo.java
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
1 change: 1 addition & 0 deletions src/main/java/org/freedesktop/cairo/Circle.java
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
}
Loading

0 comments on commit 70a4840

Please sign in to comment.