Chapter sebelumnya memanggil strcmp dan qsort lewat defaultLookup — jalur cepat untuk simbol pustaka standar yang sudah termuat. Aplikasi nyata biasanya butuh lebih dari itu: pustaka basis data, codec, atau kriptografi bawaan sistem operasi yang harus dimuat berdasarkan nama berkasnya. FFM menyediakan SymbolLookup sebagai mekanisme pemuatannya, dan desainnya sengaja mengikat masa hidup pustaka pada Arena: ketika arena ditutup, pustaka turut dilepas. Chapter ini membahas tiga sumber simbol, cara memanggil fungsi dari pustaka yang baru dimuat, dan strategi menghadapi perbedaan nama berkas antar platform — semuanya dengan contoh yang berjalan pada JDK 25.

Tiga Sumber Simbol #

SymbolLookup punya tiga pabrik dengan wilayah kerja berbeda. Pilihan pabrik menentukan simbol mana yang bisa ditemukan dan kapan pustaka dilepas dari memori:

PabrikSumber simbolMasa hidup
Linker.defaultLookup()pustaka standar yang sudah termuat (libc)ikut JVM
SymbolLookup.libraryLookup(nama, arena)pustaka dimuat eksplisit dari nama/berkasikut arena — tertutup bersama arena
SymbolLookup.loaderLookup()pustaka yang dimuat System.loadLibrary di classloader saat iniikut classloader

Metode find() selalu mengembalikan Optional<MemorySegment> sehingga simbol yang tidak ada cukup ditangani sebagai Optional kosong, bukan NullPointerException. Program berikut memuat libc secara eksplisit, memeriksa keberadaan simbol, memanggil abs, lalu membuktikan bahwa pemuatan nama yang salah gagal sejak awal:

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

public class MuatLibrary {
    public static void main(String[] args) throws Throwable {
        // Muat libc secara eksplisit; pemuatan terikat masa hidup arena.
        try (Arena arena = Arena.ofConfined()) {
            SymbolLookup libc = SymbolLookup.libraryLookup("libc.so.6", arena);

            System.out.println("strlen ada  : " + libc.find("strlen").isPresent());
            System.out.println("abs ada     : " + libc.find("abs").isPresent());
            System.out.println("xyz ada     : " + libc.find("simbol_tidak_ada").isPresent());

            // Ambil handle abs lalu panggil lewat downcall biasa.
            MethodHandle abs = Linker.nativeLinker().downcallHandle(
                    libc.find("abs").get(),
                    FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.JAVA_INT));
            System.out.println("abs(-42)    : " + (int) abs.invoke(-42));
            System.out.println("abs(99)     : " + (int) abs.invoke(99));
        }

        // Nama pustaka yang salah gagal saat dimuat, bukan saat dipanggil.
        try (Arena arena = Arena.ofConfined()) {
            SymbolLookup salah = SymbolLookup.libraryLookup("libtidakada.so", arena);
            System.out.println("baris ini tidak tercapai");
        } catch (IllegalArgumentException e) {
            System.out.println("galat muat  : " + e.getMessage());
        }
    }
}

Output yang Dihasilkan:

text
strlen ada  : true
abs ada     : true
xyz ada     : false
abs(-42)    : 42
abs(99)     : 99
galat muat  : Cannot open library: libtidakada.so

Mari kita telaah baris-baris pentingnya:

  1. Baris SymbolLookup.libraryLookup("libc.so.6", arena): JVM membuka berkas pustaka dengan nama persis itu — tidak ada prefiks atau sufiks yang ditambahkan otomatis; nama harus lengkap sesuai platform.
  2. Baris libc.find("simbol_tidak_ada").isPresent(): simbol yang tidak dikenal tidak melempar exception, melainkan Optional kosong — program boleh menyediakan jalur cadangan (misalnya implementasi murni Java) tanpa try/catch.
  3. Baris FunctionDescriptor.of(JAVA_INT, JAVA_INT): signature int abs(int) tidak memakai pointer sama sekali, jadi pemetaannya langsung int ke int tanpa MemorySegment.
  4. Blok catch (IllegalArgumentException e): kegagalan membuka berkas pustaka terjadi di titik pemuatan, dengan pesan yang menyebut nama berkasnya — kontras dengan JNI yang baru karam saat UnsatisfiedLinkError muncul di pemanggilan pertama.

Pustaka Lawas dan Nama Antar Platform #

Untuk pustaka yang sudah dimuat lewat mekanisme JNI lawas, loaderLookup mengambil simbolnya tanpa memuat ulang:

java Menjembatani pustaka JNI lawas
static {
    System.loadLibrary("audio");                 // mencari libaudio.so di java.library.path
}

SymbolLookup dariLoader = SymbolLookup.loaderLookup();
MethodHandle rms = Linker.nativeLinker().downcallHandle(
        dariLoader.find("Java_PustakaAudio_hitungRms").orElseThrow(),
        FunctionDescriptor.of(ValueLayout.JAVA_INT,
                ValueLayout.ADDRESS, ValueLayout.JAVA_INT));

Nama berkas pustaka berbeda di tiap sistem operasi, dan libraryLookup tidak menerjemahkannya. Ketiga varian berikut merujuk pustaka yang sama:

PlatformNama berkas lazim
Linuxlibsqlite3.so atau libsqlite3.so.0
macOSlibsqlite3.dylib
Windowssqlite3.dll

Karena itu program portabel memilih nama saat runtime. Pola yang umum: deteksi os.name dan os.arch, lalu susun nama berkasnya — nilai keduanya stabil per platform sehingga pemilihan pun deterministik. Alternatif yang lebih kokoh: letakkan berkas pustaka di dalam distribusi aplikasi dan muat lewat Path absolut hasil Path.of(appDir, namaBerkas) — libraryLookup juga menerima Path, bukan hanya String.

Satu disiplin yang membedakan FFM dari JNI tetap perlu dijaga: simbol yang diambil dari arena tertutup tidak boleh dipakai lagi. Setelah arena.close() berjalan, segmen hasil find() menolak akses dengan IllegalStateException — kegagalan yang terdeteksi, bukan kerusakan senyap. Dengan pemuatan pustaka terkendali, kita sudah menguasai interop di dalam proses yang sama; chapter berikutnya beralih ke jalur yang sama sekali berbeda: meluncurkan program eksternal sebagai proses terpisah lewat ProcessBuilder.

Referensi #

Berkas Sumber

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

MuatLibrary.java

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

public class MuatLibrary {
    public static void main(String[] args) throws Throwable {
        // Muat libc secara eksplisit; pemuatan terikat masa hidup arena.
        try (Arena arena = Arena.ofConfined()) {
            SymbolLookup libc = SymbolLookup.libraryLookup("libc.so.6", arena);

            System.out.println("strlen ada  : " + libc.find("strlen").isPresent());
            System.out.println("abs ada     : " + libc.find("abs").isPresent());
            System.out.println("xyz ada     : " + libc.find("simbol_tidak_ada").isPresent());

            // Ambil handle abs lalu panggil lewat downcall biasa.
            MethodHandle abs = Linker.nativeLinker().downcallHandle(
                    libc.find("abs").get(),
                    FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.JAVA_INT));
            System.out.println("abs(-42)    : " + (int) abs.invoke(-42));
            System.out.println("abs(99)     : " + (int) abs.invoke(99));
        }

        // Nama pustaka yang salah gagal saat dimuat, bukan saat dipanggil.
        try (Arena arena = Arena.ofConfined()) {
            SymbolLookup salah = SymbolLookup.libraryLookup("libtidakada.so", arena);
            System.out.println("baris ini tidak tercapai");
        } catch (IllegalArgumentException e) {
            System.out.println("galat muat  : " + e.getMessage());
        }
    }
}