Pleasant-Smoke / jvips

Java bindings for libvips using JNI or Panama (Java's incubating C FFI)

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Prototype Stage

Code Breakage

The current repository code will not generate the correct source files as they are in the process of being changed from using static and singletons to dependency injection that "looks behind" to check for any breaking api changes.

Todo

  • Handle unlisted enums in foreign.h

  • Handle Unlisted params in Heif

  • Handle options in init.c

  • When the JDK 20 reaches GA, update Panama for version 20

Usage

The underlying ref-counted resource
public class SharedReference<T, R extends Addressable<T>> {

    private static final AtomicIntegerFieldUpdater<SharedReference> updater = newUpdater(SharedReference.class, "threadCount");

    private final long id;

    private volatile int threadCount = 1; (1)

    private volatile R referent; (2)

    private final StampedLock lock = new StampedLock(); (3)

    private final T address; (4)
}
  1. The number of threads holding onto this shared image, this field is updated atomicly

  2. The underlying image, this will be nulled if the thread count goes to zero

  3. The stamped lock shared among threads

  4. Either the MemorySegment, ByteBuffer or Long address depending on the implementation

Delegation of the SharedResource, this is to prevent end-users from holding on to references that have been free’d
class VipsImageDelegate<T> implements JVipsImage<T> {

    protected final long id;

    protected final SharedReference<T, JVipsImage<T>> sharedImage; // (1)
    protected final Thread thread; // (2)

    @Override
    public int getXSize() {
        ensureOperation(); (3)
        StampedLock lock = sharedImage.getLock(); (4)
        long id = lock.tryReadLock();
        try {
            if(id != 0) {
                return getSharedImage().getXSize(); (5)
            } else {
                throw new RuntimeException("Failed to get read lock for image");
            }
        } finally {
            lock.unlockRead(id);
        }
    }

    void ensureOperation(){
        if(this.sharedImage.getImage() == null || this.sharedImage.getThreadCount() == 0){
            throw new RuntimeException("Image was GC'ed");
        }
        if(this.thread != Thread.currentThread()){
            throw new RuntimeException("Thread does not hold image");
        }
    }
}
  1. Globally shared image

  2. The owning thread

  3. Ensure the calling thread is the owning thread, also make sure the unerlying image is not null or the thread count is 0

  4. Get the shared StampedLock

  5. This could throw a null pointer exception if a something wierd happens

Example of an error, not working within the confines of JVips
public class MyImageManipulator {
    void doSomethingWithAnImage(String file){
        try(VipsContext context = JniContext.create()){ (1)
            JVipsImage<Long> img = context.loadImage(file);
            Executors.newSingleThreadPool().submit(()-> {
                context.saveAvif(img); (2)
            });
        } catch(VipsException e){
            throw new RuntimeException(e);
        }
    }
}
  1. Open an image context

  2. This will throw an error

How to use another thread
public class MyImageManipulator {

    JVipsExecutorService executor = JVipsExecutorService.ofDefault();  (1)

    void doSomethingWithAnImage(String file){
        try(VipsContext context = JniContext.create()){ (2)
            JVipsImage<Long> img = context.loadImage(file);
            executor.submit(()-> {
                JVipsImage<Long> imgref = context.transfer(Thread.currentThread(), img); (3)
                context.saveAvif(imgref);
            }); (4)

        } catch(VipsException e){
            throw new RuntimeException(e);
        }
    }
}
  1. All sub operations that happen in another thread must use the JVipsExecutorService the overridden life cycle methods will properly unref the image

  2. Create the context

  3. Create a new refrence in the underlying SharedImage

  4. After it has run the ThreadPoolExecutor#afterExecute(…​) is invoked and the ref counts are de-incremented

Code Generation

A lot of code is generated, the jvips-plugin directory which is a seperate project under a Gradle composite build

Stages
  • Read the GObject Introspection

  • Read the associated C source files

  • Register All Types

  • Register all Executable’s with the associated type

  • Parse the Documentation

    • Early Stage Parsing handles Optional Params (more on this)

    • Parse the documentation update to JavaDoc format with AsciiDoc syntax

  • Method names are:

    • Transformed to camelCase, some are elongated ie: getXres becomes getXResolution

    • Splitting on certain keywords so jpegsave_buffer and jpegsave_file become jpegSaveBuffer and jpegSaveFile

    • Take advantage of method overloading, so xyz and xyz1 both become xyz

  • Each method that has optional parameters has it add to it’s method signature as an @OptionalParam with an overloaded method that just passes null

  • Each method that has 1 optional parameter has it add to it’s method signature as a boxed primitive @OptionalParam(name = "opt")

  • Methods with more than 1 optional parameter go through a process where each one that has the same parameter names are grouped together and a method dto is created, ending with Params all the parameters are boxed primitives or any reference type

Source Parsing

  • We look at each source file

  • Look for a block like this:

    vobject_class->nickname = "thumbnail_base";
	vobject_class->description = _( "thumbnail generation" );
	vobject_class->build = vips_thumbnail_build;
  • Parse the macros that may look like this:

VIPS_ARG_IMAGE( class, "out", 2,
		_( "Output" ),
		_( "Output image" ),
		VIPS_ARGUMENT_REQUIRED_OUTPUT,
		G_STRUCT_OFFSET( VipsThumbnail, out ) );
  • The …​_base" lets us know to add it to other blocks found in the same file, there are some corner cases

Objectives

  • Create 2 Implementations

    • DirectByteBuffers using JNI

    • Panama

Resources

Strings:

It looks like the JDK team is also doing similar with threads with Panama to prevent resource leaks https://minborgsjavapot.blogspot.com/2022/12/java-20-sneak-peek-on-panama-ffm-api.html

SilkDI:

About

Java bindings for libvips using JNI or Panama (Java's incubating C FFI)

License:GNU Lesser General Public License v2.1


Languages

Language:Java 98.6%Language:C++ 0.5%Language:ANTLR 0.4%Language:Groovy 0.3%Language:Python 0.1%Language:Shell 0.1%Language:CMake 0.0%Language:Batchfile 0.0%Language:C 0.0%