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) を題材にして、様々な場合のパターンをまとめます。
基本形
1秒間寝るだけの単純なプログラムです。まずはここから。
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関数を使ってメッセージボックスを表示してみます。
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
型にします。
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ベースで扱っていくことにします。
参照渡しでデータを受け取る
- 参照渡しによりバッファの必要サイズを取得
- バッファを確保
- 文字列として結果を取得
- 結果を表示
の流れです。以下のあたりがポイントでしょうか。
- Unicode版の関数を使う(関数名の末尾にW)
-
int
型を参照渡ししたいときはIntByReference
型のオブジェクトを渡す-
getValue()
で値を取得できる - 同様に
LongByReference
なども存在する
-
- 文字列バッファの引数は
char[]
型にする- 配列の場合は何も考えなくても参照渡しになる
-
null
を渡すとNULLポインタ(void *)0
を渡したことになる
- 文字列バッファの
String
への変換はNative.toString()
で行う-
new String()
でも変換できるように見えるが、ヌル終端として解釈されず、後ろにゴミが付く
-
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
com.sun.jna.Structure
を継承したクラスで構造体を定義します。
- メンバ変数をpublicで列挙する
-
getFieldOrder()
を実装してメモリ上の並び順を定義する- メンバ変数が過不足なく列挙されている必要がある
-
this.getClass().getFields()
は順序が未定義1なため使えない
構造体(クラス)は引数で指定すると自動的に参照渡しになります。
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)
で取れる
- ハンドルやポインタ型(
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);
}
}
bfsvc.exe
explorer.exe
HelpPane.exe
hh.exe
notepad.exe
regedit.exe
RtCRU64.exe
splwow64.exe
winhlp32.exe
write.exe
例3
-
Structure.size()
メソッドでsizeof
相当の機能 - 変更されないUnicode文字列は
WString
型で定義する - 変更されるメモリ領域のアドレスを指定するときは
ByteBuffer
型メンバを定義する-
char[]
など配列型を指定してもうまく動かない(前述のように値渡し扱いになる) - メモリ領域は
ByteBuffer.allocateDirect()
で確保する - メモリの中身は好きな型の配列にコピーできる
- ByteOrder指定を忘れないように注意
-
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
関数は、見つかったウィンドウをコールバック関数により通知します。
以下のような流れになります。
-
Callback
インタフェースを継承してコールバック関数のインタフェースを定義-
invoke
メソッドで引数と戻り値型を定義
-
- 先ほど定義したインタフェースの
invoke
メソッドを実装- 無名関数で実装することも可能
ウィンドウハンドルだけを列挙してもわかりにくいので、ウィンドウのタイトルを合わせて出力する例を示します。
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から実行する必然性に乏しいですが。
この関数は引数の渡し方が特殊なのですが、文字列を1個だけ渡すときに限れば、文字列のポインタ(つまり、C言語的にはポインタのポインタ)を渡すのと同じです。(2個以上の場合の話はここではしません)
まずは文字列の配列を作り、その配列を参照渡しする方法です。
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
同じことを別の方法で。ちょっと回りくどいように見えますが、よりポインタのポインタを意識した方法です。
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()
を使わずに)そのまま渡すことができます。
このように、同じ処理をするときでも色々な書き方ができるわけですが、その時ごとに都合のいい方法を選べばいいのではないでしょうか。
まとめ
おそらくもっと色々なことができるのでしょうが、取っ掛かりとしてはこれぐらいのパターンがあれば十分かなと。
他のパターンは、やりたくなった時(やらないといけなくなった時)に調べればいいでしょう。