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

JNI – Reflection

Reflection hay Introspection là tính năng cho phép chúng ta “điều tra” xem đối tượng/lớp/biến…v.v gồm có những gì. Chẳng hạn như tìm hiểu xem một đối tượng đó gồm có những thuộc tính gì và phương thức gì.

Trong Java thì reflection được hỗ trợ rất đầy đủ trong gói java.lang.reflect, ngoài ra một số phương thức trong lớp java.lang.Objectjava.lang.Class cũng hỗ trợ reflection, chúng ta có thể gọi các lớp này ở native để thực hiện reflection cũng được, hoặc chúng ta có thể dùng một số hàm của JNI như:

  • jclass GetSuperclass(jclass cls): nhận vào một lớp, trả về lớp cha của lớp đó
  • jboolean IsAssignableFrom(jclass clsA, jclass clsB): nhận vào 2 lớp a, b trả về true hoặc false, cho biết lớp a có thể ép kiểu sang lớp b hay không
  • jclass GetObjectClass(jobject obj): chúng ta đã làm nhiều với hàm này rồi, hàm này nhận vào một đối tượng và trả về lớp của đối tượng đó
  • jboolean IsInstanceOf(jobject obj, jclass cls): nhận vào một đối tượng và một lớp, trả về true hoặc false cho biế đối tượng đó có phải sinh ra từ lớp đó hay không
  • jmethodID FromReflectedField(jobject method)chuyển một đối tượng java.lang.reflect.Method hoặc java.lang.reflect.Constructor sang một đối tượng jmethodID
  • jobject ToReflectedField(jclass cls, jfieldID fieldID, jboolean isStatic): chuyển một đối tượng jfieldID từ cls sang một đối tượng java.lang.reflect.Field, nếu trường đó là static thì isStatic là true, ngược lại là false
  • jmethodID FromReflectedMethod(jobject method): chuyển một đối tượng java.lang.reflect.Method hoặc java.lang.reflect.Constructor sang một đối tượng jmethodID
  • jobject ToReflectedMethod(jclass cls, jmethodID methodID, jboolean isStatic): chuyển một đối tượng jmethodID kế thừa từ jclass sang một đối tượng java.lang.reflect.Method hoặc java.lang.reflect.Constructor, nếu phương thức là static thì isStatic là true, ngược lại là false

JNI – String và char

Về cơ bản Java sử dụng bảng mã Unicode để lưu trữ chuỗi, còn các ngôn ngữ lập trình native có sử dụng bảng mã này hay không thì tùy thuộc vào hệ điều hành nữa (như C++ trên Windows thì có). Do đó khi chúng ta chuyển đổi chuỗi từ Java sang native thì chúng ta phải xem mình đang sử dụng hệ điều hành có hỗ trợ Unicode hay không, nếu không thì chúng ta không thể dùng các hàm như GetStringUTFChars() hay GetStringUTFRegion().

Tạo đối tượng jstring từ mảng char trong C++

Chúng ta sẽ gọi phương thức khởi tạo String(byte[] bytes) của Java từ C++.

Chúng ta có lớp Java như sau:

class MakeString {
    static { System.loadLibrary("MakeString"); }
 
    private native String makeAString();
 
    public static void main(String[] args) {
        String str = new MakeString().makeAString();
        System.out.println(str);
    }
}

Phương thức native là makeAString(), phương thức này sẽ tạo một chuỗi string nào đó ở phía native.

Code native như sau:

#include "MakeString.h"
#include <string.h>
#include <stdlib.h>
using namespace std;

jstring makeAString(JNIEnv *e, char *str) { 
    jbyteArray bytes = 0;
    int len = strlen(str);
    bytes = e->NewByteArray(len);
 
    jclass string_cls = e->FindClass("Ljava/lang/String;");
    jmethodID string_constructor = e->GetMethodID(string_cls, "<init>", "([B)V");
 
    if(bytes != NULL) {
        e->SetByteArrayRegion(bytes, 0, len, (jbyte *)str);
        jstring result = (jstring)e->NewObject(string_cls, string_constructor, bytes); 
        return result;
    }
    return NULL;
}

JNIEXPORT jstring JNICALL Java_MakeString_makeAString (JNIEnv *e, jobject obj) { 
    char *str = (char*)malloc(12); 
    strcpy(str, "Lorem Ipsum");
    return makeAString(e, str); 
}

Chúng ta viết một hàm là makeAString(), phương thức này nhận vào một mảng char* rồi chuyển thành một đối tượng jstring.

Đầu tiên chúng ta lấy độ dài của mảng char*, sau đó tạo một đối tượng jbyteArray từ phương thức NewByteArray(), phương thức này nhận vào số lượng phần tử và trả về một đối tượng mảng lưu trữ danh sách các phần tử byte.

Tiếp theo chúng ta lấy jclass của lớp java.lang.String, jmethodID của phương thức String(byte[] bytes). 

Nếu ở trên chúng ta tạo đối tượng jbyteArray thành công thì tiếp theo chúng ta sao chép nội dung của mảng char* vào đối tượng này bằng phương thức SetByteArrayRegion(), phương thức này nhận vào đối tượng jbyteArray được sao chép, vị trí bắt đầu của mảng char*, số lượng các phần tử sao chép, cuối cùng là con trỏ đến mảng char* đó.

Cuối cùng chúng ta tạo đối tượng jstring bằng phương thức NewObject(), phương thức này trả về đối tượng jobject nên chúng ta ép kiểu qua jstring.

Lorem Ipsum

Nếu hệ điều hành có hỗ trợ Unicode thì chúng ta chỉ cần gọi phương thức e->NewStringUTF(str) là được ngay một đối tượng jstring rồi chứ không cần làm rườm rà phức tạp như trên.

Chuyển jstring thành mảng char trong C++

Ví dụ chúng ta có code Java như sau:

class ConvertToChar {
    static { System.loadLibrary("ConvertToChar"); }
 
    private native void convert(String str);
 
    public static void main(String[] args) {
        new ConvertToChar().convert("Lorem ipsum dolor sit amet"); 
    }
}

Phương thức convert() nhận vào một đối tượng String và chuyển thành mảng char* bên native.

Code native:

#include "ConvertToChar.h"
#include <stdio.h>
#include <stdlib.h>
using namespace std;

JNIEXPORT void JNICALL Java_ConvertToChar_convert (JNIEnv *e, jobject obj, jstring str) { 
    char *result = 0;
 
    jclass string_class = e->GetObjectClass(str);
    jmethodID getBytesMethod = e->GetMethodID(string_class, "getBytes", "()[B"); 
 
    jbyteArray bytes = (jbyteArray)e->CallObjectMethod(str, getBytesMethod);
    if(!e->ExceptionOccurred()) {
        jint len = e->GetArrayLength(bytes);
        result = (char*)malloc(len + 1);
        e->GetByteArrayRegion(bytes, 0, len, (jbyte*)result);
        result[len] = 0;
    } 
    puts(result);
}

Trong lớp java.lang.String có sẵn phương thức getBytes() trả về một mảng byte bên Java. Chúng ta dùng phương thức này để lấy mảng đó, sau đó dùng phương thức JNI để chuyển thành một mảng char*.

Đầu tiên chúng ta lấy jclassjmethodID như bình thường, sau đó chúng ta gọi phương thức CallObjectMethod(), phương thức này sẽ gọi phương thức getBytes() của lớp java.lang.String, kiểu trả về bên Java là Byte[] và tương ứng bên native là jbyteArray do đó chúng ta ép kiểu trả về là jbyteArray.

Tiếp theo chúng ta kiểm tra xem phương thức này có báo lỗi không, nếu không thì chúng ta lấy độ dài của đối tượng jbyteArray vừa lấy được, rồi dùng hàm malloc() để cấp bộ nhớ cho mảng char*, cuối cùng gọi phương thức GetByteArrayRegion() để chép nội dung trong đối tượng jbyteArray đó vào mảng char*.

Lorem ipsum dolor sit amet

JNI – Exception

Trong phần này chúng ta sẽ tìm hiểu cách bắt lỗi Exception và giải phóng lỗi Exception từ code native.

Nếu bạn chưa biết thì Exception là lỗi xảy ra trong quá trình chạy, ví dụ đơn giản nhất là phép chia cho 0, trong Java khi gặp câu lệnh chia cho 0 thì JVM sẽ báo lỗi ArithmeticException.

Bắt lỗi Exception

Đầu tiên chúng ta có đoạn code Java như sau:

class TryCatch {
    static { System.loadLibrary("TryCatch"); }
 
    private native void catchError();
    private void division(int a, int b) throws ArithmeticException{ 
        int result = a / b; 
        System.out.println(a + " / " + b + " = " + result);
    }
 
    public static void main(String[] args) { 
        new TryCatch().catchError(); 
    }
}

Chúng ta có một phương thức native là CatchError() và một phương thức thường là division(), phương thức division() sẽ nhận vào 2 số nguyên, thực hiện phép chia 2 số nguyên này và sẽ giải phóng lỗi ArithmeticException nếu có.

Như bình thường thì chúng ta sẽ bọc đoạn code thực hiện phép chia đó trong try...catch, nhưng ở đây chúng ta không làm thế mà chúng ta sẽ xử lý lỗi này trong phần code native.

Chúng ta bắt lỗi trong native như sau:

#include "TryCatch.h"
#include <iostream>
using namespace std;

JNIEXPORT void JNICALL Java_TryCatch_catchError (JNIEnv *e, jobject obj) {    
    jclass cls = e->GetObjectClass(obj);
    jmethodID mid = e->GetMethodID(cls, "division", "(II)V");
    e->CallVoidMethod(obj, mid, 3, 0);
 
    jthrowable exc = e->ExceptionOccurred();
 
    if(exc != NULL) {
        cout << "Exception occurred:" << endl;
        e->ExceptionDescribe(); 
        e->ExceptionClear();
    }
}

Đầu tiên chúng ta lấy jclass, rồi lấy jmethodID của phương thức division() bên Java, sau đó chúng ta gọi phương thức đó với tham số 3 và 0, tức là làm phép chia 3 / 0.

Phương thức division() bên Java sẽ giải phóng lỗi ArithmeticException, ở C++ để kiểm tra xem có lỗi xảy ra không thì chúng ta gọi phương thức ExceptionOccured(), nếu có lỗi xảy ra thì phương thức này sẽ trả về một đối tượng jthrowable, chúng ta kiểm tra xem đối tượng này có NULL không, nếu có thì chúng ta in câu thông báo, sau đó gọi phương thức ExceptionDescribe(), phương thức này sẽ in stacktrace của lỗi, cuối cùng chúng ta gọi phương thức ExceptionClear() để xóa lỗi đó trong JVM.

Thực ra thì nếu không gọi ExceptionDescribe() thì JVM cũng sẽ tự động in đoạn stacktrace ra, nhưng sau đó JVM cũng sẽ dừng chương trình luôn, nếu chúng ta có gọi ExceptionDescribe() thì sau khi in stacktrace ra thì chương trình vẫn chạy cho đến khi kết thúc.

Exception occurred:
Exception in thread "main" java.lang.ArithmeticException: / by zero
    at TryCatch.division(TryCatch.java:10)
    at TryCatch.catchError(Native Method)
    at TryCatch.main(TryCatch.java:18)

Giải phóng lỗi Exception

Để giải phóng lỗi Exception từ code C++ thì cũng khá đơn giản.

Chúng ta có code Java như sau:

class TryCatch {
    static { System.loadLibrary("TryCatch"); }
 
    private native void makeError();
 
    public static void main(String[] args) { 
        try {
            new TryCatch().makeError();
         } catch(Exception e) {
             System.out.println("Something happened:");
             e.printStackTrace();
         }
    }
}

Chúng ta có phương thức native là makeError(), phương thức này sẽ giải phóng lỗi Exception.

Chúng ta làm việc đó như sau:

#include "TryCatch.h"

JNIEXPORT void JNICALL Java_TryCatch_makeError (JNIEnv *e, jobject obj) {
    jclass exceptionCls = e->FindClass("java/lang/Exception");
    e->ThrowNew(exceptionCls, "thrown from C++ code");
}

Đầu tiên chúng ta tìm một lớp Exception bên Java mà chúng ta muốn tạo, ở đây mình dùng lớp java.lang.Exception, rồi dùng phương thức FindClass() để lấy đối tượng jclass.

Sau đó để giải phóng lỗi này thì chỉ cần gọi phương thức ThrowNew() là được, phương thức này nhận vào đối tượng jclass và một đoạn chuỗi mô tả lỗi.

Something happened:
java.lang.Exception: thrown from C++ code
    at TryCatch.makeError(Native Method)
    at TryCatch.main(TryCatch.java:8)

JNI – Cache ID

Khi chúng ta lấy các đối tượng jmethodIDjfieldID thì các đối tượng này sẽ được JNI dò tìm theo phương pháp lookup table, cách này về cơ bản rất tốn thời gian, do đó trong chúng ta có một kỹ thuật gọi là cache ID, chúng ta sẽ tìm hiểu cách cache này.

Về cơ bản thì cache là chúng ta tạo ra một thứ gì đó rồi lưu lại để sử dụng sau này, giống biến static, biến toàn cục, hay constant vậy.

Cache khi khởi tạo

Cache khi khởi tạo tức là chúng ta cache lại ID đó bên code native.

Ví dụ chúng ta có lớp Java như sau:

class Cache { 
    static { System.loadLibrary("Cache"); } 

    private String s = "Hello"; 

    private native void hello(); 

    public static void main(String[] args) {
        Cache c = new Cache();
        c.hello();
    }
}

Bên native sẽ lấy ID của thuộc tính s như sau:

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

JNIEXPORT void JNICALL Java_Cache_hello (JNIEnv *e, jobject obj) {
    jclass cls = e->GetObjectClass(obj);
    static jfieldID fid_s = NULL;
 
    if(fid_s == NULL) {
        fid_s = e->GetFieldID(cls, "s", "Ljava/lang/String;"); 
    } else {
        std::cout << "s is already got";
    }    
}

Ở đây chúng ta khai báo đối tượng jfieldID là static rồi khởi tạo giá trị NULL, sau đó mỗi lần lấy đối tượng jfieldID từ phương thức GetFieldID() thì trước hết chúng ta kiểm tra xem đối tượng này có phải NULL hay không rồi mới lấy.

Lý do là vì sẽ có trường hợp chúng ta làm multithread mà có tham chiếu đến một ID nhiều lần, mỗi thread tự lookup table để tính ID sẽ chậm hơn nhiều so với việc lưu lại ID đó rồi chỉ cần một thread tính ra để các thread khác sử dụng.

Cache Java

Kiểu cache này là trước khi làm gì thì chúng ta gọi một phương thức để lấy hết các ID ra trước rồi lưu lại để sử dụng sau. Ví dụ:

class Cache {
 
    static { 
        System.loadLibrary("Cache");
        initID(); 
    }
 
    private String s;
    
    private static native void initID();
    private native void hello();
 
    public static void main(String[] args) {
        Cache c = new Cache();
        c.s = "Lorem Ippum";
        c.hello();
    }
}

Ở đây chúng ta có 2 phương thức native là initID()hello().

Phương thức initID() là phương thức static, phương thức này sẽ được gọi trước cả phương thức main(), phương thức này làm công việc lấy các ID ra trước và lưu lại, chúng ta code như sau:

#include "Cache.h"
#include <iostream>
using namespace std;

jfieldID fid_s;

JNIEXPORT void JNICALL Java_Cache_hello (JNIEnv *e, jobject obj) {
    jstring jstr = (jstring)e->GetObjectField(obj, fid_s);
    const char *str = e->GetStringUTFChars(jstr, JNI_FALSE);
    cout << str;
    e->ReleaseStringUTFChars(jstr, NULL);
}

JNIEXPORT void JNICALL Java_Cache_initID (JNIEnv *e, jclass cls) {
    fid_s = e->GetFieldID(cls, "s", "Ljava/lang/String;"); 
}

Như bạn thấy ở phương thức Java_Cache_initID() chúng ta gọi phương thức GetFieldID() và lưu kết quả vào biến fid_s, kể từ đó về sau chúng ta không cần phải gọi lại phương thức này nữa. Phương thức Java_Cache_hello() có thể làm việc với fid_s một cách bình thường.

Lorem Ipsum

JNI – Đối tượng – Phần 3

Trong phần này chúng ta sẽ tìm hiểu cách gọi phương thức constructor của Java từ C++.

Gọi constructor

Giả sử chúng ta có lớp Point trong Java như sau:

class Point {
    private int x;
    private int y;
 
    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }
 
    public int getX() { return this.x; }
    public int getY() { return this.y; }
 
    public void setX(int x) { this.x = x; }
    public void setY(int y) { this.y = y; }
}

Tiếp theo chúng ta có code main Java như sau:

class Constructor {
    static { System.loadLibrary("Constructor"); }
    private native void createConstructor();
 
    public static void main(String[] args) {
        Constructor c = new Constructor();
        c.createConstructor();
    }
}

Phương thức native là createConstructor(), chúng ta code phương thức này như sau:

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

JNIEXPORT void JNICALL Java_Constructor_createConstructor(JNIEnv *e, jobject obj) {
    jclass pointClass = e->FindClass("Point");
    jmethodID mid = e->GetMethodID(pointClass, "<init>", "(II)V");
    jobject pointObj = e->NewObject(pointClass, mid, 3, 5);
 
    jmethodID getX = e->GetMethodID(pointClass, "getX", "()I");
    jmethodID getY = e->GetMethodID(pointClass, "getY", "()I");
 
    int x = e->CallIntMethod(pointObj, getX);
    int y = e->CallIntMethod(pointObj, getY);
 
    std::cout << "Your point: (" << x << ", " << y << ")";
}

Ở đây phương thức native sẽ gọi hàm constructor của lớp Point và truyền vào tham số là 2 giá trị x, y. Sau đó chúng ta in 2 giá trị này ra màn hình.

Các bước làm thì cũng tương tự như cách gọi một phương thức bình thường, đầu tiên chúng ta lấy đối tượng jclass của lớp Point. Ở đây chúng ta phải dùng phương thức FindClass() chứ không dùng phương thức GetObjectClass(), bởi vì đối tượng jobject được gửi sang là đối tượng Constructor chứ không phải Point.

Tiếp theo chúng ta cũng lấy đối tượng jmethodID, đối với phương thức constructor thì chúng ta truyền vào tên phương thức là <init>. 

Sau đó chúng ta tạo một đối tượng jobject bằng phương thức NewObject(), phương thức này nhận vào đối tượng jclass, jmethodID và danh sách các tham số của phương thức đó.

Sau khi đã có đối tượng đó rồi chúng ta lấy jmethodID của phương thức getX()getY() rồi sử dụng như bình thường, hoặc bạn có thể lưu lại 2 biến x, y trước khi khởi tạo để khỏi mất công lấy các phương thức này ra dùng nữa.

Your point: (3, 5)

JNI – Đối tượng – Phần 2

Trong phần này chúng ta sẽ tìm hiểu cách gọi một phương thức Java từ C++.

Gọi một phương thức

Ví dụ chúng ta có lớp Java tên Methods như sau:

class Methods {
    static { System.loadLibrary("Methods"); } 
    private native void method();
    private void hello() {
        System.out.println("Hello World");
    }
 
    public static void main(String args[]) {
        new Methods().method();
    }
}

Phương thức native tên là method(), phương thức này sẽ được gọi qua C++, rồi từ C++ chúng ta gọi lại một phương thức của lớp Methods này là hello().

Chúng ta làm điều đó như sau:

#include "Methods.h"

JNIEXPORT void JNICALL Java_Methods_method (JNIEnv *e, jobject obj){
    jclass cls = e->GetObjectClass(obj);
    jmethodID mid = e->GetMethodID(cls, "hello", "()V");
    e->CallVoidMethod(obj, mid);
}

Việc thực hiện cũng khá đơn giản.

Đầu tiên chúng ta lấy một đối tượng jclass bằng phương thức GetObjectClass().

Sau đó lấy đối tượng jmethodID bằng phương thức GetMethodID(), phương thức này nhận vào đối tượng jclass, tên lớp bằng chuỗi, rồi đến chuỗi mô tả tham số và kiểu trả về. Chuỗi mô tả này có dạng như sau:

"(kiểu_dữ_liệu_tham_số)kiểu_dữ_liệu_trả_về"

Các kiểu dữ liệu có tên như sau:

TÊN KIỂU DỮ LIỆU
Z Boolean
B Byte
C Char
S Short
I Int
J Long
F Float
D Double
L<tên_đẩy_đủ_của_lớp>; Tên đầy đủ của lớp, có đường dẫn package
[<một_trong_các_kiểu_trên> Kiểu mảng

Ví dụ "()V" có nghĩa là phương thức đó không nhận vào tham số nào và có kiểu dữ liệu trả về là void.

Giả sử trong Java có phương thức:

private String println(String);

Thì chúng ta điền là:

"(Ljava/lang/String;)Ljava/lang/String;"

Ví dụ về kiểu mảng:

Java:

public static void main(String[] args);

C++:

"([Ljava/lang/String;)V"

Lưu ý là nếu phương thức nhận vào kiểu void như f(void) thì trong C++ chúng ta không ghi là "(V)I" mà để trống, tức là "()I".

Hello World

Gọi phương thức static

Gọi một phương thức static cũng tương tự như gọi phương thức bình thường, chỉ khác tên hàm gọi.

Ví dụ chúng ta có code Java như sau:

class Methods {
    static { System.loadLibrary("Methods"); } 
    private native void method();
    private static void staticPrintFactorial(int n) {
        if(n >= 17 || n < 1) {
            System.out.println(n + " is too big or too small");
            return;
        }
        int result = 1;
        for(int i = 2; i <= n ; i++) result *= i;
        System.out.println(result);
    }
 
    public static void main(String args[]) {
        new Methods().method();
    }
}

Chúng ta có một phương thức staticstaticPrintFactorial(), phương thức này nhận vào một số nguyên và in ra giai thừa của số đó.

Chúng ta gọi phương thức này bên C++ như sau:

#include "Methods.h"

JNIEXPORT void JNICALL Java_Methods_method (JNIEnv *e, jobject obj){
    jclass cls = e->GetObjectClass(obj);
    jmethodID mid = e->GetStaticMethodID(cls, "staticPrintFactorial", "(I)V");
    e->CallStaticVoidMethod(cls, mid, 10);
    e->CallStaticVoidMethod(cls, mid, 50);
}

Chúng ta cũng lấy đối tượng jclass, jmethodID, ở đây phương thức lấy là GetStaticMethodID(), các tham số vẫn giống như phương thức GetMethod(), để gọi phương thức static thì chúng ta dùng hàm CallStaticVoidMethod(), phương thức này nhận vào đối tượng jclass, đối tượng jmethodID và các tham số.

3628800
50 is too big

Các phương thức tương tự khác có thể gọi là:

STATIC NON STATIC KIỂU DỮ LIỆU
CallVoidMethod() CallStaticVoidMethod() void
CallObjectMethod() CallStaticObjectMethod() jobject
CallBooleanMethod() CallStaticBooleanMethod() jboolean
CallByteMethod()
CallStaticByteMethod() jbyte
CallCharMethod()
CallStaticCharMethod() jchar
CallShortMethod() CallStaticShortMethod() jshort
CallIntMethod() CallStaticIntMethod() jint
CallLongMethod() CallStaticLongMethod() jlong
CallFloatMethod() CallStaticFloatMethod() jfloat
CallDoubleMethod() CallStaticDoubleMethod() jdouble

JNI – Đối tượng – Phần 1

Trong phần này chúng ta sẽ tìm hiểu cách đọc và ghi thuộc tính của một đối tượng/lớp.

Đọc thuộc tính với kiểu dữ liệu cơ bản

Đầu tiên chúng ta có đoạn code main trong Java như sau:

class AccessField {
    static { System.loadLibrary("AccessField"); }
 
    public String s;
 
    private native void accessField();
    public static void main(String args[]) {
        AccessField af = new AccessField();
        af.s = "Lorem ipsum";
        af.accessField();
    }
}

Lớp AccessField có một thuộc tính là s kiểu String, phương thức native accessField() sẽ làm công việc hiển thị giá trị của thuộc tính đó lên màn hình.

Bên native chúng ta có đoạn code sau:

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

JNIEXPORT void JNICALL Java_AccessField_accessField(JNIEnv *e, jobject obj) { 
    jclass cls = e->GetObjectClass(obj); 
    jfieldID fid = e->GetFieldID(cls, "s", "Ljava/lang/String;"); 
    jstring jstr = (jstring)e->GetObjectField(obj, fid); 
 
    const char *str = e->GetStringUTFChars(jstr, NULL); 
    std::cout << str;
    e->ReleaseStringUTFChars(jstr, str);
}

Để đọc một thuộc tính thì chúng ta làm theo các bước sau:

  • Lấy đối tượng jclass bằng phương thức GetObjectClass()
  • Lấy đối tượng jfieldID bằng phương thức GetFieldID(), phương thức này nhận vào đối tượng jclass, tên thuộc tính cần lấy, và tên lớp của thuộc tính đó, có đủ đường dẫn package
  • Lấy đối tượng đó bằng phương thức GetObjectField(), phương thức này nhận vào đối tượng jobjectjfieldID, phương thức này trả về đối tượng kiểu jobject, nếu thuộc tính cần lấy là kiểu dữ liệu cơ bản thì chúng ta có thể ép kiểu trực tiếp, đối với kiểu dữ liệu đối tượng thì chúng ta cũng làm tương tự (ví dụ ở dưới).
Lorem Ipsum

Tên đường dẫn lớp trong phương thức GetFieldID() có dạng:

"L/<đường_dẫn>/<package>/<tên_lớp;"

Nếu là các kiểu dữ liệu cơ bản thì chúng ta chỉ cần ghi ngắn gọn là:

  • int: "I"
  • float: "F"
  • double: "D"
  • boolean: "Z"

Không cần ghi tên package và tên lớp.

Đọc thuộc tính với kiểu dữ liệu đối tượng

Ví dụ chúng ta có lớp Point trong Java như sau:

class Point {
    private int x;
    private int y;
 
    public Point(int x, int y) { 
        this.x = x; 
        this.y = y;
    } 
 
    public void setX(int x) { this.x = x; }
    public void setY(int y) { this.y = y; }
 
    public int getX() { return x; }
    public int getY() { return y; }
}

Tiếp theo chúng ta có đoạn code main như sau:

class AccessField {
    static { System.loadLibrary("AccessField"); }
 
    private Point p;
    private native void accessFieldObject();
 
    public static void main(String args[]) { 
        AccessField af = new AccessField();
        af.p = new Point(3, 5);
        af.accessFieldObject();
    }
}

Lớp AccessField có một thuộc tính kiểu Pointp, phương thức accessFieldObject() sẽ lấy thuộc tính p này rồi hiển thị 2 thuộc tính con của xy, sau đó hiển thị lên màn hình.

Chúng ta làm việc đó như sau:

#include <iostream>
#include "AccessField.h"
using namespace std;

JNIEXPORT void JNICALL Java_AccessField_accessFieldObject(JNIEnv *e, jobject obj) { 
    jclass cls = e->GetObjectClass(obj);
    jfieldID fid = e->GetFieldID(cls, "p", "LPoint;");
 
    jobject pointObj = e->GetObjectField(obj, fid);
    jclass pointCls = e->GetObjectClass(pointObj);
    jfieldID xFid = e->GetFieldID(pointCls, "x", "I");
    jfieldID yFid = e->GetFieldID(pointCls, "y", "I");
 
    jint x = e->GetIntField(pointObj, xFid);
    jint y = e->GetIntField(pointObj, yFid);
 
    cout << "Point: " << x << ", " << y;
}

Chúng ta cũng lần lượt lấy jclass của đối tượng obj, tức là lấy được lớp AccessField. Sau đó lấy đối tượng jfieldID của thuộc tính p, rồi lấy đối tượng jobject của thuộc tính p này, từ đó chúng ta bắt đầu lại lấy jclass của thuộc tính p này, cứ thế cho đến khi lấy 2 thuộc tính xy.

Ở đây khi lấy thuộc tính xy thì chúng ta không dùng phương thức GetObjectField() rồi ép kiểu nữa, mà chúng ta dùng phương thức GetIntField() đế lấy thẳng đối tượng jint luôn. Trong JNI thì intjint là giống nhau nên chúng ta có thể dùng cout để in ra luôn.

Point: 3, 5

Tương tự, JNI có một số phương thức lấy dữ liệu khác là:

  • GetFloatField(): jfloat
  • GetBooleanField(): jboolean
  • GetByteField(): jbyte
  • GetCharField(): jchar
  • GetShortField(): jshort
  • GetLongField(): jlong
  • GetDoubleField(): jdouble

Đọc thuộc tính static

Đọc thuộc tính static cũng giống như thuộc tính thường, chỉ khác là một số phương thức sẽ có tên ‘Static’ trong đó.

Ví dụ:

Chúng ta có code main Java như sau:

class AccessField {
    static { System.loadLibrary("AccessField"); }
   
    private static float PI = 3.14159265359f;
    private native void accessStaticField();
 
    public static void main(String args[]) { 
        new AccessField().accessStaticField();
    }
}

Thuộc tính PI có kiểu là float, đây là thuộc tính static.

Code native như sau:

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

using namespace std;

JNIEXPORT void JNICALL Java_AccessField_accessStaticField (JNIEnv *e, jobject obj) {
    jclass cls = e->GetObjectClass(obj);
    jfieldID fid = e->GetStaticFieldID(cls, "PI", "F");
    jfloat PI = e->GetStaticFloatField(cls, fid);
 
    printf("PI: %.9f", PI);
}

Phương thức lấy jfieldIDGetStaticFieldID(), lấy dữ liệu là GetStaticFloatField().

PI: 3.141592741

Tương tự, các phương thức lấy dữ liệu khác là

  • GetStaticObjectField()
  • GetStaticBooleanFielđ()
  • GetStaticByteField()
  • GetStaticCharField()
  • GetStaticShortField() 
  • GetStaticIntField() 
  • GetStaticLongField() 
  • GetStaticDoubleField()

Ghi dữ liệu vào thuộc tính

Ví dụ chúng ta có đoạn code main như sau:

class AccessField {
    static { System.loadLibrary("AccessField"); }
 
    private static float PI = 3.14159265359f;
    private static float radius;
 
    private native void setRadius();
 
    public static void main(String args[]) { 
        AccessField af = new AccessField();
        af.setRadius();
        System.out.println("Circle area: " + (radius * radius * PI));
    }
}

Lớp AccessField() có thuộc tính radius kiểu float, phương thức native setRadius() sẽ thiết lập giá trị cho thuộc tính này.

Code native như sau:

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

using namespace std;

JNIEXPORT void JNICALL Java_AccessField_setRadius (JNIEnv *e, jobject obj) {
    jclass cls = e->GetObjectClass(obj);
    jfieldID fid = e->GetStaticFieldID(cls, "radius", "F");
    jfloat radius = e->GetStaticFloatField(cls, fid);
 
    radius = 5.0f;
    e->SetStaticFloatField(cls, fid, radius);
}

Mọi quá trình vẫn giống như cũ, chúng ta lấy jclass, jfieldID, lấy biến, sau đó gán giá trị cho biến.

Cuối cùng để thiết lập giá trị này cho thuộc tính ở phía Java thì chúng ta gọi phương thức SetStaticFloatField(), phương thức này nhận vào đối tượng jclass, nếu thuộc tính không phải là static thì chúng ta dùng phương thức SetFloatField() và truyền vào một đối tượng jobject, sau đó là jfieldID và tới giá trị.

Circle area: 78.53982

Các phương thức tương tự là:

STATIC NON STATIC KIỂU DỮ LIỆU
SetStaticObjectField() SetObjectField() jobject
SetStaticBooleanField() SetBooleanField() jboolean
SetStaticByteField() SetByteField() jbyte
SetStaticCharField() SetCharField() jchar
SetStaticShortField() SetShortField() jshort
SetStaticIntField() SetIntField() jint
SetStaticLongField() SetLongField() jlong
SetStaticFloatField() SetFloatField() jfloat
SetStaticDoubleField() SetDoubleField() jdouble

JNI – Mảng

Trong phần này chúng ta sẽ tìm hiểu cách thao tác với mảng.

Mảng dữ liệu cơ bản

Chúng ta sẽ viết hàm tính tổng các phần tử số nguyên trong một mảng int.

Đầu tiên chúng ta viết đoạn code main trong Java như sau:

class IntArray {
    static {
        System.loadLibrary("SumIntArr");
    }
    private native int sum(int[] arr);
    public static void main(String[] args) {
        int[] arr = new int[] {18, 4, 5, 7, 1, 2, 9, 3, 7, 9};
        int s = new IntArray().sum(arr);
        System.out.println("Sum of array 'arr' is " + s);
    } 
}

Ở đây chúng ta tạo một mảng int có 10 phần tử, phương thức native là sum() sẽ tính tổng mảng này. Tên thư viện native chúng ta đặt là SumIntArr.

Sau đó chúng ta dịch và tạo file header C++:

javac IntArray.java
...
javah -classpath . -jni IntArray
...

File IntArray.h được tạo ra với phương thức native như sau:

...
JNIEXPORT jint JNICALL Java_IntArray_sum
 (JNIEnv *, jobject, jintArray);
...

Theo như bạn thấy thì kiểu int[] trong Java sẽ được chuyển tương ứng thành jintArray trong JNI.

Bây giờ chúng ta tạo file C++ tên SumIntArray.cpp để code phương thức trên như sau:

#include "IntArray.h"

JNIEXPORT jint JNICALL Java_IntArray_sum (JNIEnv *e, jobject obj, jintArray arr) { 
    jint length = e->GetArrayLength(arr);
   
    jint convertedArr[length]; 
    e->GetIntArrayRegion(arr, 0, length, convertedArr); 
 
    jint result = 0;
    for(jint i = 0 ; i < length ; i++) 
        result += convertedArr[i]; 
    return result;
}

Kiểu jintArray không phải là một mảng, mảng int trong JNI có cú pháp là jint a[...]. Do đó chúng ta phải chuyển đổi kiểu jintArray này.

Để chuyển thì chúng ta gọi phương thức GetIntArrayRegion(), phương thức này nhận vào 4 tham số là đối tượng jintArray, vị trí bắt đầu mảng, vị trí kết thúc mảng, và mảng jint [] đích sẽ được chuyển đổi. Vị trí bắt đầu là 0, vị trí kết thúc là độ dài của mảng, độ dài này chúng ta lấy bằng phương thức GetArrayLength(). Mảng jint chúng ta tạo ra trước có tên là convertedArr.

Sau khi đã chuyển đổi xong thì chúng ta có thể dùng vòng lặp for, while để duyệt như mảng bình thường.

Chạy:

g++ -I"C:\Program Files\Java\jdk1.8.0_65\include" -I"C:\Program Files\Java\jdk1.8.0_65\include\win32" -o SumIntArr.o -c SumIntArr.cpp
...
g++ -Wl,--add-stdcall-alias -shared -o SumIntArr.dll SumIntArr.o
...
java -classpath . -Djava.library.path=. IntArray
...
Sum of array 'arr' is 65

Ngoài phương thức GetIntArrayRegion() trên thì chúng ta có thể dùng phương thức GetIntArrayElements() như sau:

#include "IntArray.h"

JNIEXPORT jint JNICALL Java_IntArray_sum (JNIEnv *e, jobject obj, jintArray arr) {  
    jint *convertedArr;
    convertedArr = e->GetIntArrayElements(arr, NULL);

    jint length = e->GetArrayLength(arr);

    jint result = 0;
    for(jint i = 0 ; i < length ; i++)
        result += convertedArr[i];

    e->ReleaseIntArrayElements(arr, convertedArr, 0);
    return result;
}

Điểm khác biệt giữa 2 phương thức là phương thức GetIntArrayRegion() lấy một khoảng cố định trong mảng trong khi GetIntArrayElements() sẽ lấy tất cả các phần tử trong mảng. Sau khi dùng phương thức GetIntArrayElements() thì chúng ta gọi phương thức ReleaseIntArrayElements() để giải phóng mảng đó ra khỏi bộ nhớ.

Đối với các kiểu dữ liệu khác thì chúng ta cũng làm tương tự, bảng dưới đây là tên các phương thức ứng với từng kiểu dữ liệu:

PHƯƠNG THỨC Array Type Native Type
GetBooleanArrayElements() jbooleanArray jboolean
GetByteArrayElements() jbyteArray jbyte
GetCharArrayElements() jcharArray jchar
GetShortArrayElements() jshortArray jshort
GetIntArrayElements() jintArray jint
GetLongArrayElements() jlongArray jlong
GetFloatArrayElements() jfloatArray jfloat
GetDoubleArrayElements() jdoubleArray jdouble

Mảng hai chiều

Mảng 2 chiều là mảng chứa các phàn tử là mảng khác, thao tác với mảng 2 chiều cũng tương tự như với mảng một chiều.

Ví dụ chúng ta có code main Java như sau:

class TwoDArray {
    static {
        System.loadLibrary("Display2DArray");
    }
 
    private static native void display(int arr[][]);
    public static void main(String[] args) {
        int arr[][] = new int[][] {
            {9, 3, 5},
            {1, 2, 4},
            {4, 5, 7}
        };
        new TwoDArray().display(arr);
    }
}

Ở đây chúng ta có phương thức native là display(), phương thức này nhận vào một mảng 2 chiều.

Ở phía C++ chúng ta sẽ code phương thức này làm việc hiển thị các phần tử trong mảng như sau:

#include <iostream>
#include "TwoDArray.h"
using namespace std;

JNIEXPORT void JNICALL Java_TwoDArray_display
    (JNIEnv *e, jclass cls, jobjectArray arr) {
 
    jint n = e->GetArrayLength(arr);
 
    for(jint i = 0 ; i < n ; i++) {
 
        jintArray innerArrObj = (jintArray)e->GetObjectArrayElement(arr, i);
        jint m = e->GetArrayLength(innerArrObj);
        jint *innerArr = e->GetIntArrayElements(innerArrObj, NULL);
 
        for(jint j = 0 ; j < m ; j++) {
           cout << innerArr[j] << " ";
        }
        cout << endl;
    } 
}

Đầu tiên chúng ta lấy độ dài mảng chính, đặt tên là n.

Sau đó chúng ta lặp n lần, mỗi lần lặp chúng ta lấy từng mảng con ra, và vì mảng 2 chiều là mảng chứa các con trỏ, không phải các kiểu dữ liệu cơ bản nên chúng ta dùng phương thức GetObjectArrayElement(), tham số đầu tiên là biến mảng chính, tham số thứ 2 là vị trí phần tử. Ở đây khi lấy về chúng ta ép kiểu sang jintArray, tùy loại dữ liệu mà bạn sẽ ép sang từng kiểu khác nhau.

Sau đó chúng ta lại lặp qua mảng một chiều vừa lấy được như trong ví dụ đầu tiên.

9 3 5
1 2 4
4 5 7

JNI – Các kiểu dữ liệu cơ bản

Các kiểu dữ liệu tham số ở phía native cũng tương tự với kiểu dữ liệu của ngôn ngữ đó.

Đối với các kiểu dữ liệu dạng primitive như int, float, char…v.v thì khi chuyển sang JNI cũng gần như là tương tự nhau, chỉ cần thêm kí tự 'j' vào trước tên kiểu dữ liệu, như int trong Java thì chuyển sang jint trong JNI, float thành jfloat…v.v

Bảng dưới đây mô tả các kiểu dữ liệu tương ứng giữa Java và JNI:

Java

JNI

Ghi chú

boolean jboolean 8 bit không dấu
byte jbyte 8 bit có dấu
char jchar 16 bit không dấu
short jshort 16 bit có dấu
int jint 32 bit có dấu
long jlong 64 bit có dấu
float jfloat 32 bit
double jdouble 64 bit

 

JNI – Tham số JNI

Chúng ta sẽ tìm hiểu về các tham số trong các phương thức native.

Để làm việc này thì chúng ta sẽ viết hàm native trong Java, hàm này sẽ nhận vào một chuỗi kí tự, bên C++ sẽ nhận chuỗi kí tự này và in ra màn hình.

Đầu tiên chúng ta tạo file .java như sau:

public class Arg {
    static {
        System.loadLibrary("Arg");
    }
 
    private native void sendName(String name);
 
    public static void main(String args[]){
        String name = "Pho Code";
        Arg arg = new Arg();
        arg.sendName(name);
    }
}

Phương thức nativesendName(), chúng ta truyền vào với chuỗi là "Pho Code".

Sau đó dịch và tạo file header C++:

javac Arg.java
...
javah -classpath . -jni Arg
...

File Arg.h với phương thức native được chuyển sang C++ sẽ được sinh ra như thế này:

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

#ifndef _Included_Arg
#define _Included_Arg
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class: Arg
 * Method: sendName
 * Signature: (Ljava/lang/String;)V
 */
JNIEXPORT void JNICALL Java_Arg_sendName
 (JNIEnv *, jobject, jstring);

#ifdef __cplusplus
}
#endif
#endif

Trong đó JNIEXPORTJNICALL là các macro được định nghĩa trong file jni.h, các macro này cho biết là hàm đó có thể được gọi từ nơi khác (giống như từ khóa extern trong C). Bạn cũng có thể để ý tên phương thức được tạo nên từ Java_<tên_lớp>_<tên_phương_thức>.

Và trong phương thức native này bao giờ cũng sẽ có ít nhất là 2 tham số JNIEnv*jobject. Về cơ bản thì đối tượng JNIEnv* là một con trỏ trỏ tới một con trỏ, con trỏ này trỏ tới một mảng, mảng này lại chứa các con trỏ trỏ tới các phương thức của JNI để chúng ta có thể sử dụng.

Tham số jobject sẽ khác nhau tùy thuộc vào phương thức native ở bên phía Java như thế nào. Nếu phương thức native là phương thức static thì jobject sẽ đại diện cho lớp chứa phương thức đó, còn nếu không thì jobject sẽ là đối tượng được tạo ra từ lớp đó.

Ngoài 2 tham số bắt buộc phải có kia thì chúng ta có thể truyền thêm các tham số khác như bình thường.

Bây giờ chúng ta viết code cho phương thức native C++ như sau:

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

JNIEXPORT void JNICALL Java_Arg_sendName (JNIEnv *e, jobject obj, jstring name){
    const char *str;
    str = e->GetStringUTFChars(name, JNI_FALSE);
    if(str == NULL)
        return;
    std::cout << "Hello " << str;
    e->ReleaseStringUTFChars(name, str);
}

Tham số jstring ở bên Java là biến là một đối tượng java.lang.String, nhưng C++ thì lại không có kiểu dữ liệu nào như thế này. Do đó chúng ta phải chuyển kiểu dữ liệu, may mắn là trong số các phương thức JNI có phương thức để chuyển, ở đây chúng ta dùng phương thức GetStringUTFChars(), phương thức này sẽ chuyển jstring sang một mảng char, tham số đầu tiên là biến jstring, tham số thứ 2 là một giá trị bool, chúng ta có thể dùng JNI_FALSE hoặc JNI_TRUE, ý nghĩa của tham số thứ 2 là có muốn thực hiện copy hay không.

Sau khi chuyển xong chúng ta có thể in ra như thường, ngoài ra chúng ta cũng có thể kiểm tra xem việc chuyển đổi có thành công hay không bằng cách kiểm tra xem strNULL hay không.

Cuối cùng chúng ta gọi phương hức ReleaseStringUTFChars() để xóa biến này khỏi bộ nhớ.

Bây giờ chúng ta có thể dịch và build file thư viện và chạy:

g++ -I"C:\Program Files\Java\jdk1.8.0_65\include" 
-I"C:\Program Files\Java\jdk1.8.0_65\include\win32 -o Arg.o -c Arg.cpp
...
g++ -Wl,--add-stdcall-alias -shared -o Arg.dll Arg.o
...
java -classpath . -Djava.library.path=. Arg
Hello Pho Code

Ngoài 2 phương thức GetStringUTFChars()ReleaseStringUTFChars() ở trên thì còn rất nhiều phương thức khác như:

  • jstring NewString(JNIEnv *, const jchar *, jsize): tạo chuỗi java.lang.String
  • jsize GetStringLength(JNIEnv *, jstring): lấy độ dài của chuỗi jstring
  • const jchar * GetStringChars(JNIEnv *, jstring, jboolean *)tương tự GetStringUTFChars()
  • void ReleaseStringChars(JNIEnv *, jstring, const jchar *): tương tự ReleaseStringUTFChars()

Bạn có thể xem danh sách tại đây.