Category Archives: Java

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.

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

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

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

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

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

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

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

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

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

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

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

Ví dụ

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

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

Bước 1: Viết code Java

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

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

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

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

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

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

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

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

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

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

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

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

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

#ifdef __cplusplus
}
#endif
#endif

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

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

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

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

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

using namespace std;

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

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

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

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

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

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

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

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

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

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

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

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

Bước 6: Run

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

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

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

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