JNI menuntut berkas header hasil generasi dan kode C yang rawan kesalahan memori — beban yang dibahas pada chapter sebelumnya. Foreign Function and Memory API (FFM API) menghapus kedua syarat itu: fungsi native dipanggil langsung dari Java sebagai MethodHandle, dan argumennya berpindah lewat MemorySegment yang masa hidupnya dijaga Arena. API ini difinalkan pada Java 22 melalui JEP 454 setelah seri incubator dan preview yang panjang (JEP 191, lalu 442). Chapter Foreign Function and Memory API pada kelas Modern Java sudah membahas sisi memori — MemorySegment, Arena, dan alokasi off-heap — jadi fokus kita sekarang adalah sisi interop: panggilan turun (downcall) ke fungsi native, panggilan naik (upcall) dari native kembali ke Java, dan pemetaan tipe antara dua dunia. Seluruh contoh berjalan pada JDK 25 tanpa flag tambahan.

Panggilan Turun dengan Linker #

Panggilan turun adalah arah Java menuju native. Bahan bakunya ada empat: Linker.nativeLinker() menyediakan jembatan khusus platform, SymbolLookup mencari alamat simbol (misalnya strcmp di libc), FunctionDescriptor menyatakan signature fungsi dengan ValueLayout, dan hasilnya menjadi MethodHandle biasa yang bisa di-invoke(). Pemetaan tipe C ke FFM mengikuti tabel berikut:

Tipe CLayout FFMTipe Java di MethodHandle
intJAVA_INTint
long, size_tJAVA_LONGlong
char* / pointerADDRESSMemorySegment
void (nilai balik)FunctionDescriptor.ofVoid(...)tidak ada hasil

Program berikut memanggil strcmp dari libc — fungsi yang membandingkan dua string C dan mengembalikan bilangan bertanda:

import java.lang.foreign.Arena;
import java.lang.foreign.FunctionDescriptor;
import java.lang.foreign.Linker;
import java.lang.foreign.MemorySegment;
import java.lang.foreign.ValueLayout;
import java.lang.invoke.MethodHandle;

public class BandingkanKata {
    public static void main(String[] args) throws Throwable {
        // int strcmp(const char *a, const char *b) dari libc.
        Linker linker = Linker.nativeLinker();
        MethodHandle strcmp = linker.downcallHandle(
                linker.defaultLookup().find("strcmp").get(),
                FunctionDescriptor.of(ValueLayout.JAVA_INT,
                        ValueLayout.ADDRESS, ValueLayout.ADDRESS));

        try (Arena arena = Arena.ofConfined()) {
            MemorySegment apel = arena.allocateFrom("apel");
            MemorySegment jeruk = arena.allocateFrom("jeruk");

            int a = (int) strcmp.invoke(apel, jeruk);
            int b = (int) strcmp.invoke(jeruk, apel);
            int c = (int) strcmp.invoke(apel, apel);

            System.out.println("strcmp(\"apel\", \"jeruk\") = " + a);
            System.out.println("strcmp(\"jeruk\", \"apel\") = " + b);
            System.out.println("strcmp(\"apel\", \"apel\")  = " + c);
            System.out.println("tanda hasil: " + Integer.signum(a) + ", "
                    + Integer.signum(b) + ", " + Integer.signum(c));
        }
    }
}

Output yang Dihasilkan:

text
strcmp("apel", "jeruk") = -9
strcmp("jeruk", "apel") = 9
strcmp("apel", "apel")  = 0
tanda hasil: -1, 1, 0

Mari kita telaah baris-baris kuncinya:

  1. Baris linker.defaultLookup().find("strcmp").get(): defaultLookup mencari simbol di pustaka standar yang sudah termuat; hasilnya Optional, lalu .get() mengambil alamat simbolnya.
  2. Baris FunctionDescriptor.of(JAVA_INT, ADDRESS, ADDRESS): nilai balik int dinyatakan duluan, diikuti dua parameter pointer — persis mengikuti signature int strcmp(const char*, const char*).
  3. Baris arena.allocateFrom("apel"): string Java disalin ke memori native lengkap dengan penghenti nol versi C, sehingga aman dibaca strcmp.
  4. Baris Integer.signum(a): glibc mengembalikan selisih karakter pertama yang berbeda ('a' - 'j' = -9), jadi program menormalkan hasilnya menjadi tanda -1, 0, 1 — konvensi yang stabil untuk perbandingan.

Panggilan Naik dengan Upcall Stub #

Panggilan naik adalah arah sebaliknya: native memanggil Java. Banyak pustaka C meminta function pointer — qsort misalnya, yang mengurutkan array memakai pembanding buatan pemanggil. FFM menjembatani lewat upcallStub: method Java dibungkus menjadi alamat fungsi yang bisa diterima pihak native, dengan masa hidup terikat arena. Program berikut mengurutkan array int di memori native dengan qsort, sementara pembandingnya ditulis dalam Java:

java UrutNative.java
import java.lang.foreign.Arena;
import java.lang.foreign.FunctionDescriptor;
import java.lang.foreign.Linker;
import java.lang.foreign.MemorySegment;
import java.lang.foreign.SymbolLookup;
import java.lang.foreign.ValueLayout;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.util.Arrays;

import static java.lang.foreign.ValueLayout.ADDRESS;
import static java.lang.foreign.ValueLayout.JAVA_INT;
import static java.lang.foreign.ValueLayout.JAVA_LONG;

public class UrutNative {
    public static void main(String[] args) throws Throwable {
        Linker linker = Linker.nativeLinker();
        SymbolLookup libc = linker.defaultLookup();

        // void qsort(void *base, size_t nmemb, size_t size,
        //            int (*compar)(const void *, const void *));
        MethodHandle qsort = linker.downcallHandle(
                libc.find("qsort").get(),
                FunctionDescriptor.ofVoid(ADDRESS, JAVA_LONG, JAVA_LONG, ADDRESS));

        // Deskriptor fungsi pembanding: pointer dengan target layout int.
        FunctionDescriptor deskriptorBanding = FunctionDescriptor.of(JAVA_INT,
                ADDRESS.withTargetLayout(JAVA_INT),
                ADDRESS.withTargetLayout(JAVA_INT));
        MethodHandle pembanding = MethodHandles.lookup().findStatic(
                UrutNative.class, "bandingkan", deskriptorBanding.toMethodType());

        try (Arena arena = Arena.ofConfined()) {
            int[] data = {42, 7, 19, 3, 88, 1, 56, 23};
            MemorySegment larik = arena.allocateFrom(JAVA_INT, data);

            // Upcall stub: method Java dibungkus menjadi function pointer.
            MemorySegment fungsiBanding = linker.upcallStub(
                    pembanding, deskriptorBanding, arena);

            qsort.invoke(larik, (long) data.length, JAVA_INT.byteSize(),
                    fungsiBanding);

            System.out.println("sebelum : " + Arrays.toString(data));
            System.out.println("sesudah : " + Arrays.toString(larik.toArray(JAVA_INT)));
        }
    }

    // Dipanggil dari dalam qsort lewat upcall stub.
    static int bandingkan(MemorySegment a, MemorySegment b) {
        return Integer.compare(a.get(JAVA_INT, 0L), b.get(JAVA_INT, 0L));
    }
}

Output yang Dihasilkan:

text
sebelum : [42, 7, 19, 3, 88, 1, 56, 23]
sesudah : [1, 3, 7, 19, 23, 42, 56, 88]

Bedah baris-per-baris untuk bagian upcall-nya:

  1. Baris FunctionDescriptor.ofVoid(ADDRESS, JAVA_LONG, JAVA_LONG, ADDRESS): signature qsort — pointer array, jumlah anggota, ukuran elemen, dan function pointer pembanding; size_t dipetakan ke JAVA_LONG.
  2. Baris ADDRESS.withTargetLayout(JAVA_INT): parameter pembanding adalah pointer ke elemen int; target layout memberi tahu FFM bahwa segment yang masuk boleh dibaca sebagai int sehingga bandingkan cukup memanggil get(JAVA_INT, 0L) tanpa konversi tambahan.
  3. Baris MethodHandles.lookup().findStatic(...): method handle untuk method Java bandingkan dicari lewat refleksi ringan dengan MethodType yang dibangun langsung dari deskriptor fungsi.
  4. Baris linker.upcallStub(pembanding, deskriptorBanding, arena): stub dibuat dan masa hidupnya diikat arena — begitu arena ditutup, alamat stub tidak valid lagi, mencegah native menyimpan pointer yang sudah mati.
  5. Baris qsort.invoke(...): satu panggilan lintas batas untuk seluruh pengurutan; selama eksekusi, qsort memanggil kembali bandingkan berkali-kali lewat stub — dua arah bekerja dalam satu operasi.

Dibandingkan JNI, tidak ada satu baris C yang kita tulis: signature dinyatakan dalam Java, data berpindah lewat segmen yang terlindungi batas dan masa hidup, dan kesalahan akses melempar exception alih-alih merusak JVM. Pertanyaan berikutnya: bagaimana memuat pustaka native tertentu — bukan sekadar libc bawaan — dan mencari simbol di dalamnya? Jawabannya ada di chapter Memanggil Library Native.

Referensi #

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

BandingkanKata.java

Unduh berkas
java
import java.lang.foreign.Arena;
import java.lang.foreign.FunctionDescriptor;
import java.lang.foreign.Linker;
import java.lang.foreign.MemorySegment;
import java.lang.foreign.ValueLayout;
import java.lang.invoke.MethodHandle;

public class BandingkanKata {
    public static void main(String[] args) throws Throwable {
        // int strcmp(const char *a, const char *b) dari libc.
        Linker linker = Linker.nativeLinker();
        MethodHandle strcmp = linker.downcallHandle(
                linker.defaultLookup().find("strcmp").get(),
                FunctionDescriptor.of(ValueLayout.JAVA_INT,
                        ValueLayout.ADDRESS, ValueLayout.ADDRESS));

        try (Arena arena = Arena.ofConfined()) {
            MemorySegment apel = arena.allocateFrom("apel");
            MemorySegment jeruk = arena.allocateFrom("jeruk");

            int a = (int) strcmp.invoke(apel, jeruk);
            int b = (int) strcmp.invoke(jeruk, apel);
            int c = (int) strcmp.invoke(apel, apel);

            System.out.println("strcmp(\"apel\", \"jeruk\") = " + a);
            System.out.println("strcmp(\"jeruk\", \"apel\") = " + b);
            System.out.println("strcmp(\"apel\", \"apel\")  = " + c);
            System.out.println("tanda hasil: " + Integer.signum(a) + ", "
                    + Integer.signum(b) + ", " + Integer.signum(c));
        }
    }
}

UrutNative.java

Unduh berkas
java
import java.lang.foreign.Arena;
import java.lang.foreign.FunctionDescriptor;
import java.lang.foreign.Linker;
import java.lang.foreign.MemorySegment;
import java.lang.foreign.SymbolLookup;
import java.lang.foreign.ValueLayout;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.util.Arrays;

import static java.lang.foreign.ValueLayout.ADDRESS;
import static java.lang.foreign.ValueLayout.JAVA_INT;
import static java.lang.foreign.ValueLayout.JAVA_LONG;

public class UrutNative {
    public static void main(String[] args) throws Throwable {
        Linker linker = Linker.nativeLinker();
        SymbolLookup libc = linker.defaultLookup();

        // void qsort(void *base, size_t nmemb, size_t size,
        //            int (*compar)(const void *, const void *));
        MethodHandle qsort = linker.downcallHandle(
                libc.find("qsort").get(),
                FunctionDescriptor.ofVoid(ADDRESS, JAVA_LONG, JAVA_LONG, ADDRESS));

        // Deskriptor fungsi pembanding: pointer dengan target layout int.
        FunctionDescriptor deskriptorBanding = FunctionDescriptor.of(JAVA_INT,
                ADDRESS.withTargetLayout(JAVA_INT),
                ADDRESS.withTargetLayout(JAVA_INT));
        MethodHandle pembanding = MethodHandles.lookup().findStatic(
                UrutNative.class, "bandingkan", deskriptorBanding.toMethodType());

        try (Arena arena = Arena.ofConfined()) {
            int[] data = {42, 7, 19, 3, 88, 1, 56, 23};
            MemorySegment larik = arena.allocateFrom(JAVA_INT, data);

            // Upcall stub: method Java dibungkus menjadi function pointer.
            MemorySegment fungsiBanding = linker.upcallStub(
                    pembanding, deskriptorBanding, arena);

            qsort.invoke(larik, (long) data.length, JAVA_INT.byteSize(),
                    fungsiBanding);

            System.out.println("sebelum : " + Arrays.toString(data));
            System.out.println("sesudah : " + Arrays.toString(larik.toArray(JAVA_INT)));
        }
    }

    // Dipanggil dari dalam qsort lewat upcall stub.
    static int bandingkan(MemorySegment a, MemorySegment b) {
        return Integer.compare(a.get(JAVA_INT, 0L), b.get(JAVA_INT, 0L));
    }
}