Handling Rust Vectors (Vec<T>1) in Java

Rust Side

In Rust, a Vec<T> is a dynamically-sized array that includes metadata such as capacity and length. Working with vectors across FFI boundaries requires us to manage these fields carefully on both sides.

Example Rust Vector:

#![allow(unused)]
fn main() {
#[no_mangle]
extern "C" fn create_vector() -> *mut Vec<i32> {
    Box::into_raw(Box::new(vec![10, 20, 30]))
}

#[no_mangle]
extern "C" fn vector_push(vec: *mut Vec<i32>, value: i32) {
    unsafe {
        if let Some(vec) = vec.as_mut() {
            vec.push(value);
        }
    }
}

#[no_mangle]
extern "C" fn vector_get(vec: *const Vec<i32>, index: usize) -> i32 {
    unsafe {
        if let Some(vec) = vec.as_ref() {
            vec[index]
        } else {
            0 // Or some error handling
        }
    }
}

#[no_mangle]
extern "C" fn vector_len(vec: *const Vec<i32>) -> usize {
    unsafe {
        if let Some(vec) = vec.as_ref() {
            vec.len()
        } else {
            0
        }
    }
}
}

Explanation:

create_vector: Initializes a Vec<i32> and returns a raw pointer to allow Java to manage the vector.

vector_push: Provides functionality for adding elements to the vector, with error handling in case of null pointers.

vector_get and vector_len: Fetch elements from the vector and get its length, making direct access possible from Java.

Java Side

To handle Vec<T> in Java:

  1. Define a StructLayout that represents the memory layout for a Rust vector (data pointer, length, and capacity).
  2. Use MethodHandles to call Rust functions to manipulate the vector.

Example Java Code:

// Define the layout for Vec<i32>
StructLayout vecLayout = MemoryLayout.structLayout(
    ValueLayout.ADDRESS.withName("ptr"),  // Data pointer
    ValueLayout.JAVA_LONG.withName("cap"), // Capacity
    ValueLayout.JAVA_LONG.withName("len")  // Length
);

// MethodHandles to call Rust functions
MethodHandle vectorPush = linker.downcallHandle(
    symbolLookup.lookup("vector_push").get(),
    FunctionDescriptor.ofVoid(ValueLayout.ADDRESS, ValueLayout.JAVA_INT)
);

MethodHandle vectorGet = linker.downcallHandle(
    symbolLookup.lookup("vector_get").get(),
    FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.ADDRESS, ValueLayout.JAVA_LONG)
);

MethodHandle vectorLen = linker.downcallHandle(
    symbolLookup.lookup("vector_len").get(),
    FunctionDescriptor.of(ValueLayout.JAVA_LONG, ValueLayout.ADDRESS)
);

Explanation:

vecLayout: Defines the structure of the Vec<T> memory, including data pointer, length, and capacity.

MethodHandles (vectorPush, vectorGet, vectorLen): Enable Java to interact with the vector’s core functions.

Allocating and Using the Vector

var arena = Arena.ofConfined();
MemorySegment vecSegment = arena.allocate(vecLayout);

vectorPush.invokeExact(vecSegment, 42);  // Push 42 to vector
long len = (long) vectorLen.invokeExact(vecSegment); // Get vector length
int value = (int) vectorGet.invokeExact(vecSegment, 0L); // Get first element

Explanation:

Arena Allocation: Using an Arena for safe memory management. Push, Length, and Get: MethodHandle invocations facilitate direct manipulation of the Rust vector from Java.