Petua Java 17: Mengintegrasikan Java dengan C ++

Dalam artikel ini, saya akan membincangkan beberapa masalah yang terlibat dalam penyatuan kod C ++ dengan aplikasi Java. Setelah memberitahu tentang mengapa seseorang ingin melakukan ini dan apa rintangannya, saya akan membina program Java yang berfungsi yang menggunakan objek yang ditulis dalam C ++. Sepanjang perjalanan, saya akan membincangkan beberapa implikasi dari melakukan ini (seperti interaksi dengan pengumpulan sampah), dan saya akan memberikan gambaran mengenai apa yang dapat kita harapkan di kawasan ini pada masa akan datang.

Mengapa mengintegrasikan C ++ dan Java?

Mengapa anda mahu mengintegrasikan kod C ++ ke dalam program Java di tempat pertama? Bagaimanapun, bahasa Java diciptakan, sebahagiannya, untuk mengatasi beberapa kekurangan C ++. Sebenarnya, ada beberapa sebab mengapa anda mungkin mahu mengintegrasikan C ++ dengan Java:

  • Persembahan. Walaupun anda membangun untuk platform dengan penyusun tepat waktu (JIT), kemungkinan kod yang dihasilkan oleh jangka masa JIT jauh lebih perlahan daripada kod C ++ yang setara. Seiring dengan peningkatan teknologi JIT, ini tidak menjadi faktor. (Sebenarnya, dalam waktu terdekat, teknologi JIT yang baik mungkin bermaksud Java berjalan lebih pantas daripada kod C ++ yang setara.)
  • Untuk penggunaan semula kod warisan dan penyatuan ke dalam sistem warisan.
  • Untuk mengakses perkakasan secara langsung atau melakukan aktiviti peringkat rendah yang lain.
  • Untuk memanfaatkan alat yang belum tersedia untuk Java (OODBMSes dewasa, ANTLR, dan sebagainya).

Sekiranya anda mengambil keputusan dan memutuskan untuk mengintegrasikan Java dan C ++, anda melepaskan beberapa kelebihan penting dari aplikasi Java sahaja. Berikut adalah kelemahannya:

  • Aplikasi C ++ / Java campuran tidak dapat berjalan sebagai applet.
  • Anda melepaskan keselamatan penunjuk. Kod C ++ anda bebas untuk menyalahgunakan objek, mengakses objek yang dipadam, atau memori yang rosak dengan cara lain yang sangat mudah di C ++.
  • Kod anda mungkin tidak mudah alih.
  • Persekitaran binaan anda pasti tidak mudah alih - anda harus memikirkan cara meletakkan kod C ++ di perpustakaan bersama di semua platform yang diminati.
  • API untuk mengintegrasikan C dan Java sedang berjalan dan kemungkinan besar akan berubah dengan perpindahan dari JDK 1.0.2 ke JDK 1.1.

Seperti yang anda lihat, mengintegrasikan Java dan C ++ bukan untuk lemah hati! Walau bagaimanapun, jika anda ingin meneruskan, baca terus.

Kita akan mulakan dengan contoh ringkas yang menunjukkan cara memanggil kaedah C ++ dari Java. Kami kemudian akan memaparkan contoh ini untuk menunjukkan cara menyokong corak pemerhati. Corak pemerhati, selain menjadi salah satu landasan pengaturcaraan berorientasikan objek, berfungsi sebagai contoh yang baik dari aspek yang lebih terlibat dalam pengintegrasian kod C ++ dan Java. Kami kemudian akan membina program kecil untuk menguji objek C ++ berbungkus Java kami, dan kami akan berakhir dengan perbincangan mengenai petunjuk masa depan untuk Java.

Memanggil C ++ dari Java

Apa yang sukar untuk mengintegrasikan Java dan C ++, anda bertanya? Bagaimanapun, Tutorial Java SunSoft memiliki bagian tentang "Mengintegrasikan Kaedah Asli ke dalam Program Java" (lihat Sumber). Seperti yang akan kita lihat, ini memadai untuk memanggil kaedah C ++ dari Java, tetapi tidak cukup untuk kita memanggil kaedah Java dari C ++. Untuk melakukan itu, kita perlu melakukan lebih banyak kerja.

Sebagai contoh, kami akan mengambil kelas C ++ sederhana yang ingin kami gunakan dari dalam Java. Kami akan menganggap bahawa kelas ini sudah ada dan kami tidak dibenarkan mengubahnya. Kelas ini dipanggil "C ++ :: NumberList" (untuk kejelasan, saya akan awalan semua nama kelas C ++ dengan "C ++ ::"). Kelas ini menerapkan senarai nombor yang mudah, dengan kaedah untuk menambahkan nombor ke dalam senarai, meminta ukuran senarai, dan mendapatkan elemen dari senarai. Kami akan membuat kelas Java yang tugasnya adalah mewakili kelas C ++. Kelas Java ini, yang akan kita panggil NumberListProxy, akan mempunyai tiga kaedah yang sama, tetapi pelaksanaan kaedah ini adalah memanggil setara C ++. Ini digambarkan dalam rajah teknik pemodelan objek (OMT) berikut:

Contoh Java dari NumberListProxy perlu merujuk kepada contoh C ++ dari NumberList yang sesuai. Ini cukup mudah, jika sedikit tidak mudah alih: Sekiranya kita berada di platform dengan penunjuk 32-bit, kita hanya boleh menyimpan penunjuk ini di int; jika kita berada di platform yang menggunakan penunjuk 64-bit (atau kita fikir mungkin dalam masa terdekat), kita boleh menyimpannya dalam masa yang lama. Kod sebenar untuk NumberListProxy mudah, jika agak tidak kemas. Ini menggunakan mekanisme dari bahagian "Mengintegrasikan Kaedah Asli ke dalam Program Java" di Tutorial Java SunSoft.

Potongan pertama di kelas Java kelihatan seperti ini:

kelas awam NumberListProxy {statik {System.loadLibrary ("NumberList"); } NumberListProxy () {initCppSide (); } addNumber batal asli awam (int n); saiz int asli awam (); orang awam int getNumber (int i); kekosongan asli persendirian initCppSide (); nombor int peribadiListPtr_; // Daftar Nombor *}

Bahagian statik dijalankan semasa kelas dimuat. System.loadLibrary () memuatkan pustaka bersama yang dinamakan, yang dalam kes kami mengandungi versi C ++ :: NumberList yang disusun. Di bawah Solaris, pihaknya akan menjumpai perpustakaan bersama "libNumberList.so" di suatu tempat di $ LD_LIBRARY_PATH. Konvensyen penamaan perpustakaan bersama mungkin berbeza dalam sistem operasi lain.

Sebilangan besar kaedah dalam kelas ini dinyatakan sebagai "asli." Ini bermaksud bahawa kita akan memberikan fungsi C untuk melaksanakannya. Untuk menulis fungsi C, kami menjalankan javah dua kali, pertama sebagai "javah NumberListProxy", kemudian sebagai "javah -stubs NumberListProxy." Ini secara automatik menghasilkan beberapa kod "gam" yang diperlukan untuk runtime Java (yang dimasukkan dalam NumberListProxy.c) dan menghasilkan pernyataan untuk fungsi C yang akan kita laksanakan (dalam NumberListProxy.h)

Saya memilih untuk melaksanakan fungsi-fungsi ini dalam fail bernama NumberListProxyImpl.cc. Ia bermula dengan beberapa arahan khas #include:

// // NumberListProxyImpl.cc // // // Fail ini mengandungi kod C ++ yang menerapkan stub yang dihasilkan // oleh "javah -stubs NumberListProxy". rujuk NumberListProxy.c. #include #include "NumberListProxy.h" #include "NumberList.h"

adalah sebahagian daripada JDK, dan merangkumi sejumlah pernyataan sistem penting. NumberListProxy.h dihasilkan untuk kami oleh javah, dan merangkumi deklarasi fungsi C yang akan kami tulis. NumberList.h mengandungi perisytiharan NumberList kelas C ++.

Dalam konstruktor NumberListProxy, kita memanggil kaedah asli initCppSide (). Kaedah ini mesti mencari atau membuat objek C ++ yang ingin kita wakili. Untuk tujuan artikel ini, saya hanya akan memperuntukkan objek C ++ baru, walaupun secara umum kami mungkin ingin menghubungkan proksi kami ke objek C ++ yang dibuat di tempat lain. Pelaksanaan kaedah asli kami kelihatan seperti ini:

batal NumberListProxy_initCppSide (struct HNumberListProxy * javaObj) {NumberList * list = NumberList baru (); unsand (javaObj) -> senarai numberListPtr_ = (panjang); }

Seperti yang dijelaskan dalam Tutorial Java , kami melewati "menangani" ke objek Java NumberListProxy. Kaedah kami membuat objek C ++ baru, kemudian melampirkannya ke anggota data numberListPtr_ objek Java.

Sekarang ke kaedah menarik. Kaedah ini memulihkan penunjuk ke objek C ++ (dari anggota data numberListPtr_), kemudian memanggil fungsi C ++ yang diinginkan:

batal NumberListProxy_addNumber (struct HNumberListProxy * javaObj, panjang v) {NumberList * list = (NumberList *) unsand (javaObj) -> numberListPtr_; senarai-> addNumber (v); } NumberListProxy_size panjang (struct HNumberListProxy * javaObj) {NumberList * list = (NumberList *) unsand (javaObj) -> numberListPtr_; senarai pemulangan-> saiz (); } NumberListProxy_getNumber panjang (struct HNumberListProxy * javaObj, panjang i) {NumberList * list = (NumberList *) unsand (javaObj) -> numberListPtr_; senarai pemulangan-> getNumber (i); }

Nama fungsi (NumberListProxy_addNumber, dan yang lain) ditentukan untuk kita oleh javah. Untuk maklumat lebih lanjut mengenai hal ini, jenis argumen yang dikirimkan ke fungsi, makro tak terkendali (), dan perincian lain tentang dukungan Java untuk fungsi C asli, sila lihat Tutorial Java .

Walaupun "lem" ini agak membosankan untuk ditulis, ia cukup mudah dan berfungsi dengan baik. Tetapi apa yang berlaku ketika kita ingin memanggil Java dari C ++?

Memanggil Java dari C ++

Sebelum mengetahui kaedah memanggil kaedah Java dari C ++, izinkan saya menerangkan mengapa ini perlu dilakukan. Dalam gambarajah yang saya tunjukkan sebelumnya, saya tidak memaparkan keseluruhan kisah kelas C ++. Gambar kelas C ++ yang lebih lengkap ditunjukkan di bawah:

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 kita perlukan adalah cara untuk memberitahu pengumpul sampah bahwa kita berencana untuk mempertahankan referensi ke objek Java, dan meminta semacam "rujukan global" pada objek Java yang dijamin tetap valid. Malangnya, JDK 1.0.2 tidak mempunyai mekanisme sedemikian. (Satu mungkin akan tersedia di JDK 1.1; lihat bahagian akhir artikel ini untuk maklumat lebih lanjut mengenai petunjuk masa depan.) Sementara kami menunggu, kami dapat mengatasi masalah ini.