Add support for specialized GErrorExceptions #363

Merged
jwharm merged 2 commits from specialized-gerror-exceptions into main 2026-05-15 19:53:57 +02:00
Owner

Java methods generated by Java-GI throw a GErrorException when the native function sets a trailing GError** parameter. With this PR, the exception type will be specialized with subclasses extending from GErrorException and based on GLib error domains, such as IOException (for GIOError) or CssParserException for GtkCssParserError.

Because error domains are set at runtime and not introspectable from the GIR files, the Java method signatures will still throw GErrorException. However, in the try-catch clause we can catch the actual type. For example:

try {
    // Convert string to number
    GLib.asciiStringToSigned("lalala", 10, 0, 100, new Out<>());
} catch (NumberParserException npe) {
    IO.println("Invalid number!");
} catch (GErrorException _) {
}

Because GErrorException is a checked exception, the catch (GErrorException) clause is still required by the Java compiler. It can be omitted in a Kotlin application.

The functionality is implemented with a global static HashMap that maps error domains to exception constructors. (An Exception class is generated for each error domain.) The HashMap is filled during initialization of a module. For example, during initialization of the Gtk bindings, the following error domains are registered:

GErrorException.registerErrorDomain("gtk-builder-error-quark", BuilderException::new);
GErrorException.registerErrorDomain("gtk-constraint-vfl-parser-error-quark", ConstraintVflParserException::new);
GErrorException.registerErrorDomain("gtk-css-parser-error-quark", CssParserException::new);
GErrorException.registerErrorDomain("gtk-dialog-error-quark", DialogException::new);
GErrorException.registerErrorDomain("gtk-file-chooser-error-quark", FileChooserException::new);
GErrorException.registerErrorDomain("gtk-icon-theme-error-quark", IconThemeException::new);
GErrorException.registerErrorDomain("gtk-print-error-quark", PrintException::new);
GErrorException.registerErrorDomain("gtk-recent-manager-error-quark", RecentManagerException::new);
GErrorException.registerErrorDomain("GtkSvgError", SvgException::new);

The generated exception classes are really small:

///
/// Represents a GError with domain {@link BuilderError}.
///
@Generated("org.javagi.JavaGI")
public class BuilderException extends GErrorException {
    public BuilderException(GError err) {
        super(err);
    }

    public BuilderError getEnum() {
        return BuilderError.of(getCode());
    }
}

When a method raises an exception, it passes a pointer to the GError to GErrorException.take() which will return a GErrorException instance that subsequently will be thrown. GErrorException.take() used to always create a GErrorException instance, but now it will instead read the error domain and create a specialized type, based on the registered mappings. When an error domain is not registered, a base GErrorException is created as a fallback:

public static GErrorException take(MemorySegment gerrorPtr) {
    GError err = new GError(gerrorPtr.get(ValueLayout.ADDRESS, 0));
    var constructor = ERROR_DOMAINS.get(GLib.quarkToString(err.readDomain()));
    GErrorException exc = constructor != null ? constructor.apply(err) : new GErrorException(err);
    err.free();
    return exc;
}
Java methods generated by Java-GI throw a `GErrorException` when the native function sets a trailing `GError**` parameter. With this PR, the exception type will be specialized with subclasses extending from `GErrorException` and based on GLib error domains, such as `IOException` (for `GIOError`) or `CssParserException` for `GtkCssParserError`. Because error domains are set at runtime and not introspectable from the GIR files, the Java method signatures will still throw `GErrorException`. However, in the `try-catch` clause we can catch the actual type. For example: ```java try { // Convert string to number GLib.asciiStringToSigned("lalala", 10, 0, 100, new Out<>()); } catch (NumberParserException npe) { IO.println("Invalid number!"); } catch (GErrorException _) { } ``` Because GErrorException is a checked exception, the `catch (GErrorException)` clause is still required by the Java compiler. It can be omitted in a Kotlin application. The functionality is implemented with a global static HashMap that maps error domains to exception constructors. (An Exception class is generated for each error domain.) The HashMap is filled during initialization of a module. For example, during initialization of the Gtk bindings, the following error domains are registered: ```java GErrorException.registerErrorDomain("gtk-builder-error-quark", BuilderException::new); GErrorException.registerErrorDomain("gtk-constraint-vfl-parser-error-quark", ConstraintVflParserException::new); GErrorException.registerErrorDomain("gtk-css-parser-error-quark", CssParserException::new); GErrorException.registerErrorDomain("gtk-dialog-error-quark", DialogException::new); GErrorException.registerErrorDomain("gtk-file-chooser-error-quark", FileChooserException::new); GErrorException.registerErrorDomain("gtk-icon-theme-error-quark", IconThemeException::new); GErrorException.registerErrorDomain("gtk-print-error-quark", PrintException::new); GErrorException.registerErrorDomain("gtk-recent-manager-error-quark", RecentManagerException::new); GErrorException.registerErrorDomain("GtkSvgError", SvgException::new); ``` The generated exception classes are really small: ```java /// /// Represents a GError with domain {@link BuilderError}. /// @Generated("org.javagi.JavaGI") public class BuilderException extends GErrorException { public BuilderException(GError err) { super(err); } public BuilderError getEnum() { return BuilderError.of(getCode()); } } ``` When a method raises an exception, it passes a pointer to the GError to `GErrorException.take()` which will return a GErrorException instance that subsequently will be thrown. `GErrorException.take()` used to always create a GErrorException instance, but now it will instead read the error domain and create a specialized type, based on the registered mappings. When an error domain is not registered, a base GErrorException is created as a fallback: ```java public static GErrorException take(MemorySegment gerrorPtr) { GError err = new GError(gerrorPtr.get(ValueLayout.ADDRESS, 0)); var constructor = ERROR_DOMAINS.get(GLib.quarkToString(err.readDomain())); GErrorException exc = constructor != null ? constructor.apply(err) : new GErrorException(err); err.free(); return exc; } ```
jwharm merged commit dd9917a862 into main 2026-05-15 19:53:57 +02:00
jwharm deleted branch specialized-gerror-exceptions 2026-05-15 19:54:04 +02:00
Sign in to join this conversation.
No description provided.