Javaの高度な使用法:JNAでのコールバック

一緒に書く習慣を身につけましょう!「ナゲッツデイリーニュープラン・4月アップデートチャレンジ」に参加して12日目です。クリックしてイベントの詳細をご覧ください

序章

コールバックとは何ですか?簡単に言うと、コールバックはコールバック通知です。メソッドが完了した後、またはイベントがトリガーされた後に特定のタスクに通知する必要がある場合は、コールバックを使用する必要があります。

コールバックを目にする可能性が最も高い言語はjavascriptです。基本的に、javascriptでは、コールバックはいたるところにあります。コールバックによって引き起こされるコールバック地獄の問題を解決するために、ES6はこの問題を解決するために特別にpromiseを導入しました。

ネイティブメソッドとの対話を容易にするために、JNAはコールバックのコールバックも提供します。JNAのコールバックの本質は、ネイティブ関数へのポインターであり、これを介してネイティブ関数のメソッドを呼び出すことができます。見てみましょう。

JNAでのコールバック

JNAでのコールバックの定義を最初に見てください。

public interface Callback {
    interface UncaughtExceptionHandler {
        void uncaughtException(Callback c, Throwable e);
    }

    String METHOD_NAME = "callback";

    List<String> FORBIDDEN_NAMES = Collections.unmodifiableList(
            Arrays.asList("hashCode", "equals", "toString"));
}
复制代码

すべてのコールバックメソッドは、このコールバックインターフェイスを実装する必要があります。コールバックインターフェイスは非常にシンプルで、インターフェイスと2つのプロパティを定義します。

最初にこのインターフェースを見てみましょう。インターフェース名はUncaughtExceptionHandlerで、これにはuncaughtExceptionメソッドがあります。このインターフェースは主に、JAVAのコールバックコードでキャッチされない例外を処理するために使用されます。

uncaughtExceptionメソッドでは、例外をスローすることはできず、このメソッドからスローされた例外はすべて無視されることに注意してください。

METHOD_NAMEフィールドは、コールバックによって呼び出されるメソッドを指定します。

Callbackクラスで定義されているパブリックメソッドが1つしかない場合、デフォルトのコールバックメソッドはこのメソッドです。Callbackクラスで複数のパブリックメソッドが定義されている場合、METHOD_NAME="callback"のメソッドがコールバックとして選択されます。

最后一个属性就是FORBIDDEN_NAMES。表示在这个列表里面的名字是不能作为callback方法使用的。

目前看来是有三个方法名不能够被使用,分别是:"hashCode", "equals", "toString"。

Callback还有一个同胞兄弟叫做DLLCallback,我们来看下DLLCallback的定义:

public interface DLLCallback extends Callback {
    @java.lang.annotation.Native
    int DLL_FPTRS = 16;
}
复制代码

DLLCallback主要是用在Windows API的访问中。

对于callback对象来说,需要我们自行负责对callback对象的释放工作。如果native代码尝试访问一个被回收的callback,那么有可能会导致VM崩溃。

callback的应用

callback的定义

因为JNA中的callback实际上映射的是native中指向函数的指针。首先看一下在struct中定义的函数指针:

struct _functions {
  int (*open)(const char*,int);
  int (*close)(int);
};
复制代码

在这个结构体中,定义了两个函数指针,分别带两个参数和一个参数。

对应的JNA的callback定义如下:

public class Functions extends Structure {
  public static interface OpenFunc extends Callback {
    int invoke(String name, int options);
  }
  public static interface CloseFunc extends Callback {
    int invoke(int fd);
  }
  public OpenFunc open;
  public CloseFunc close;
}
复制代码

我们在Structure里面定义两个接口继承自Callback,对应的接口中定义了相应的invoke方法。

然后看一下具体的调用方式:

Functions funcs = new Functions();
lib.init(funcs);
int fd = funcs.open.invoke("myfile", 0);
funcs.close.invoke(fd);
复制代码

另外Callback还可以作为函数的返回值,如下所示:

typedef void (*sig_t)(int);
sig_t signal(int signal, sig_t sigfunc);
复制代码

对于这种单独存在的函数指针,我们需要自定义一个Library,并在其中定义对应的Callback,如下所示:

public interface CLibrary extends Library {
    public interface SignalFunction extends Callback {
        void invoke(int signal);
    }
    SignalFunction signal(int signal, SignalFunction func);
}
复制代码

callback的获取和应用

如果callback是定义在Structure中的,那么可以在Structure进行初始化的时候自动实例化,然后只需要从Structure中访问对应的属性即可。

如果callback定义是在一个普通的Library中的话,如下所示:

    public static interface TestLibrary extends Library {

        interface VoidCallback extends Callback {
            void callback();
        }
        interface ByteCallback extends Callback {
            byte callback(byte arg, byte arg2);
        }

        void callVoidCallback(VoidCallback c);
        byte callInt8Callback(ByteCallback c, byte arg, byte arg2);
    }
复制代码

上例中,我们在一个Library中定义了两个callback,一个是无返回值的callback,一个是返回byte的callback。

JNA提供了一个简单的工具类来帮助我们获取Callback,这个工具类就是CallbackReference,对应的方法是CallbackReference.getCallback,如下所示:

Pointer p = new Pointer("MultiplyMappedCallback".hashCode());
Callback cbV1 = CallbackReference.getCallback(TestLibrary.VoidCallback.class, p);
Callback cbB1 = CallbackReference.getCallback(TestLibrary.ByteCallback.class, p);
log.info("cbV1:{}",cbV1);
log.info("cbB1:{}",cbB1);
复制代码

输出结果如下:

INFO com.flydean.CallbackUsage - cbV1:Proxy interface to native function@0xffffffffc46eeefc (com.flydean.CallbackUsage$TestLibrary$VoidCallback)
INFO com.flydean.CallbackUsage - cbB1:Proxy interface to native function@0xffffffffc46eeefc (com.flydean.CallbackUsage$TestLibrary$ByteCallback)
复制代码

可以看出,这两个Callback实际上是对native方法的代理。如果详细看getCallback的实现逻辑:

private static Callback getCallback(Class<?> type, Pointer p, boolean direct) {
        if (p == null) {
            return null;
        }

        if (!type.isInterface())
            throw new IllegalArgumentException("Callback type must be an interface");
        Map<Callback, CallbackReference> map = direct ? directCallbackMap : callbackMap;
        synchronized(pointerCallbackMap) {
            Reference<Callback>[] array = pointerCallbackMap.get(p);
            Callback cb = getTypeAssignableCallback(type, array);
            if (cb != null) {
                return cb;
            }
            cb = createCallback(type, p);
            pointerCallbackMap.put(p, addCallbackToArray(cb,array));

            // No CallbackReference for this callback
            map.remove(cb);
            return cb;
        }
    }
复制代码

可以看到它的实现逻辑是首先判断type是否是interface,如果不是interface则会报错。然后判断是否是direct mapping。实际上当前JNA的实现都是interface mapping,所以接下来的逻辑就是从pointerCallbackMap中获取函数指针对应的callback。然后按照传入的类型来查找具体的Callback。

如果没有查找到,则创建一个新的callback,最后将这个新创建的存入pointerCallbackMap中。

大家要注意, 这里有一个关键的参数叫做Pointer,实际使用的时候,需要传入指向真实naitve函数的指针。上面的例子中,为了简便起见,我们是自定义了一个Pointer,这个Pointer并没有太大的实际意义。

如果真的要想在JNA中调用在TestLibrary中创建的两个call方法:callVoidCallback和callInt8Callback,首先需要加载对应的Library:

TestLibrary lib = Native.load("testlib", TestLibrary.class);
复制代码

然后分别创建TestLibrary.VoidCallback和TestLibrary.ByteCallback的实例如下,首先看一下VoidCallback:

final boolean[] voidCalled = { false };
        TestLibrary.VoidCallback cb1 = new TestLibrary.VoidCallback() {
            @Override
            public void callback() {
                voidCalled[0] = true;
            }
        };
        lib.callVoidCallback(cb1);
        assertTrue("Callback not called", voidCalled[0]);
复制代码

这里我们在callback中将voidCalled的值回写为true表示已经调用了callback方法。

再看看带返回值的ByteCallback:

final boolean[] int8Called = {false};
        final byte[] cbArgs = { 0, 0 };
        TestLibrary.ByteCallback cb2 = new TestLibrary.ByteCallback() {
            @Override
            public byte callback(byte arg, byte arg2) {
                int8Called[0] = true;
                cbArgs[0] = arg;
                cbArgs[1] = arg2;
                return (byte)(arg + arg2);
            }
        };

final byte MAGIC = 0x11;
byte value = lib.callInt8Callback(cb2, MAGIC, (byte)(MAGIC*2));
复制代码

我们直接在callback方法中返回要返回的byte值即可。

在多线程环境中使用callback

デフォルトでは、コールバックメソッドは現在のスレッドで実行されます。コールバックメソッドを別のスレッドで実行する場合は、CallbackThreadInitializerを作成し、デーモン、デタッチ、名前、およびthreadGroupのプロパティを指定できます。

        final String tname = "VoidCallbackThreaded";
        ThreadGroup testGroup = new ThreadGroup("Thread group for callVoidCallbackThreaded");
        CallbackThreadInitializer init = new CallbackThreadInitializer(true, false, tname, testGroup);
复制代码

次に、コールバックのインスタンスを作成します。

TestLibrary.VoidCallback cb = new TestLibrary.VoidCallback() {
            @Override
            public void callback() {
                Thread thread = Thread.currentThread();
                daemon[0] = thread.isDaemon();
                name[0] = thread.getName();
                group[0] = thread.getThreadGroup();
                t[0] = thread;
                if (thread.isAlive()) {
                    alive[0] = true;
                }

                ++called[0];
                if (THREAD_DETACH_BUG && called[0] == 2) {
                    Native.detach(true);
                }
            }
        };
复制代码

次に、電話します。

 Native.setCallbackThreadInitializer(cb, init);
复制代码

コールバックをCallbackThreadInitializerに関連付けます。

最後に、コールバックメソッドを呼び出します。

lib.callVoidCallbackThreaded(cb, 2, 2000, "callVoidCallbackThreaded", 0);
复制代码

要約する

JNAのコールバックは、メソッドをネイティブメソッドに渡す機能を実現できます。これは、場合によっては非常に便利です。

この記事のコード:github.com/ddean2009/l…

この記事はwww.flydean.com/09-jna-callに含まれています…

最も人気のある解釈、最も深遠な乾物、最も簡潔なチュートリアル、そしてあなたが知らない多くのトリックがあなたが発見するのを待っています!

私の公式アカウントに注意を払うことを歓迎します:「それらをプログラムする」、テクノロジーを理解し、あなたをよりよく理解する!

おすすめ

転載: juejin.im/post/7085650510794997797