Tip Java 17: Mengintegrasikan Java dengan C ++

Pada artikel ini, saya akan membahas beberapa masalah yang terlibat dalam mengintegrasikan kode C ++ dengan aplikasi Java. Setelah sepatah kata tentang mengapa seseorang ingin melakukan ini dan apa saja rintangannya, saya akan membangun program Java yang berfungsi yang menggunakan objek yang ditulis dalam C ++. Di sepanjang jalan, saya akan membahas beberapa implikasi dari melakukan hal ini (seperti interaksi dengan pengumpulan sampah), dan saya akan menyajikan sekilas tentang apa yang dapat kita harapkan di bidang ini di masa mendatang.

Mengapa mengintegrasikan C ++ dan Java?

Mengapa Anda ingin mengintegrasikan kode C ++ ke dalam program Java? Bagaimanapun, bahasa Java telah dibuat, sebagian, untuk mengatasi beberapa kekurangan C ++. Sebenarnya, ada beberapa alasan mengapa Anda mungkin ingin mengintegrasikan C ++ dengan Java:

  • Performa. Meskipun Anda mengembangkan platform dengan kompiler just-in-time (JIT), kemungkinan besar kode yang dihasilkan oleh runtime JIT secara signifikan lebih lambat daripada kode C ++ yang setara. Seiring dengan peningkatan teknologi JIT, ini seharusnya menjadi faktor yang tidak terlalu penting. (Faktanya, dalam waktu dekat, teknologi JIT yang baik dapat berarti bahwa Java berjalan lebih cepat daripada kode C ++ yang setara.)
  • Untuk penggunaan kembali kode lama dan integrasi ke dalam sistem lama.
  • Untuk mengakses perangkat keras secara langsung atau melakukan aktivitas tingkat rendah lainnya.
  • Untuk memanfaatkan alat yang belum tersedia untuk Java (OODBMS yang matang, ANTLR, dan sebagainya).

Jika Anda mengambil risiko dan memutuskan untuk mengintegrasikan Java dan C ++, Anda melepaskan beberapa keuntungan penting dari aplikasi khusus Java. Inilah kerugiannya:

  • Aplikasi campuran C ++ / Java tidak dapat dijalankan sebagai applet.
  • Anda menyerahkan keamanan penunjuk. Kode C ++ Anda bebas untuk salah memilih objek, mengakses objek yang dihapus, atau merusak memori dengan cara lain yang sangat mudah di C ++.
  • Kode Anda mungkin tidak portabel.
  • Lingkungan binaan Anda pasti tidak akan portabel - Anda harus mencari cara untuk meletakkan kode C ++ di pustaka bersama di semua platform yang diminati.
  • API untuk mengintegrasikan C dan Java sedang dalam proses dan kemungkinan besar akan berubah dengan perpindahan dari JDK 1.0.2 ke JDK 1.1.

Seperti yang Anda lihat, mengintegrasikan Java dan C ++ bukanlah untuk orang yang lemah hati! Namun, jika Anda ingin melanjutkan, baca terus.

Kami akan mulai dengan contoh sederhana yang menunjukkan cara memanggil metode C ++ dari Java. Kami kemudian akan memperluas contoh ini untuk menunjukkan bagaimana mendukung pola pengamat. Pola pengamat, selain menjadi salah satu landasan pemrograman berorientasi objek, berfungsi sebagai contoh bagus dari aspek yang lebih terlibat dalam mengintegrasikan C ++ dan kode Java. Kami kemudian akan membuat program kecil untuk menguji objek C ++ yang dibungkus Java, dan kami akan mengakhiri dengan diskusi tentang arah masa depan untuk Java.

Memanggil C ++ dari Java

Apa sulitnya mengintegrasikan Java dan C ++, Anda bertanya? Lagipula, Tutorial Java SunSoft memiliki bagian tentang "Mengintegrasikan Metode Native ke dalam Program Java" (lihat Sumberdaya). Seperti yang akan kita lihat, ini cukup untuk memanggil metode C ++ dari Java, tetapi tidak cukup untuk memanggil metode Java dari C ++. Untuk melakukan itu, kita perlu melakukan lebih banyak pekerjaan.

Sebagai contoh, kita akan mengambil kelas C ++ sederhana yang ingin kita gunakan dari dalam Java. Kami akan berasumsi bahwa kelas ini sudah ada dan kami tidak diizinkan untuk mengubahnya. Kelas ini disebut "C ++ :: NumberList" (untuk kejelasan, saya akan mengawali semua nama kelas C ++ dengan "C ++ ::"). Kelas ini mengimplementasikan daftar angka sederhana, dengan metode untuk menambahkan angka ke daftar, menanyakan ukuran daftar, dan mendapatkan elemen dari daftar. Kita akan membuat kelas Java yang tugasnya mewakili kelas C ++. Kelas Java ini, yang akan kita sebut NumberListProxy, akan memiliki tiga metode yang sama, tetapi implementasi metode ini akan memanggil setara C ++. Ini digambarkan dalam diagram teknik pemodelan objek (OMT) berikut:

Instance Java NumberListProxy perlu menyimpan referensi ke instance C ++ yang sesuai dari NumberList. Ini cukup mudah, jika sedikit non-portabel: Jika kita berada pada platform dengan pointer 32-bit, kita dapat menyimpan pointer ini dalam sebuah int; jika kita berada di platform yang menggunakan pointer 64-bit (atau kita pikir mungkin dalam waktu dekat), kita bisa menyimpannya dalam waktu yang lama. Kode sebenarnya untuk NumberListProxy sangat mudah, jika agak berantakan. Ia menggunakan mekanisme dari bagian "Mengintegrasikan Metode Native ke Program Java" dari Tutorial Java SunSoft.

Potongan pertama di kelas Java terlihat seperti ini:

kelas publik NumberListProxy {static {System.loadLibrary ("NumberList"); } NumberListProxy () {initCppSide (); } public native void addNumber (int n); publik ukuran int asli (); publik asli int getNumber (int i); private native void initCppSide (); private int numberListPtr_; // NumberList *}

Bagian statis dijalankan saat kelas dimuat. System.loadLibrary () memuat pustaka bersama bernama, yang dalam kasus kami berisi versi terkompilasi dari C ++ :: NumberList. Di bawah Solaris, ia akan menemukan perpustakaan bersama "libNumberList.so" di suatu tempat di $ LD_LIBRARY_PATH. Konvensi penamaan pustaka bersama mungkin berbeda di sistem operasi lain.

Sebagian besar metode di kelas ini dideklarasikan sebagai "native". Artinya kami akan menyediakan fungsi C untuk mengimplementasikannya. Untuk menulis fungsi C, kita menjalankan javah dua kali, pertama sebagai "javah NumberListProxy," lalu sebagai "javah -stubs NumberListProxy." Ini secara otomatis menghasilkan beberapa kode "perekat" yang diperlukan untuk runtime Java (yang diletakkan di NumberListProxy.c) dan menghasilkan deklarasi untuk fungsi C yang akan kita implementasikan (di NumberListProxy.h).

Saya memilih untuk mengimplementasikan fungsi-fungsi ini dalam file bernama NumberListProxyImpl.cc. Ini dimulai dengan beberapa arahan #include yang khas:

// // NumberListProxyImpl.cc // // // File ini berisi kode C ++ yang mengimplementasikan stub yang dihasilkan // oleh "javah -stubs NumberListProxy". cf. NumberListProxy.c. #include #include "NumberListProxy.h" #include "NumberList.h"

adalah bagian dari JDK, dan mencakup sejumlah deklarasi sistem penting. NumberListProxy.h dibuat untuk kita oleh javah, dan menyertakan deklarasi fungsi C yang akan kita tulis. NumberList.h berisi deklarasi kelas C ++ NumberList.

Dalam konstruktor NumberListProxy, kita memanggil metode asli initCppSide (). Metode ini harus menemukan atau membuat objek C ++ yang ingin kita wakili. Untuk keperluan artikel ini, saya hanya akan mengalokasikan heap-mengalokasikan objek C ++ baru, meskipun secara umum kita mungkin ingin menautkan proxy kita ke objek C ++ yang dibuat di tempat lain. Penerapan metode asli kami terlihat seperti ini:

void NumberListProxy_initCppSide (struct HNumberListProxy * javaObj) {NumberList * list = new NumberList (); lepas tangan (javaObj) -> numberListPtr_ = (panjang) daftar; }

Seperti yang dijelaskan di Tutorial Java , kami memberikan "pegangan" ke objek Java NumberListProxy. Metode kami membuat objek C ++ baru, lalu melampirkannya ke anggota data numberListPtr_ dari objek Java.

Sekarang ke metode yang menarik. Metode ini memulihkan penunjuk ke objek C ++ (dari anggota data numberListPtr_), lalu menjalankan fungsi C ++ yang diinginkan:

void NumberListProxy_addNumber (struct HNumberListProxy * javaObj, long v) {NumberList * list = (NumberList *) tidak terkendali (javaObj) -> numberListPtr_; daftar-> addNumber (v); } panjang NumberListProxy_size (struct HNumberListProxy * javaObj) {NumberList * list = (NumberList *) tidak terkendali (javaObj) -> numberListPtr_; daftar kembali-> size (); } panjang NumberListProxy_getNumber (struct HNumberListProxy * javaObj, panjang i) {NumberList * list = (NumberList *) lepas tangan (javaObj) -> numberListPtr_; daftar kembali-> getNumber (i); }

Nama fungsi (NumberListProxy_addNumber, dan lainnya) ditentukan oleh javah untuk kita. Untuk informasi lebih lanjut tentang ini, jenis argumen yang dikirim ke fungsi, makro unhand (), dan detail lain dari dukungan Java untuk fungsi C asli, lihat Tutorial Java .

Meskipun "lem" ini agak membosankan untuk ditulis, ini cukup mudah dan bekerja dengan baik. Tetapi apa yang terjadi ketika kita ingin memanggil Java dari C ++?

Memanggil Java dari C ++

Sebelum mempelajari cara memanggil metode Java dari C ++, izinkan saya menjelaskan mengapa ini diperlukan. Dalam diagram yang saya tunjukkan sebelumnya, saya tidak menyajikan keseluruhan cerita dari kelas C ++. Gambaran yang lebih lengkap tentang kelas C ++ ditunjukkan di bawah ini:

As you can see, we're dealing with an observable number list. This number list might be modified from many places (from NumberListProxy, or from any C++ object that has a reference to our C++::NumberList object). NumberListProxy is supposed to faithfully represent all of the behavior of C++::NumberList; this should include notifying Java observers when the number list changes. In other words, NumberListProxy needs to be a subclass of java.util.Observable, as pictured here:

It's easy enough to make NumberListProxy a subclass of java.util.Observable, but how does it get notified? Who will call setChanged() and notifyObservers() when C++::NumberList changes? To do this, we'll need a helper class on the C++ side. Luckily, this one helper class will work with any Java observable. This helper class needs to be a subclass of C++::Observer, so it can register with C++::NumberList. When the number list changes, our helper class' update() method will be called. The implementation of our update() method will be to call setChanged() and notifyObservers() on the Java proxy object. This is pictured in OMT:

Before going into the implementation of C++::JavaObservableProxy, let me mention some of the other changes.

NumberListProxy has a new data member: javaProxyPtr_. This is a pointer to the instance of C++JavaObservableProxy. We'll need this later when we discuss object destruction. The only other change to our existing code is a change to our C function NumberListProxy_initCppSide(). It now looks like this:

 void NumberListProxy_initCppSide(struct HNumberListProxy *javaObj) { NumberList* list = new NumberList(); struct HObservable* observable = (struct HObservable*) javaObj; JavaObservableProxy* proxy = new JavaObservableProxy(observable, list); unhand(javaObj)->numberListPtr_ = (long) list; unhand(javaObj)->javaProxyPtr_ = (long) proxy; } 

Note that we cast javaObj to a pointer to an HObservable. This is OK, because we know that NumberListProxy is a subclass of Observable. The only other change is that we now create a C++::JavaObservableProxy instance and maintain a reference to it. C++::JavaObservableProxy will be written so that it notifies any Java Observable when it detects an update, which is why we needed to cast HNumberListProxy* to HObservable*.

Given the background so far, it may seem that we just need to implement C++::JavaObservableProxy:update() such that it notifies a Java observable. That solution seems conceptually simple, but there is a snag: How do we hold onto a reference to a Java object from within a C++ object?

Maintaining a Java reference in a C++ object

It might seem like we could simply store a handle to a Java object within a C++ object. If this were so, we might code C++::JavaObservableProxy like this:

 class JavaObservableProxy public Observer { public: JavaObservableProxy(struct HObservable* javaObj, Observable* obs) { javaObj_ = javaObj; observedOne_ = obs; observedOne_->addObserver(this); } ~JavaObservableProxy() { observedOne_->deleteObserver(this); } void update() { execute_java_dynamic_method(0, javaObj_, "setChanged", "()V"); } private: struct HObservable* javaObj_; Observable* observedOne_; }; 

Unfortunately, the solution to our dilemma is not so simple. When Java passes you a handle to a Java object, the handle] will remain valid for the duration of the call. It will not necessarily remain valid if you store it on the heap and try to use it later. Why is this so? Because of Java's garbage collection.

First of all, we're trying to maintain a reference to a Java object, but how does the Java runtime know we're maintaining that reference? It doesn't. If no Java object has a reference to the object, the garbage collector might destroy it. In this case, our C++ object would have a dangling reference to an area of memory that used to contain a valid Java object but now might contain something quite different.

Even if we're confident that our Java object won't get garbage collected, we still can't trust a handle to a Java object after a time. The garbage collector might not remove the Java object, but it could very well move it to a different location in memory! The Java spec contains no guarantee against this occurrence. Sun's JDK 1.0.2 (at least under Solaris) won't move Java objects in this way, but there are no guarantees for other runtimes.

Yang benar-benar kami perlukan adalah cara memberi tahu pengumpul sampah bahwa kami berencana untuk mempertahankan referensi ke objek Java, dan meminta semacam "referensi global" ke objek Java yang dijamin akan tetap valid. Sayangnya, JDK 1.0.2 tidak memiliki mekanisme seperti itu. (Satu mungkin akan tersedia di JDK 1.1; lihat akhir artikel ini untuk informasi lebih lanjut tentang arah masa depan.) Sementara kita menunggu, kita bisa mengatasi masalah ini.