🌉

JNIを使う(1) とりあえずビルド

2024/12/02に公開

この記事は「大分高専 Advent Calendar 2024」2日目の記事です。

はじめに

JNIの日本語情報が少なかったので、本シリーズで自分なりに使い方をまとめてみようと思います。
今回はとりあえずビルドということで、コマンドライン上で、手動でJNIを用いたプログラムをビルドします。

実行環境

Ubuntu 24.04.1 LTS
openjdk 21.0.5 2024-10-15
gcc (Ubuntu 13.2.0-23ubuntu4) 13.2.0

Java側

ディレクトリ/パッケージ構造

JNIの挙動をわかりやすくするために、今回はパッケージ名をちゃんとつけることにしましょう。今回は試しにcom.example.XXXのようなパッケージ構成にすることにします。

ネイティブメソッドの定義

まず、Java側でネイティブメソッドを定義します。メソッド名の前にnativeキーワードがついており、メソッドの本体を与えられていないものがネイティブメソッドとして認識され、その実体がネイティブコードによって与えられます。

JniClass.java
package com.example.jni;

public class JniClass {
 public native void theNativeMethod();  
}

このコードでは、theNativeMethodがネイティブメソッドになります。

ネイティブライブラリ読み込み処理の記述

次に、あとで作成するネイティブライブラリを読み込む処理と、先ほど定義したネイティブメソッドを呼び出す処理を記述しましょう。

Main.java
package com.example.jni;

public class Main {
 static{
     System.loadLibrary("Native");
 }

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

読み込みはSystem.loadLibrary関数によって実行されます。引数内に"Native"と指定していますが、この場合、UNIXではlibNative.so, WindowsではNative.dllを指定するライブラリ用ディレクトリから探索されます。つまり、この引数はプラットフォームに依存しないライブラリ名を指定するものであり、プレフィックスやサフィックスは自動的に付加されるので、書いてはいけません
ここでは読み込みをstaticブロック内で行っていますが、必ずしも静的に行わなければいけないわけではなく、動的に読み込ませることもできます。

ヘッダファイルの生成

次に、ネイティブメソッドを定義したクラスから、C/C++用のヘッダファイルを生成します。
ここで生成するヘッダファイルは必須というわけではありませんが、ヘッダファイルを生成させると自動的にネイティブメソッドのシグネチャと一致する関数プロトタイプを宣言してくれるため便利です。

ネイティブメソッドを定義したクラスから、C/C++用のヘッダファイルを生成するには、以下のコマンド構文を使います。

javac <target>.java -h <directory>

今回は、先程のJniClass.javaファイルからヘッダファイルを生成します。とりあえず、nativeディレクトリにヘッダファイルを格納しました。

javac java/com/example/jni/JniClass.java -h native/

生成されたヘッダファイルを以下に示します。

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

#ifndef _Included_com_example_jni_JniClass
#define _Included_com_example_jni_JniClass
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     com_example_jni_JniClass
 * Method:    theNativeMethod
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_com_example_jni_JniClass_theNativeMethod
  (JNIEnv *, jobject);

#ifdef __cplusplus
}
#endif
#endif

コードに書かれている通り、このヘッダファイルは自動作成されるものなので編集するべきではないです。
生成された関数プロトタイプを見てみましょう。定義したネイティブメソッドが、以下の形に変換されていることがわかります。

JNIEXPORT void JNICALL Java_com_example_jni_JniClass_theNativeMethod
  (JNIEnv *, jobject);

java側のネイティブメソッドへの完全修飾パスがスネークケースで表現されていることがわかります。
また、引数については、theNativeMethodには引数がなかったことから推察できる通り、これはJNIが与えるものです。JNIのメソッドは、少なくとも先頭に以下の2つの引数をとります。

  1. JNIEnv*: Java実行環境を表現するJNIEnv構造体のポインタ。
  2. jobject: メソッドが所属するjavaクラスのthisへの参照(つまり、インスタンスへの参照)。

JNIが定義するネイティブ型については、後の記事で詳しく説明するつもりですので、とりあえず上の2つが何を指すのかだけは確認しておきましょう。

C側

今回は、ネイティブコードとしてCを使います。
先程生成されたヘッダファイルをインクルードして、関数プロトタイプを実装します。

com_example_jni_HelloWorld.c
#include <stdio.h>
#include "com_example_jni_JniClass.h"

JNIEXPORT void JNICALL Java_com_example_jni_JniClass_theNativeMethod
(JNIEnv *, jobject){
 printf("Hello JNI World\n");
}

今回は、単純に標準出力を行う処理を記述しました。

ライブラリのビルド

今回は、Linuxでネイティブコードを動かす想定なので、共有オブジェクトライブラリ(.so)を作成します。Windowsの場合はdllファイルをビルドすることになるでしょう。

.soファイル作成のため、まずオブジェクトファイル(.o)を作成します。jni.h等への参照が要るため、$JAVA_HOME/include$JAVA_HOME/include/linuxをインクルードパスに追加しましょう。

 gcc -c -I$JAVA_HOME/include -I$JAVA_HOME/include/linux com_example_jni_HelloWorld.c -o com_example_jni_HelloWorld.o

オブジェクトファイルが生成できたら、それを用いて.soファイルを作成しましょう。先述の通り、libNative.soというファイル名にします。

gcc -shared -fPIC -o libNative.so com_example_jni_HelloWorld.o -lc

実行

長い道のりでしたが、ついに準備が整いました。先ほど生成した.soファイルが含まれるディレクトリをLD_LIBRARY_PATH環境変数またはjavaコマンド実行時のjava.library.path変数に指定して、実行しましょう!

java -Djava.library.path=../../../../native com.example.jni.Main
実行結果
Hello JNI World

Cで記述したネイティブコードを、JVMから呼び出すことが出来ました🎉

よくあるミス

ここで、UnsatisfiedLinkErrorを吐かれた場合、次の2つのどちらかの原因によるエラーである場合が多いです。

  1. ネイティブライブラリへのパスが適切に参照できていない
  2. 関数シグネチャが間違っている

1の場合、パスを指定している箇所を見直すべきです。2の場合は、ネイティブメソッド宣言を変更したのに、関数プロトタイプを再生成していないことに起因するため、ヘッダファイルを再生成するべきです。

おわりに

今回は、とりあえずJavaのネイティブメソッドをC言語で実装し、コマンドライン上で手動ビルドしました。やってみて思われたでしょうが、この手順は非常に面倒で、これからJNIを使う上で毎回この作業をするのは非常に非効率的なので、次回はGradleを使ったビルドを紹介しようと思います。

Discussion