天天看點

JNI的替代者—使用JNA通路Java外部功能接口

1. JNA簡單介紹

先說JNI(Java Native Interface)吧,有過不同語言間通信經曆的一般都知道,它允許Java代碼和其他語言(尤其C/C++)寫的代碼進行互動,隻要遵守調用約定即可。首先看下JNI調用C/C++的過程,注意寫程式時自下而上,調用時自上而下。

JNI的替代者—使用JNA通路Java外部功能接口

可 見步驟非常的多,很麻煩,使用JNI調用.dll/.so共享庫都能體會到這個痛苦的過程。如果已有一個編譯好的.dll/.so檔案,如果使用JNI技 術調用,我們首先需要使用C語言另外寫一個.dll/.so共享庫,使用SUN規定的資料結構替代C語言的資料結構,調用已有的 dll/so中公布的函 數。然後再在Java中載入這個庫dll/so,最後編寫Java  native函數作為連結庫中函數的代理。經過這些繁瑣的步驟才能在Java中調用 本地代碼。是以,很少有Java程式員願意編寫調用dll/.so庫中原生函數的java程式。這也使Java語言在用戶端上乏善可陳,可以說JNI是 Java的一大弱點!

那麼JNA是什麼呢?

JNA(Java Native Access)是一個開源的Java架構,是Sun公司推出的一種調用本地方法的技術,是建立在經典的JNI基礎之上的一個架構。之是以說它是JNI的替 代者,是因為JNA大大簡化了調用本地方法的過程,使用很友善,基本上不需要脫離Java環境就可以完成。

如果要和上圖做個比較,那麼JNA調用C/C++的過程大緻如下:

JNI的替代者—使用JNA通路Java外部功能接口

可以看到步驟減少了很多,最重要的是我們不需要重寫我們的動态連結庫檔案,而是有直接調用的API,大大簡化了我們的工作量。

JNA隻需要我們寫Java代碼而不用寫JNI或本地代碼。功能相對于Windows的Platform/Invoke和Python的ctypes。

2. JNA技術原理

JNA使用一個小型的JNI庫插樁程式來動态調用本地代碼。開發者使用Java接口描述目标本地庫的功能和結構,這使得它很容易利用本機平台的功能,而不會産生多平台配置和生成JNI代碼的高開銷。這樣的性能、準确性和易用性顯然受到很大的重視。

此外,JNA包括一個已與許多本地函數映射的平台庫,以及一組簡化本地通路的公用接口。

注意:

JNA是建立在JNI技術基礎之上的一個Java類庫,它使您可以友善地使用java直接通路動态連結庫中的函數。

原來使用JNI,你必須手工用C寫一個動态連結庫,在C語言中映射Java的資料類型。

JNA中,它提供了一個動态的C語言編寫的轉發器,可以自動實作Java和C的資料類型映射,你不再需要編寫C動态連結庫。

也許這也意味着,使用JNA技術比使用JNI技術調用動态連結庫會有些微的性能損失。但總體影響不大,因為JNA也避免了JNI的一些平台配置的開銷。

3. JNA簡單使用

JNA的項目已遷移至Github,目前最新版本是4.1.0,已有打包好的jar檔案可供下載下傳。

JNA把一個.dll/.so檔案看做是一個Java接口,下面以一個簡單的執行個體來說明怎麼使用。

當然要從最經典的HelloWorld開始,我們調用C的printf函數列印出“HelloWorld”(官方的例子),前提是已将jar包加入你的classpath。

package com.sun.jna.examples;

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

/** Simple example of JNA interface mapping and usage. */
public class HelloWorld {

    // This is the standard, stable way of mapping, which supports extensive
    // customization and mapping of Java to native types.

    public interface CLibrary extends Library {
        CLibrary INSTANCE = (CLibrary)
            Native.loadLibrary((Platform.isWindows() ? "msvcrt" : "c"),
                               CLibrary.class);

        void printf(String format, Object... args);
    }

    public static void main(String[] args) {
        CLibrary.INSTANCE.printf("Hello, World\n");
        for (int i=0;i < args.length;i++) {
            CLibrary.INSTANCE.printf("Argument %d: %s\n", i, args[i]);
        }
    }
}      

運作程式,如果沒有帶參數則隻列印出“Hello, World”,如果帶了參數,則會列印出所有的參數。

很簡單,不需要寫一行C代碼,就可以直接在Java中調用外部動态連結庫中的函數!

下面來解釋下這個程式。

(1)需要定義一個接口,繼承自

Library

StdCallLibrary

預設的是繼承

Library

,如果動态連結庫裡的函數是以stdcall方式輸出的,那麼就繼承

StdCallLibrary

,比如衆所周知的kernel32庫。比如上例中的接口定義:

public interface CLibrary extends Library {

}      

(2)接口内部定義

接口内部需要一個公共靜态常量:

INSTANCE,

通過這個常量,就可以獲得這個接口的執行個體,進而使用接口的方法,也就是調用外部dll/so的函數。

該常量通過Native.loadLibrary()這個API函數獲得,該函數有2個參數:

  • 一個參數是動态連結庫dll/so的名稱,但不帶.dll或.so這樣的字尾,這符合JNI的規範,因為帶了字尾名就不可以跨作業系統平台了。搜尋動态鍊

    接庫路徑的順序是:先從目前類的目前檔案夾找,如果沒有找到,再在工程目前檔案夾下面找win32/win64檔案夾,找到後搜尋對應的dll檔案,如果

    找不到再到WINDOWS下面去搜尋,再找不到就會抛異常了。比如上例中printf函數在Windows平台下所在的dll庫名稱是msvcrt,而在

    其它平台如Linux下的so庫名稱是c。

  • 第二個參數是本接口的Class類型。JNA通過這個Class類型,根據指定的.dll/.so檔案,動态建立接口的執行個體。該執行個體由JNA通過反射自動生成。
CLibrary INSTANCE = (CLibrary)
            Native.loadLibrary((Platform.isWindows() ? "msvcrt" : "c"),
                               CLibrary.class);      

接口中隻需要定義你要用到的函數或者公共變量,不需要的可以不定義,如上例隻定義printf函數:

void printf(String format, Object... args);      

注意參數和傳回值的類型,應該和連結庫中的函數類型保持一緻。

(3)調用連結庫中的函數

定義好接口後,就可以使用接口中的函數即相應dll/so中的函數了,前面說過調用方法就是通過接口中的執行個體進行調用,非常簡單,如上例中:

CLibrary.INSTANCE.printf("Hello, World\n");
        for (int i=0;i < args.length;i++) {
            CLibrary.INSTANCE.printf("Argument %d: %s\n", i, args[i]);
        }      

這就是JNA使用的簡單例子,可能有人認為這個例子太簡單了,因為使用的是系統自帶的動态連結庫,應該還給出一個自己實作的庫函數例子。其實我覺得這個完全沒有必要,這也是JNA的友善之處,不像JNI使用使用者自定義庫時還得定義一大堆配置資訊,對于JNA來說,使用使用者自定義庫與使用系統自帶的庫是完全一樣的方法,不需要額外配置什麼資訊。比如我在Windows下建立一個動态庫程式:

#include "stdafx.h"

extern "C"_declspec(dllexport) int add(int a, int b);

int add(int a, int b) {
    return a + b;
}      

然後編譯成一個dll檔案(比如CDLL.dll),放到目前目錄下,然後編寫JNA程式調用即可:

public class DllTest {

    public interface CLibrary extends Library {
        CLibrary INSTANCE = (CLibrary)Native.loadLibrary("CDLL", CLibrary.class);

        int add(int a, int b);
    }

    public static void main(String[] args) {
        int sum = CLibrary.INSTANCE.add(3, 6);

        System.out.println(sum);
    }
}      

4. JNA技術難點

有過跨語言、跨平台開發的程式員都知道,跨平台、語言調用的難點,就是不同語言之間資料類型不一緻造成的問題。絕大部分跨平台調用的失敗,都是這個問題造成的。關于這一點,不論何種語言,何種技術方案,都無法解決這個問題。JNA也不例外。

上面說到接口中使用的函數必須與連結庫中的函數原型保持一緻,這是JNA甚至所有跨平台調用的難點,因為C/C++的類型與Java的類型是不一樣的,你必須轉換類型讓它們保持一緻,比如printf函數在C中的原型為:

void printf(const char *format, [argument]);      

你不可能在Java中也這麼寫,Java中是沒有char *指針類型的,是以const char *轉到Java下就是String類型了。

這就是類型映射(Type Mappings),JNA官方給出的預設類型映射表如下:

JNI的替代者—使用JNA通路Java外部功能接口

還有很多其它的類型映射,需要的請到JNA官網檢視。

另外,JNA還支援

類型映射定制,比如有的Java中可能找不到對應的類型(在Windows API中可能會有很多類型,在Java中找不到其對應的類型)

,JNA中TypeMapper類和相關的接口就提供了這樣的功能。

5. JNA能完全替代JNI嗎?

這可能是大家比較關心的問題,但是遺憾的是,JNA是不能完全替代JNI的,因為有些需求還是必須求助于JNI。

使用JNI技術,不僅可以實作Java通路C函數,也可以實作C語言調用Java代碼。

而JNA隻能實作Java通路C函數,作為一個Java架構,自然不能實作C語言調用Java代碼。此時,你還是需要使用JNI技術。

JNI是JNA的基礎,是Java和C互操作的技術基礎。有時候,你必須回歸到基礎上來。

6.  參考文獻

(1)JNA—JNI終結者

(2)C++DLL程式設計詳解

JNI的替代者—使用JNA通路Java外部功能接口

作者:Alexia(minmin)

如果您認為閱讀這篇部落格讓您有些收獲,不妨點選一下右下角的【推薦】

如果您希望與我交流互動,歡迎微網誌互粉

本文版權歸作者和部落格園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接配接,否則保留追究法律責任的權利。