java高階用法之:JNA中的回撥

flydean發表於2022-05-10

簡介

什麼是callback呢?簡單點說callback就是回撥通知,當我們需要在某個方法完成之後,或者某個事件觸發之後,來通知進行某些特定的任務就需要用到callback了。

最有可能看到callback的語言就是javascript了,基本上在javascript中,callback無處不在。為了解決callback導致的回撥地獄的問題,ES6中特意引入了promise來解決這個問題。

為了方便和native方法進行互動,JNA中同樣提供了Callback用來進行回撥。JNA中回撥的本質是一個指向native函式的指標,通過這個指標可以呼叫native函式中的方法,一起來看看吧。

JNA中的Callback

先看下JNA中Callback的定義:

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"));
}

所有的Callback方法都需要實現這個Callback介面。Callback介面很簡單,裡面定義了一個interface和兩個屬性。

先來看這個interface,interface名字叫做UncaughtExceptionHandler,裡面有一個uncaughtException方法。這個interface主要用於處理JAVA的callback程式碼中沒有捕獲的異常。

注意,在uncaughtException方法中,不能丟擲異常,任何從這個方法丟擲的異常都會被忽略。

METHOD_NAME這個欄位指定了Callback要呼叫的方法。

如果Callback類中只定義了一個public的方法,那麼預設callback方法就是這個方法。如果Callback類中定義了多個public方法,那麼會選擇METHOD_NAME = "callback"的這個方法作為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

預設情況下, callback方法是在當前的執行緒中執行的。如果希望callback方法是在另外的執行緒中執行,則可以建立一個CallbackThreadInitializer,指定daemon,detach,name,和threadGroup屬性:

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

然後建立callback的例項:

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);

將callback和CallbackThreadInitializer進行關聯。

最後呼叫callback方法即可:

lib.callVoidCallbackThreaded(cb, 2, 2000, "callVoidCallbackThreaded", 0);

總結

JNA中的callback可以實現向native方法中傳遞方法的作用,在某些情況下用處還是非常大的。

本文的程式碼:https://github.com/ddean2009/learn-java-base-9-to-20.git

本文已收錄於 http://www.flydean.com/09-jna-callbacks/

最通俗的解讀,最深刻的乾貨,最簡潔的教程,眾多你不知道的小技巧等你來發現!

歡迎關注我的公眾號:「程式那些事」,懂技術,更懂你!

相關文章