LoginSignup
39
43

More than 5 years have passed since last update.

JNA (Java Native Access) パターン集

Last updated at Posted at 2018-07-14

JNAとは

  • C/C++で書かれた共有ライブラリ(いわゆる.dllとか.soとか)をJavaから呼ぶ方法の一つ
  • 従来のJNI (Java Native Interface) より手軽に使える
    • C/C++のコードを追加する必要がない
    • Pythonのctypesの感覚に近い(個人的な意見)

Java Native Access - Wikipedia
Overview - JNA API Documentation

Pythonのctypesについてはこちらに書きました。
Python: ctypesパターン集

新しい題材を考えるのも面倒なので サンプルの内容は全く同じです。

準備

以下のサイトからそれぞれ.jarを持ってきます。
https://mvnrepository.com/artifact/net.java.dev.jna/jna
https://mvnrepository.com/artifact/net.java.dev.jna/jna-platform

バージョンを選び、Filesの欄の「jar」をダウンロードします。
ダウンロードしたjarはclasspathに追加します。

Gradleをお使いの場合は、自分でjarをダウンロードせずに、build.gradleのdependenciesに2行追加します。

dependencies {
    // ...

    // 以下の2行を追加
    implementation 'net.java.dev.jna:jna:4.5.2'
    implementation 'net.java.dev.jna:jna-platform:4.5.2'

    // ...
}

パターン別の解法

Windows API (Win32API) を題材にして、様々な場合のパターンをまとめます。

基本形

Sleep 関数 - MSDN

1秒間寝るだけの単純なプログラムです。まずはここから。

JNISample.java
import com.sun.jna.Library;
import com.sun.jna.Native;

public class JNISample {
    public interface Kernel32 extends Library {
        Kernel32 INSTANCE = (Kernel32) Native.loadLibrary("kernel32", Kernel32.class);
        void Sleep(int dwMilliseconds);
    }

    public static void main(String[] args) {
        System.out.println("started");
        Kernel32.INSTANCE.Sleep(1000);
        System.out.println("finished");
    }
}

文字列を渡す

MessageBox関数を使ってメッセージボックスを表示してみます。

MessageBox 関数 - MSDN

JNISample.java
import com.sun.jna.Library;
import com.sun.jna.Native;
import com.sun.jna.Pointer;

public class JNISample {
    public interface User32 extends Library {
        User32 INSTANCE = (User32) Native.loadLibrary("user32", User32.class);
        // A/Wの区別がある場合はAを付ける
        int MessageBoxA(Pointer hWnd, String lpText, String lpCaption, int uType);
    }

    public static void main(String[] args) {
        User32.INSTANCE.MessageBoxA(null, "テスト", "タイトル", 0);
    }
}

また後で出てきますが、ウィンドウハンドルは64bit OSならば64bitの値を持つので、Pointer型の引数としています。

ソースコードをMS932 (Shift-JIS, CP932) で書いているならこれでも動きますが、UTF-8でソースコードを書いていると文字化けしてしまいます。
そこでUnicode版のAPIを使い、文字列引数をWString型にします。

JNISample.java
import com.sun.jna.Library;
import com.sun.jna.Native;
import com.sun.jna.Pointer;
import com.sun.jna.WString;

public class JNISample {
    public interface User32 extends Library {
        User32 INSTANCE = (User32) Native.loadLibrary("user32", User32.class);
        // A/Wの区別がある場合はWを付ける
        int MessageBoxW(Pointer hWnd, WString lpText, WString lpCaption, int uType);
    }

    public static void main(String[] args) {
        User32.INSTANCE.MessageBoxW(null, new WString("テスト"), new WString("タイトル"), 0);
    }
}

これでOK。
javac-encoding は適切に設定してください。(Eclipse使っていればあまり気にしなくてOK)
Windows APIに限らず、wchar_t *型などを受け取る関数があったら、WStringで渡しましょう。

以降、文字列の受け渡しはUnicodeベースで扱っていくことにします。

参照渡しでデータを受け取る

GetComputerName 関数 - MSDN

  1. 参照渡しによりバッファの必要サイズを取得
  2. バッファを確保
  3. 文字列として結果を取得
  4. 結果を表示

の流れです。以下のあたりがポイントでしょうか。

  • Unicode版の関数を使う(関数名の末尾にW)
  • int 型を参照渡ししたいときは IntByReference 型のオブジェクトを渡す
    • getValue() で値を取得できる
    • 同様に LongByReference なども存在する
  • 文字列バッファの引数は char[] 型にする
    • 配列の場合は何も考えなくても参照渡しになる
    • null を渡すとNULLポインタ (void *)0 を渡したことになる
  • 文字列バッファの String への変換は Native.toString() で行う
    • new String() でも変換できるように見えるが、ヌル終端として解釈されず、後ろにゴミが付く
JNISample.java
import com.sun.jna.Library;
import com.sun.jna.Native;
import com.sun.jna.ptr.IntByReference;

public class JNISample {
    public interface Kernel32 extends Library {
        Kernel32 INSTANCE = (Kernel32) Native.loadLibrary("kernel32", Kernel32.class);
        boolean GetComputerNameW(char[] lpBuffer, IntByReference lpnSize);
    }

    public static void main(String[] args) {
        IntByReference lenComputerName = new IntByReference();
        Kernel32.INSTANCE.GetComputerNameW(null, lenComputerName);
        char[] computerName = new char[lenComputerName.getValue()];
        Kernel32.INSTANCE.GetComputerNameW(computerName, lenComputerName);
        System.out.println(Native.toString(computerName));
    }
}
実行結果:コンピュータ名
taro-pc

配列は参照渡しになるので、IntByReferenceの代わりに配列を使う方法も可能です。

import com.sun.jna.Library;
import com.sun.jna.Native;

public class JNISample {
    public interface Kernel32 extends Library {
        Kernel32 INSTANCE = (Kernel32) Native.loadLibrary("kernel32", Kernel32.class);
        // lpnSizeの型を int[] にする
        boolean GetComputerNameW(char[] lpBuffer, int[] lpnSize);
    }

    public static void main(String[] args) {
        // 長さ1の配列を利用する
        int[] lenComputerName = new int[1];
        Kernel32.INSTANCE.GetComputerNameW(null, lenComputerName);
        char[] computerName = new char[lenComputerName[0]];
        Kernel32.INSTANCE.GetComputerNameW(computerName, lenComputerName);
        System.out.println(Native.toString(computerName));
    }
}

構造体

例1

GetCursorPos 関数 - MSDN

com.sun.jna.Structure を継承したクラスで構造体を定義します。

  • メンバ変数をpublicで列挙する
  • getFieldOrder() を実装してメモリ上の並び順を定義する
    • メンバ変数が過不足なく列挙されている必要がある
    • this.getClass().getFields() は順序が未定義1なため使えない

構造体(クラス)は引数で指定すると自動的に参照渡しになります。

JNISample.java
import java.util.Arrays;
import java.util.List;

import com.sun.jna.Library;
import com.sun.jna.Native;
import com.sun.jna.Structure;

public class JNISample {
    public interface User32 extends Library {
        User32 INSTANCE = (User32) Native.loadLibrary("user32", User32.class);
        boolean GetCursorPos(POINT lpPoint);
    }

    public static class POINT extends Structure {
        @Override
        protected List<String> getFieldOrder() {
            return Arrays.asList("X", "Y");
        }
        public int X;
        public int Y;
    }

    public static void main(String[] args) {
        POINT pt = new POINT();
        User32.INSTANCE.GetCursorPos(pt);
        System.out.println(String.format("x = %d, y = %d", pt.X, pt.Y));
    }
}
実行結果:マウスカーソルの位置
x = 340, y = 1061

例2

次は構造体の中に固定長のchar配列とか別の構造体が入ってきた時の話です。

FindFirstFile 関数 - MSDN
FindNextFile関数 - MSDN
FindCLose 関数 - MSDN

  • 構造体メンバとして配列型を指定すると、ポインタではなく値渡し相当になる
    • 引数と異なり、自動で参照渡し(ポインタ渡し)にはならない。参照渡しにしたいならPointer型とかByteBuffer型とかで。
    • すべての配列型メンバにインスタンスを代入するまで、構造体サイズは決まらない
    • つまり暗黙の初期値が存在しない(必ずインスタンスを代入しないといけない)
  • 構造体メンバとして構造体を指定すると、ポインタではなく値渡し相当になる
    • 引数と異なり自動で参照渡し(ポインタ渡し)にはならない
    • 暗黙の初期値はオールゼロ
  • 32bit OSで32bit、64bit OSで64bitのサイズを持つメンバはPointer型で定義する
    • ハンドルやポインタ型(UINT_PTRなども含む)
    • WPARAM/LPARAM
    • 実際の値を見たくなったら Pointer.nativeValue(ptr) で取れる
JNISample.java
import java.util.Arrays;
import java.util.List;

import com.sun.jna.Library;
import com.sun.jna.Native;
import com.sun.jna.Pointer;
import com.sun.jna.Structure;
import com.sun.jna.WString;

public class JNISample {
    public interface Kernel32 extends Library {
        Kernel32 INSTANCE = (Kernel32) Native.loadLibrary("kernel32", Kernel32.class);
        Pointer FindFirstFileW(WString lpFileName, WIN32_FIND_DATAW lpFindFileData);
        boolean FindNextFileW(Pointer hFindFile, WIN32_FIND_DATAW lpFindFileData);
        boolean FindClose(Pointer hFindFile);
    }

    public static final int MAX_PATH = 260;
    public static final Pointer INVALID_HANDLE_VALUE = new Pointer(-1);

    public static class FILETIME extends Structure {
        @Override
        protected List<String> getFieldOrder() {
            return Arrays.asList("dwLowDateTime", "dwHighDateTime");
        }
        public int dwLowDateTime;
        public int dwHighDateTime;
    }

    public static class WIN32_FIND_DATAW extends Structure {
        @Override
        protected List<String> getFieldOrder() {
            return Arrays.asList(
                    "dwFileAttributes", "ftCreationTime", "ftLastAccessTime", "ftLastWriteTime",
                    "nFileSizeHigh", "nFileSizeLow", "dwReserved0", "dwReserved1",
                    "cFileName", "cAlternateFileName", "dwFileType", "dwCreatorType", "wFinderFlags"
            );
        }
        public int      dwFileAttributes;
        public FILETIME ftCreationTime;
        public FILETIME ftLastAccessTime;
        public FILETIME ftLastWriteTime;
        public int      nFileSizeHigh;
        public int      nFileSizeLow;
        public int      dwReserved0;
        public int      dwReserved1;
        public char[]   cFileName          = new char[MAX_PATH];
        public char[]   cAlternateFileName = new char[14];
        public int      dwFileType;
        public int      dwCreatorType;
        public short    wFinderFlags;
    }

    public static void main(String[] args) {
        String pattern = "C:\\Windows\\*.exe";
        WIN32_FIND_DATAW findData = new WIN32_FIND_DATAW();
        Pointer hfind = Kernel32.INSTANCE.FindFirstFileW(new WString(pattern), findData);
        if (hfind != INVALID_HANDLE_VALUE) {
            do {
                System.out.println(Native.toString(findData.cFileName));
            } while (Kernel32.INSTANCE.FindNextFileW(hfind, findData));
        }
        Kernel32.INSTANCE.FindClose(hfind);
    }
}
実行結果:Windowsフォルダ直下にある.exeファイル一覧
bfsvc.exe
explorer.exe
HelpPane.exe
hh.exe
notepad.exe
regedit.exe
RtCRU64.exe
splwow64.exe
winhlp32.exe
write.exe

例3

GetOpenFileName 関数 - MSDN

  • Structure.size()メソッドでsizeof相当の機能
  • 変更されないUnicode文字列はWString型で定義する
  • 変更されるメモリ領域のアドレスを指定するときはByteBuffer型メンバを定義する
    • char[] など配列型を指定してもうまく動かない(前述のように値渡し扱いになる)
    • メモリ領域は ByteBuffer.allocateDirect() で確保する
    • メモリの中身は好きな型の配列にコピーできる
      • ByteOrder指定を忘れないように注意
JNISample.java
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.CharBuffer;
import java.util.Arrays;
import java.util.List;

import com.sun.jna.Library;
import com.sun.jna.Native;
import com.sun.jna.Pointer;
import com.sun.jna.Structure;
import com.sun.jna.WString;

public class JNISample {
    public interface Comdlg32 extends Library {
        Comdlg32 INSTANCE = (Comdlg32) Native.loadLibrary("comdlg32", Comdlg32.class);
        boolean GetOpenFileNameW(OPENFILENAME lpofn);
    }

    public static class OPENFILENAME extends Structure {
        @Override
        protected List<String> getFieldOrder() {
            return Arrays.asList(
                    "lStructSize", "hwndOwner", "hInstance", "lpstrFilter", "lpstrCustomFilter", "nMaxCustFilter",
                    "nFilterIndex", "lpstrFile", "nMaxFile", "lpstrFileTitle", "nMaxFileTitle", "lpstrInitialDir",
                    "lpstrTitle", "Flags", "nFileOffset", "nFileExtension", "lpstrDefExt", "lCustData", "lpfnHook",
                    "lpTemplateName", "pvReserved", "dwReserved", "FlagsEx"
            );
        }
        public int        lStructSize;
        public Pointer    hwndOwner;
        public Pointer    hInstance;
        public WString    lpstrFilter;
        public WString    lpstrCustomFilter;
        public int        nMaxCustFilter;
        public int        nFilterIndex;
        public ByteBuffer lpstrFile;
        public int        nMaxFile;
        public WString    lpstrFileTitle;
        public int        nMaxFileTitle;
        public WString    lpstrInitialDir;
        public WString    lpstrTitle;
        public int        Flags;
        public short      nFileOffset;
        public short      nFileExtension;
        public WString    lpstrDefExt;
        public Pointer    lCustData;
        public Pointer    lpfnHook;
        public WString    lpTemplateName;
        public Pointer    pvReserved;
        public int        dwReserved;
        public int        FlagsEx;
    }

    public static void main(String[] args) {
        OPENFILENAME ofn = new OPENFILENAME();
        final int lenFilenameBufferInChars = 1024;
        ByteBuffer buf = ByteBuffer.allocateDirect(lenFilenameBufferInChars * 2);
        ofn.lStructSize = ofn.size();
        ofn.lpstrFilter = new WString("テキストファイル\0*.txt\0\0");
        ofn.lpstrFile = buf;
        ofn.nMaxFile = lenFilenameBufferInChars;
        ofn.lpstrTitle = new WString("ファイルを選択してください");
        ofn.Flags = 0x00001000; // OFN_FILEMUSTEXIST
        boolean ret = Comdlg32.INSTANCE.GetOpenFileNameW(ofn);
        if (ret) {
            CharBuffer cbuf = buf.order(ByteOrder.LITTLE_ENDIAN).asCharBuffer();
            char[] arr = new char[cbuf.capacity()];
            cbuf.get(arr);
            System.out.println(Native.toString(arr));
        } else {
            System.out.println("キャンセルされました");
        }
    }
}
実行結果:選択されたファイル名
C:\Users\taro\test.txt

ここまで来ると、結構難しくなってきます。
構造体のメンバの型をどうするかでかなり苦戦しました。

コールバック関数

Windows APIの関数の中には、イベントの発生に応じて指定したコールバック関数を呼び出すものがあります。
例えば、存在するウィンドウを列挙するEnumWindows関数は、見つかったウィンドウをコールバック関数により通知します。

EnumWindows 関数 - MSDN

以下のような流れになります。

  • Callbackインタフェースを継承してコールバック関数のインタフェースを定義
    • invokeメソッドで引数と戻り値型を定義
  • 先ほど定義したインタフェースのinvokeメソッドを実装
    • 無名関数で実装することも可能

ウィンドウハンドルだけを列挙してもわかりにくいので、ウィンドウのタイトルを合わせて出力する例を示します。

JNISample.java
import com.sun.jna.Callback;
import com.sun.jna.Library;
import com.sun.jna.Native;
import com.sun.jna.Pointer;

public class JNISample {
    public interface User32 extends Library {
        User32 INSTANCE = (User32) Native.loadLibrary("user32", User32.class);
        boolean EnumWindows(EnumWindowsProc lpEnumFunc, Pointer lParam);
        int GetWindowTextW(Pointer hWnd, char[] lpString, int nMaxCount);
    }

    public interface EnumWindowsProc extends Callback {
        public boolean invoke(Pointer hWnd, Pointer lParam);
    }

    public static void main(String[] args) {
        User32.INSTANCE.EnumWindows(new EnumWindowsProc() {
            @Override
            public boolean invoke(Pointer hWnd, Pointer lParam) {
                char[] windowText = new char[1024];
                User32.INSTANCE.GetWindowTextW(hWnd, windowText, windowText.length);
                System.out.println(String.format("%x: %s", Pointer.nativeValue(hWnd), Native.toString(windowText)));
                return true;
            }
        }, null);
    }
}
実行結果:開かれているウィンドウ一覧(抜粋)
20730: クイック・アクセス
10226: バッテリ メーター
9d09aa: eclipse
f05b4: 電卓

ポインタのポインタ

Windows APIではあまり使うことがないので何をサンプルにしようか悩みましたが、ここでは文字列フォーマット関数であるwvsprintf関数を使ってみます。
JavaではString.format()関数を使えば同様のことができるので、わざわざJavaから実行する必然性に乏しいですが。

wvsprintf 関数 - MSDN

この関数は引数の渡し方が特殊なのですが、文字列を1個だけ渡すときに限れば、文字列のポインタ(つまり、C言語的にはポインタのポインタ)を渡すのと同じです。(2個以上の場合の話はここではしません)

まずは文字列の配列を作り、その配列を参照渡しする方法です。

JNISample.java
import com.sun.jna.Library;
import com.sun.jna.Native;
import com.sun.jna.WString;

public class JNISample {
    public interface User32 extends Library {
        User32 INSTANCE = (User32) Native.loadLibrary("user32", User32.class);
        int wvsprintfW(char[] lpOutput, WString lpFormat, WString[] arglist);
    }

    public static void main(String[] args) {
        char[] buf = new char[1024];
        String name = "Michael";
        WString[] argArray = new WString[] {new WString(name)};
        User32.INSTANCE.wvsprintfW(buf, new WString("My name is %s"), argArray);
        System.out.println(Native.toString(buf));
    }
}
実行結果:フォーマットされた文字列
My name is Michael

同じことを別の方法で。ちょっと回りくどいように見えますが、よりポインタのポインタを意識した方法です。

JNISample.java
import com.sun.jna.Library;
import com.sun.jna.Native;
import com.sun.jna.Pointer;
import com.sun.jna.WString;
import com.sun.jna.platform.win32.WTypes;
import com.sun.jna.ptr.PointerByReference;

public class JNISample {
    public interface User32 extends Library {
        User32 INSTANCE = (User32) Native.loadLibrary("user32", User32.class);
        int wvsprintfW(char[] lpOutput, WString lpFormat, Pointer arglist);
    }

    public static void main(String[] args) {
        char[] buf = new char[1024];
        String name = "Michael";
        WTypes.LPWSTR pname = new WTypes.LPWSTR(name); // wchar_t *
        PointerByReference argArray = new PointerByReference(pname.getPointer()); // wchar_t **
        User32.INSTANCE.wvsprintfW(buf, new WString("My name is %s"), argArray.getPointer());
        System.out.println(Native.toString(buf));
    }
}

Windows APIで見かけるLPWSTR型が出てきましたが、実態はPointerTypeのサブクラスです。
IntByReferenceなどもPointerTypeを継承しているので、これらByReference系と似たように扱うことができます。

arglistの引数をPointer型にしていますが、PointerByReference型にしてもいいです。その場合、argArrayを変換なしに(getPointer()を使わずに)そのまま渡すことができます。
このように、同じ処理をするときでも色々な書き方ができるわけですが、その時ごとに都合のいい方法を選べばいいのではないでしょうか。

まとめ

おそらくもっと色々なことができるのでしょうが、取っ掛かりとしてはこれぐらいのパターンがあれば十分かなと。
他のパターンは、やりたくなった時(やらないといけなくなった時)に調べればいいでしょう。

参考ページ

39
43
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
39
43