意外と、JNIに関する記事へのアクセスが多いのを受けて、もう少し具体的なJNIのサンプルコードを作成してみました。
サンプルは次の2つです。
- ClipCursor(マウスカーソルの移動制限)
- ShellExecute(関連付けられたファイルの実行)
私の知っている限り、この2つはPureJavaでは実行できないと思います。
後者は、cmd.exeのプロセスを起動すればできるのかもしれないですが、それも結局はJNIを使わないだけでWindows依存のコードとなってしまいます。
Win32 APIを使って、ネイティブメソッドでこれらを実現したいと思います。
ソースコードは、こちらになります。
JNIを使ったWin32 APIを実行するサンプル
ClipCursor(マウスカーソルの移動制限)
MSDNリファレンス:ClipCursor
マウスカーソル( マウスポインタ)の移動可能な範囲を、指定された長方形の内側に制限します。以後、マウスを動かしたり、SetCursorPos 関数を呼び出した結果、マウスカーソルが長方形より外側になると、システムは座標を自動的に調整して、この長方形の中にとどまらせます。
BOOL ClipCursor(
CONST RECT *lpRect // 画面の座標
);
このAPIを使用します。
Java側からRECT構造体を作って渡すことはできないので、4つの値(left, top, right, bottom)を渡すようにしました。
public native static int clipCursor(int left, int top, int right, int bottom);
JavaにはRECT構造体に相当するクラス、java.awt.Rectangleがあります。当初はそれをパラメータとして渡そうと考えたのですが、ネイティブ(C言語)のロジックで、Javaオブジェクトにアクセスするのは、手間なので分解して渡すことにしました。
このネイティブメソッドを記述したクラスから、JNIヘッダファイルを作成します。これはjavahを実行するだけです。
>javah -classpath classes -d cpp JNISample
これでcppディレクトリに、JNISample.hが作成されました。
あとは、JNISample.hのプロトタイプ宣言に合うように、C++ソースコードを記述します。
#include "JNISample.h"
#include
JNIEXPORT jint JNICALL Java_JNISample_clipCursor
(JNIEnv *env, jclass obj, jint left, jint top, jint right, jint bottom){
RECT r;
if (0 < left){
r.left = left;
r.top = top;
r.right = right;
r.bottom = bottom;
return ClipCursor(&r);
} else {
return ClipCursor(NULL);
}
}
leftが0未満の場合は、ClipCursorにNULLを設定し、マウスカーソル制限を解除するという作りにしてあります。
これでコンパイルします。ここで使用しているのはBorland C++Compilerです。他のコンパイラでも基本は変わらないと思います。
cppディレクトリに移動して、次のようなコマンドからdllを作成できました。
>bcc32 -WD -I%JAVA_HOME%\include -I%JAVA_HOME%\include\win32 JNISample.cpp
※%JAVA_HOME%は、JDKのインストールディレクトリです。(JREではなくJDKです)
Javaソースコードから、ネイティブライブラリをロードするのは、次のように記述します。
static {
//JNIモジュールのロード
System.loadLibrary("jnisample");
}
ネイティブライブラリのロードは、JVMが起動中に一度だけ行えばよいので、このようにstaticイニシャライザで記述するのが一般的です。
Javaプログラムを実行し、確かにカーソル移動が制限されることを確認できました。
ShellExecute(関連付けられたファイルの実行)
次は、ShellExecuteです。スタートメニューの「ファイル名を指定して実行」に入力したのと同じ効果が得られます。
例えば、"notepad"と入力すると、メモ帳が起動します。これはパスが通っている実行ファイルを起動しています。
また、"http://www.yahoo.co.jp/"と入力すると、関連付けられたブラウザが起動し、Yahoo!のサイトが表示されます。
このように結構使い勝手が良い機能なのですが、PureJavaでは実現できません。(Java SE 6からは関連付けられたブラウザの起動などが可能になるようです)
さて呼び出すWin32 APIは下記の仕様になっています。
MSDNリファレンス:ShellExecute
指定されたファイルに対して、指定された操作を実行します。
HINSTANCE ShellExecute(
HWND hwnd, // 親ウィンドウのハンドル
LPCTSTR lpVerb, // 操作
LPCTSTR lpFile, // 操作対象のファイル
LPCTSTR lpParameters, // 操作のパラメータ
LPCTSTR lpDirectory, // 既定のディレクトリ
INT nShowCmd // 表示状態
);
今回は、ShellExecuteを完全実装せずに、簡易的なサポートとするために、lpVerbを"open"固定にして、実行時にlpFileに相当する文字列だけを渡すように実装します。基本的な流れは前述のClipCursorと同じです。
Java側のネイティブメソッドインターフェースはこのようにしました。
public native static int shellExecute(String file);
C++言語で記述したネイティブメソッドは次のようにしてみました。
JNIEXPORT jint JNICALL Java_JNISample_shellExecute
(JNIEnv *env, jclass obj, jstring file)
{
const char* utf8_file = env->GetStringUTFChars(file, NULL);
long ret = (long)ShellExecute(NULL, "open", utf8_file,
NULL, NULL, SW_SHOWNORMAL);
env->ReleaseStringUTFChars(file, utf8_file);
return (jint)ret;
}
新しい処理として、GetStringUTFChars
と、ReleaseStringUTFChars
が記述されています。これはJavaのStringオブジェクトから、C言語で使用するchar配列へのポインタを取得するメソッドです。
GetStringUTFCharsでは、JavaのStringオブジェクトのポインタjstringから、UTF8の文字列が得られます。
isCopyフラグが、JNI_TRUEのときは、必ずReleaseStringUTFCharsを実行します。
これは、JavaのStringオブジェクトで使用している領域を、連続した配列領域として一時的にコピーした事を示します。
常にReleaseStringUTFCharsで開放してあげないと、メモリリークになります。
これは決まりごとと思っても問題ないです。isCopyフラグをチェックして必ず開放するようにしましょう。
余談ですが、私は当初isCopyフラグの意味が分からず、フラグをチェックせずに常に開放するロジックを記述していました。しかし、(たまたま運よくですが)、今のところ問題が起きたことはありませんでした。仕様どおり記述するとしたら、isCopyフラグを必ずチェックしましょう。
以前のサンプルコードでは、以下のように、isCopyフラグを判断してReleaseStringUTFCharsを行うかどうかを判別していました。
jboolean isCopy;
const char* utf8_file = env->GetStringUTFChars(file, &isCopy);
...
if(isCopy == JNI_TRUE){
env->ReleaseStringUTFChars(file, utf8_file);
}
しかし、GetStringUTFCharsの後には、isCopyフラグの判断関係なく、常にReleaseStringUTFCharsすべきなようです。訂正いたします。naozoさんご指摘ありがとうございます。
GetStringUTFCharsに似たメソッドにGetStringCharsがあります。これはUnicode形式で文字列を取得するメソッドです。
UTF8の場合は、GetStringUTFCharsと、ReleaseStringUTFChars。
Unicodeの場合は、GetStringCharsと、ReleaseStringChars。
というわけです。
JNIについて詳しく書かれた貴重な一冊。
JNIプログラミング時に必携の本です。ASCII文字だけの受け渡しならば、これでOKですが、fileに全角文字が混ざっている場合には、このままでは機能しません。それは、ShellExecuteの引数で使用する文字列は、Shift_JISでなけらばならないからです。
fileがASCII文字だけの場合は、UTF-8とShift_JISはコードが対応するため、正常に動作します。
今回は簡略化のために、UTF-8で処理しています。次回補足として、Shift_JIS化した処理を説明したいと思います。
できあがったdllを使って実行してみると、"c:\"と指定して実行すると、エクスプローラでCドライブが表示されます。
"http://www.yahoo.co.jp/”と指定すると関連付けられたブラウザで、Yahoo!が表示されることが確認できました。
> isCopyフラグをチェックして必ず開放するようにしましょう。
とありますが、
3.2.4 Other JNI String Functions
http://java.sun.com/docs/books/jni/html/objtypes.html#5161
には、
The ReleaseString-Chars call is necessary whether GetStringChars has set *isCopy to JNI_TRUE or JNI_FALSE.
と記述されています。
ここではGetStringChars, ReleaseStringCharsの話になっているようですが、GetStringUTFChars, ReleaseStringUTFCharsでもisCopyは同じようにありますから、同じではないかと思えます。
isCopy
はチェックする必要はないのではないでしょうか。
Most often you pass NULL as the isCopy argument because you do not care whether the Java virtual machine returns a copy of the characters in the java.lang.String instance or a direct pointer to the original.
と書かれていますし、isCopyではNULLを指定しておいて、常に、ReleaseStringCharsまたはReleaseStringUTFCharsを呼び出す、
3.2.1 Converting to Native Strings
http://java.sun.com/docs/books/jni/html/objtypes.html#4013
にあるような感じで呼び出せば良いように思います。
ご指摘ありがとうございます。
確かに参照先の文書を読むと、おっしゃるとおりに思います。近いうちに修正いたします。
紹介している「JNI Java Native Interfaceプログラミング」を参考にすると、
第6章 配列と文字列>UTF-8文字列の処理 という箇所で、
とあり、私のようなif文でisCopyを判別してReleaseStringUTFCharsを呼び出しています。
紹介されている記事を読むと、JNI_FALSEで、ダイレクトポインタが返された場合に文字列を変更することもすべきではないようなので、書籍の方が誤っているように思いました。