CLOVER🍀

That was when it all began.

夏休みの宿題は、JNI

特定の方向けのエントリです。経緯とかは端折るので、読みにくいエントリかもしれませんが、ご容赦を。

さて、とある方から、JNIのサンプルコードを書いて欲しいという依頼を受けました。C/C++には詳しい方なのですが、Javaはそうでもない様子。で、「Java-JNIでこういうインターフェースで処理をしたいんだけど、教えてくれない?」のようなことを聞かれました。

希望を聞いたJavaのnativeメソッド宣言を、C言語的に表記すると、こんな感じだと思います。

long int testfunc(int *num, char *s)

つまり、longを返して引数は出力変数として書き換えたい…と。

Javaで解釈すると、こうなりますね。

int num = ...;
String s = ...;
long ret = testfunc(num, s);  // これでnumとsの値が変わる

Javaを知っていれば分かるのですが、この仕様はJavaでは実現不可能です。なぜなら、Javaのプリミティブ(int, long, float, doubleなどなど)およびStringは、変更不可だからです。
※リフレクションを使用してどうのこうの、というのはここでは置いておきます
あと、プリミティブはポインタ(参照)が扱えないので、そもそも参照渡し自体ができないですしね。

依頼をしてきた方は、試行錯誤の結果…

int[] intArray = ....;
long ret = func1(intArray);  // ここで、intArrayの中身を変更
int intValue = intArray[0];
String stringValue = func2(intValue);

のように、nativeメソッドを2つに分割することで対処したらしいですが、func1にintが渡せるといいなーと嘆いておられましたが、前述の通りintは変更不可なのでそれはできません。

ですので…変更可能なクラスやパラメータ/戻り値にオブジェクトを利用しないとあまりスマートな解は出ないのですが、ちょっとだけサンプルを書いてみたいと思います。

Eclipseは嫌いだったのと、すでに動く環境が手元にあるみたいなので端折ります(笑)。

以後、HelloJni.javaという名前のファイルで作業をします。最終的には、HelloJniクラス内にインタークラスも定義します。
コンパイルコマンドは、以下の通りです。

$ javac HelloJni.java
$ javah HelloJni
$ gcc -fPIC -c HelloJni.cpp -I /usr/lib/jvm/java-6-sun/include/ -I /usr/lib/jvm/java-6-sun/include/linux/
$ gcc -shared HelloJni.o -o libHelloJni.so

JNIのヘッダファイルの場所は、適宜読み替えてくださいね。

実行コマンドは、以下の通りです。

$java -Djava.library.path=[libHelloJni.soが置いてあるディレクトリ] HelloJni 

なお、Javaファイル中に

System.loadLibrary("HelloJni");

と記述するのをお忘れ無く。

当方の実行環境は、以下の通りです。

$ lsb_release -a
No LSB modules are available.
Distributor ID:	Ubuntu
Description:	Ubuntu 11.04
Release:	11.04
Codename:	natty
$ uname -a
Linux ubuntu 2.6.38-10-generic #46-Ubuntu SMP Tue Jun 28 15:07:17 UTC 2011 x86_64 x86_64 x86_64 GNU/Linux
$ java -version
java version "1.6.0_26"
Java(TM) SE Runtime Environment (build 1.6.0_26-b03)
Java HotSpot(TM) 64-Bit Server VM (build 20.1-b02, mixed mode)
$ gcc --version
gcc (Ubuntu/Linaro 4.5.2-8ubuntu4) 4.5.2
Copyright (C) 2010 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

64bit Ubuntu Linuxです。

それでは、これからのサンプルではnativeメソッドのインターフェースは変更しますが、呼び出し回数は1回ですませるようトライします。

解法その1:Objectの配列を使う

あまりキレイなやり方ではありません。Objectの配列をnativeメソッドに渡すことで、intとStringのパラメータを同時に渡し、かつ配列の要素が変更可能なことを利用して値を書き換える方法です。

Java側はこちら。

    public native long writeObjectArray(Object[] objects);

        Object[] objects = {new Integer(5), "HelloWorld"};
        long ret = writeObjectArray(objects);
        System.out.println(String.format("num = %d, str = %s, ret = %d", objects[0], objects[1], ret));

C++側はこちら。
※ヘッダファイルは端折ります

JNIEXPORT jlong JNICALL Java_HelloJni_writeObjectArray
  (JNIEnv *env, jobject thisj, jobjectArray objects)
{
  jlong ret = 15;
  jclass integerClass = env->FindClass("java/lang/Integer");
  jmethodID integerConstructor = env->GetMethodID(integerClass, "<init>", "(I)V");
  jmethodID intValueMethod = env->GetMethodID(integerClass, "intValue", "()I");
  
  jint intValue = env->CallIntMethod(env->GetObjectArrayElement(objects, 0), intValueMethod);
  jstring stringValue = (jstring)env->GetObjectArrayElement(objects, 1);
  const char *s = env->GetStringUTFChars(stringValue, NULL);

  printf("第1要素[%d], 第2要素[%s]\n", intValue, s);

  env->SetObjectArrayElement(objects, 0, env->NewObject(integerClass, integerConstructor, intValue * 2));
  env->SetObjectArrayElement(objects, 1, env->NewStringUTF("Hello JNI World"));

  env->ReleaseStringUTFChars(stringValue, s);

  return ret;
}

SetObjectArrayElementで、配列の要素を書き換えていることがポイントです。ホントはObject配列の中にintを詰めたかったのですが、そこからintを取得するのが難しくて断念しました…。
実行すると、こうなります。

第1要素[5], 第2要素[HelloWorld]
num = 10, str = Hello JNI World, ret = 15
解法その2:変更可能なクラスを渡し、呼び出し先でその値を変更する

setter/getterを持ったクラスを定義して、それをメソッドのパラメータとすることでC++側で値を書き換えます。
FindClassはクラスを探すため、GetMethodIDはメソッドを探すための関数です。

まず、HelloJniクラスのインナークラスとして、以下のようなクラスを定義します。
※別にインナークラスである必要はありません。別ソースにするのが面倒だったので、インナークラスを利用しただけです

    public static class SendReceiveObject {
        private int intValue;
        private String stringValue;

        public void setIntValue(int intValue) {
            this.intValue = intValue;
        }

        public int getIntValue() {
            return intValue;
        }

        public void setStringValue(String stringValue) {
            this.stringValue = stringValue;
        }

        public String getStringValue() {
            return stringValue;
        }
    }

実際に利用する時は、こんな感じです。

    public native long writeResult(SendReceiveObject sendReceive);

        SendReceiveObject sendReceive = new SendReceiveObject();
        sendReceive.setIntValue(5);
        sendReceive.setStringValue("Hello World");
        ret = writeResult(sendReceive);
        System.out.println(String.format("sendreceive.num = %d, sendreceive.str = %s, ret = %d",
                                         sendReceive.getIntValue(), sendReceive.getStringValue(), ret));

C++側。

JNIEXPORT jlong JNICALL Java_HelloJni_writeResult
  (JNIEnv *env, jobject thisj, jobject sendReceive)
{
  jlong ret = 15;

  jclass sendReceiveClass = env->FindClass("HelloJni$SendReceiveObject");
  jmethodID getIntValueMethod, setIntValueMethod, getStringValueMethod, setStringValueMethod;

  getIntValueMethod = env->GetMethodID(sendReceiveClass, "getIntValue", "()I");
  setIntValueMethod = env->GetMethodID(sendReceiveClass, "setIntValue", "(I)V");
  getStringValueMethod = env->GetMethodID(sendReceiveClass, "getStringValue", "()Ljava/lang/String;");
  setStringValueMethod = env->GetMethodID(sendReceiveClass, "setStringValue", "(Ljava/lang/String;)V");

  jint intValue = env->CallIntMethod(sendReceive, getIntValueMethod);
  jstring stringValue = (jstring)env->CallObjectMethod(sendReceive, getStringValueMethod);
  const char *s = env->GetStringUTFChars(stringValue, NULL);

  printf("intValue[%d], stringValue[%s]\n", intValue, s);

  env->CallIntMethod(sendReceive, setIntValueMethod, intValue * 2);
  env->CallObjectMethod(sendReceive, setStringValueMethod, env->NewStringUTF("Hello JNI World"));

  env->ReleaseStringUTFChars(stringValue, s);

  return ret;
}

クラスやメソッドを探さなくてはならないため、少々面倒ですが少なくともJava側はちゃんとintやStringの型で扱うことができるようになります。

実行結果。

intValue[5], stringValue[Hello World]
sendreceive.num = 10, sendreceive.str = Hello JNI World, ret = 15

ただ、この方法の不満点は呼び出し側で引数の値を勝手に変更するのは、最近のプログラミングスタイルとしてはどうなのかなぁと思います。

解法その3:呼び出し先で新しいインスタンスを生成して返す

その2の発展形です。引数としてパラメータクラスを受け取り、その内容から新しいインスタンスを生成して返却します。

ここでも、インナークラスを利用しています。

    public static class ParamObject {
        private int intValue;
        private String stringValue;
        private long longValue;

        public ParamObject(int intValue, String stringValue) {
            this.intValue = intValue;
            this.stringValue = stringValue;
        }

        public ParamObject(int intValue, String stringValue, long longValue) {
            this(intValue, stringValue);
            this.longValue = longValue;
        }

        public int getIntValue() {
            return intValue;
        }

        public String getStringValue() {
            return stringValue;
        }

        public long getLongValue() {
            return longValue;
        }
    }

利用側はこちら。

    public native ParamObject createNewParam(ParamObject param);

        ParamObject param = new ParamObject(5, "HelloWorld");
        ParamObject result = createNewParam(param);
        System.out.println(String.format("result.num = %d, result.str = %s, result.ret = %d",
                                         result.getIntValue(), result.getStringValue(), result.getLongValue()));

C++側はこちら。

JNIEXPORT jobject JNICALL Java_HelloJni_createNewParam
  (JNIEnv *env, jobject thisj, jobject param)
{
  jclass paramClass = env->FindClass("HelloJni$ParamObject");
  jmethodID getIntValueMethod, getStringValueMethod;
  jmethodID paramConstructor;

  getIntValueMethod = env->GetMethodID(paramClass, "getIntValue", "()I");
  getStringValueMethod = env->GetMethodID(paramClass, "getStringValue", "()Ljava/lang/String;");
  paramConstructor = env->GetMethodID(paramClass, "<init>", "(ILjava/lang/String;J)V");

  jint intValue = env->CallIntMethod(param, getIntValueMethod);
  jstring stringValue = (jstring)env->CallObjectMethod(param, getStringValueMethod);
  const char *s = env->GetStringUTFChars(stringValue, NULL);

  printf("intValue[%d], stringValue[%s]\n", intValue, s);

  jobject ret = env->NewObject(paramClass,
			       paramConstructor,
			       intValue * 2,
			       env->NewStringUTF("Hello JNI World"),
			       15);

  env->ReleaseStringUTFChars(stringValue, s);

  return ret;
}

引数ParamObjectの内容を読み取り、その内容を元に(Stringは無視していますが…)新しいParamObjectクラスのインスタンスを生成して返却します。

実行結果はこちら。

intValue[5], stringValue[HelloWorld]
result.num = 10, result.str = Hello JNI World, result.ret = 15

パラメータのくせに戻り値を持っているのが若干微妙ですが、やっぱり新しいインスタンスを生成して返すスタイルの方がいいかなーと思います。

完全なソースコード

一応、掲載しておきます。
HelloJni.java

public class HelloJni {
    public static void main(String[] args) {
        new HelloJni().execute();   
    }

    public native long writeObjectArray(Object[] objects);
    public native long writeResult(SendReceiveObject sendReceive);
    public native ParamObject createNewParam(ParamObject param);

    public void execute() {
        System.loadLibrary("HelloJni");

        Object[] objects = {new Integer(5), "HelloWorld"};
        long ret = writeObjectArray(objects);
        System.out.println(String.format("num = %d, str = %s, ret = %d", objects[0], objects[1], ret));

        SendReceiveObject sendReceive = new SendReceiveObject();
        sendReceive.setIntValue(5);
        sendReceive.setStringValue("Hello World");
        ret = writeResult(sendReceive);
        System.out.println(String.format("sendreceive.num = %d, sendreceive.str = %s, ret = %d",
                                         sendReceive.getIntValue(), sendReceive.getStringValue(), ret));

        ParamObject param = new ParamObject(5, "HelloWorld");
        ParamObject result = createNewParam(param);
        System.out.println(String.format("result.num = %d, result.str = %s, result.ret = %d",
                                         result.getIntValue(), result.getStringValue(), result.getLongValue()));
    }

    public static class SendReceiveObject {
        private int intValue;
        private String stringValue;

        public void setIntValue(int intValue) {
            this.intValue = intValue;
        }

        public int getIntValue() {
            return intValue;
        }

        public void setStringValue(String stringValue) {
            this.stringValue = stringValue;
        }

        public String getStringValue() {
            return stringValue;
        }
    }

    public static class ParamObject {
        private int intValue;
        private String stringValue;
        private long longValue;

        public ParamObject(int intValue, String stringValue) {
            this.intValue = intValue;
            this.stringValue = stringValue;
        }

        public ParamObject(int intValue, String stringValue, long longValue) {
            this(intValue, stringValue);
            this.longValue = longValue;
        }

        public int getIntValue() {
            return intValue;
        }

        public String getStringValue() {
            return stringValue;
        }

        public long getLongValue() {
            return longValue;
        }
    }
}

HelloJni.cpp

#include "HelloJni.h"

JNIEXPORT jlong JNICALL Java_HelloJni_writeObjectArray
  (JNIEnv *env, jobject thisj, jobjectArray objects)
{
  jlong ret = 15;
  jclass integerClass = env->FindClass("java/lang/Integer");
  jmethodID integerConstructor = env->GetMethodID(integerClass, "<init>", "(I)V");
  jmethodID intValueMethod = env->GetMethodID(integerClass, "intValue", "()I");
  
  jint intValue = env->CallIntMethod(env->GetObjectArrayElement(objects, 0), intValueMethod);
  jstring stringValue = (jstring)env->GetObjectArrayElement(objects, 1);
  const char *s = env->GetStringUTFChars(stringValue, NULL);

  printf("第1要素[%d], 第2要素[%s]\n", intValue, s);

  env->SetObjectArrayElement(objects, 0, env->NewObject(integerClass, integerConstructor, intValue * 2));
  env->SetObjectArrayElement(objects, 1, env->NewStringUTF("Hello JNI World"));

  env->ReleaseStringUTFChars(stringValue, s);

  return ret;
}

JNIEXPORT jlong JNICALL Java_HelloJni_writeResult
  (JNIEnv *env, jobject thisj, jobject sendReceive)
{
  jlong ret = 15;

  jclass sendReceiveClass = env->FindClass("HelloJni$SendReceiveObject");
  jmethodID getIntValueMethod, setIntValueMethod, getStringValueMethod, setStringValueMethod;

  getIntValueMethod = env->GetMethodID(sendReceiveClass, "getIntValue", "()I");
  setIntValueMethod = env->GetMethodID(sendReceiveClass, "setIntValue", "(I)V");
  getStringValueMethod = env->GetMethodID(sendReceiveClass, "getStringValue", "()Ljava/lang/String;");
  setStringValueMethod = env->GetMethodID(sendReceiveClass, "setStringValue", "(Ljava/lang/String;)V");

  jint intValue = env->CallIntMethod(sendReceive, getIntValueMethod);
  jstring stringValue = (jstring)env->CallObjectMethod(sendReceive, getStringValueMethod);
  const char *s = env->GetStringUTFChars(stringValue, NULL);

  printf("intValue[%d], stringValue[%s]\n", intValue, s);

  env->CallIntMethod(sendReceive, setIntValueMethod, intValue * 2);
  env->CallObjectMethod(sendReceive, setStringValueMethod, env->NewStringUTF("Hello JNI World"));

  env->ReleaseStringUTFChars(stringValue, s);

  return ret;
}

JNIEXPORT jobject JNICALL Java_HelloJni_createNewParam
  (JNIEnv *env, jobject thisj, jobject param)
{
  jclass paramClass = env->FindClass("HelloJni$ParamObject");
  jmethodID getIntValueMethod, getStringValueMethod;
  jmethodID paramConstructor;

  getIntValueMethod = env->GetMethodID(paramClass, "getIntValue", "()I");
  getStringValueMethod = env->GetMethodID(paramClass, "getStringValue", "()Ljava/lang/String;");
  paramConstructor = env->GetMethodID(paramClass, "<init>", "(ILjava/lang/String;J)V");

  jint intValue = env->CallIntMethod(param, getIntValueMethod);
  jstring stringValue = (jstring)env->CallObjectMethod(param, getStringValueMethod);
  const char *s = env->GetStringUTFChars(stringValue, NULL);

  printf("intValue[%d], stringValue[%s]\n", intValue, s);

  jobject ret = env->NewObject(paramClass,
			       paramConstructor,
			       intValue * 2,
			       env->NewStringUTF("Hello JNI World"),
			       15);

  env->ReleaseStringUTFChars(stringValue, s);

  return ret;
}

ちょっとでも参考になればよいのですが…どうでしょ?