Hướng dẫn lập trình JNI (Java Native Interface)

From CodeForLife
Jump to: navigation, search

Giới thiệu tổng quan

JNI (Java Native Interface) là giao diện lập trình cho phép sử dụng các ngôn ngữ khác (C/C++, Assembly) trong Java. JNI thường sử dụng xây dựng các hàm core, giúp tăng hiệu năng hệ thống.

Mô hình đơn giản bạn xem ảnh dưới:
RTENOTITLE

Mô hình chi tiết các tầng:
RTENOTITLE

Lý do sử dụng JNI:

  • Vấn đề hiệu năng: Thông thường những tác vụ cần hiệu năng cao thường được xử lý C/C++, còn Java chỉ gọi lại mà thôi.
  • Truy cập APIs mức OS
  • Chuyển ứng dụng hoặc dịch vụ từ C/C++ sang Java: Trường hợp này bạn sử dụng JNI sẽ nhanh hơn rất nhiều so với việc code lại toàn bộ ứng dụng bằng Java.

Chuẩn bị môi trường

Để build và chạy ứng dụng, bạn cần cài đặt tối thiểu hai công cụ sau:

  • JDK: Từ JDK 2  trở lên, nhưng thường sử dụng từ JDK 6. Hiện tại đã có JDK 8.
  • C/C++ Compiler: Trình biên dịch C/C++. Tùy trên OS nào mà bạn chọn cho phù hợp.

Ngoài ra, bạn cài đặt thêm IDE nào đó giúp cho việc lập trình đơn giản hơn, chẳng hạn: Netbeans hoặc Eclipse.

Việc cài đặt các công cụ này khác nhau đôi chút, phụ thuộc vào hệ điều hành. Ở đây tôi xin hướng dẫn bạn build và chạy trên 02 hệ điều hành:

  • Hệ điều hành Windows
  • Hệ điều hành Ubuntu

Chuẩn bị môi trường trên Ubuntu

Các bước cài đặt môi trường:

  • B1: Cài đặt OpenJDK
    Trên Ubuntu, để biết hệ điều hành hỗ trợ cài đặt OpenJDK phiên bản nào hãy đánh lệnh sau:
    sudo apt-get update
    sudo apt-get install openjdk-<Nhấn phím tab>

    Bạn sẽ thấy danh sách các phiên bản openjdk cho phép cài đặt như sau:
    openjdk-7-dbg           openjdk-8-demo          openjdk-9-dbg
    openjdk-7-demo          openjdk-8-doc           openjdk-9-demo
    openjdk-7-doc           openjdk-8-jdk           openjdk-9-doc
    openjdk-7-jdk           openjdk-8-jdk-headless  openjdk-9-jdk
    openjdk-7-jre           openjdk-8-jre           openjdk-9-jdk-headless
    openjdk-7-jre-headless  openjdk-8-jre-dcevm     openjdk-9-jre
    openjdk-7-jre-lib       openjdk-8-jre-headless  openjdk-9-jre-headless
    openjdk-7-jre-zero      openjdk-8-jre-jamvm     openjdk-9-jre-zero
    openjdk-7-source        openjdk-8-jre-zero      openjdk-9-source
    openjdk-8-dbg           openjdk-8-source
           
    Bạn chọn phiên bản để cài đặt, khi cài đặt OpenJDK chú ý thứ tự cài đặt là: openjdk-x-jre-headless đến openjdk-x-jre rồi mới đến openjdk-x-jdk.
    Ví dụ lệnh cài đặt OpenJDK 7:
    sudo apt-get install openjdk-7-jre-headless
    sudo apt-get install openjdk-7-jre
    sudo apt-get install openjdk-7-jdk

    Ví dụ lệnh cài đặt OpenJDK 8:
    sudo apt-get install openjdk-8-jre-headless
    sudo apt-get install openjdk-8-jre
    sudo apt-get install openjdk-8-jdk
  • B2: Cài đặt gcc
    Bạn đánh lệnh sau để cài đặt gcc:
    sudo apt-get install gcc
  • B3: Cấu hình biến môi trường
    Sau khi cài đặt sau, bạn tiến hành cấu hình biến môi trường để tiện cho việc build ứng dụng sau này. Thực hiện như sau:
    Chỉnh sửa tệp .bashrc
    gedit ~/.bashrc
    Thêm hai dòng sau vào cuối tệp:
    export C_INCLUDE_PATH=$C_INCLUDE_PATH:/usr/lib/jvm/java-8-openjdk-amd64/include:/usr/lib/jvm/java-8-openjdk-amd64/include/linux:/usr/include
    export CPLUS_INCLUDE_PATH=$CPLUS_INCLUDE_PATH:/usr/lib/jvm/java-8-openjdk-amd64/include:/usr/lib/jvm/java-8-openjdk-amd64/include/linux:/usr/include/

    Sau đó bạn lưu tệp này lại.
    Ghi chú:
    • Đường dẫn thực tế trên máy bạn có thể khác đường dẫn này, phụ thuộc vào phiên bản JDK và phiên bản OS. Nếu khác thì bạn phải cập nhật lại cho đúng.
    • Ngoài cấu hình tệp .bashrc, bạn có cấu hình file khác như /etc/profile,...
  • B4: Bạn thực hiện đăng xuất rồi đăng nhập lại để hệ thống cập nhật biến môi trường.
    Ngoài ra bạn có thể cài thêm Netbeans, Eclipse, Qt,... để tiện cho lập trình nếu cần. Đối project lớn thì nên sử dụng một trong các IDE nào đó. Tôi thường sử dụng Eclipse/Netbeans để làm cho các project có cả phần C/C++ và java.

Chuẩn bị môi trường trên Windows

Các bước cài đặt môi trường:

  • B1: Cài đặt JDK
    Bạn download phiên bản JDK SE cho Windows trên trang http://www.oracle.com/technetwork/java/javase/downloads/index.html
    Bạn chọn đúng phiên bản mình cần, thường thì hay sư dụng JDK 8 hoặc JDK 7. Bạn sử dụng JDK 64 hay 32 bit phụ thuộc vào MinGW build ra DLL 32 hay 64 bit nhé.
  • B2: Cài đặt MinGW
    Có hai phiên bản 32 và 64 bit, tùy bạn chọn phiên bản nào. Sau khi cài đặt xong MinGW bạn vẫn có thể cài đặt thêm gói bằng cách sử dụng "MinGW Installation Manager" (GUI) hoặc sử dụng lệnh mingw-get (Lệnh này tương tự như apt-get trên Ubuntu).
  • B3: Cấu hình biến môi trường
    Trên Windows, bạn vào "Advanced System Settings", trong tab  "Advanced" kích vào nút "Environment Variables", bạn thực hiện:
    - Thêm đường dẫn tới thư mục bin của JDK và thư mục bin của MinGW vào biến môi trường PATH
    - Thêm hai biến môi trường C_INCLUDE_PATH và CPLUS_INCLUDE_PATH
    Ví dụ như ảnh dưới:
    RTENOTITLE
    Chú ý: JDK và MinGW phải tương ứng.
  • B4: Cài đặt thêm IDE khác nếu cần
    Ngoài ra bạn có thể cài thêm Netbeans, Eclipse, Qt,... để tiện cho lập trình nếu cần. Đối project lớn thì nên sử dụng một trong các IDE nào đó. Tôi thường sử dụng Eclipse/Netbeans để làm cho các project có cả phần C/C++ và java.

Từng bước tạo ví dụ đơn giản với JNI

Các bước xây dựng ứng dụng JNI

Để viết một ứng dụng JNI, ta phải thực hiện từng bước như sau:

RTENOTITLE

STT Bước thực hiện Mô tả chi tiết
1 Viết mã Java - Trước tiên bạn cần thiết kế hàm, các tham số trước để java có thể gọi xuống C/C++. 
- Tạo lớp chứa các hàm cần gọi, các hàm này yêu cầu bắt buộc phải có từ khóa native. Hàm này bạn có thể để private/protected/public, có thể để static hoặc không tùy bạn.
- Thêm đoạn code load thư viện bằng hàm System.loadLibrary() hoặc hàm System.load() khi lớp này sử dụng.
- Bạn có thể đặt hàm native cùng tên nhưng khác tham số, nhưng khuyên cáo dung tên hàm native khác nhau để tránh nhầm lẫn trong C/C++.
2 Biên dịch thành tệp class Sử dụng công cụ javac của jdk để build thành tệp .class. Nếu bạn dùng IDE (Netbean, Eclipse,…) thì tương đương việc bạn build ứng dụng.
3 Sinh tệp .h - Sử dụng công cụ javah của JDK để tạo từ tệp java. Nhìn vào tệp .h sinh ra bạn sẽ hiểu được cú phấn ánh xạ tên hàm giữa Java và C++.
- Bạn để ý thấy tất cả các hàm trên JNI đều phải có hai tham sốJNIEnv*jclass/jobject, sau đó mới là các tham số của hàm tương ứng trên Java, trong đó: jclass sử dụng cho các hàm static, jobject sử dụng cho các hàm không phải static
- Bạn cũng cần chú ý đối với các hàm native cùng tên nhưng khác tham số.
4 Viết code trên C/C++ Cài đặt các hàm native trên C/C++
5 Dịch native code Sử dụng gcc để dịch native code thành thư viện .so trên Ubuntu/Linux, .dll trên Windows.
6 Chạy ứng dụng Sử dụng java để chạy ứng dụng

JNIDemo1: Ứng dụng JNI đầu tiên

Đây là ví dụ đơn giản sử dụng command line để giúp bạn:

  • Hiểu về JNI và cách JNI hoạt động
  • Java gọi hàm của C/C++ như thế nào
  • Biết cách thao tác với đối tượng jstring.

Dưới đây từng bước hướng dẫn bạn tạo ra ứng dụng JNI đầu tiên theo các bước như trong mục đã nêu ở trên.

Ví dụ này cũng như tất cả các ví dụ sau này thì đều dùng môi trường chuẩn chạy là Ubuntu, các lệnh được thực hiện trên Terminal. Riêng trong ví dụ JNIDemo1 này, hướng dẫn thêm bạn cách chạy trên Windows, các ví dụ khác bạn có thể làm tương tự.

Build và chạy trên môi trường Ubuntu

Môi trường build và chạy:

  • Ubuntu 16.04.2
  • OpenJDK 1.8.0_131 64-Bit
  • gcc (Ubuntu 5.4.0-6ubuntu1~16.04.4) 5.4.0 20160609

Đầu tiên, bạn tạo tệp "JNIDemo1.java" với nội dung như sau:

public class JNIDemo1 {
	static {
		System.loadLibrary("JNIDemo1"); // Java trên Ubuntu tìm tệp libJNIDemo1.so
	}

	public native static String serverName();
	public native void sayHello(String name);
	public native void sayHello();
	public native void goodBye();
	
	public static void main(String[] args) {
		System.out.println("Server name: " + JNIDemo1.serverName());
		
		JNIDemo1 demo = new JNIDemo1();
		if (args.length>0) {
			demo.sayHello(args[0]);
		} else {
			demo.sayHello();
		}
		demo.goodBye();
	}
}

Trong đoạn code trên bạn thấy:

  • Có 04 hàm được khai báo với từ khóa native. Những hàm này là những hàm JNI, sẽ được cài đặt ở tầng C/C++, trên Java không cài đặt hàm này.
  • Ngay phần đầu tiên, bạn thấy đoạn mã "System.loadLibrary("JNIDemo1");" => Đoạn mã này yêu cầu Java tìm và load thư tệp thư viện "libJNIDemo1.so". Bạn có thể sử dụng hàm System.load(), khi đó bạn phải truyền đường dẫn tuyệt đối đến tệp so này.
  • Trong hàm main(), thực hiện tạo đối tượng JNIDemo1 và gọi các hàm JNI.

Sau khi code xong phần Java, bạn dùng lệnh javac sau để biên dịch tệp "JNIDemo1.java" thành tệp "JNIDemo1.class" (Yêu cầu bắt buộc phải cài đặt JDK):
javac JNIDemo1.java
Sau bước này bạn sẽ nhịn thấy tệp JNIDemo1.class trong cùng thư mục.

Bây giờ bạn sử dụng lệnh javah sau để sinh ra tệp header sử dụng trong C/C++:
javah JNIDemo1
Sau khi chạy lệnh trên, bạn sẽ thấy tệp "JNIDemo1.h" được sinh ra với nội dung như dưới. Bạn nhìn qua tệp bạn sẽ hiểu cú pháp tên hàm tương ứng bên JNI:

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

#ifndef _Included_JNIDemo1
#define _Included_JNIDemo1
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     JNIDemo1
 * Method:    serverName
 * Signature: ()Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL Java_JNIDemo1_serverName
  (JNIEnv *, jclass);

/*
 * Class:     JNIDemo1
 * Method:    sayHello
 * Signature: (Ljava/lang/String;)V
 */
JNIEXPORT void JNICALL Java_JNIDemo1_sayHello__Ljava_lang_String_2
  (JNIEnv *, jobject, jstring);

/*
 * Class:     JNIDemo1
 * Method:    sayHello
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_JNIDemo1_sayHello__
  (JNIEnv *, jobject);

/*
 * Class:     JNIDemo1
 * Method:    goodBye
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_JNIDemo1_goodBye
  (JNIEnv *, jobject);

#ifdef __cplusplus
}
#endif
#endif

Bây giờ bạn cài đặt hàm JNI trong tệp "JNIDemo1.cpp" như dưới. Tệp JNIDemo1.cpp sẽ include tệp JNIDemo1.h sinh ra ở trên:

#include "JNIDemo1.h"
#include <stdio.h>
																																									
JNIEXPORT jstring JNICALL Java_JNIDemo1_serverName (JNIEnv *env, jclass clz) {
	return env->NewStringUTF("JNIDemo1 Server");
}

JNIEXPORT void JNICALL Java_JNIDemo1_sayHello__Ljava_lang_String_2 (JNIEnv *env, jobject obj, jstring name) {
	// Chuyen tu JNI String (jstring) sang C-String (char*)
	const char* pszName = env->GetStringUTFChars(name, NULL);
	if (NULL == pszName) return;

	// Hien thi man hinh
	printf("Hi %s!\n", pszName);

	// Giai phong tai nguyen
	env->ReleaseStringUTFChars(name, pszName);
}

JNIEXPORT void JNICALL Java_JNIDemo1_sayHello__(JNIEnv *env, jobject obj) {
	printf("Hi Annonymous!\n");
}

JNIEXPORT void JNICALL Java_JNIDemo1_goodBye (JNIEnv *env, jobject obj) {
	printf("Goodbye!\n");
}

Tiếp theo, bạn đánh lệnh sau để build ra tệp "libJNIDemo1.so":
gcc -o libJNIDemo1.so -shared -fPIC JNIDemo1.cpp

Nếu bạn thấy xuất hiện lỗi như sau:
In file included from JNIDemo1.cpp:1:0:
JNIDemo1.h:2:17: fatal error: jni.h: No such file or directory
compilation terminated.

Lỗi này là do trình biên dịch GCC không biết tệp jni.h ở đâu. Trường hợp này, bạn kiểm tra lại xem bạn đã thực hiện cấu hình đúng như trong phần: Chuẩn bị môi trường

Trong lệnh build trên, bạn cần chú ý một số điểm sau:

  • Tham số “-o libJNIDemo1.so -shared” báo cho GCC biết tạo thư viện liên kết động tên là libJNIDemo1.so, nếu không có dòng này GCC sẽ hiểu là build ứng dụng và báo lỗi và không tìm thấy hàm main.
  • Tham số “-fPIC” cơ chế build, nếu build cho các chip ARM thường để chế độ “-fPIE”. 
    • PIE: Position-independent executables
    • PIC: Position-independent code
  • Trường hợp chưa cấu hình thư mục include thì các bên thêm tham số -I để xác định các thư mục include của JNI.

Sau khi build xong, bạn sẽ thấy có tệp "libJNIDemo1.so" được tạo trong cùng thư mục.

Đến đây việc build hoàn thành, bây giờ bạn chạy ứng dụng bằng lệnh sau và xem kết quả:

java -Djava.library.path=. JNIDemo1
Server name: JNIDemo1 Server
Hi Annonymous!
Goodbye!


'''java -Djava.library.path=. JNIDemo1 Thang'''
Server name: JNIDemo1 Server
Hi Thang!
Goodbye!

Trong lệnh chạy bạn chú ý phải có tham số “-Djava.library.path=.” để java biết tìm thư viện liên kết động trong thư mục hiện tại.

Tải source code ví dụ: JNIDemo1.zip
Trong bộ source code này (Các ví dụ về sau cũng vậy), để tiện cho việc build và chạy ứng dụng, tôi tạo ra 02 Shell Script:

  • build.sh: Đóng gọi toàn bộ lệnh build vào tệp này. Để build ví dụ bạn chỉ cần đánh lệnh:
    ./build.sh
  • run.sh: Đóng gói các lệnh chạy ví dụ. Để chạy ví dụ bạn chỉ cần đánh lệnh:
    ./run.sh

Build và chạy trên môi trường Windows

Môi trường build và chạy (Nhớ phải cấu hình biến môi trường như đã nói ở phần trước):

Về cơ bản source code vẫn giống như chạy với Ubuntu, nhưng có một chút thay đổi nhỏ liên quan tới khai báo hàm JNI: Các hàm bây giờ thêm kí từ _ (Underscore) vào trước Java_JNIDemo1_xxx để thành _Java_JNIDemo1_xxx (Sửa cả file h và cpp)

Tệp "JNIDemo1.java" vẫn giữ nguyên nội dung:

public class JNIDemo1 {
	static {
		System.loadLibrary("JNIDemo1"); // Java trên Windows sẽ tìm tệp JNIDemo1.dll
	}

	public native static String serverName();
	public native void sayHello(String name);
	public native void sayHello();
	public native void goodBye();
	
	public static void main(String[] args) {
		System.out.println("Server name: " + JNIDemo1.serverName());
		
		JNIDemo1 demo = new JNIDemo1();
		if (args.length>0) {
			demo.sayHello(args[0]);
		} else {
			demo.sayHello();
		}
		demo.goodBye();
	}
}

Sau khi code xong phần Java, bạn dùng lệnh javac sau để biên dịch tệp "JNIDemo1.java" thành tệp "JNIDemo1.class" (Yêu cầu bắt buộc phải cài đặt JDK):
javac JNIDemo1.java
Sau bước này bạn sẽ nhịn thấy tệp JNIDemo1.class trong cùng thư mục.

Bây giờ bạn sử dụng lệnh javah sau để sinh ra tệp header sử dụng trong C/C++:
javah JNIDemo1
Sau khi chạy lệnh trên, bạn sẽ thấy tệp "JNIDemo1.h" được sinh ra tự động, nhưng bạn phải thêm kí tự gạch dưới vào trước Java_JNIDemo1 để được tệp như dưới:

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

#ifndef _Included_JNIDemo1
#define _Included_JNIDemo1
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     JNIDemo1
 * Method:    serverName
 * Signature: ()Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL _Java_JNIDemo1_serverName
  (JNIEnv *, jclass);

/*
 * Class:     JNIDemo1
 * Method:    sayHello
 * Signature: (Ljava/lang/String;)V
 */
JNIEXPORT void JNICALL _Java_JNIDemo1_sayHello__Ljava_lang_String_2
  (JNIEnv *, jobject, jstring);

/*
 * Class:     JNIDemo1
 * Method:    sayHello
 * Signature: ()V
 */
JNIEXPORT void JNICALL _Java_JNIDemo1_sayHello__
  (JNIEnv *, jobject);

/*
 * Class:     JNIDemo1
 * Method:    goodBye
 * Signature: ()V
 */
JNIEXPORT void JNICALL _Java_JNIDemo1_goodBye
  (JNIEnv *, jobject);

#ifdef __cplusplus
}
#endif
#endif

Tệp "JNIDemo1.cpp" bây giờ cũng sẽ thay đổi khai báo hàm tương ứng:

#include "JNIDemo1.h"
#include <stdio.h>
																																								
JNIEXPORT jstring JNICALL _Java_JNIDemo1_serverName (JNIEnv *env, jclass clz) {
	return env->NewStringUTF("JNIDemo1 Server");
}

JNIEXPORT void JNICALL _Java_JNIDemo1_sayHello__Ljava_lang_String_2 (JNIEnv *env, jobject obj, jstring name) {
	// Chuyen tu JNI String (jstring) sang C-String (char*)
	const char* pszName = env->GetStringUTFChars(name, NULL);
	if (NULL == pszName) return;

	// Hien thi man hinh
	printf("Hi %s!\n", pszName);

	// Giai phong tai nguyen
	env->ReleaseStringUTFChars(name, pszName);
}

JNIEXPORT void JNICALL _Java_JNIDemo1_sayHello__(JNIEnv *env, jobject obj) {
	printf("Hi Annonymous!\n");
}

JNIEXPORT void JNICALL _Java_JNIDemo1_goodBye (JNIEnv *env, jobject obj) {
	printf("Goodbye!\n");
}

Tiếp theo, bạn đánh lệnh sau để build ra tệp "JNIDemo1.dll":
gcc -o JNIDemo1.dll -shared -fPIC JNIDemo1.cpp
Sau khi build xong bạn sẽ thấy tệp JNIDemo1.dll trong cùng thư mục. Nếu dụng PE Deconstructor (https://files.quickmediasolutions.com/exe/pedeconstructor_0.1_amd64.exe) để check sẽ biết tệp này chạy trên môi trường 32 bit.

Bây giờ bạn thực hiện chạy bằng hai lênh sau và xem kết quả:
java -Djava.library.path=. JNIDemo1
java -Djava.library.path=. JNIDemo1 Thang

RTENOTITLE

Tải source code ví dụ: JNIDemo1-Win.zip

Ghi chú: Lý do vì sao trên Windows phải thêm kí tự gạch dưới trước Java thì bạn có thể tham chiếu tới link: http://baruchyoussin.com/en/java-native-interface-jni-programmer-guide-tutorial-javah.html. Cụ thể tác giả của bài viết này giải thích:
The C/C++ header file that was created by javah (see above), defined my function with JNICALL (see an example in the Wikipedia article). As I looked for the definition of JNICALL , I found it in the header file jni_md.h which is include ‘d in the header file jni.h (both mentioned above). The header jni_md.h defines JNICALL as __stdcall . This is a calling convention and according to Microsoft documentation __stdcall prefixes an underscore _ to the name and suffixes it with @ followed by the decimal number of bytes in the argument list.

JNIDemo2: Demo từ C/C++ gọi hàm callback Java

Ví dụ này tạo trong C++ các Thread, và cứ sau 1 phút (Trong ví dụ thời gian này có thể khác) các thread này lại gọi hàm callback của java.

Trong ví dụ này, tôi chia thành thư mục riêng phần C/C++ và Java, riêng Java các lớp đều được đặt trong gói "com.example.jni.demo2". Cấu trúc thư mục ví dụ như sau:
Jnidemo2-folder.png

Lớp com.example.jni.demo2.JNIDemo2: Lớp này tạo hai hàm jni gọi xuống tầng C/C++ để tạo Thread trong C/C++ và một hàm callback để C/C++ gọi lên java:

package com.example.jni.demo2;

public class JNIDemo2 {
    static {
        System.loadLibrary("JNIDemo2");
    }
    
    public static native boolean createThread(int id);
    public static native boolean stopThreads();
    
    public static void threadCallback(int id, String time) {
        System.out.println("Thread " + id + " return time: " + time);
    }
}

Lớp com.example.jni.demo2.Main:

package com.example.jni.demo2;

public class Main {

    public static void main(String[] args) {
        boolean ret1 = JNIDemo2.createThread(10);
        boolean ret2 = JNIDemo2.createThread(20);
        if (ret1 || ret2) {
            try {
                Thread.sleep(1000L*2600);
            } catch (Exception e) {}
            JNIDemo2.stopThreads();
        }
    }    
}

Tệp JNIDemo2.cpp:

#include "JNIDemo2.h"
#include <pthread.h>
#include <unistd.h>
#include <string.h>

using namespace std;

#define NUM_THREADS     5

int threadNum = 0;
pthread_t arrThreads[NUM_THREADS];

JavaVM* pJavaVM = NULL;
jclass clsJNIDemo2 = NULL;
jmethodID midCallback = NULL;
    
// Ham lay thoi gian hien tai
void currentDateTime(char* pszDateTime) {
    time_t now = time(0);
    struct tm  tstruct;
    strcpy(pszDateTime, "");
    tstruct = *localtime(&now);
    strftime(pszDateTime, 20, "%H:%M:%S", &tstruct);
}

// Ham goi callback len java
void callJavaCallback(int id, const char* pszDateTime) {    
    JNIEnv* pJNIEnv = NULL;

    bool isAttached = false;
    if (pJavaVM->GetEnv(reinterpret_cast<void**>(&pJNIEnv), JNI_VERSION_1_6) != JNI_OK) {
        if (pJavaVM->AttachCurrentThread((void**)&pJNIEnv, NULL)<0) {
            printf("threadTask: Unable to attach current thread");
            return;
        }
        isAttached = true;
    }
    
    // Kiem tra chua co thi tao
    if (clsJNIDemo2==NULL) {
        jclass dataClass = pJNIEnv->FindClass("com/example/jni/demo2/JNIDemo2");
        if (dataclass="=NULL)" {
            printf("Unable to found class: com.example.jni.demo2.JNIDemo2");
        }
        clsJNIDemo2 = (jclass) pJNIEnv->NewGlobalRef(dataClass);
        if (clsJNIDemo2!=NULL) {
            midCallback = pJNIEnv->GetStaticMethodID(clsJNIDemo2, "threadCallback", "(ILjava/lang/String;)V");
        }
    }
    
    // Kiem tra lai lan nua
    if (midCallback==NULL) {
        return;
    }
    
    jstring jDateTime = pJNIEnv->NewStringUTF(pszDateTime);
    pJNIEnv->CallStaticVoidMethod(clsJNIDemo2, midCallback, id, jDateTime);
    if (pJNIEnv->ExceptionCheck()) {
        pJNIEnv->ExceptionDescribe();
        pJNIEnv->ExceptionClear();
    }
    
    if (isAttached) {
        pJavaVM->DetachCurrentThread();
    }
}

void *threadTask(void * t) {
    int id = *((int *)&t);
    int maxcount = 1000;
    int count = 0;
    char szPreDateTime[50];
    char szDateTime[50];
    
    currentDateTime(szDateTime);
    callJavaCallback(id, szDateTime);
    strcpy(szPreDateTime, szDateTime);
    
    while (count<maxcount) {
        count++;
        currentDateTime(szDateTime);
        printf("Current time: %s\n", szDateTime);
        if (strcmp(szDateTime, szPreDateTime)!=0) {
            callJavaCallback(id, szDateTime);
            strcpy(szPreDateTime, szDateTime);
        }
        sleep(1);
    }
    
    pthread_exit(NULL);
}

JNIEXPORT jboolean JNICALL Java_com_example_jni_demo2_JNIDemo2_createThread
  (JNIEnv *env, jclass cls, jint jid) {
    int rc;
    int id = jid;
    
    // Kiem tra so luong thread hien tai
    if (threadNum>=NUM_THREADS) {
        printf("Error: Unable to create more than %d threads.\n", NUM_THREADS);
        return false;
    }
    
    // Tao thread
    rc = pthread_create(&arrThreads[threadNum], NULL, &threadTask, (void *)id);
    if (rc) {
        printf("Error (%d): Unable to create Thread %d\n", rc, id);
        return false;
    } else {
        printf("Create Thread %d successfully\n", id);
    }
    threadNum++;

    return true;
}

JNIEXPORT jboolean JNICALL Java_com_example_jni_demo2_JNIDemo2_stopThreads(JNIEnv *env, jclass cls) {
    for (int i=0; i<threadNum; i++) {
        pthread_cancel(arrThreads[i]);
    }
    threadNum = 0;
}

JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) {
    pJavaVM = vm;
    return JNI_VERSION_1_6;
}

JNIEXPORT void JNICALL JNI_OnUnload(JavaVM *vm, void *reserved) {
    if (clsJNIDemo2!=NULL) {
        JNIEnv* pJNIEnv = NULL;
        if (vm->GetEnv(reinterpret_cast<void**>(&pJNIEnv), JNI_VERSION_1_6)==JNI_OK) {
            pJNIEnv->DeleteGlobalRef(clsJNIDemo2);
        }
    }
    
    clsJNIDemo2 = NULL;
    midCallback = NULL;
    pJavaVM = NULL;
}

Để build ví dụ bạn có thể chạy lệnh:
./build.sh
hoặc chạy tập các lệnh:
javac -cp bin -d bin java/src/com/example/jni/demo2/JNIDemo2.java java/src/com/example/jni/demo2/Main.java
javah -classpath bin -o c/JNIDemo2.h com.example.jni.demo2.JNIDemo2
gcc -o bin/libJNIDemo2.so -shared -fPIC c/JNIDemo2.cpp

Để chạy ví dụ bạn có thể chạy lệnh:
./run.sh
hoặc chạy lệnh sau:
java -Djava.library.path=bin -classpath bin com.example.jni.demo2.Main

Kết quả chạy như sau:

Jnidemo2-output.png

Tải source code ví dụ: JNIDemo2.zip

Cơ bản về JNI

JNIEnv và JavaVM

Trong các hàm Native luôn luôn có hai tham số:

  • JNIEnv: Đây là con trỏ trỏ tới cấu trúc JNINativeInterface, nơi lưu trữ tất cả các con trỏ hàm Native, và cung cấp nhiều hàm tiện ích giúp tương tác với JVM và làm việc với đối tượng Java. Chi tiết xem: http://docs.oracle.com/javase/7/docs/technotes/guides/jni/spec/functions.html
  • jclass/jobject: Con trỏ tham chiếu tới cấu trúc chứa thông tin lớp và đối tượng trên Java.

JNI cho phép bạn chồng hai hàm JNI_OnLoadJNI_OnUnload để xác định khi nào thư viện Native được load và qua hàm này bạn lưu lại đối tượngJavaVM* để sử dụng:

JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved);
JNIEXPORT void JNICALL JNI_OnUnload(JavaVM *vm, void *reserved);

Việc lưu lại đối tượng JavaVM sẽ giúp bạn có thể chủ động gọi hàm callback lên Java.Chi tiết sẽ trình bày ở phần dưới.

Ánh xạ kiểu dữ liệu giữa C/C++ và Java

Bảng dưới là ánh xạ kiểu dữ liệu nguyên thủy giữa C/C++ và Java:

Java Type Native Type Description
boolean jboolean unsigned 8 bits
byte jbyte signed 8 bits
char jchar unsigned 16 bits
short jshort signed 16 bits
int jint signed 32 bits
long jlong signed 64 bits
float jfloat 32 bits
double jdouble 64 bits
void void N/A

JNI hỗ trợ kiểu dữ liệu tham chiếu tới một số đối tượng của Java:
RTENOTITLE

Ghi chú: Trong JNI, có thể thao tác trực tiếp với các kiểu dữ liệu nguyên thủy bình thường. Nhưng với dữ liệu object, để thao tác được bạn phải thông qua các hàm JNI hỗ trợ.

Cú pháp gọi hàm JNI trong C và C++

Bạn chú ý, biến JNIEnv* env trong C và C++ khác nhau. Trong C++, biến JNIEnv là một class thực sự, còn trong C thì nó chỉ là con trỏ đơn thuần. Vì thế việc gọi các hàm JNI trong C và C++ khác nhau đôi chút, các bạn cần chú ý:

  • Trong C++: Cú pháp như sau:
    env->JNIFuncName(param1 ...);
    Trong đó JNIFuncName là tên hàm JNI, tiếp theo là các tham số cho hàm.
  • Trong C: Cú pháp như sau:
    (*env)->JNIFuncName(env, param1 ...);
    Trong đó JNIFuncName là tên hàm JNI, tham số đầu tiên chính là env, sau đó mới là các tham số khác.

Ghi chú: Trong tất cả các ví dụ của bài viết này đều sử dụng cú pháp C++ cho tiện.

Chữ ký của phương thức và biến thành viên trên Java

Khi làm việc với JNI, ở tầng C/C++, đôi khi bạn cần thao tác tới một đối tượng nào đó trên Java thông qua:

  • Gọi một hàm của một đối tượng nào đó trên Java.
  • Truy cập vào biến thành viên nào đó trên Java.

Để làm được việc này, ngoài việc bạn xác định đối tượng trên Java thông qua biến kiểu jobject/jclass thì bạn cần biết "Chữ ký hàm" hoặc "Chữ ký biến thành viên". Chi tiết gọi hàm, truy cập biến thành viên trên Java sẽ được trình bày ở các phần sau, phần này tập trung phần chữ ký.

Chữ ký được xác định thông qua kiểu dữ liệu. Trong đó:

  • Chữ ký biến thành viên theo định dạng sau: "type"
  • Chữ ký phương thức theo định dạng như sau: "(argument-types)return-type".

Trong đó mỗi kiểu (type) được mã hóa theo bảng sau:

Signature Java Programming Language Type Signature Java Programming Language Type
V void J long
Z boolean F float
B byte D double
C char L fully-qualified-class; fully-qualified-class
S short [type type[]
I int ( arg-types ) ret-type method type

Ví dụ:

  • Ví dụ chữ ký biến thành viên:
    • Ljava/lang/String; Chữ ký cho biến thành viên có kiểu String.
    • I Chữ ký cho biến thành viên kiểu int.
    • BChữ ký cho biến thành viên kiểu byte.
    • [I Chũ ký cho biến thành viên kiểu mảng int[].
  • Ví dụ chữ ký phương thức:
    • (Ljava/lang/String;)Ljava/lang/String; Chữ ký cho phương thức có 1 tham số String và trả về String.
    • ([Ljava/lang/String;)V Chữ ký cho phương thức có 1 tham số vào mảng String[] và trả về void.

Đơn giản nhất bạn sử dụng công cụ javap để sinh chữ ký:
    javap -s -p <Tên class java, không có extension>

Thao tác với đối tượng Java String

Khi Java truyền đối tượng String tới một phương thức native, trong native sẽ hiểu đó là kiểu jstring. Kiểu jstring này khác kiểu xâu (char *) trong C, nếu bạn sử dụng trực tiếp đối tượng jstring này sẽ gây crash VM. Vì vậy ngăn cấm việc sử dụng đối tượng jstring như trong đoạn code sau:

/* Không được sử dụng theo cách này !!! */
JNIEXPORT jstring JNICALL Java_Prompt_getLine(JNIEnv *env, jobject obj, jstring prompt)
{
    printf("%s", prompt);
    ...
}

Để thao tác với xâu trong C/C++, bạn phải chuyển đối tượng jstring sang đối tượng xâu trong C/C++ bằng hàm GetStringUTFChars, hàm nay có nhiệm vụ chuyển xâu Unicode trên Java sang xâu dạng UTF-8 trên C/C++. Sau khi sử dụng xong bạn phải giải phóng bằng lệnh ReleaseStringUTFChars. Trường hợp bạn muốn trả về xâu jstring trong hàm native, bạn phải sử dụng hàm NewStringUTF:

JNIEXPORT jstring JNICALL Java_Prompt_getLine(JNIEnv *env, jobject obj, jstring prompt)
{
    char buf[128];
    // Chuyển từ jstring sang C-string
    const char *str = env->GetStringUTFChars(prompt, 0);
    
    // Xử lý ở đây
    // …
    printf("%s", str);
    // …
    
    // Giải phóng tài nguyên khi ko sử dụng
    env->ReleaseStringUTFChars(prompt, str);
    
    scanf("%s", buf);

    // Chuyển từ C-string sang jstring
    return env->NewStringUTF(buf);
}

Dưới đây là một số hàm JNI cung cấp để thao tác xâu jstring:

Hàm Mô tả
Cho xâu UTF-8
const char * GetStringUTFChars(JNIEnv *env, jstring string, jboolean *isCopy); Chuyển từ jstring sang UTF-8 C-string
void ReleaseStringUTFChars(JNIEnv *env, jstring string, const char *utf); Giải phóng tài nguyên UTF-8 C-string đã cấp phát
jstring NewStringUTF(JNIEnv *env, const char *bytes); Tạo đối tượng jstring từ UTF-8 C-string
jsize GetStringUTFLength(JNIEnv *env, jstring string); Trả về độ dài xâu UTF-8
void GetStringUTFRegion(JNIEnv *env, jstring str, jsize start, jsize length, char *buf); Chuyển length kí tự Unicode bắt đầu từ vị trí start vào vùng nhớ buf.
Cho xâu Unicode
const jchar * GetStringChars(JNIEnv *env, jstring string, jboolean *isCopy); Chuyển từ jstring sang Unicode C-string
void ReleaseStringChars(JNIEnv *env, jstring string, const jchar *chars); Giải phóng tài nguyên Unicode C-string đã cấp phát
jstring NewString(JNIEnv *env, const jchar *unicodeChars, jsize length); Tạo đối tượng jstring từ Unicode C-string
size GetStringLength(JNIEnv *env, jstring string); Lấy độ dài xâu Unicode C-string
void GetStringRegion(JNIEnv *env, jstring str, jsize start, jsize length, jchar *buf); Chuyển length kí tự Unicode bắt đầu từ vị trí start vào vùng nhớ buf.

Để hiểu hơn, bạn xem lại ví dụ JNIDemo1 trong phần: JNIDemo1: Ứng dụng JNI đầu tiên

Thao tác với đối tượng Java Array

JNI sử dụng đối tượng jarray để tham chiếu tới mảng của Java. Tương tự như jstring, bạn không thể thao tác trực tiếp với đối tượng này mà phải sử dụng các hàm chuyển đổi mà JNI cung cấp:

/*  Đoạn mã dưới là không hợp lệ */
JNIEXPORT jint JNICALL
Java_IntArray_sumArray(JNIEnv *env, jobject obj, jintArray arr)
{
    int i, sum = 0;
    for (i=0; i<10; i++) {
        sum += arr[i];
    }
    ...
}

Để sử dụng mảng bạn phải gọi hàm GetArrayLength để lấy độ dài mảng, hàm Get<PrimitiveType>ArrayElements để lấy con trỏ trỏ đến mảng, và sau khi sử dụng xong bạn gọi hàm Release<PrimitiveType>ArrayElements. Đoạn mã dưới ví dụ thao tác mảng jintArray:

JNIEXPORT jint JNICALL Java_IntArray_sumArray(JNIEnv *env, jobject obj, jintArray arr)
{
    int i, sum = 0;
    
    // Lấy độ dài mảng
    jsize len = env->GetArrayLength(arr);
    // Lấy dữ liệu mảng
    jint *body = env->GetIntArrayElements(arr, 0);

    // Xử lý tính tổng
    for (i=0; i<len; i++) {
        sum += body[i];
    }

    // Giải phóng tài nguyên
    env->ReleaseIntArrayElements(arr, body, 0);
    return sum;
}

Dưới đây là một số hàm thao tác với

Hàm Mô tả
NativeType * Get<PrimitiveType>ArrayElements(JNIEnv *env, ArrayType array, jboolean *isCopy); Lấy dữ liệu mạng đối tượng jarray
void Release<PrimitiveType>ArrayElements(JNIEnv *env, ArrayType array, NativeType *elems, jint mode); Giải phóng tài nguyên cấp phát
void Get<PrimitiveType>ArrayRegion(JNIEnv *env, ArrayType array, jsize start, jsize length, NativeType *buffer); Lấy ra một vùng dữ liệu mảng
void Set<PrimitiveType>ArrayRegion(JNIEnv *env, ArrayType array, jsize start, jsize length, const NativeType *buffer); Thiết lập một vùng dữ liệu mảng
ArrayType New<PrimitiveType>Array(JNIEnv *env, jsize length); Tạo đối tượng jarray từ mảng
void * GetPrimitiveArrayCritical(JNIEnv *env, jarray array, jboolean *isCopy); Hàm này cho phép sử dụng trực tiếp vùng nhớ trong jarray thay vì phải copy. Nhưng khi gọi hàm này thì JNI lock không cho các hàm khác truy cập vào vùng nhớ này cho đến khi hàm ReleasePrimitiveArrayCritical.
void ReleasePrimitiveArrayCritical(JNIEnv *env, jarray array, void *carray, jint mode);
Get/Set<PrimitiveType>ArrayRegion Cho phép truy cập đến một phần tử xác định trong mảng. Nếu bạn chỉ muốn truy cập vào 1vài  phần tử trong mảng lớn thì nên sử dụng hàm này.

Trong đó:

  • ArrayType: jintArray, jbyteArray, jshortArray, jlongArray, jfloatArray, jdoubleArray, jcharArray, jbooleanArray
  • PrimitiveType: Int, Byte, Short, Long, Float, Double, Char, Boolean
  • NativeType: jint, jbyte, jshort, jlong, jfloat, jdouble, jchar, jboolean

Để hiểu rõ hơn phần này, chúng ta xem source code của ví dụ JNIArray. Ví dụ này sử dụng C/C++ để tính tổng và trung bình cộng của một dãy số truyền từ Java xuống. Đây là ví dụ đơn giản giúp bạn hiểu cách thức thao tác với đối tượng Java Array trong JNI. Từ ví dụ này sẽ không đi sâu vào lệnh build nữa mà chỉ tập trung vào mã nguồn để bạn hiểu được cách thức xử lý.

Tệp JNIArray.java:

  • Tệp này ta khai báo một hàm native tên sumAndAverage, tham số vào là mảng số nguyên int[] và đầu ra là mảng số thực gồm hai phần tử trong đó phần tử đầu là tổng, phần tử thứ hai là trung bình.
  • Trong hàm main gọi hàm native.
public class JNIArray {
   static {
      System.loadLibrary("MyLib");
   }
 
   // Khai bao phuong thuc native sumAndAverage() voi tham so la mang int[] va
   // tra ve mang double[2] trong do phan tu [0] la tong, phan tu [1] la trung binh
   private native double[] sumAndAverage(int[] numbers);
 
   // Test
   public static void main(String args[]) {
      int[] numbers = {22, 33, 33};
      double[] results = new JNIArray().sumAndAverage(numbers);
      System.out.println("In Java, the sum is " + results[0]);
      System.out.println("In Java, the average is " + results[1]);
   }
}

Tệp JNIArray.cpp: Tệp này sử dụng các hàm JNI hỗ trợ thao tác với mảng để xử lý và tính toán.

#include <jni.h>
#include <stdio.h>
#include "JNIArray.h"

JNIEXPORT jdoubleArray JNICALL Java_JNIArray_sumAndAverage(JNIEnv *env, jobject thisObj, jintArray inJNIArray) {
   // Chuyen doi mang jintarray thanh mang jint[]
   jint *inCArray = env->GetIntArrayElements(inJNIArray, NULL);
   if (NULL == inCArray) return NULL;
   jsize length = env->GetArrayLength(inJNIArray);
 
   // Thuc hien xu ly
   jint sum = 0;
   int i;
   for (i = 0; i < length; i++) {
      sum += inCArray[i];
   }
   jdouble average = (jdouble)sum / length;
	
   // Giai phong tai nguyen
   env->ReleaseIntArrayElements(inJNIArray, inCArray, 0);
 
   jdouble outCArray[] = {sum, average};
 
   // Chuyen mang jdouble[] thanh jdoubleArray va tra ve ket qua xu ly cho ham
   jdoubleArray outJNIArray = env->NewDoubleArray(2);			// Cap phat
   if (NULL == outJNIArray) return NULL;
   env->SetDoubleArrayRegion(outJNIArray, 0 , 2, outCArray);  		// Sao chep
   return outJNIArray;
}

Kết quả chạy như sau:

java -Djava.library.path=. JNIArray
In Java, the sum is 88.0
In Java, the average is 29.333333333333332

Tải source code ví dụ: JNIArray.zip

Gọi hàm Java từ C/C++

Bạn muốn truy cập vào một phương thức trên Java từ Native code, bạn phải thực hiện ba bước như sau:

  • Lấy tham chiếu đến class của đối tượng:
    • T/Hợp bạn có jobjectbạn có thể gọi hàm GetObjectClass()
    • T/Hợp chưa có bạn có thể gọi hàm FindClass()
  • Lấy Method ID bằng cách gọi hàm GetMethodID(). Khi gọi hàm này bạn phải truyền vào tên và chữ ký của phương thức.
  • Gọi hàm theo cú pháp: Call<ReturnType>Method()

JNI thực hiệntìm kiếm hàm của Java dựa vào tên và chữ ký của phương thức. Chi tiết phần này xem mục: Chữ ký của phương thức và biến thành viên trên Java

Bảng dưới thống kê các hàm JNI hỗ trợ cho thao tác gọi hàm Java từ C/C++:

Hàm Mô tả
Hàm lấy tham chiếu tới class của đối tượng
jclass GetObjectClass(JNIEnv *env, jobject obj); Hàm lấy tham chiếu tới class của đối tượng
Thao tác với phương thức của đối tượng (Instance method)
jmethodID GetMethodID(JNIEnv *env, jclass cls, const char *name, const char *sig); Trả về Method ID phương thức của đối tượng

NativeType Call<type>Method(JNIEnv *env, jobject obj, jmethodID methodID, ...);

NativeType Call<type>MethodA(JNIEnv *env, jobject obj, jmethodID methodID, const jvalue *args); NativeType Call<type>MethodV(JNIEnv *env, jobject obj, jmethodID methodID, va_list args);

Các hàm gọi phương thức Java từ C/C++
Thao tác với phương thức tĩnh (Static method)
jmethodID GetStaticMethodID(JNIEnv *env, jclass cls, const char *name, const char *sig); Trả về Method ID phương thức tĩnh của lớp

NativeType CallStatic<type>Method(JNIEnv *env, jclass clazz, jmethodID methodID, ...);

NativeType CallStatic<type>MethodA(JNIEnv *env, jclass clazz, jmethodID methodID, const jvalue *args); NativeType CallStatic<type>MethodV(JNIEnv *env, jclass clazz, jmethodID methodID, va_list args);

Các hàm gọi phương thức tĩnh Java từ C/C++
Thao tác với phương thức của đối tượng cha

NativeType CallNonvirtual<type>Method(JNIEnv *env, jobject obj, jclass cls, jmethodID methodID, ...);

NativeType CallNonvirtual<type>MethodA(JNIEnv *env, jobject obj, jclass cls, jmethodID methodID, const jvalue *args); NativeType CallNonvirtual<type>MethodV(JNIEnv *env, jobject obj, jclass cls, jmethodID methodID, va_list args);

Các hàm gọi phương thức của đối tượng cha (Super class)

Để hiểu rõ hơn, chúng ta xem source code ví dụ JNICallJavaMethod. Đây là ví du đơn giản demo gọi hàm Java từ một hàm native trong C/C++ cho các trường hợp:

  • Hàm không tham số và không có trả về
  • Hàm có tham số và không có trả về
  • Hàm có tham số và có trả về
  • Hàm tĩnh

Tệp JNICallJavaMethod.java: Tệp này khai báo hàm native nativeMethod, và một số hàm callback (callback, callbackAverage, callbackStatic) để C/C++ gọi.

public class JNICallJavaMethod {
   static {
      System.loadLibrary("MyLib");
   }
 
   private native void nativeMethod();
 
   // Cac ham duoi de C/C++ goi
   private void callback() {
      System.out.println("In Java");
   }
 
   private void callback(String message) {
      System.out.println("In Java with " + message);
   }
 
   private double callbackAverage(int n1, int n2) {
      return ((double)n1 + n2) / 2.0;
   }
 
   // Static method to be called back
   private static String callbackStatic() {
      return "From static Java method";
   }

   // Test 
   public static void main(String args[]) {
      new JNICallJavaMethod().nativeMethod();
   }
}

Tệp JNICallJavaMethod.cpp: Trong hàm native, thực hiện gọi lại hàm java của cùng đối tượng Java. Lưu ý khi gọi hàm java có trả về giá trị thì phải thực hiện ép kiểu để tránh thông báo lỗi build ứng dụng.

#include <jni.h>
#include <stdio.h>
#include "JNICallJavaMethod.h"
 
JNIEXPORT void JNICALL Java_JNICallJavaMethod_nativeMethod(JNIEnv *env, jobject thisObj) {
   // Lay jclass cua doi tuong
   jclass thisClass = env->GetObjectClass(thisObj);
 
   // Lay Method ID cho phuong thuc "callback" (Phuong thuc nay khong co tham so va tra ve kieu void)
   jmethodID midCallBack = env->GetMethodID(thisClass, "callback", "()V");
   if (NULL == midCallBack) return;
   printf("In C, call back Java's callback()\n");
   env->CallVoidMethod(thisObj, midCallBack);		   // Goi ham java
 
   // Lay Method ID cho phuong thuc "callback" (Phuong thuc nay co mot tham so String va tra ve kieu void)
   jmethodID midCallBackStr = env->GetMethodID(thisClass, "callback", "(Ljava/lang/String;)V");
   if (NULL == midCallBackStr) return;
   printf("In C, call back Java's called(String)\n");
   jstring message = env->NewStringUTF("Hello from C");
   env->CallVoidMethod(thisObj, midCallBackStr, message);
 
   // Lay Method ID cho phuong thuc "callbackAverage" (Phuong thuc nay co hai tham so deu kieu int va tra ve kieu double
   jmethodID midCallBackAverage = env->GetMethodID(thisClass, "callbackAverage", "(II)D");
   if (NULL == midCallBackAverage) return;
   jdouble average = env->CallDoubleMethod(thisObj, midCallBackAverage, 2, 3);
   printf("In C, the average is %f\n", average);
 
   // Lay Method ID cho phuong thuoc tinh "callbackStatic" (Ham nay khong co tham so va tra ve kieu String)
   jmethodID midCallBackStatic = env->GetStaticMethodID(thisClass, "callbackStatic", "()Ljava/lang/String;");
   if (NULL == midCallBackStatic) return;
   jstring resultJNIStr = (jstring) env->CallStaticObjectMethod(thisClass, midCallBackStatic);
   const char *resultCStr = env->GetStringUTFChars(resultJNIStr, NULL);
   if (NULL == resultCStr) return;
   printf("In C, the returned string is %s\n", resultCStr);
   env->ReleaseStringUTFChars(resultJNIStr, resultCStr);
}

Kết quả chạy ví dụ như sau:

java -Djava.library.path=. JNICallJavaMethod
In C, call back Java's callback()
In Java
In C, call back Java's called(String)
In Java with Hello from C
In C, the average is 2.500000
In C, the returned string is From static Java method

Tải source code ví dụ: JNICallJavaMethod.zip

Truy cập biến thành viên trên Java

Để truy cập vào biến thành viên/biến tĩnh trong Java, bạn thực hiện các bước sau:

  • Lấy tham chiếu đến class của đối tượng:
    • T/Hợp bạn có jobject bạn có thể gọi hàm GetObjectClass()
    • T/Hợp chưa có bạn có thể gọi hàm FindClass()
  • Lấy Field ID bằng cách gọi hàm GetFieldID(). Với biến tĩnh bạn gọi hàm GetStaticFieldID(). Khi gọi hàm này bạn phải truyền vào tên và chữ ký của biến thành viên. Chi tiết chữ ký xem phần: Chữ ký của phương thức và biến thành viên trên Java
  • Lấy giá trị biến bằng cách gọi hàm GetObjectField() hoặc Get<primitive-type>Field() dựa trên Field ID. Với biến tĩnh bạn gọi hàm GetStaticObjectField(), GetStatic<Primitive-type>Field().
  • Cập nhật giá trị cho biến bằng cách gọi hàm SetObjectField() hoặc Set<primitive-type>Field() dựa trên Field ID. Với biến tĩnh bạn gọi hàm SetStaticObjectField(), SetStatic<Primitive-type>Field().

Bảng dưới mô tả các hàm sử dụng:

Hàm Mô tả
Hàm lấy tham chiếu tới class của đối tượng
jclass GetObjectClass(JNIEnv *env, jobject obj); Hàm lấy tham chiếu tới class của đối tượng
Thao tác với biến thành viên (Instance variables)
jfieldID GetFieldID(JNIEnv *env, jclass cls, const char *name, const char *sig); Trả về Field ID của biến thành viên
NativeType Get<type>Field(JNIEnv *env, jobject obj, jfieldID fieldID); Lấy giá trị của biến thành viên
void Set<type>Field(JNIEnv *env, jobject obj, jfieldID fieldID, NativeType value); Thiết lập giá trị mới cho biến thành viên
Thao tác với biến tĩnh (Static variables)
jfieldID GetStaticFieldID(JNIEnv *env, jclass cls, const char *name, const char *sig); Trả về Field ID của biến tĩnh
NativeType GetStatic<type>Field(JNIEnv *env, jclass clazz, jfieldID fieldID); Lấy giá trị của biến tĩnh
void SetStatic<type>Field(JNIEnv *env, jclass clazz, jfieldID fieldID, NativeType value); Thiết lập giá trị mới cho biến tĩnh

Để hiểu rõ hơn, ta xem source code ví dụ JNIVariable ở dưới. Đây là ví du đơn giản demo truy cập biến thành viên/biến static của Java từ C/C++:

  • Truy cập biến thành viên kiểu int
  • Truy cập biến thành viên kiểu String
  • Truy cập vào biến tĩnh kiểu double

Tệp JNIVariable.java: Tệp java này khai báo các biến thành viên và biến tĩnh đồng thời khai báo hai hàm native để thao tác với các biến này từ C/C++.

public class JNIVariable {
   static {
      System.loadLibrary("MyLib");
   }
 
   // Cac bien thanh vien
   private int number = 88;
   private String message = "Hello from Java";

   // Bien tinh
   private static double staticNumber = 55.66;
 
   // Khai bao phuong thuc native de thay doi bien thanh vien
   private native void modifyInstanceVariable();

   // Khai bao phuong thuc native de thay doi bien tinh
   private native void modifyStaticVariable();
 
   // Test   
   public static void main(String args[]) {
      JNIVariable test = new JNIVariable();

      test.modifyInstanceVariable();
      System.out.println("In Java, int is " + test.number);
      System.out.println("In Java, String is " + test.message);
      System.out.println();

      test.modifyStaticVariable();
      System.out.println("In Java, the double is " + staticNumber);
   }
}

Tệp JNIVariable.cpp: Trong hai hàm native tương ứng, thực hiện truy cập vào các biến thành viên và biến tĩnh để lấy giá trị và thay đổi giá trị.

#include <jni.h>
#include <stdio.h>
#include "JNIVariable.h"
 
JNIEXPORT void JNICALL Java_JNIVariable_modifyInstanceVariable(JNIEnv *env, jobject thisObj) {
   // Lay tham chieu toi lop cua doi tuong
   jclass thisClass = env->GetObjectClass(thisObj);
 
   // Lay Field ID cua bien thanh vien "number"
   jfieldID fidNumber = env->GetFieldID(thisClass, "number", "I");
   if (NULL == fidNumber) return;
 
   // Lay gia tri bien thanh vien "number"
   jint number = env->GetIntField(thisObj, fidNumber);
   printf("In C, the int is %d\n", number);
 
   // Thay doi gia tri cua bien thanh vien "number"
   number = 99;
   env->SetIntField(thisObj, fidNumber, number);
 
   // Lay Field ID cua bien thanh vien "message"
   jfieldID fidMessage = env->GetFieldID(thisClass, "message", "Ljava/lang/String;");
   if (NULL == fidMessage) return;
 
   // Lay gia tri bien thanh vien "message"
   jstring message = (jstring) env->GetObjectField(thisObj, fidMessage);
 
   // Chuyen tu jstring sang C-string
   const char *cStr = env->GetStringUTFChars(message, NULL);
   if (NULL == cStr) return;
 
   printf("In C, the string is %s\n", cStr);

   // Giai phong tai nguyen
   env->ReleaseStringUTFChars(message, cStr);
 
   // Tao doi tuong jstring tu C-string
   message = env->NewStringUTF("Hello from C");
   if (NULL == message) return;
 
   // Chinh sua gia tri bien thanh vien "message"
   env->SetObjectField(thisObj, fidMessage, message);
}

JNIEXPORT void JNICALL Java_JNIVariable_modifyStaticVariable(JNIEnv *env, jobject thisObj) {
   // Lay tham chieu toi lop cua doi tuong
   jclass thisClass = env->GetObjectClass(thisObj);

   // Lay Field ID cua bien tinh "staticNumber"
   jfieldID fidStaticNumber = env->GetStaticFieldID(thisClass, "staticNumber", "D");
   if (NULL == fidStaticNumber) return;

   // Lay gia tri cua bien tinh "staticNumber"
   jdouble staticNumber = env->GetStaticDoubleField(thisClass, fidStaticNumber);
   printf("In C, the double is %f\n", staticNumber);
   
   // Thay doi gia tri bien tinh "staticNumber"
   staticNumber = 77.88;
   env->SetStaticDoubleField(thisClass, fidStaticNumber, staticNumber);
}

Kết quả chạy ví dụ như sau:

java -Djava.library.path=. JNIVariable
In C, the int is 88
In C, the string is Hello from Java
In Java, int is 99
In Java, String is Hello from C

In C, the double is 55.660000
In Java, the double is 77.88

Tải source code ví dụ: JNIVariable.zip

Nâng cao về JNI

Tạo đối tượng Java mới trong C/C++

Trong mã Native Code cho phép bạn tạo một đối tượng Java bất kỳ bằng cách sử dụng các hàm sau:

Hàm Mô tả
jclass FindClass(JNIEnv *env, const char *name); Hàm lấy tham chiếu tới một class trong Java

jobject NewObject(JNIEnv *env, jclass cls, jmethodID methodID, ...);
jobject NewObjectA(JNIEnv *env, jclass cls, jmethodID methodID, const jvalue *args);
jobject NewObjectV(JNIEnv *env, jclass cls, jmethodID methodID, va_list args);

Khởi tạo một đối tượng Java mới. Tham số methodID xác định phương thức khởi tạo được gọi khi tạo đối tượng.
jobject AllocObject(JNIEnv *env, jclass cls); Tạo đối tượng Java mới mà không gọi bất kỳ hàm khởi tạo nào.

Thông thường các bước thực hiện như sau:

  • Lấy tham chiếu đến class của đối tượng: Bằng cách gọi hàm FindClass()
  • Lấy Method ID của hàm khởi tạo bằng cách gọi hàm GetMethodID(). Sử dụng tên phương thức là “<init>” để lấy hàm khởi tạo đối tượng trên Java.
  • Gọi hàm NewObject()/NewObjectA()/NewObjectV() để khởi tạo đối tượng.

Để hiểu rõ hơn, bạn xem source code ví dụ JNINewJavaObj. Đây là demo tạo đối tượng Java trong Native Code (C/C++) và trả về để trên Java sử dụng.

Tệp JNINewJavaObj.java: Tệp java này khai báo các biến thành viên và biến tĩnh đồng thời khai báo hai hàm native để thao tác với các biến này từ C/C++.

public class JNINewJavaObj {
   static {
      System.loadLibrary("MyLib");
   }
 
   // Phuong thuc Native tra ve doi tuong Integer tren Java
   private native Integer getIntegerObject(int number);
 
   public static void main(String args[]) {
      JNINewJavaObj obj = new JNINewJavaObj();
      System.out.println("In Java, the number is :" + obj.getIntegerObject(9999));
   }
}

Tệp JNINewJavaObj.cpp: Trong hai hàm native tương ứng, thực hiện truy cập vào các biến thành viên và biến tĩnh để lấy giá trị và thay đổi giá trị.

#include <jni.h>
#include <stdio.h>
#include "JNINewJavaObj.h"
 
JNIEXPORT jobject JNICALL Java_JNINewJavaObj_getIntegerObject(JNIEnv *env, jobject thisObj, jint number) {
   // Lay tham chieu toi class java.lang.Integer
   jclass cls = env->FindClass("java/lang/Integer");
 
   // Lay Method ID cua phuong thuc khoi tao
   jmethodID midInit = env->GetMethodID(cls, "<init>", "(I)V");
   if (NULL == midInit) return NULL;

   // Tao doi tuong java.lang.String va goi ham khoi tao
   jobject newObj = env->NewObject(cls, midInit, number);
 
   // Goi ham toString cua doi tuong de hien thi trong C
   jmethodID midToString = env->GetMethodID(cls, "toString", "()Ljava/lang/String;");
   if (NULL == midToString) return NULL;
   jstring resultStr = (jstring) env->CallObjectMethod(newObj, midToString);
   const char *resultCStr = env->GetStringUTFChars(resultStr, NULL);
   printf("In C: the number is %s\n", resultCStr);
 
   // Tra ve doi tuong
   return newObj;
}

Kết quả chạy chương trình như sau:

java -Djava.library.path=. JNINewJavaObj
In C: the number is 9999
In Java, the number is :9999

Tải source code ví dụ: JNISyncObj.zip

Thao tác mảng đối tượng

JNI hỗ trợ cho phép thao tác với mảng các đối tượng trên Java. Để thao tác được, bạn sử dụng các hàm sau:

Hàm Mô tả
jobjectArray NewObjectArray(JNIEnv *env, jsize length, jclass elementClass, jobject initialElement); Hàm khởi tạo mảng đối tượng (jobjectArray)
jobject GetObjectArrayElement(JNIEnv *env, jobjectArray array, jsize index); Trả về một phần tử trong mảng
void SetObjectArrayElement(JNIEnv *env, jobjectArray array, jsize index, jobject value); Thiết lập một phần tử vào mảng

Chi tiết hơn bạn xem source code ví dụ JNIObjectArray. Ví dụ này demo việc sử dụng mảng đối tượng truyền vào từ Java, từ JNI tạo mảng đối tượng và trả về cho java.

Tệp JNIObjectArray.java: Tệp java này khai báo phương thức native tham số vào là mảng đối tượng Integer, và trả về mảng đối tượng Double.

import java.util.ArrayList;
 
public class JNIObjectArray {
   static {
      System.loadLibrary("MyLib");
   }

   // Khai bao phuong thuc Native voi tham so vao la mang so Integer[]
   // tra ve mang Double[2] voi phan tu [0] chua tong, phan tu [1] chua gia tri trung binh
   private native Double[] sumAndAverage(Integer[] numbers);
 
   public static void main(String args[]) {
      Integer[] numbers = {11, 22, 32};
      Double[] results = new JNIObjectArray().sumAndAverage(numbers);
      System.out.println("In Java, the sum is " + results[0]);
      System.out.println("In Java, the average is " + results[1]);
   }
}

Tệp JNIObjectArray.cpp: Tệp này cài đặt hàm native thực hiện:

  • Lấy giá trị từ mảng đối tượng java.lang.Integer
  • Thực hiện tính tổng và giá trị trung bình
  • Trả về mảng java.lang.Double chứa hai giá trị vừa tính ở trên.
#include <jni.h>
#include <stdio.h>
#include "JNIObjectArray.h"
 
JNIEXPORT jobjectArray JNICALL Java_JNIObjectArray_sumAndAverage(JNIEnv *env, jobject thisObj, jobjectArray inJNIArray) {
   // Lay tham chieu den lop java.lang.Integer
   jclass classInteger = env->FindClass("java/lang/Integer");

   // Su dung Integer.intValue() de lay gia tri
   jmethodID midIntValue = env->GetMethodID(classInteger, "intValue", "()I");
   if (NULL == midIntValue) return NULL;
 
   // Lay gia tri cua moi doi tuong Integer trong mang
   jsize length = env->GetArrayLength(inJNIArray);
   jint sum = 0;
   int i;
   for (i = 0; i < length; i++) {
      jobject objInteger = env->GetObjectArrayElement(inJNIArray, i);
      if (NULL == objInteger) return NULL;
      jint value = env->CallIntMethod(objInteger, midIntValue);
      sum += value;
   }
   double average = (double)sum / length;
   printf("In C, the sum is %d\n", sum);
   printf("In C, the average is %f\n", average);
 
   // Lay tham chieu den lop java.lang.Double
   jclass classDouble = env->FindClass("java/lang/Double");
 
   // Cap phat mang gom 2 phan tu java.lang.Double
   jobjectArray outJNIArray = env->NewObjectArray(2, classDouble, NULL);
 
   // Lay tham chieu den phuong thuc khoi tao lop java.lang.Double
   jmethodID midDoubleInit = env->GetMethodID(classDouble, "<init>", "(D)V");
   if (NULL == midDoubleInit) return NULL;
   jobject objSum = env->NewObject(classDouble, midDoubleInit, (double)sum);
   jobject objAve = env->NewObject(classDouble, midDoubleInit, average);

   // Thiet lap gia tri
   env->SetObjectArrayElement(outJNIArray, 0, objSum);
   env->SetObjectArrayElement(outJNIArray, 1, objAve);
 
   return outJNIArray;
}

Kết quả chạy chương trình như sau:

java -Djava.library.path=. JNIObjectArray
In C, the sum is 65
In C, the average is 21.666667
In Java, the sum is 65.0
In Java, the average is 21.666666666666668

Tải source code ví dụ: JNIObjectArray.zip

Tham chiếu toàn cục và tham chiếu cục bộ

Quản lý các tham chiếu là vấn đề quan trọng trong việc viết các chương trình hiệu quả. Chẳng hạn chúng ta thường sử dụng FindClass(), GetMethodID(), GetFieldID() để lưu trữ các tham chiếu jclass, jmethodID, jfieldID bên trong các hàm native. Thay vì phải gọi hàm này nhiều lần bạn có thể lưu trữ lại các tham chiếu này để sử dụng lại, qua đó giảm chi phí chung và tối ưu ứng dụng.

JNI chia tham chiếu đối tượng thành hai loại:

  • Tham chiếu cục bộ (Local references): Tham chiếu được tạo trong hàm native, được giải phóng khi thoát khỏi phương thức. Tham chiếu này hợp lệ trong vòng đời của phương thức. Bạn có thể sử dụng hàm DeleteLocalRef() để hủy tham chiếu này, khi gọi hàm này hệ thống sẽ thực hiện dọn dẹp ngay lập tức. Các đối tượng tham số trong hàm native (Tham số 1, Tham số 2) đều là tham chiếu cục bộ và được giải phóng khi ra khỏi hàm JNI.
  • Tham chiếu toàn cục (Global references): Tham chiếu toàn cục sẽ được giữ lại cho đến khi kết thúc ứng dụng hoặc đến khi gọi hàm DeleteGlobalRef(). Bạn có thể tạo một tham chiếu toàn cục từ một tham chiếu cục bộ từ hàm NewGlobalRef(). Việc sử dụng hàm NewGlobalRef() rất quan trọng, nếu không sử dụng hàm này mà bạn gán trực tiếp vào biến toàn cục sẽ gây lỗi khi thực thi (Runtime error).

Để hiểu rõ hơn, bạn xem source code ví dụ JNIReference. Ví dụ này demo việc sử dụng tham chiếu toàn cục, giúp giảm việc gọi lại một số hàm nhiều lần và giúp tối ưu hệ thống.

Tệp JNIReference.java: Khai báo hai phương thức giống hệt nhau và gọi liên tục hai phương thức này.

public class JNIReference {
   static {
      System.loadLibrary("MyLib");
   }
 
   // Khai bao phuong thuc native
   private native Integer getIntegerObject(int number);
 
   // Khai bao phuong thuc khac voi tham so dau vao va gia tri tra ra tuong tu nhu tren
   private native Integer anotherGetIntegerObject(int number);
 
   public static void main(String args[]) {
      JNIReference test = new JNIReference();
      System.out.println("In java, function test.getIntegerObject:" + test.getIntegerObject(1));
      System.out.println("In java, function test.getIntegerObject:" + test.getIntegerObject(2));
      System.out.println("In java, function test.anotherGetIntegerObject:" + test.anotherGetIntegerObject(11));
      System.out.println("In java, function test.anotherGetIntegerObject:" + test.anotherGetIntegerObject(12));
      System.out.println("In java, function test.getIntegerObject:" + test.getIntegerObject(3));
      System.out.println("In java, function test.anotherGetIntegerObject:" + test.anotherGetIntegerObject(13));
   }
}

Tệp JNIReference.cpp: Tệp này thực hiện:

  • Khai báo hai biến toàn cục classInteger kiểu jclass và midIntegerInit  kiểu jmethodID.
  • Khai báo 1 hàm xử lý chung. Hàm này kiểm tra nếu biến toàn cục classInteger, nếu đã thiết lập thì sử dụng luôn, nếu chưa thì gọi hàm FindClass để lấy tham chiếu cục bộ tới lớp, sau đó phải dùng hàm NewGlobalRef để tạo tham chiếu toàn cục và sử dụng lại.
  • Các bạn chú ý rằng jmethodID jfieldID không phải là đối tượng, nên không thể tạo thể tạo tham chiếu toàn cục.
#include <jni.h>
#include <stdio.h>
#include "JNIReference.h"
 
// Khai bao tham chieu toan cuc toi lop "java.lang.Integer"
static jclass classInteger;
static jmethodID midIntegerInit;
 
jobject getInteger(JNIEnv *env, jobject thisObj, jint number) {
if  (NULL == classInteger) {
		printf("Find java.lang.Integer\n");
//		classInteger = env->FindClass("java/lang/Integer");
		classInteger = (jclass) env->NewGlobalRef(env->FindClass("java/lang/Integer"));
   }

   if (NULL == classInteger) return NULL;
 
   // Lay Method ID cua ham khoi tao
   if (NULL == midIntegerInit) {
      printf("Get Method ID for java.lang.Integer's constructor\n\n");
      midIntegerInit = env->GetMethodID(classInteger, "<init>", "(I)V");
   }
   if (NULL == midIntegerInit) return NULL;
 
   // Call back constructor to allocate a new instance, with an int argument
   jobject newObj = env->NewObject(classInteger, midIntegerInit, number);
   printf("In C, constructed java.lang.Integer with number %d\n", number);
   return newObj;
}
 
JNIEXPORT jobject JNICALL Java_JNIReference_getIntegerObject
          (JNIEnv *env, jobject thisObj, jint number) {
   return getInteger(env, thisObj, number);
}
 
JNIEXPORT jobject JNICALL Java_JNIReference_anotherGetIntegerObject
          (JNIEnv *env, jobject thisObj, jint number) {
   return getInteger(env, thisObj, number);
}

Kết quả khi chạy chương trình:

java -Djava.library.path=. JNIReference
Find java.lang.Integer
Get Method ID for java.lang.Integer's constructor

In C, constructed java.lang.Integer with number 1
In java, function test.getIntegerObject:1
In C, constructed java.lang.Integer with number 2
In java, function test.getIntegerObject:2
In C, constructed java.lang.Integer with number 11
In java, function test.anotherGetIntegerObject:11
In C, constructed java.lang.Integer with number 12
In java, function test.anotherGetIntegerObject:12
In C, constructed java.lang.Integer with number 3
In java, function test.getIntegerObject:3
In C, constructed java.lang.Integer with number 13
In java, function test.anotherGetIntegerObject:13

Nếu trong đoạn mã trên bạn không sử dụng hàm NewGlobalRef thì khi chạy sẽ báo lỗi. Ví dụ bạn thay đoạn mã bôi vàng trong hàm jobject getInteger(JNIEnv *env, jobject thisObj, jint number)  bằng đoạn mã sau:

java -Djava.library.path=. JNIReference
Find java.lang.Integer
Get Method ID for java.lang.Integer's constructor

In C, constructed java.lang.Integer with number 1
In java, function test.getIntegerObject:1
#
# A fatal error has been detected by the Java Runtime Environment:
#
#  SIGSEGV (0xb) at pc=0x00007f8fa3fc2a85, pid=63605, tid=0x00007f8fa569f700
#
# JRE version: OpenJDK Runtime Environment (8.0_131-b11) (build 1.8.0_131-8u131-b11-2ubuntu1.16.04.3-b11)
# Java VM: OpenJDK 64-Bit Server VM (25.131-b11 mixed mode linux-amd64 compressed oops)
# Problematic frame:
# V  [libjvm.so+0x671a85]
#
# Failed to write core dump. Core dumps have been disabled. To enable core dumping, try "ulimit -c unlimited" before starting Java again
#
# An error report file with more information is saved as:
# /home/ubuntu/work/workspace/Examples/JNIReference/hs_err_pid63605.log
#
# If you would like to submit a bug report, please visit:
#   http://bugreport.java.com/bugreport/crash.jsp
#
Aborted (core dumped)

Tải source code ví dụ: JNIReference.zip

Điều khiển lỗi Java từ trong Native Code

Khi một ngoại lệ (Exception) bắn ra trong Java, JVM tự động tìm và gọi điều khiển ngoại lệ (Exception handler) gần nhất và loại bỏ nó khỏi ngăn xếp nếu cần. Cách xử lý này giúp lập trình viên không phải bắt lỗi ở mọi nơi trong chương trình mà có thể bắt lỗi tập trung, do đó giải phóng cho lập trình viên rất nhiều công sức.

Việc điều khiển lỗi trong C/C++ thì khác, không có cách chung và thống nhất để ném và bắt ngoại lệ (throw and catch exception). Vì thế, JNI yêu cầu bạn phải kiểm tra ngoại lệ có thể xảy ra sau khi gọi hàm. Trong JNI cũng cung cấp hàm cho phép trong hàm native có thể ném ngoại lệ lên Java. Sau khi phương thức native bắt và xử lý ngoại lệ, nó có thể xóa ngoại để tiếp tục tính toán hoặc ném ngoại lệ cho điều khiển ngoại lệ bên ngoài.

Rất nhiều hàm JNI có thể sinh ra các ngoại lệ như hàm GetFieldID ném ra ngoại lệ NoSuchFieldError nếu không tìm thấy. Để đơn giản hóa việc kiểm tra lỗi, hầu hết các hàm JNI sử dụng kết hợp đồng thời cả mã lỗi và ngoại lệ, tức là bạn kiểm tra giá trị trả về của GetFieldID có bằng NULL (0) hay không thay vì phải gọi hàm ExceptionOccurred của JNI. Như vậy, trong JNI có hai cách để bạn kiểm soát ngoại lệ:

  • C1: Kiểm tra giá trị trả về. Đây là cách thông dụng hay dùng.
  • C2: Gọi hàm ExceptionOccurred của JNI để bắt ngoại lệ, sau đó nhớ dùng hàm ExceptionClear để xác định ngoại lệ này đã được xử lý và hệ thống không xử lý tiếp.

Chi tiết xem source code ví dụ JNICatchThrow. Ví dụ này demo việc bắt và xử lý ngoại lệ trên JNI xử dụng hàm ExceptionOccurred ExceptionClear của JNI.

Tệp JNICatchThrow.java: Khai báo một phương thức native cho phép bắn ngoại lệ lên java (Hàm catchThrow()) và hàm callback trả về Exception để gọi từ C/C++:

class JNICatchThrow {
  static {
    System.loadLibrary("MyLib");
  }

  private native void catchThrow() throws IllegalArgumentException;
  
  private void callback() throws NullPointerException {
    throw new NullPointerException("Thrown in CatchThrow.callback");
  }
  
  public static void main(String args[]) {
    JNICatchThrow c = new JNICatchThrow();
    try {
      c.catchThrow();
    } catch (Exception e) {
      System.out.println("\nIn Java:\n  " + e);
    }
  }
}

Tệp JNICatchThrow.cpp: Thực hiện gọi hàm callback trên Java và bắt Exception sinh ra trên Java, đồng thời bắn lại một Exception khác lên Java.

#include <jni.h>
#include <stdio.h>
#include "JNICatchThrow.h"

JNIEXPORT void JNICALL Java_JNICatchThrow_catchThrow(JNIEnv *env, jobject obj) {
  jclass cls = env->GetObjectClass(obj);
  jmethodID mid = env->GetMethodID(cls, "callback", "()V");
  jthrowable exc = 0;
  if (mid == 0) {
    return;
  }
  env->CallVoidMethod(obj, mid);
  exc = env->ExceptionOccurred();
  if (exc) {
    /* Nhan duoc Exception, dung ham ExceptionDescribe de hien thi ra man hinh,
       dong thoi tao ra mot Exception moi va day len Java */
    jclass newExcCls;

    printf("In C:\n");
    env->ExceptionDescribe();
    env->ExceptionClear();

    newExcCls = env->FindClass("java/lang/IllegalArgumentException");
    if (newExcCls == 0) {
      // Truong hop khong tim thay lop java.lang.IllegalArgumentException thi bo qua
      return;
    }
    env->ThrowNew(newExcCls, "Thrown from C code");
  }
}

Kết quả khi chạy ví dụ:

java -Djava.library.path=. JNICatchThrow
In C:
Exception in thread "main" java.lang.NullPointerException: Thrown in CatchThrow.callback
	at JNICatchThrow.callback(JNICatchThrow.java:9)
	at JNICatchThrow.catchThrow(Native Method)
	at JNICatchThrow.main(JNICatchThrow.java:15)

In Java:
  java.lang.IllegalArgumentException: Thrown from C code

Tải source code ví dụ: JNICatchThrow.zip

Đồng bộ thread trong các phương thức native

Java là hệ thống đa luồng (multithreaded), vì thế mỗi phương thức native phải là môt Thread an toàn. Trong lập trình JNI, bạn luôn phải giả sử có nhiều Thread cùng thực hiện một phương thức Native ở thời điểm bất kỳ, như thế bạn đảm bảo xử lý đúng, đặc biệt liên quan tới vấn đề toàn vẹn dữ liệu.

Khi lập trình JNI bạn phải chú ý:

  • JNI interface pointer (JNIEnv *): Chỉ hợp lệ trong Thread hiện tại. Bạn không thể truyền nó từ một Thread sang một Thread khác, hoặc lưu giữ con trỏ này để sử dụng cho các Thread hay hàm native khác.
  • Bạn không thể sử dụng tham chiếu cục bộ (Local reference) từ một Thread sang một Thread khác. Trong trường hợp này bạn phải chuyển tham chiếu cục bộ thành tham chiếu toàn cục. Xem mục: Tham chiếu toàn cục và tham chiếu cục bộ
  • Kiểm tra cẩn thận khi sử dụng biến toàn cục: Nhiều Thread có thể truy cập vào các biến toàn cục ở cùng một thời điểm, vì thế phải đảm bảo chắc chắn rằng bạn đã xử lý đồng bộ và đặt lock ở vị trí thích hợp để đảm bảo an toàn.
  • Nhiều thread có thể truy cập vào các biến toàn cục cùng một lúc. Vì vậy bạn nhớ đặt lock ở vị trí thích hợp để đảm bảo an toàn.

Trong Java, bạn sử dụng lệnh synchronized để cài đặt một khối đồng bộ:

synchronized (obj) {
    ...                   /* synchronized block */
    ...
}

Còn JNI cung cấp cho bạn hai hàm MonitorEnter, MonitorExit để thực hiện việc này:

...
(*env)->MonitorEnter(env, obj);
...                      /* synchronized block */
(*env)->MonitorExit(env, obj);
...

Để hiểu rõ hơn, bạn xem source code ví dụ JNISyncObj. Ví dụ này demo việc sử dụng hàm MonitorEnter, MonitorExit để đảm bảo tính toàn vẹn dữ liệu.

Tệp JNISyncObj.java: Khai báo một phương thức native có nhiệm vụ nối các từ lại thành một câu. Trên java tạo nhiều Thread cùng gọi hàm native này:

public class JNISyncObj {
    private native void joinWords(int id, String[] words);

    public static void main(String[] args) {
        String[] sentences = {
            "Hom nay troi rat dep",
            "Co gai trong xinh qua",
            "Di an com thoi",
            "Ngay mai troi lai sang"
        };
        int num = sentences.length;
        final JNISyncObj test = new JNISyncObj();
        for (int i=0; i<num; i++) {
            final int id = i + 1;
            final String[] words = sentences[i].split(" ");
            Thread t = new Thread() {
                public void run() {
                    test.joinWords(id, words);
                }
            };
            t.start();
        }
    }

    static {
      System.loadLibrary("MyLib");
   }
}

Tệp JNISyncObj.cpp: Cài đặt hàm native thực hiện nối các từ lại thành 1 câu. Hàm này sử dụng thông qua biến toàn cục, tức là tất cả các Thread đều sử dụng biến này. Để đảm bảo tính toàn vẹn dữ liệu (Tức là chương trình phải in ra các câu có nghĩa giống như từ java truyền vào), sử dụng hàm MonitorEnter, MonitorExit. Trong mã sử dụng thêm hàm nsleep giúp tăng thời gian xử lý nghiệp vụ, qua đó thấy được sự khác nhau giữa việc sử dụng và không sử dụng hàm trên:

#include "JNISyncObj.h"
#include <stdio.h>
#include <cstring>
#include <unistd.h>
#include <stdlib.h>
#include <time.h>

static char gzBuffer[512];
static time_t t;
static bool isInitSrand = false;

int randInt(int min, int max) {
    if (!isInitSrand) {
        srand((unsigned) time(&t));
        isInitSrand = true;
    }
    return (rand() % (max + 1 - min)) + min;
}

JNIEXPORT void JNICALL Java_JNISyncObj_joinWords(JNIEnv *env, jobject obj, jint id, jobjectArray words) {
    char buf[256];

    env->MonitorEnter(obj);
    int num = env->GetArrayLength(words);
    strcpy(gzBuffer, "");
    for (int i=0; i<num; i++) {
        jstring word = (jstring) env->GetObjectArrayElement(words, i);
        int len = env->GetStringLength(word);
        env->GetStringUTFRegion(word, 0, len, buf);
        strcat(gzBuffer, buf);
        strcat(gzBuffer, " ");
        usleep(randInt(10000, 30000));
    }
    printf("Thread %d finish: gzBuffer = %s\n\n", id, gzBuffer);
    env->MonitorExit(obj);
}

Đây là kết quả chương trình khi sử dụng hàm MonitorEnter, MonitorExit. Chương trình in ra 4 câu có nghĩa giống như java truyền vào:

java -Djava.library.path=. JNISyncObj
Thread 2 finish: gzBuffer = Co gai trong xinh qua 

Thread 4 finish: gzBuffer = Ngay mai troi lai sang 

Thread 3 finish: gzBuffer = Di an com thoi 

Thread 1 finish: gzBuffer = Hom nay troi rat dep 

Bây giờ bạn thực hiện comment hai dòng sử dụng MonitorEnter, MonitorExit sẽ thấy kết quả như sau:

java -Djava.library.path=. JNISyncObj
Thread 4 finish: gzBuffer = Ngay gai an mai nay trong troi com troi xinh lai rat thoi sang qua dep 

Thread 3 finish: gzBuffer = Ngay gai an mai nay trong troi com troi xinh lai rat thoi sang qua dep 

Thread 1 finish: gzBuffer = Ngay gai an mai nay trong troi com troi xinh lai rat thoi sang qua dep 

Thread 2 finish: gzBuffer = Ngay gai an mai nay trong troi com troi xinh lai rat thoi sang qua dep

Tải source code ví dụ: JNISyncObj.zip

Tài liệu tham khảo: