2014年3月14日金曜日

JNI (Java Native Interface) (2) 最も簡単なコードを書いてみる

ここでは、最も簡単なコードを書いてみましょう。
世界が広がる「Hello World!」です。

ただ実行するだけでなく、出来るだけ簡単に build 出来る環境も作成しましょう。

流れ


Java と c や c++ の両方の経験があれば、難しい処理は出てきません。

Java だけしか経験が無い場合、少し戸惑うかもしれませんが、知らなかったり、わからない部分は c や c++ のコードを実行するのに必要な作業と思えばよいと思います。

まず、Javaのコードを書きます。
その際、メソッドに nativeキーワード を指定して、nativeメソッド宣言します。
Javaクラスにはnativeメソッドの本体は定義しないので注意してください。

javah 使って、nativeメソッド毎の関数プロトタイプが記述された ヘッダファイル を生成します。

その関数プロトタイプの記述に従って、c や cpp で本体の関数を記述します。

関数プロトタイプには、入力(仮引数) と 出力(戻り値) を定義するので、native関数はそれに従って記述する必要があります。

本体のnative関数は、プラットフォーム固有のコンパイラとリンカを使用してコンパイラします。ここは、プラットフォーム毎に手順が違うと思います。

JVMは、それぞれの関数プロトタイプに定義された呼び出しプロトコルを使って、nativeメソッドを実行します。

これが、一連の流れです。
ここでは、native メソッドのプログラムは C や C++ で考えることにします。

ネイティブコードへのインターフェース記述

Javaコード と native のコードを統合するときの課題は、Javaのnativeメソッドの仮引数を、既存のAPIの仮引数にマッピングすることです。

一般の解決法は以下の 2 つです。

  1. 従来のデータを Java で設計し、そのデータを扱えるうまいメソッドを定義する
  2. 既存のインターフェースを、自分の Java のnativeメソッド宣言に直接マップ
どちらも難しそうです。
後者は、Cの構造体をJavaのオブジェクトで置き換えるために適用できる方法ですが、後で詳しく見ていくことにします。


順番に見ていきましょう。


Javaコードの生成

Javaクラスに、nativeメソッドを定義します。
例えば以下の様な感じです。

public class NativeSampleMethod{
public native void theNativeMethod();
public vod javaMethod(){
theNativeMethod();
}
}




javac でクラスファイルを生成

IDEを使用していれば、ここは意識しないところですね。

簡単に以下の様にコマンドすれば出来ます。

% javac NativeSampleMethod.java

javahでインクルードするヘッダファイルを生成

nativeメソッドの宣言が入ったJavaクラスファイルを作成したら、対応する ANSI C関数プロトタイプを生成します。

native宣言の入ったすべてのクラスファイルで、javah を走らせます。

Javaクラスファイルの場所は、環境変数 CLASSPATH、javahのオプション -classpath で指定します。

% javah -jni NativeSampleMethod

生成されたヘッダファイルは、nativeメソッドを実装したソースファイルにインクルードが出来る。

ヘッダファイルの名前は、入力したクラスの完全修飾された名前で決まります。つまり、パッケージ名までを含んだ一意な名前になります。


例えば以下のようになります。

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class jp_sprix_jni_NativeSampleMathod */
#ifndef _Included_jp_sprix_jni_NativeSampleMathod
#define _Included_jp_sprix_jni_NativeSampleMathod
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: jp_sprix_jni_NativeSampleMathod
* Method: theNativeMethod
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_jp_sprix_jni_NativeSampleMathod_theNativeMethod
(JNIEnv *, jobject);
#ifdef __cplusplus
}
#endif
#endif

これはjp.sprix.jniパッケージの配下にjavaファイルを置いている場合です。

次に、nativeコードを記述します。

#include "jp_sprix_jni_NativeSampleMathod.h"
#include <iostream>
#include <cv.h>
#include <core/core.hpp>
#include <highgui/highgui.hpp>
using namespace cv;
using namespace std;
JNIEXPORT void JNICALL
Java_jp_sprix_jni_NativeSampleMathod_theNativeMethod(JNIEnv* env, jobject thisObj){
cout << "test" << endl;
}

以下の3つの制限に従っていることに注意しましょう。

  1. nativeメソッドの実装関数は、最初の引数が JNIEnvへのポインタ、2番目がオブジェクト参照
  2. 入力引数の型はJNIが定義する
  3. 戻り値の型はJNIが定義する
  4. 関数プロトタイプが含む ヘッダファイルをインクルードしなければならない

ライブラリのビルド

プラットフォーム固有になる部分です。

Linuxであれば .so 、Windowsであれば .dll をビルドします。


  1. nativeコードをコンパイルし、jni.hをインクルードするためにふさわしい場所はすべて指定
  2. nativeライブラリをビルド


今回は、ant contrib を使用して、ビルドしましょう。
詳細な方法は、以前のブログに書いています。

http://masabloggers.blogspot.jp/2013/05/jni-javacpp.html



今回必要になるbuild.xmlは以下です。

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- WARNING: Eclipse auto-generated file.
Any modifications will be overwritten.
To include a user specific buildfile here, simply create one in the same
directory with the processing instruction <?eclipse.ant.import?>
as the first entry and export the buildfile again. -->
<project basedir="." default="build" name="jni">
<property environment="env" />
<property name="ECLIPSE_HOME" value="../../eclipse" />
<property name="debuglevel" value="source,lines,vars" />
<property name="target" value="1.7" />
<property name="source" value="1.7" />
<path id="jni.classpath">
<pathelement location="bin" />
</path>
<target name="init">
<mkdir dir="bin" />
<copy includeemptydirs="false" todir="bin">
<fileset dir="src">
<exclude name="**/*.java" />
</fileset>
</copy>
</target>
<target name="clean">
<delete dir="bin" />
</target>
<target depends="clean" name="cleanall" />
<target depends="build-subprojects,build-project,makeh" name="build" />
<target name="build-subprojects" />
<target depends="init" name="build-project">
<echo message="${ant.project.name}: ${ant.file}" />
<javac debug="true" debuglevel="${debuglevel}" destdir="bin" includeantruntime="false" source="${source}" target="${target}">
<src path="src" />
<classpath refid="jni.classpath" />
</javac>
</target>
<property name="h_dir" location="/root/git/rjni/jni/jnih" />
<target name="makeh">
<taskdef resource="cpptasks.tasks" />
<javah classpath="/root/git/rjni/jni/bin" class="jp.sprix.jni.NativeSampleMathod" destdir="${h_dir}" />
<cc link="shared" outfile="/root/git/rjni/jni/dist/NativeSampleMathod" objdir="${h_dir}">
<includepath location="${env.JAVA_HOME}/include" />
<includepath location="${env.JAVA_HOME}/include/linux" />
<includepath location="/usr/local/include/opencv" />
<includepath location="/usr/local/include/opencv2" />
<fileset dir="${h_dir}" includes="NativeSampleMathod.cpp" />
<libset dir="/usr/local/lib" libs="opencv_features2d,opencv_highgui,opencv_core,opencv_imgproc,opencv_objdetect,opencv_contrib,opencv_flann" />
<libset libs="stdc++" />
</cc>
</target>
<target description="Build all projects which reference this project. Useful to propagate changes." name="build-refprojects" />
<target description="copy Eclipse compiler jars to ant lib directory" name="init-eclipse-compiler">
<copy todir="${ant.library.dir}">
<fileset dir="${ECLIPSE_HOME}/plugins" includes="org.eclipse.jdt.core_*.jar" />
</copy>
<unzip dest="${ant.library.dir}">
<patternset includes="jdtCompilerAdapter.jar" />
<fileset dir="${ECLIPSE_HOME}/plugins" includes="org.eclipse.jdt.core_*.jar" />
</unzip>
</target>
<target description="compile project with Eclipse compiler" name="build-eclipse-compiler">
<property name="build.compiler" value="org.eclipse.jdt.core.JDTCompilerAdapter" />
<antcall target="build" />
</target>
</project>
view raw build_tuto.xml hosted with ❤ by GitHub

この構成に必要となる構造は以下です。

src -  javaパッケージ、javaソースコード、ANSI 関数プロトタイプヘッダファイル格納
dist - 動的ライブラリの格納ディレクトリ
jnih - ANSI 関数プロトタイプヘッダファイル、native実装ファイル
build.xml

build.xmlファイルを読めば、大体のディレクトリ構造はわかると思います。
また、gccコンパイルに必要なインクルードディレクトリやリンカーの設定もしてあります。

これでbuildしたxmlファイルは、実行時に以下のjava オプションでnativeライブラリの場所を指定すます。

LD_LIBRARY_PATH

以上が、Hello World と ビルド方法についてです。


次は、起きやすい問題やその対処について書こうと思います。

0 件のコメント:

コメントを投稿