torutkのブログ

ソフトウェア・エンジニアのブログ

JNRを使ってJavaからネイティブアクセス

先日のJJUGナイトセミナー(JJUGナイトセミナー JVM特集に参加して #JJUG - torutkのブログ に参加メモ)で紹介のあったJNR(Java Native Runtime)を実際に使ってみようと試行錯誤した内容を書きます。

環境は、Windows 7 64bit版、JDK 8u60です。

JNRライブラリの入手

まず、JNRはGitHubの次のURLでホストされています。
https://github.com/jnr
ここには多数のプロジェクトがありますが、必要なものはjnr-ffi、jffiで、あとは必要に応じて追加すればいいようです。
ここで、githubからダウンロードできるのはソースファイルなので、mavenリポジトリからビルド済みのライブラリ、Javadoc、ソースの3点セットをダウンロードしておきます。

jnrプロジェクトのmavenリポジトリトップディレクトリは次のURLです。
http://central.maven.org/maven2/com/github/jnr/

jnr-ffi

ここから、jnr-ffiディレクトリへ降りて、最新バージョン番号のディレクトリ(本日時点では2.0.3)に降りると、3点セットが存在します。
http://central.maven.org/maven2/com/github/jnr/jnr-ffi/2.0.3/

また、pomファイルの内容を見て、コンパイルおよび実行時(compile、runtime)に必要な依存ライブラリを調べておきます。

  • compile
    • jffi 1.2.9
    • asm
    • asm-commons
    • asm-analysis
    • asm-tree
    • asm-util
    • jnr-x86asm
  • runtime
    • jffi 1.2.9
jffi

jnrプロジェクトのmavenリポジトリトップディレクトリから、jffiディレクトリへ降りて、最新バージョン番号のディレクトリ(本日時点では1.2.9)に降りると、3点セットが存在します。
http://central.maven.org/maven2/com/github/jnr/jffi/1.2.9/

  • jffi-1.2.9.jar
  • jffi-1.2.9-javadoc.jar
  • jffi-1.2.9-sources.jar

また、3点セット以外に、jffi-1.2.9-native.jarというのがあります。必要そうに見えるのでこれも取得しておきます。

  • jffi-1.2.9-native.jar

中を調べると、各OS用のネイティブライブラリが含まれています(Windows 64bit用jffi-1.2.dll、など)。

また、pomファイルの内容を見て、実行時(runtime)に必要な依存ライブラリを調べておきます。
→ なし

asm関係

ASMJavaバイトコード操作ツールです。次のダウンロードページからダウンロードします。
http://forge.ow2.org/project/showfiles.php?group_id=23&release_id=5660

  • asm-5.0.4-bin.zip

この中を見ると、asm-5.0.4.jar, asm-analysis-5.0.4.jar, asm-commons-5.0.4.jar, asm-parent-5.0.4.jar, asm-tree-5.0.4.jar, asm-util-5.0.4.jar, asm-xml-5.0.4.jarが入っています。また、asm-all-5.0.4.jarという個別の内容をひとまとめにしたものもあります。
今回は、asm-all-5.0.4.jarを使ってみます。

簡単なサンプル

JJUGナイトセミナーのセッションでは、Linuxglibc(libc.so)を使ったgetpid関数呼び出しのサンプルを紹介していました。
ここでは、Windowsマシン上で動かすので、WindowsのWin32APIに含まれるプロセスID取得関数GetCurrentProcessId呼び出しのサンプルを作成し実行します。

Win32 APIのGetCurrentProcessIdの定義は次です。

DWORD GetCurrentProcessId(VOID);

これを、JNRを使うJavaコードから呼び出します。

package hellojnr;

import jnr.ffi.LibraryLoader;

public class HelloJnr {

    public interface Kernel32 {
        long GetCurrentProcessId();
    }

    public static void main(String[] args) {
        Kernel32 kernel32 = LibraryLoader.create(Kernel32.class).load("Kernel32");
        long pid = kernel32.GetCurrentProcessId();
        System.out.println("My process id is " + pid);
    }
}

まずは、ネイティブコードの関数に対応するメソッドを宣言したインタフェースを定義します。上の例ではKernel32インタフェースとします。(ネスト型にしていますが、独立した型でも構いません。というかその方が普通かもしれません。)
ネイティブ関数の名前、引数、戻り値に対応するメソッドをインタフェースに定義します。

メソッド名は、大文字/小文字を一致させる必要があります*1

DWORDはWindows 32bit/64bit版ともに符号無し32bit整数(typedef unsigned long DWORD)です。Javaでは符号無し32bit整数型を収容するには符号有り64bit整数型のlongに対応させるのがよさそうです(要確認)。

GetCurrentProcessIdは、Kernel32.dllで定義される関数なので、ライブラリ名"Kernel32"を指定してjnr.ffi.LibraryLoaderクラスを使ってロードします。Windowsの場合は、呼び出すDLLのファイル名から拡張子.dllを除いた名前を指定します。DLLは環境変数PATHの通っているディレクトリにあれば、名前だけの指定でいいようです。

実行時のライブラリ

このサンプルコードの実行時に指定するライブラリは次です。

  • jnr-ffi-2.0.3.jar
  • jffi-1.2.9.jar
  • jffi-1.2.9-native.jar
  • asm-all-5.0.4.jar

Windows APIの関数名について

プログラマーにとって、Windows API(Win32 API)の代表格といえばMessageBoxです。
ところが、実はMessageBoxという名前の関数は実在せず、MessageBoxAもしくはMessageBoxWという名前で2つの関数が実在します。違いは、引数の文字/文字列の型にあります。
MessageBoxAは、文字/文字列をマルチバイト(1つの文字を1バイトもしくは2バイトとして扱い、文字を扱う型は1バイト(char型))で受け取ります。

MessageBoxWは、文字/文字列をユニコードUTF-16、文字を扱う型はwchar_t型)で受け取ります。
Visual C++プログラミング上ではMessageBoxというマクロ関数が用意されているので、プログラマーはMessageBoxAとMessageBoxWを明示的に使い分ける必要はなく、UNICODEデファインの有無によりMessageBoxマクロがMessageBoxAかMessageBoxWに置き換わります。実際、User32.dllの公開シンボルをVisual C++付属ツールdumpbinで調べると、MessageBoxについては次の関数が定義されていました。

C:\Windows\System32>dumpbin /exports user32.dll | more
     :(略)
2043 212 000712B8 MessageBoxA
2044 213 00071370 MessageBoxExA
2045 214 00071394 MessageBoxExW
2046 215 00071668 MessageBoxIndirectA
2047 216 00071874 MessageBoxIndirectW
2048 217 0007148C MessageBoxTimeoutA
2049 218 000713B8 MessageBoxTimeoutW
2050 219 00071314 MessageBoxW
     :(略)

JNRで呼び出し関数を定義するときは、マクロは存在しないので、明示的にMessageBoxAかMessageBoxWを使う必要があります。MessageBoxを使ってしまうとUnsatisfiedLinkErrorがスローされます。

その他、STDCALL規約やら、文字コードエンコーディングなどの指定要否も確認する必要があります。

Windows APIで文字列に日本語を入れるとき

JNRでは、C言語のchar* をJavaのStringに対応させる設計となっているようです。
日本語を入れるときは、引数にアノテーションエンコーディングを指定します。
マルチバイト文字版(関数名の末尾がA)では、日本語はCP932(シフトJIS系)になるので、アノテーションではJavaの指定であるWindows-31jを使っています。

import jnr.ffi.annotations.Encoding;
  :
    public interface User32 {
        int MessageBoxA(
            int hwnd,
            @Encoding("Windows-31j") String text, 
            @Encoding("Windows-31j") String caption,
            int type
        );
    }

*1:最初、getCurrentProcessIdとJava風の命名ルールで先頭を小文字で定義したところ、java.lang.UnsatisfiedLinkErrorが発生しました。