Author Archives: Phở Code

Angular – Cài đặt

Đầu tiên chúng ta sẽ cần cài Node.js vì chúng ta sẽ cài Angular thông qua hệ thống npm, ngoài ra trong npm còn có gói TypeScript, nếu thích thì bạn có thể cài các trình biên dịch khác cũng được, chẳng hạn như trong VisualStudio, Sublime, Emacs… cũng có trình để dịch TypeScript.

Để cài đặt Node.js thì bạn có thể xem bài này, ở đây mình dùng Node phiên bản 4.5.0.

Cài TypeScript

Sau đó bạn mở Command Prompt (cmd) trên Windows ra và gõ lệnh npm install -g typescript để node cài đặt gói typescript.

C:\>npm install -g typescript

Ở đây phiên bản typescript của mình là phiên bản 2.1.4

Cài Angular CLI

Bản thân Angular chỉ là một thư viện Javascript mở rộng dành cho Node.js chứ không phải một ứng dụng hoàn toàn, tuy nhiên nhóm phát triển Angular đã viết thêm gói Angular CLI là một chương trình dòng lệnh để hỗ trợ chúng ta tạo project, thêm file, xóa file, khởi động project… một cách dễ dàng.

Bạn mở Command Prompt (cmd) lên và gõ lệnh sau để cài gói Angular CLI vào npm:

npm install -g @angular/cli

npm sẽ tải và cài bộ thư viện này về máy của bạn, bộ Angular CLI này khá lớn nên quá trình này sẽ mất một lúc.

Sau khi hoàn tất thì bạn có thể tạo một project Angular bằng cách chạy lệnh

ng new my-app

Trong đó my-app là tên project, Angular sẽ tạo một thư mục có tên my-app và tạo một số file cần thiết, chúng ta sẽ tìm hiểu về chúng sau.

Để chạy project này thì chúng ta dùng lệnh cd để chuyển thư mục hiện hành trong CMD vào thư mục project rồi chạy lệnh sau:

ng serve --open

Angular sẽ khởi động server tại địa chỉ http://localhost:4200 và dòng chữ app works sẽ hiển thị trên trình duyệt.

Quickstart

Quickstart là một project Angular mẫu, gồm những file cần thiết dành cho một project Angular, thay vì ngồi tạo project và từng file bằng tay thì chúng ta có thể lấy bộ source của quickstart từ trang GitHub để dùng cũng được.

Nếu bạn có cài Git trên máy thì chỉ cần mở Command Prompt (cmd) lên, chuyển đến thư mục mà bạn muốn cài bằng lệnh cd, sau đó gõ lệnh

git clone https://github.com/angular/quickstart.git quickstart

 là được, và chúng ta sẽ được một thư mục có tên quickstart.

Nếu không cài Git thì bạn có thể download và giải nén tại địa chỉ:

https://github.com/angular/quickstart/archive/master.zip

Sau đó bạn chuyển thư mục hiện tại trong Command Prompt vào thư mục quickstart đó rồi chạy lệnh npm install để npm cài các module cần thiết, vậy là đã xong.

Để chạy được project này thì trong Command Prompt bạn dùng lệnh npm start.

Angular sẽ tự mở trình duyệt đến địa chỉ http://localhost:3000, và bạn sẽ thấy kết quả như hình dưới.

Angular – Giới thiệu

AngularJS là một web framework phía front-end có thể tương tác rất tốt với các framework back-end, hỗ trợ tạo các ứng dụng mạnh mẽ một cách dễ dàng. AngularJS được thiết kế trên mô hình MVC, tức là có các tính năng như:

  • Dễ mở rộng: bạn có thể dễ dàng tìm hiểu kiến trúc của các ứng dụng AngularJS phức tạp, từ đó dễ dàng nâng cấp và bảo trì cũng như thêm/bớt các tính năng.
  • Dễ bảo trì: các ứng dụng AngularJS rất dễ debug và sửa lỗi, tức là việc bảo trì rất đơn giản
  • Dễ kiểm nghiệm: AngularJS có hỗ trợ tính năng unit testing và end-to-end testing nên việc kiểm nghiệm cũng rất dễ dàng
  • Dễ phát triển: AngularJS luôn được nâng cấp để có thể tận dụng các tính năng đi cùng với trình duyệt, qua đó cho phép bạn tạo các ứng dụng có thể sử dụng các công nghệ mới nhất

AngularJS là một thư viện Javascript mã nguồn mở được Google tài trợ và bảo trì, rất nheieuf ứng dụng lớn và phức tạp sử dụng AngularJS.

Để có thể học AngularJS thì bạn cần biết các khái niệm cơ bản trong việc phát triển web, biết cách làm việc với HTML, CSS và tất nhiên là Javascript nữa, tuy nhiên chúng ta sẽ sử dụng TypeScript là chính, nếu bạn chưa từng làm việc với TypeScript thì sẽ có bài hướng dẫn sơ qua.

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