JavaからC言語の関数を呼び出そう

ふぃぃ〜.やっと卒業研究終わったぞー!!イエス!!
じゃが,相変わらず教授様に仕事を頼まれて忙しいにゃー.
やっとゲーム作れると思ったのにぃいー!!!きぃぃー!!(`△´)/

結局,日々のグチからスタートしてしまったプログラミング日記.
毎回毎回グチってる気がする・・・
傍目から見ると社会不適合者に見えてしまうじゃろうか・・・


さて,卒業研究中にJavaを使う機会があった.そのとき,JavaからC言語の関数を呼ぶ出す方法ないかなーと思っていたら,あるんですね.ちゃんと.しかも標準で.でも,説明が英語だったんだよね.(ちゃんと探せば日本語のもあったかも・・・)
Javaから他の言語の関数を呼び出すことを,JNI(Java Native Interface)と言うらしいです.(ん?でもインタフェースっていうくらいだから機能のことではないかも?)

http://java.sun.com/docs/books/tutorial/native1.1/stepbystep/index.html

1回目に見つけたのは,古いバージョンのJDK用のヤツだったんだけど,新しいヤツはなんだか簡単になってるね.スタブとか作らなくていいし.忘れないうちに手順をメモっておこう.φ(-_- )
概要は,C言語で作成した関数群をDLL(Dynamic Link Library)として作成して,このDLL内部の関数をJavaアプリケーション側から呼び出すって感じです.というわけで,DLLを呼び出すためのクラスと,DLLを作成しなくてはなりません.


1.普通にアプリケーション作成

とりあえず,mainを含むエントリクラスを作ろう. JniNativeクラスはC言語の関数を呼び出すためのクラスで,この後作成します.この段階では,本当に普通のアプリケーションのプログラムと変わりないね.

JniDemo.java
class JniDemo
{
  public static void main ( String[] args )
  {
    JniNative jn = new JniNative ();
  
    //普通にメソッドを呼び出す
    jn.nativeMethod ();
  }
}

2.C言語の関数を呼び出すためのクラスを作成する

次に,JNIのポイントとなる,「他の言語で記述した関数を呼び出す」ためのクラスをつくります.「他の言語」とは言ったけど,現状でサポートしているのはC言語だけみたい…
ここで,ポイントとなるのは,static{}で囲った部分と,native がついてるメソッドです.static{}囲んだ部分はプログラムの開始時に実行されます.System.loadLibrary ( "NativeDll" ); によって「NativeDll.dll」ファイルがリンクされることになります.このとき.dllって書かなくてもいいんですね.他のプラットフォームではDLLじゃない場合があるからでしょうねぇ.(ていうか Dynamic Link Library って呼ぶのはWindowsだけ? )
そいで,native がついてるメソッドが「ネイティブメソッド」として呼び出されるものです.呼び出し方は,上の JniDemo.java に書いたみたいに,普通のJavaのクラスのメソッドして呼び出すだけです.違いは,メソッドの本体がこのクラスにあるのではなく,C言語で記述したDLLの中にあるってだけ.うーん,こうなるとまるで,JniNativeクラスはクラスというよりインタフェースみたいだにゃー・・・
Σ( ̄ロ ̄;)はっ!? ひょっとして,これが「ネイティブ・メソッド・インタフェース」なのか? そうなのか!?
JniNative.java
class JniNative
{
  // このメソッドの本体はC言語で記述します.
  public native void nativeMethod ();

  // これでプログラム開始時に NativeDll.dll をリンクします.
  static 
  {
    System.loadLibrary ( "NativeDll" );
  }
}

3.DLL本体を作ろう

いよいよ,DLL本体を作ります.まずはDLL用のヘッダファイルを作ったりしないと行けないんですが,これもヘッダファイル作成のツールが標準で用意されています.んまぁ,親切♪ とりあえず,さっきの JniNative.java をコンパイルして JniNative.class を作成しておきます.
> javac JniNative.java
次に,この.classファイルから,DLL用のヘッダファイルを作成します.このとき使用するツールが javah です.また,指定するファイルは .javaファイルではないので .javaは付けちゃダメです.
> javah JniNative
javahを実行すると,次のようなC言語用のヘッダファイル(JniNative.h)が作成されます.
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class JniNative */

#ifndef _Included_JniNative
#define _Included_JniNative
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     JnikNative
 * Method:    displayMessage
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_JniNative_nativeMethod
  (JNIEnv *, jobject);

#ifdef __cplusplus
}
#endif
#endif
後は,このヘッダファイルに基づいて DLL を作成すればOKです.今回はVC6.0で作りました.さて,このときメソッドに変化がおきているのに気をつけましょう.JavaとC言語間で変数やオブジェクトの扱いが異なるので,微妙に変わってます.まず,Javaクラスのフィールド(メンバ変数)等にアクセスできるように,引数の無かったメソッドでも引数が2つ増やされています.次に,メソッドの名前も変わってます.変更後の命名規則は
Java_クラス名_メソッド名
となります.(…と書かれてますがこの限りでは無いです.)


でわ,C言語でネイティブメソッド本体を記述します.今回は,ただコンソールに文字を表示するだけの簡単なヤツ.
#include <jni.h>
#include "JniNative.h"
#include <stdio.h>

JNIEXPORT void JNICALL Java_JniNative_nativeMethod
  (JNIEnv *env, jobject obj)
{
  printf( "JNIのテスト\n同情よりも暇をくれ\n" );
}
さて,Jniを使用する場合は jni.h をインクルードしておかなくてはならないのですが,このファイルや,jni.h内部で呼び出される他のヘッダファイルなどは,JDKをインストールしたディレクトリの中の include ディレクトリの中に一式入ってます.
あとは,これをコンパイルして NativeDll.dll を作成しまっす.

4.実行しよう

もう,後はまだコンパイルしていない.javaファイルをコンパイルするなどして,実行するだけです.
> javac *.java
現在のカレントディレクトリが,CurrentDir という名前だとすると,実行に必要なファイル構成は次のようになっているハズです.
CurrentDir
  +- JniDemo.class
  +- JniNative.class
  +- NativeDll.dll
いよいよ実行!!
> java JniDemo

JNIのテスト
同情よりも暇をくれ
むぅーん.以外と簡単だったね.すごいねSUN!(●^o^●)



パッケージ化してJNIを使う

とまぁ,以外と簡単だったJNIだったんだけど,これがパッケージ化するとうまくいきやがらねぇ!!(早くもキレ気味.Ca不足か?)
Nao的には,
CurrentDir
  +- JniDemo.class
  +- PackageDir
      +- JniNative.class
      +- NativeDll.dll
といった環境で使いたかったわけさ.dllやそれを呼び出すクラスなんかは自分で設計してパッケージ化(上の例ではPackageDir)してしまい,そのクラス(あるいはパッケージ)を他の人が使用してアプリケーション(上の例ではJniDemo.class)とかを作るっつーのを夢見ていたワケさ.

とりあえずやってみよう

…で,パッケージ化するに当たり,JniDemo.java,JniNative.java をちょこっと変更します.

JniDemo.java
import PackageDir.JniNative ;

class JniDemo
{
  public static void main ( String[] args )
  {
    JniNative jn = new JniNative ();
  
    //普通にメソッドを呼び出す
    jn.nativeMethod ();
  }
}
JniNative.java
package PackageDir ;

class JniNative
{
  // このメソッドの本体はC言語で記述します.
  public native void nativeMethod ();

  // これでプログラム開始時に NativeDll.dll をリンクします.
  static 
  {
    System.loadLibrary ( "NativeDll" );
  }
}
そして,コンパイルして,次のディレクトリ構成にして実行します.
CurrentDir
  +- JniDemo.class
  +- PackageDir
      +- JniNative.class
      +- NativeDll.dll
> java JniDemo

Exception in thread "main" java.lang.UnsatisfiedLinkError: no NativeDll in java.library.path
ぐっ… やっぱりダメだったにゃー.WindowsだとDLLをプログラムを起動するディレクトリか,SYSTEMディレクトリに置かないといけないからにゃー.

DLLを置く場所を変更してみよう

しょうが無いので,カレントディレクトリににDLLを置くことにした.
CurrentDir
  +- JniDemo.class
  +- NativeDll.dll ←ここに置くしかないかにゃー
  +- PackageDir
      +- JniNative.class
よーし,今度こそッ!!
> java JniDemo

Exception in thread "main" java.lang.UnsatisfiedLinkError: nativeMethod
なんでですのん?(TдT)
リファレンスによれば UnsatisfiedLinkError 例外が発生するのは・・・
「Java Virtual Machine が,native と宣言されたメソッドの適切なネイティブ言語の定義を見つけることができない場合にスローされます.」
ふぅうむ.やっぱりJNIがらみなわけ? 何が悪いの? パッケージ化する前はこんなこと無かったのににゃー.(;ω;)


あった!あったぜ!解決策!!

ぷはぁ( ̄σ ̄)=3 (酒)


・・・と,ヤケ酒に明け暮れるNaoに朗報がッ!!!
なんでも,javah の時に変換した命名規則が問題らしいっす.さっきは,
Java_クラス名_メソッド名
って書いてあるっていいましたが,あれはパッケージ化してない場合の命名規則らしく,パッケージ化した場合は次のようになります.
Java_パッケージ名_クラス名_メソッド名
がびーん.Σ( ̄ロ ̄;)
javah 自動ではやってくれないのね・・・うーん,惜しいぜ! というわけで,メソッドの名前を変更しておきます.
JNIEXPORT void JNICALL Java_PackageDir_JniNative_nativeMethod
さて,名前の変更後,もう一度 NativeDll.dll を作り直して,実行します.
CurrentDir
  +- JniDemo.class
  +- NativeDll.dll
  +- PackageDir
      +- JniNative.class
> java JniDemo

DLLのテスト
同情よりも暇をくれ
やったにゃー!! 成功!!


あぁ・・・でも,マジで暇欲しい・・・ゲームを作りたい.3Dグラフィックスも練習したい.ゲーム会社に就職したい・・・(やっぱり締めもグチなのね…)


<< Back to Diary...