javajava-native-interfaceproject-panamajava-18

Java VarHandle to a C string with java.lang.foreign API


I would like to use foreign function interface from project panama to access C library from Java19. The C interface is quite simple:

typedef struct {
  int len;
  char name[100];
} ent;

ent* foo();

When called, function foo returns pointer to struct ent, where len tells the size of the string name.

The corresponding Java side is:

private static final MemoryLayout ENT_LAYOUT = MemoryLayout.structLayout(
        JAVA_INT.withName("len"),
        MemoryLayout.sequenceLayout(100, ValueLayout.JAVA_BYTE).withName("name")
);

For ease of access I would like use VarHandle:

private static final VarHandle VH_ENT_LEN = ENT_LAYOUT.varHandle(groupElement("len"));

and later on

int len = (int)VH_ENT_LEN.get(segment);
String name = segment.asSlice(ENT_LAYOUT.byteOffset(groupElement("name")), len).getUtf8String(0);

Which is still a bit messy.

My naive expectation ware, that the solution should be something like:

private static final VarHandle VH_ENT_NAME = ENT_LAYOUT.varHandle(groupElement("name"), sequenceElement());

byte[] nameRaw = (byte[])VH_ENT_NAME.get(segment);

However I get:

java.lang.RuntimeException: java.lang.invoke.WrongMethodTypeException:
   cannot convert MethodHandle(VarHandle,MemorySegment,long)byte to (VarHandle,MemorySegment)byte[]

So, the question is: is there an elegant solution to access arrays from java foreign API, or we should stick to mix of VarHandle and slice.


Solution

  • VarHandles, at their root, are only for accessing memory that can fit in a primitive type, and char[100] does not fit in a primitive.

    What you get when doing:

    ENT_LAYOUT.varHandle(groupElement("name"), sequenceElement());
    

    Is a VarHandle that selects a single byte from the array, for which the index is supplied dynamically:

    long index = 42; // select element 42
    byte nameByte = (byte) VH_ENT_NAME.get(segment, index);
    

    should stick to mix of VarHandle and slice

    Yes, slice is needed to access anything that's too big for a primitive. It is essentially the same as doing this in C:

    ent* x = foo();
    char* name = x->name;
    

    You can use MemoryLayout::sliceHandle as well to get a MethodHandle that embeds the offset computations:

    MethodHandle MH_ENT_NAME = ENT_LAYOUT.sliceHandle(groupElement("name"));
    

    Method handles can also be combined further (just like varhandles), to create one that directly gets the string from the segment:

    MethodHandle MH_getUtf8String = MethodHandles.lookup().findVirtual(MemorySegment.class, "getUtf8String", MethodType.methodType(String.class, long.class));
    MethodHandle mh = MethodHandles.insertArguments(MH_getUtf8String, 1, 0); // always access string at offset 0
    mh = MethodHandles.filterArguments(result, 0, MH_ENT_NAME);
    
    String name = (String) mh.invokeExact(segment);
    

    Though, it is often simpler just to define a static helper method that does the above:

    public static String getName(MemorySegment segment) {
        try {
            MemorySegment nameSegment = (MemorySegment) MH_ENT_NAME.invokeExact(segment);
            return nameSegment.getUtf8String(0);
        } catch(Throwable t) {
            throw new RuntimeException(t);
        }
    }