1. 概要

このチュートリアルでは、Java Native Accessライブラリ(略してJNA)を使用して、 JNI(Java Native Interface)コードを記述せずにネイティブライブラリにアクセスする方法を説明します。

2. なぜJNAなのか?

長年にわたり、Javaおよびその他のJVMベースの言語は、「一度書けばどこでも実行できる」というモットーを大部分実現してきました。 ただし、一部の機能を実装するためにネイティブコードを使用する必要がある場合があります

  • C /C++またはネイティブコードを作成できるその他の言語で記述されたレガシーコードを再利用する
  • 標準のJavaランタイムでは使用できないシステム固有の機能へのアクセス
  • 特定のアプリケーションの特定のセクションの速度やメモリ使用量を最適化します。

当初、この種の要件は、JNI –Javaネイティブインターフェイスを使用する必要があることを意味していました。 効果的ではありますが、このアプローチには欠点があり、いくつかの問題のために一般的に回避されました:

  • 開発者は、JavaとネイティブコードをブリッジするためにC /C++「グルーコード」を作成する必要があります
  • すべてのターゲットシステムで利用可能な完全なコンパイルおよびリンクツールチェーンが必要です
  • JVMとの間で値をマーシャリングおよびアンマーシャリングすることは、面倒でエラーが発生しやすいタスクです。
  • Javaとネイティブライブラリを混在させる場合の法的およびサポート上の懸念

JNAは、JNIの使用に関連する複雑さのほとんどを解決するようになりました。 特に、ダイナミックライブラリにあるネイティブコードを使用するためにJNIコードを作成する必要がないため、プロセス全体がはるかに簡単になります。

もちろん、いくつかのトレードオフがあります。

  • 静的ライブラリを直接使用することはできません
  • 手作りのJNIコードと比較すると遅い

ただし、ほとんどのアプリケーションでは、JNAの単純さの利点がこれらの欠点をはるかに上回ります。 そのため、非常に具体的な要件がない限り、今日のJNAは、Javaまたはその他のJVMベースの言語からネイティブコードにアクセスするための最良の選択肢であると言っても過言ではありません。

3. JNAプロジェクトの設定

JNAを使用するために最初に行う必要があるのは、プロジェクトのpom.xmlに依存関係を追加することです。

<dependency>
    <groupId>net.java.dev.jna</groupId>
    <artifactId>jna-platform</artifactId>
    <version>5.6.0</version>
</dependency>

jna-platformの最新バージョンはMavenCentralからダウンロードできます。

4. JNAの使用

JNAの使用は、次の2段階のプロセスです。

  • まず、JNAの Library インターフェイスを拡張して、ターゲットのネイティブコードを呼び出すときに使用されるメソッドとタイプを記述するJavaインターフェイスを作成します。
  • 次に、このインターフェイスをJNAに渡します。これにより、ネイティブメソッドを呼び出すために使用するこのインターフェイスの具体的な実装が返されます。

4.1. C標準ライブラリからのメソッドの呼び出し

最初の例では、JNAを使用して、ほとんどのシステムで使用可能な標準Cライブラリからcosh関数を呼び出します。 このメソッドは、 double 引数を取り、その双曲線コサインを計算します。 ACプログラムは、を含めるだけでこの機能を使用できます。 ヘッダーファイル:

#include <math.h>
#include <stdio.h>
int main(int argc, char** argv) {
    double v = cosh(0.0);
    printf("Result: %f\n", v);
}

このメソッドを呼び出すために必要なJavaインターフェイスを作成しましょう。

public interface CMath extends Library { 
    double cosh(double value);
}

次に、JNAの Native クラスを使用して、このインターフェイスの具体的な実装を作成し、APIを呼び出すことができるようにします。

CMath lib = Native.load(Platform.isWindows()?"msvcrt":"c", CMath.class);
double result = lib.cosh(0);

ここで本当に興味深い部分は、load()メソッドの呼び出しです。 ダイナミックライブラリ名と、使用するメソッドを説明するJavaインターフェイスの2つの引数を取ります。 このインターフェイスの具体的な実装を返し、そのメソッドのいずれかを呼び出すことができます。

現在、ダイナミックライブラリ名は通常システムに依存しており、C標準ライブラリも例外ではありません。ほとんどのLinuxベースのシステムでは libc.so ですが、Windowsではmsvcrt.dllです。 これが、JNAに含まれている Platform ヘルパークラスを使用して、実行しているプラットフォームを確認し、適切なライブラリ名を選択した理由です。

暗黙的に示されているように、.soまたは.dll拡張子を追加する必要がないことに注意してください。 また、Linuxベースのシステムの場合、共有ライブラリの標準である「lib」プレフィックスを指定する必要はありません。

ダイナミックライブラリはJavaの観点からはSingletonsのように動作するため、一般的な方法は、インターフェイス宣言の一部としてINSTANCEフィールドを宣言することです。

public interface CMath extends Library {
    CMath INSTANCE = Native.load(Platform.isWindows() ? "msvcrt" : "c", CMath.class);
    double cosh(double value);
}

4.2. 基本的なタイプのマッピング

最初の例では、呼び出されたメソッドは、引数と戻り値の両方としてプリミティブ型のみを使用していました。 JNAはこれらのケースを自動的に処理し、通常はCタイプからマッピングするときに対応する自然なJavaを使用します。

  • char=>バイト
  • 短い=>短い
  • wchar_t => char
  • int => int
  • long => com.sun.jna.NativeLong
  • long long => long
  • float => float
  • ダブル=>ダブル
  • char*=>文字列

奇妙に見えるかもしれないマッピングは、ネイティブの long タイプに使用されるマッピングです。これは、C / C ++では、longタイプが32ビットまたは64ビットの値を表す場合があるためです。 32ビットシステムと64ビットシステムのどちらで実行しているか。

この問題に対処するために、JNAは NativeLong タイプを提供します。これは、システムのアーキテクチャーに応じて適切なタイプを使用します。

4.3. 構造とユニオン

別の一般的なシナリオは、いくつかへのポインターを期待するネイティブコードAPIを扱うことです構造体また連合タイプアクセスするJavaインターフェースを作成する場合、対応する引数または戻り値は、拡張するJavaタイプである必要があります。 構造または連合 、 それぞれ。

たとえば、このC構造体が与えられた場合:

struct foo_t {
    int field1;
    int field2;
    char *field3;
};

そのJavaピアクラスは次のようになります。

@FieldOrder({"field1","field2","field3"})
public class FooType extends Structure {
    int field1;
    int field2;
    String field3;
};

JNAには、 @FieldOrder アノテーションが必要です。これにより、データをターゲットメソッドの引数として使用する前に、データをメモリバッファーに適切にシリアル化できます。

または、同じ効果を得るために getFieldOrder()メソッドをオーバーライドすることもできます。 単一のアーキテクチャ/プラットフォームを対象とする場合、通常は前者の方法で十分です。 後者を使用して、プラットフォーム間の配置の問題に対処できます。これには、追加のパディングフィールドを追加する必要がある場合があります。

Unions は、いくつかの点を除いて、同様に機能します。

  • @FieldOrder アノテーションを使用したり、 getFieldOrder()を実装したりする必要はありません。
  • ネイティブメソッドを呼び出す前に、 setType()を呼び出す必要があります

簡単な例でそれを行う方法を見てみましょう:

public class MyUnion extends Union {
    public String foo;
    public double bar;
};

それでは、架空のライブラリでMyUnionを使用してみましょう。

MyUnion u = new MyUnion();
u.foo = "test";
u.setType(String.class);
lib.some_method(u);

foobarの両方が同じタイプの場合、代わりにフィールドの名前を使用する必要があります。

u.foo = "test";
u.setType("foo");
lib.some_method(u);

4.4. ポインタの使用

JNAは、型なしポインターで宣言されたAPI(通常は void * )の処理に役立つPointer抽象化を提供します。 このクラスは、基盤となるネイティブメモリバッファへの読み取りおよび書き込みアクセスを可能にするメソッドを提供しますが、これには明らかなリスクがあります。

このクラスの使用を開始する前に、参照されるメモリを誰が「所有」するかを毎回明確に理解する必要があります。 これを怠ると、メモリリークや無効なアクセスに関連するデバッグエラーが発生する可能性があります。

(いつものように)私たちが何をしているのかを知っていると仮定して、よく知られている malloc()および free()関数をJNAで使用する方法を見てみましょう。メモリバッファを解放します。 まず、ラッパーインターフェイスをもう一度作成しましょう。

public interface StdC extends Library {
    StdC INSTANCE = // ... instance creation omitted
    Pointer malloc(long n);
    void free(Pointer p);
}

それでは、これを使用してバッファを割り当て、試してみましょう。

StdC lib = StdC.INSTANCE;
Pointer p = lib.malloc(1024);
p.setMemory(0l, 1024l, (byte) 0);
lib.free(p);

setMemory()メソッドは、基になるバッファーを定数バイト値(この場合はゼロ)で埋めるだけです。 Pointer インスタンスは、それが何を指しているのか、ましてやそのサイズを認識していないことに注意してください。これは、そのメソッドを使用してヒープを非常に簡単に破損できることを意味します。

JNAのクラッシュ保護機能を使用して、このようなエラーを軽減する方法については、後で説明します。

4.5. エラーの処理

標準Cライブラリの古いバージョンでは、グローバル errno 変数を使用して、特定の呼び出しが失敗した理由を格納していました。 たとえば、これは通常の open()呼び出しがCでこのグローバル変数を使用する方法です。

int fd = open("some path", O_RDONLY);
if (fd < 0) {
    printf("Open failed: errno=%d\n", errno);
    exit(1);
}

もちろん、最近のマルチスレッドプログラムでは、このコードは機能しませんよね? さて、Cのプリプロセッサのおかげで、開発者はまだこのようなコードを書くことができ、それはうまく機能します。 現在、errnoは関数呼び出しに展開されるマクロであることが判明しました:

// ... excerpt from bits/errno.h on Linux
#define errno (*__errno_location ())

// ... excerpt from <errno.h> from Visual Studio
#define errno (*_errno())

現在、このアプローチはソースコードをコンパイルするときに正常に機能しますが、JNAを使用する場合はそのようなことはありません。 ラッパーインターフェイスで拡張関数を宣言して明示的に呼び出すこともできますが、JNAはより良い代替手段を提供します:LastErrorException

throws LastErrorException を使用してラッパーインターフェイスで宣言されたメソッドには、ネイティブ呼び出し後のエラーのチェックが自動的に含まれます。 エラーが報告された場合、JNAはLastErrorExceptionをスローします。これには元のエラーコードが含まれます。

この機能の動作を示すために以前に使用したStdCラッパーインターフェイスにいくつかのメソッドを追加してみましょう。

public interface StdC extends Library {
    // ... other methods omitted
    int open(String path, int flags) throws LastErrorException;
    int close(int fd) throws LastErrorException;
}

これで、try /catch句でopen()を使用できます。

StdC lib = StdC.INSTANCE;
int fd = 0;
try {
    fd = lib.open("/some/path",0);
    // ... use fd
}
catch (LastErrorException err) {
    // ... error handling
}
finally {
    if (fd > 0) {
       lib.close(fd);
    }
}

catch ブロックでは、 LastErrorException.getErrorCode()を使用して、元の errno 値を取得し、エラー処理ロジックの一部として使用できます。

4.6. アクセス違反の処理

前述のように、JNAは、特にネイティブコードとの間で受け渡されるメモリバッファーを処理する場合に、特定のAPIの誤用から保護しません。 通常の状況では、このようなエラーはアクセス違反を引き起こし、JVMを終了します。

JNAは、Javaコードがアクセス違反エラーを処理できるようにする方法をある程度サポートしています。 それをアクティブにする2つの方法があります:

  • jna.protectedシステムプロパティをtrueに設定します
  • Native.setProtected(true)を呼び出す

この保護モードをアクティブにすると、JNAは、通常はクラッシュを引き起こすアクセス違反エラーをキャッチし、 java.lang.Error例外をスローします。 無効なアドレスで初期化されたPointerを使用し、それにデータを書き込もうとすると、これが機能することを確認できます。

Native.setProtected(true);
Pointer p = new Pointer(0l);
try {
    p.setMemory(0, 100*1024, (byte) 0);
}
catch (Error err) {
    // ... error handling omitted
}

ただし、ドキュメントに記載されているように、この機能はデバッグ/開発の目的でのみ使用する必要があります。

5. 結論

この記事では、JNIと比較して、JNAを使用してネイティブコードに簡単にアクセスする方法を示しました。

いつものように、すべてのコードはGitHubを介して利用できます。