JNIのはじめの一歩

たまに必要になるJNI。ノート替わりにメモしておく。

例えばこんな感じのSample.javaコードを用意して、ここからヘッダファイルを生成する。

public class Sample {
  public static native int readValue();
}
javac Sample.java -h .

javacのヘルプみればわかるけど、-hの後ろにはヘッダファイルを生成するディレクトリを指定する。

生成されたヘッダファイルSample.hは以下。

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

#ifndef _Included_Sample
#define _Included_Sample
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     Sample
 * Method:    readValue
 * Signature: ()I
 */
JNIEXPORT jint JNICALL Java_Sample_readValue
  (JNIEnv *, jclass);

#ifdef __cplusplus
}
#endif
#endif

このヘッダファイルをincludeしたcのコードは以下。

#include "Sample.h"

JNIEXPORT jint JNICALL Java_Sample_readValue
  (JNIEnv *env, jclass clazz) {
    return 13;
}

ヘッダファイルでは引数の変数名が省略されているので、コピペする場合は引数名を決めること。

とりあえずgccコンパイルする。(最近はAmazon Correttoをつかっているので。)

gcc -I/usr/lib/jvm/java-11-amazon-corretto/include -I/usr/lib/jvm/java-11-amazon-corretto/include/linux sample.c -shared -o libSample.so

このとき-sharedを指定して、-oで共有オブジェクト.soとしてコンパイルする必要がある。(そりゃそうだ)

あとは、Javaコードからsoをロードする必要がある。とりあえずはstaticブロックに追加して再コンパイルする。

public class Sample {
  static {
    System.loadLibrary("Sample");
  }
  public static native int readValue();
}

で、これは普通に呼び出せる。

public static class Main {
  public static void main(String[] args) {
    System.out.printf("readValue: %d\n", Sample.readValue());
  }
}

上記のコードをMain.javaとすると以下でコンパイルする。

javac Main.java

soをロードするにはjava.library.pathをパスを指定する必要があるので実行は以下。

java -Djava.library.path=. Main

実行結果は以下となる。

readValue: 13

これで最低限のJNIが呼び出せるようになった。

ちなみにWindowsの場合は.soが.dllになったりするのでコンパイルオプションなど取り扱いが変わる。