Menggunakan JNA untuk Mengakses Perpustakaan Dinamik Asli

1. Gambaran keseluruhan

Dalam tutorial ini, kita akan melihat bagaimana menggunakan perpustakaan Java Native Access (JNA untuk pendek) untuk mengakses perpustakaan asli tanpa menulis kod JNI (Java Native Interface).

2. Mengapa JNA?

Selama bertahun-tahun, Java dan bahasa berasaskan JVM lain, sebagian besar, memenuhi cogan kata "tulis sekali, jalankan di mana-mana". Namun, kadang-kadang kita perlu menggunakan kod asli untuk melaksanakan beberapa fungsi :

  • Menggunakan semula kod warisan yang ditulis dalam C / C ++ atau bahasa lain yang dapat membuat kod asli
  • Mengakses fungsi khusus sistem tidak tersedia dalam jangka masa Java standard
  • Mengoptimumkan kelajuan dan / atau penggunaan memori untuk bahagian tertentu dari aplikasi tertentu.

Pada mulanya, syarat seperti ini bermaksud kita harus menggunakan JNI - Java Native Interface. Walaupun berkesan, pendekatan ini mempunyai kekurangan dan umumnya dihindari kerana beberapa masalah:

  • Memerlukan pemaju untuk menulis C / C ++ "gam gam" untuk merapatkan Java dan kod asli
  • Memerlukan kompilasi dan rantai alat pautan yang tersedia untuk setiap sistem sasaran
  • Nilai Marshall dan unmarsalling ke dan dari JVM adalah tugas yang membosankan dan ralat
  • Masalah undang-undang dan sokongan semasa mencampurkan perpustakaan Java dan asli

JNA datang untuk menyelesaikan sebahagian besar kerumitan yang berkaitan dengan penggunaan JNI. Khususnya, tidak perlu membuat kod JNI untuk menggunakan kod asli yang terletak di perpustakaan dinamik, yang menjadikan keseluruhan proses lebih mudah.

Sudah tentu, terdapat beberapa pertukaran:

  • Kami tidak boleh menggunakan perpustakaan statik secara langsung
  • Lebih perlahan jika dibandingkan dengan kod JNI buatan tangan

Walau bagaimanapun, bagi kebanyakan aplikasi, kesederhanaan JNA jauh lebih besar daripada kekurangan tersebut. Oleh itu, adalah wajar untuk mengatakan bahawa, melainkan jika kita mempunyai syarat yang sangat spesifik, JNA hari ini mungkin merupakan pilihan terbaik untuk mengakses kod asli dari Java - atau bahasa berasaskan JVM lain.

3. Persediaan Projek JNA

Perkara pertama yang harus kita lakukan untuk menggunakan JNA adalah menambahkan kebergantungannya ke pom.xml projek kita :

 net.java.dev.jna jna-platform 5.6.0  

Versi terbaru jna-platform boleh dimuat turun dari Maven Central.

4. Menggunakan JNA

Menggunakan JNA adalah proses dua langkah:

  • Pertama, kami membuat antara muka Java yang memperluas antara muka Perpustakaan JNA untuk menerangkan kaedah dan jenis yang digunakan ketika memanggil kod asli sasaran
  • Seterusnya, kami meneruskan antara muka ini ke JNA yang mengembalikan pelaksanaan konkrit antara muka ini yang kami gunakan untuk menggunakan kaedah asli

4.1. Kaedah Memanggil dari Perpustakaan C Standard

Sebagai contoh pertama, mari kita gunakan JNA untuk memanggil fungsi cosh dari pustaka C standard, yang terdapat di kebanyakan sistem. Kaedah ini mengambil argumen berganda dan menghitung kosinus hiperboliknya. Program AC boleh menggunakan fungsi ini hanya dengan memasukkan fail pengepala:

#include  #include  int main(int argc, char** argv) { double v = cosh(0.0); printf("Result: %f\n", v); }

Mari buat antara muka Java yang diperlukan untuk memanggil kaedah ini:

public interface CMath extends Library { double cosh(double value); } 

Seterusnya, kami menggunakan kelas Native JNA untuk membuat pelaksanaan konkrit antara muka ini sehingga kami dapat memanggil API kami:

CMath lib = Native.load(Platform.isWindows()?"msvcrt":"c", CMath.class); double result = lib.cosh(0); 

Bahagian yang benar-benar menarik di sini adalah panggilan kepada beban () kaedah . Ia memerlukan dua argumen: nama perpustakaan dinamik dan antara muka Java yang menerangkan kaedah yang akan kami gunakan. Ini mengembalikan pelaksanaan konkrit antara muka ini, yang memungkinkan kita memanggil kaedahnya.

Sekarang, nama perpustakaan dinamik biasanya bergantung pada sistem, dan perpustakaan standard C tidak terkecuali: libc.so di kebanyakan sistem berasaskan Linux, tetapi msvcrt.dll di Windows. Inilah sebabnya mengapa kami menggunakan kelas pembantu Platform , termasuk dalam JNA, untuk memeriksa platform yang kami jalankan dan pilih nama perpustakaan yang sesuai.

Perhatikan bahawa kita tidak perlu menambahkan pelanjutan .so atau .dll , kerana tersirat. Juga, untuk sistem berasaskan Linux, kita tidak perlu menentukan awalan "lib" yang standard untuk perpustakaan bersama.

Oleh kerana perpustakaan dinamik berkelakuan seperti Singleton dari perspektif Java, praktik umum adalah untuk menyatakan bidang INSTANCE sebagai sebahagian daripada deklarasi antara muka:

public interface CMath extends Library { CMath INSTANCE = Native.load(Platform.isWindows() ? "msvcrt" : "c", CMath.class); double cosh(double value); } 

4.2. Pemetaan Jenis Asas

Dalam contoh awal kami, kaedah yang disebut hanya menggunakan jenis primitif sebagai argumen dan nilai pengembaliannya. JNA menangani kes-kes tersebut secara automatik, biasanya menggunakan rakan-rakan Java semula jadi ketika memetakan dari jenis C:

  • char => bait
  • pendek => pendek
  • wchar_t => char
  • int => int
  • panjang => com.sun.jna.NativeLong
  • panjang panjang => panjang
  • apungan => apungan
  • berganda => berganda
  • char * => Rentetan

Pemetaan yang mungkin kelihatan ganjil adalah yang digunakan untuk jenis panjang asli . Ini kerana, dalam C / C ++, jenis panjang mungkin mewakili nilai 32- atau 64-bit, bergantung pada sama ada kita menjalankan sistem 32- atau 64-bit.

Untuk mengatasi masalah ini, JNA menyediakan jenis NativeLong , yang menggunakan jenis yang tepat bergantung pada seni bina sistem.

4.3. Struktur dan Kesatuan

Senario lain yang biasa adalah berkaitan dengan API kod asli yang mengharapkan penunjuk kepada beberapa jenis struktur atau kesatuan . Semasa membuat antara muka Java untuk mengaksesnya, argumen atau nilai kembali yang sesuai mestilah jenis Java yang memanjang Struktur atau Kesatuan , masing-masing.

For instance, given this C struct:

struct foo_t { int field1; int field2; char *field3; };

Its Java peer class would be:

@FieldOrder({"field1","field2","field3"}) public class FooType extends Structure { int field1; int field2; String field3; };

JNA requires the @FieldOrder annotation so it can properly serialize data into a memory buffer before using it as an argument to the target method.

Alternatively, we can override the getFieldOrder() method for the same effect. When targeting a single architecture/platform, the former method is generally good enough. We can use the latter to deal with alignment issues across platforms, that sometimes require adding some extra padding fields.

Unions work similarly, except for a few points:

  • No need to use a @FieldOrder annotation or implement getFieldOrder()
  • We have to call setType() before calling the native method

Let's see how to do it with a simple example:

public class MyUnion extends Union { public String foo; public double bar; }; 

Now, let's use MyUnion with a hypothetical library:

MyUnion u = new MyUnion(); u.foo = "test"; u.setType(String.class); lib.some_method(u); 

If both foo and bar where of the same type, we'd have to use the field's name instead:

u.foo = "test"; u.setType("foo"); lib.some_method(u);

4.4. Using Pointers

JNA offers a Pointer abstraction that helps to deal with APIs declared with untyped pointer – typically a void *. This class offers methods that allow read and write access to the underlying native memory buffer, which has obvious risks.

Before start using this class, we must be sure we clearly understand who “owns” the referenced memory at each time. Failing to do so will likely produce hard to debug errors related to memory leaks and/or invalid accesses.

Assuming we know what we're doing (as always), let's see how we can use the well-known malloc() and free() functions with JNA, used to allocate and release a memory buffer. First, let's again create our wrapper interface:

public interface StdC extends Library { StdC INSTANCE = // ... instance creation omitted Pointer malloc(long n); void free(Pointer p); } 

Now, let's use it to allocate a buffer and play with it:

StdC lib = StdC.INSTANCE; Pointer p = lib.malloc(1024); p.setMemory(0l, 1024l, (byte) 0); lib.free(p); 

The setMemory() method just fills the underlying buffer with a constant byte value (zero, in this case). Notice that the Pointer instance has no idea to what it is pointing to, much less its size. This means that we can quite easily corrupt our heap using its methods.

We'll see later how we can mitigate such errors using JNA's crash protection feature.

4.5. Handling Errors

Old versions of the standard C library used the global errno variable to store the reason a particular call failed. For instance, this is how a typical open() call would use this global variable in C:

int fd = open("some path", O_RDONLY); if (fd < 0) { printf("Open failed: errno=%d\n", errno); exit(1); }

Of course, in modern multi-threaded programs this code would not work, right? Well, thanks to C's preprocessor, developers can still write code like this and it will work just fine. It turns out that nowadays, errno is a macro that expands to a function call:

// ... excerpt from bits/errno.h on Linux #define errno (*__errno_location ()) // ... excerpt from  from Visual Studio #define errno (*_errno())

Now, this approach works fine when compiling source code, but there's no such thing when using JNA. We could declare the expanded function in our wrapper interface and call it explicitly, but JNA offers a better alternative: LastErrorException.

Any method declared in wrapper interfaces with throws LastErrorException will automatically include a check for an error after a native call. If it reports an error, JNA will throw a LastErrorException, which includes the original error code.

Let's add a couple of methods to the StdC wrapper interface we've used before to show this feature in action:

public interface StdC extends Library { // ... other methods omitted int open(String path, int flags) throws LastErrorException; int close(int fd) throws LastErrorException; } 

Now, we can use open() in a try/catch clause:

StdC lib = StdC.INSTANCE; int fd = 0; try { fd = lib.open("/some/path",0); // ... use fd } catch (LastErrorException err) { // ... error handling } finally { if (fd > 0) { lib.close(fd); } } 

In the catch block, we can use LastErrorException.getErrorCode() to get the original errno value and use it as part of the error handling logic.

4.6. Handling Access Violations

As mentioned before, JNA does not protect us from misusing a given API, especially when dealing with memory buffers passed back and forth native code. In normal situations, such errors result in an access violation and terminate the JVM.

JNA supports, to some extent, a method that allows Java code to handle access violation errors. There are two ways to activate it:

  • Setting the jna.protected system property to true
  • Calling Native.setProtected(true)

Setelah kami mengaktifkan mod terlindung ini, JNA akan menangkap kesalahan pelanggaran akses yang biasanya akan mengakibatkan kemalangan dan membuang pengecualian kesalahan java.lang.Error Kami dapat mengesahkan bahawa ini berfungsi menggunakan Pointer yang diinisialisasi dengan alamat yang tidak sah dan cuba menulis beberapa data kepadanya:

Native.setProtected(true); Pointer p = new Pointer(0l); try { p.setMemory(0, 100*1024, (byte) 0); } catch (Error err) { // ... error handling omitted } 

Namun, seperti yang dinyatakan dalam dokumentasi, fitur ini hanya boleh digunakan untuk tujuan penyahpepijatan / pengembangan.

5. Kesimpulan

Dalam artikel ini, kami telah menunjukkan cara menggunakan JNA untuk mengakses kod asli dengan mudah jika dibandingkan dengan JNI.

Seperti biasa, semua kod boleh didapati di GitHub.