Java Native Interface (JNI) là một framework lập trình cho phép mã Java khi thực thi trong môi trường máy ảo Java (JVM) có thể gọi hoặc bị gọi bởi các ứng dụng cục bộ (Các chương trình xác định đối với một loại phần cứng và một platform hệ điều hành) và các thư viện viết bằng các ngôn ngữ khác như C, C++ hay assembly.
JNI sử dụng để viết các phương thức cục bộ xử lý các tình huống khi một ứng dụng không được viết hoàn toàn bằng ngôn ngữ lập trình Java, chẳng hạn như khi thư viện lớp Java chuẩn không hỗ trợ thư viện chương trình hay các tính năng độc lập platform. Nó còn dùng để thay đổi các ứng dụng đang tồn tại viết bằng một ngôn ngữ lập trình khác để có thể truy nhâp tới các ứng dụng Java. Rất nhiều lớp thư viện chuẩn phụ thuộc vào JNI để thực hiện các chức năng của nhà phát triển và người dùng như khả năng đọc các file vào ra hay âm thanh. Với việc triển khai API phân biệt platform và hiệu năng trong thư viện chuẩn cho phép tất cả các ứng dụng Java thực hiện chức năng này trong chế độ an toàn và cô lập với các platform. Trước khi sử dụng chức năng này các nhà phát triển JNI phải chắc chắn rằng nó không chưa được cung cấp trong các thư viện chuẩn.
Framework JNI sử dụng các phương thức cục bộ để tối ưu các đối tượng Java giống cách mà Java sử dụng những đối tượng này. Một phương thức cục bộ có thể tạo ra các đối tượng Java và sau đó kiểm tra và sử dụng chúng để thực hiện các chức năng của chính nó. Một phương thức cục bộ còn cho phép kiểm tra và sử dụng các đối tượng do ứng dụng Java tạo ra.
JNI được gọi là “van an toàn” dành cho các nhà phát triển Java bởi vì nó giúp họ thêm các chức năng vào ứng dụng Java của mình mà Java API không thể cung cấp. Nó còn sử dụng để tương tác với mã viết bằng các ngôn ngữ lập trình khác như C++. Ngoài ra nó còn sử dụng trong tính toán thời điểm tới hạn hoặc các thao tác như giải quyết phương trình toán học phức tạp bởi vì mã cục bộ thậm chí có thể nhanh hơn cả mã JVM.
JNI không dễ sử dụng và cần phải chi phí đáng kể để sử dụng nó. Một số người khuyến cáo chí những lập trình viên cấp cao mới nên sử dụng JNI. Tuy nhiên, với khả năng giao tiếp với C++ và Assembly của Java đã loại bỏ các hạn chế trong chức năng các chương trình Java thực thi. Các lập trình viên khi cân nhắc sử dụng JNI nên quan tâm đến những vấn đề sau:
1. JNI không dễ học như JNI
2. Chỉ các ứng dụng và applet đã đăng ký mới được thực thi JNI.
3. Một ứng dụng dựa vào công nghệ JNI sẽ mất tính khả chuyển platform mã Java đưa ra (Có một cách giải quyết khác là viết mã JNI riêng biệt cho một platform và nhờ Java xác nhận hệ điều hành rồi tải mã thích hợp vào lúc thực thi).
4. Không có sự thu nhặt rác đối với JNI (Mã JNI phải ngừng cấp phát tường minh)
5. Kiểm tra lỗi mang tính bắt buộc và nó tiềm ẩn làm hỏng phía JNI và JVM.
Minh họa về JNI
Bước 1: Triển khai một ứng dụng Java
Tạo mới một file mang tên JavaSide.java chứa class sau rồi biên dịch nó:
public class JavaSide {
public native void sayHello();
static {
System.loadLibrary("nativeSideImpl");
}
public static void main(String[] args) {
JavaSide app = new JavaSide();
app.sayHello();
}
}
Đoạn mã trên rất giống với mã Java thường, ngoại trừ 2 thứ: từ khóa native và phương thức trong bộ khởi tạo static. Từ khóa native chỉ ra rằng phương thức sayHello() được triển khai đâu đó trong thư viện cục bộ. Phương thức này được thực hiện giống như các phương thức Java khác, vì vậy đoạn mã gọi sayHello() không cần quan tâm đến nó là phương thức cục bộ hay không.
Mỗi platform có một vị trí mặc định chứa các thư viện cục bộ. Phương thức System.loadLibrary() tải tên thư viện “nativeSideImpl” trong vị trí mặc định này. Trong hầu hết các platform, đuôi mở rộng của các platform cụ thể được thêm vào trong tên của thư viện để lấy được tên file tương ứng.
Bước 2: Tạo file tiêu đề .h
Việc triển khai bằng C++ cần một số mã gắn kết để tương tác với JVM cho nên bước tiếp theo chúng ta sẽ tạo ra file tiêu đề viết bằng C/C++ bằng cách sử dụng công cụ javah. Javah là một phần của JDK và nằm trong cùng thư mục với javac. Sau khi biên dịch file JavaSide, nhập vào dòng lệnh
javah JavaSide
với JavaSide là tên class. Câu lệnh trên tạo ra file tiêu đề JavaSide.h.
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class JavaSide */
#ifndef _Included_JavaSide
#define _Included_JavaSide
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: JavaSide
* Method: sayHello
* Signature: (I)V
*/
JNIEXPORT void JNICALL Java_JavaSide_sayHello
(JNIEnv *, jobject);
#ifdef __cplusplus
}
#endif
#endif
Bước 3: Triển khai phương thức cục bộ bằng C++
Tạo một file mới mang tên Cục bộSideImpl.cpp với nội dung:
#include <stdio.h>
#include "JavaSide.h"
JNIEXPORT void JNICALL Java_JavaSide_sayHello
(JNIEnv *env, jobject obj)
{
printf("Hello Native World!");
}
Sau đó biên dịch thành một thư viện với một trình dịch C++.
Trong minh họa trên, tham số env là một con trỏ trỏ tới ngữ cảnh JNI sẽ liên kết giữa mã JNI và JVM. Tham số obj là tham chiếu this tường minh truyền vào các phương thức non-static của Java.
Lưu ý là bạn cần include thư mục “include” và tất cả thư mục con trong JDK với vai trò như đường dẫn để tìm kiếm sau này. Các thư mục này chứa tất cả file tiêu đề JNI để biên dịch. Dưới đây là minh họa việc sử dụng trình biên dịch Visual Studio .NET 2003 của Microsoft.
cl /LD /I"C:\Program Files\Java\Include" /I"C:\Program Files\Java\Include\win32" Cục bộSideImpl.cpp
Bước 4: Thực thi ví dụ trên
Gõ vào câu lệnh:
java JavaSide
Và kết quả sẽ là:
Hello Native World!
JNI làm việc như thế nào?
Trong JNI, các hàm cục bộ được triển khai trong các file .c hay .cpp riêng biệt (C++ cung cấp các giao tiếp với JNI rõ ràng hơn). Khi JVM gọi một hàm, nó truyền con trỏ JNIEnv, một con trỏ jobject và bất cứ đối số Java nào khai báo trong phương thức Java. Một hàm JNI có dạng như sau:
JNIEXPORT void JNICALL Java_ClassName_MethodName
(JNIEnv *env, jobject obj)
{
//Implement native Method Here
}
Con trỏ env là một cấu trúc chứa các giao diện với JVM. Nó bao gồm tất cả các hàm cần thiết để tương tác với JVM và để làm việc với các đối tượng Java. Minh họa về hàm JNI là biến đổi các mảng cục bộ từ/thành các mảng Java, biến đổi xâu cục bộ từ/thành xâu của Java, khởi tạo các đối tượng, ném ngoại lệ… Về cơ bản, bất cứ thứ gì mã Java có thể thực hiến sử dụng JNIEnv, mặc dù với chi phí nhỏ hơn.
Dưới đây là minh họa biến đổi xâu Java thành xâu cục bộ.
//C++ code
JNIEXPORT void JNICALL Java_ClassName_MethodName
(JNIEnv *env, jobject obj, jstring javaString)
{
//Get the cục bộ string from javaString
const char *cục bộString = env->GetStringUTFChars(javaString, 0);
//Do something with the nativeString
//DON'T FORGET THIS LINE!!!
env->ReleaseStringUTFChars(javaString, cục bộString);
}
//C code
JNIEXPORT void JNICALL Java_ClassName_MethodName
(JNIEnv *env, jobject obj, jstring javaString)
{
//Get the native string from javaString
const char *nativeString = (*env)->GetStringUTFChars(env, javaString, 0);
//Do something with the nativeString
//DON'T FORGET THIS LINE!!!
(*env)->ReleaseStringUTFChars(env, javaString, nativeString);
}
Lưu ý là mã viết bằng C++ rõ ràng hơn mã viết bằng C vì giống như Java, C++ sử dụng gọi phương thức đối tượng. Nghĩa là trong tham số env viết bằng C được tái tham chiếu sử dụng (*env)-> và env phải truyền tường minh vào các phương thức JNIEnv. Trong C++, tham số env được truyền tường minh như một phần của việc gọi phương thức đối tượng.
Ánh xạ kiểu dữ liệu
Bảng dưới đây liệt kê ánh xạ kiểu dữ liệu giữa Java và mã cục bộ.
Java Language Type
Kiểu cục bộ
Mô tả
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
Trên đây là những kiểu dữ liệu có thể hoán đổi được. Ta có thể sử dụng jint thay cho int, và ngược lại mà không cần phải ép kiểu.
Tuy nhiên, sự ánh xạ giữa các xâu và mảng trong java thành xâu và mảng cục bộ là khác nhau. Nếu bạn sử dụng jstring thay cho char*, đoạn mã này sẽ có vấn đề trong JVM.
/***********************************************
*NO!!! NO!!! NO!!! NO!!! NO!!! *
***********************************************/
JNIEXPORT void JNICALL Java_ClassName_MethodName
(JNIEnv *env, jobject obj, jstring javaString)
{
printf("%s", javaString);
}
/***********************************************
*YES!!! YES!!! YES!!! YES!!! YES!!! *
***********************************************/
JNIEXPORT void JNICALL Java_ClassName_MethodName
(JNIEnv *env, jobject obj, jstring javaString)
{
//Get the native string from javaString
const char *nativeString = env->GetStringUTFChars(env,javaString, 0);
printf("%s", nativeString);
//DON'T FORGET THIS LINE!!!
env->ReleaseStringUTFChars(env,javaString, nativeString);
}
Tương tự đối với mảng trong Java, dưới đây là minh họa tính tổng các phần tử của một mảng.
/***********************************************
*NO!!! NO!!! NO!!! NO!!! NO!!! *
***********************************************/
JNIEXPORT jint JNICALL
Java_ClassName_MethodName(JNIEnv *env, jobject obj, jintArray arr)
{
int i, sum = 0;
for (i = 0; i < 10; i++) {
sum += arr[i];
}
return sum;
}
/***********************************************
*YES!!! YES!!! YES!!! YES!!! YES!!! *
***********************************************/
JNIEXPORT jint JNICALL
Java_IntArray_sumArray(JNIEnv *env, jobject obj, jintArray arr)
{
jint buf[10];
jint i, sum = 0;
env->GetIntArrayRegion(arr, 0, 10, buf);
for (i = 0; i < 10; i++) {
sum += buf[i];
}
return sum;
}
Native AWT painting
Không chỉ mã cục bộ tương tác với Java, nó còn có thể vẽ trên một Java Canvas thực hiện với giao diện cục bộ Java AWT. Quá trình này diễn ra tương tự, chỉ có một chút thay đổi. Java AWT Native Interface chỉ có từ J2SE 1.3.
RNI của hãng Microsoft
Việc triển khai máy ảo Java của Microsoft thực hiện theo cơ chế tương tự để gọi mã Windows cục bộ từ Java, được gọi là Raw Native Interface (RNI).
Kết luận
Rõ ràng là việc triển khai JNI trong một ứng dụng là không dễ chút nào. Tuy nhiên, với hiệu năng, sự bảo vệ mã hợp lệ và thêm vào các chức năng cho ứng dụng Java sẽ giúp vượt qua những trở ngại trên.