Category Archives: JNI – Lập trình Java Native Interface

JNI – Dịch một chương trình JNI

Trước tiên chúng ta cần có các công cụ sau đây:

  • JDK 2 trở lên, phiên bản mới nhất tại thời điểm viết bài là JDK 8.
  • Trình biên dịch MinGW.

Bạn lên mạng tải và cài đặt 2 trình biên dịch trên, JDK có thể tải trực tiếp từ website của Oracle. MinGW có thể tải về từ mingw.org, lưu ý là các IDE phổ biến như DevC++ hoặc CodeBlocks cũng thường có sẵn MinGW khi tải về rồi, nếu bạn đã từng tải và cài 2 IDE trên mà có sử dụng bản đính kèm MinGW thì bạn không cần phải tải MinGW về nữa.

Lưu ý là mặc dù bạn có cài bằng IDE thì ở đây chúng ta sẽ biên dịch trên dòng lệnh hết, tức là sẽ không có bấm nút màu xanh lá cây hay bấm Ctrl + F9, F6… gì cả.

Sau khi đã tải và cài xong thì bạn thêm đường dẫn đến thư mục bin trong thư mục cài đặt của 2 trình biên dịch này vào biến môi trường PATH để tiện sử dụng. Ví dụ trên Windows 10 thì bạn vào My Computer → Systems properties → Advanced system settings → Environment Variables… Sau đó trong phần System variables bạn tìm biến PATH, bấm Edit… và thêm đường dẫn đến 2 thư mục trên vào.

Xong xuôi rồi thì bạn có thể kiểm tra bằng cách mở Command Prompt (cmd) lên và gõ lệnh java -version để xem phiên bản Java và g++ --version để xem phiên bản C++.

Kết quả ra tương tự như hình dưới đây là được:

Liên kết Java và C++ bằng JNI

Để có thể gọi code C++ từ code Java thì chúng ta làm như sau:

  • Bước 1: Viết phương thức native trong Java
  • Bước 2: Dùng lệnh javac để dịch code đó ra file .class
  • Bước 3: Dùng lệnh javah -jni để tạo file .h (Header trong C++)
  • Bước 4: Viết hàm/lớp/phương thức C++ mà sẽ được gọi từ phương thức native trong Java đã khai báo ở trên
  • Bước 5: Dịch hàm/lớp/phương thức C++ trên thành file thư viện liên kết động .dll (Windows) hoặc .so (Linux)
  • Bước 6: Load file thư viện đó trong main ở phía Java và sử dụng

Ví dụ

Chúng ta sẽ viết hàm in dòng chữ “Hello World” bằng C++, sau đó biên dịch hàm đó ra file .dll rồi load file đó vào trong Java, từ Java chúng ta sẽ gọi hàm in dòng chữ “Hello World” đó.

Đầu tiên chúng ta tạo một thư mục chứa project, ở đây mình tạo thư mục HelloWorld trong ổ đĩa C.

Bước 1: Viết code Java

Chúng ta tạo một file có tên HelloWorld.java trong thư mục HelloWorld có nội dung như sau:

public class HelloWorld {
    static {
        System.loadLibrary("HelloWorld");
    }
 
    private native void print();
 
    public static void main(String[] args) {
        new HelloWorld().print();
    } 
}

Đoạn code trên rất đơn giản, đầu tiên chúng ta gọi phương thức System.loadLibrary("HelloWorld") để chương trình nạp file HelloWorld.dll vào, file HelloWorld.dll sẽ được tạo sau.

Sau đó ở dưới chúng ta định nghĩa phương thức có tên print(), phương thức này là phương thức native, chúng ta dùng từ khóa native để khai báo một phương thức là native. Phương thức native là phương thức được gọi từ file thư viện .dll.

Cuối cùng là hàm main(), trong đó chúng ta tạo đối tượng HelloWorld rồi gọi phương thức print().

Bước 2: biên dịch code Java

Chúng ta dùng lệnh javac HelloWorld.java để dịch ra file HelloWorld.class

Bước 3: Tạo file header .h

Chúng ta dùng lệnh javah -jni -classpath . HelloWorld để tạo ra file HelloWorld.h.

Trong lệnh trên thì tham số -classpath có giá trị là đường dẫn đến file .class đã được tạo ra ở bước 2, ở đây chúng ta truyền vô giá trị là dấu chấm, tức là đường dẫn đến thư mục hiện tại trong Command Prompt.

File HelloWorld.h sẽ được tạo ra tự động có nội dung như sau:

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class HelloWorld */

#ifndef _Included_HelloWorld
#define _Included_HelloWorld
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class: HelloWorld
 * Method: print
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_HelloWorld_print
 (JNIEnv *, jobject);

#ifdef __cplusplus
}
#endif
#endif

Chúng ta sẽ không đụng chạm gì tới các file .h này, có một lưu ý là ở đầu file này có include file thư viện jni.h, file này nằm trong thư mục include trong thư mục cài đặt JDK, bạn có thể tìm và mở ra xem, trong đó lại có dòng include đến file jni_md.h nữa, và file này nằm trong thư mục include/win32 trong thư mục cài đặt JDK, chúng ta sẽ cần lưu ý đến 2 thư mục này.

Một lưu ý nữa là trong đoạn code trên có phần khai báo prototype cho hàm Java_HelloWorld_print(JNIEnv *, jobject), không có phần thân hàm {}. Nhiệm vụ của chúng ta là phải code hàm này ở đâu đó.

Bước 4: viết hàm native có trong file header

Do đó bây giờ chúng ta tạo một file có tên HelloWorld.cpp trong thư mục đó với nội dung như sau:

#include <iostream>
#include "HelloWorld.h"

using namespace std;

JNIEXPORT void JNICALL Java_HelloWorld_print (JNIEnv *e, jobject obj) {
    cout << "Hello World printed from C++" << endl;
}

Ở đây chúng ta viết giống như viết code C++ bình thường, chỉ khác là chúng ta dùng những từ khóa của JNI như JNIEXPORT, JNICALl, JNIEnv, jobject…v.v chúng ta sẽ tìm hiểu chúng sau, một điểm khác nữa là không có hàm main(), đơn giản là vì chúng ta đang viết thư viện chứ không viết một chương trình.

Bước 5: dịch hàm native C++ ra file thư viện

Nếu bạn đã từng dịch C++ bằng dòng lệnh thì bạn sẽ biết là chúng ta cần dịch ra file object trước, sau đó mới link các file object thành file exe, ở đây cũng vậy, chỉ khác là chúng ta link thành file .dll (hoặc .so).

Đầu tiên chúng ta chạy lệnh g++ để tạo file object, ở đây chúng ta chạy lệnh này với cú pháp như sau:

g++ -I"<đường_dẫn_thư_mục_header" -c <tên_file_cpp> -o <tên_file_object>

Trong đó -I là tham số nhận vào đường dẫn đến các thư mục chứa file .h được dùng thêm, tại vì ở đây chúng ta có dùng đến file jni.hjni_md.h trong thư mục include của JDK nên chúng ta phải truyền vào tham số -I. Tham số -c nhận vào tên file .cpp mà chúng ta đã viết ra ở bước 4, tham số -o nhận vào tên file .o sẽ được tạo ra sau khi chạy lệnh này.

Giả sử thư mục cài đặt JDK của mình nằm ở C:\Programs Files\Java\jdk1.8.0_65 thì mình sẽ gõ lệnh trên như hình sau:

Tiếp theo chúng ta tạo file thư viện .dll theo cú pháp như sau:

g++ -Wl,--add-stdcall-alias -shared -o <tên_file_dll> <tên_file_object>

Bạn cần đưa vào tham số -Wl,--add-stdcall-alias nếu bạn dùng hệ điều hành Windows. Tham số -shared cho biết chúng ta sẽ tạo file thư viện, tham số -o có giá trị là tên file .dll sẽ được tạo ra, cuối cùng là tên file .o đã được tạo ra ở trên.

Lệnh mình gõ sẽ tương tự như sau:

Bước 6: Run

Vậy là xong, chúng ta có thể chạy code Java được rồi, chúng ta dùng lệnh sau:

java -classpath . -Djava.library.path=<đường_dẫn_đến_file_dll> <tên_lớp_main>

Chúng ta dùng lệnh Java để chạy một file đã được dịch thành file .class, tuy nhiên ở đây chúng ta có dùng thêm thư viện nữa, mặc dù file thư viện .dll nằm chung thư mục với file .class (nếu bạn làm giống như những gì đã hướng dẫn ở trên) nhưng Java vẫn sẽ không tự nhận diện file đó, do đó chúng ta phải thêm đường dẫn đến thư mục chứa file .dll đó trong tham số –Djava.library.path.

Kết quả giống như hình dưới:

JNI – Giới thiệu

Chúng ta biết môi trường Java là một môi trường lập trình bao gồm 2 thứ: máy ảo Java (JVM – Java Virtual Machine) và các thư viện API. Khi dịch thì Java sẽ được dịch sang mã máy có thể chạy được trên JVM.

Một khái niệm khác là môi trường máy chủ, tức là hệ điều hành được cài trên máy tính đó, môi trường này cũng có các API riêng, tập lệnh CPU riêng, ngôn ngữ lập trình riêng (thường là C/C++, Assembly).

Môi trường Java được cài nằm trên môi trường máy chủ, ứng với mỗi hệ điều hành (môi trường máy chủ riêng) mà có một môi trường Java riêng, chẳng hạn trên Windows và Solaris thì môi trường Java này là Java Runtime Environment (JRE).

Vai trò của JNI

Khi một môi trường Java được cài trên một hệ điều hành, sẽ có trường hợp người lập trình muốn sử dụng các thư viện của riêng hệ điều hành đó. Lý do là vì dùng thư viện của riêng hệ điều hành sẽ nhanh hơn, hiệu suất cao hơn.

Ngoài ra đối với hệ điều hành Windows thì số lượng các thư viện do cộng đồng viết ra rất nhiều, và có một số thư viện cực kỳ đồ sộ, việc viết lại các thư viện này bằng ngôn ngữ Java sẽ mất nhiều thời gian hơn so với việc tìm cách sử dụng chúng từ Java.

JNI là một tính năng cực kỳ mạnh mẽ cho phép chúng ta sử dụng code từ các ngôn ngữ khác, JNI có tính chất 2 chiều, tức là code từ các ngôn ngữ khác cũng có thể gọi lại code từ Java nữa.

Nhược điểm của JNI

Chúng ta đã biết rằng Java là một ngôn ngữ viêt một lần-chạy mọi nơi, tức là chỉ cần viết code Java, sau đó biên dịch rồi đem lên một hệ điều hành có cài JVM là có thể chạy bình thường. Tuy nhiên khi chúng ta sử dụng JNI để “hợp tác” với code của hệ điều hành, thì lại không thể đem chương trình đó đi chạy trên máy có hệ điều hành khác được, do đó mất đi tính viết một lần-chạy mọi nơi.

Một điều nữa là Java có tính năng type-safe, tức là bạn khai báo kiểu dữ liệu gì thì chỉ được thao tác với kiểu dữ liệu đó, nhưng các ngôn ngữ hệ điều hành thì có thể không có tính năng type-safe, do đó khi viết code JNI bạn sẽ phải chú ý cẩn thận, chỉ cần khác kiểu dữ liệu cũng có thể crash chương trình.

Khi nào nên sử dụng JNI

Trước khi quyết định sử dụng JNI, bạn nên tìm hiểu xem kỹ càng xem có thư viện nào hỗ trợ yêu cầu của mình không, nếu không còn thì hãy dùng JNI, luôn dùng JNI làm giải pháp cuối cùng.