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

1. JNA簡單介紹

先說JNI(Java Native Interface)吧,有過不一樣語言間通訊經歷的通常都知道,它容許Java代碼和其餘語言(尤爲C/C++)寫的代碼進行交互,只要遵照調用約定便可。首先看下JNI調用C/C++的過程,注意寫程序時自下而上,調用時自上而下。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的一大弱點!git

那麼JNA是什麼呢?程序員

JNA(Java Native Access)是一個開源的Java框架,是Sun公司推出的一種調用本地方法的技術,是創建在經典的JNI基礎之上的一個框架。之因此說它是JNI的替 代者,是由於JNA大大簡化了調用本地方法的過程,使用很方便,基本上不須要脫離Java環境就能夠完成。github

若是要和上圖作個比較,那麼JNA調用C/C++的過程大體以下:編程

能夠看到步驟減小了不少,最重要的是咱們不須要重寫咱們的動態連接庫文件,而是有直接調用的API,大大簡化了咱們的工做量。數據結構

JNA只須要咱們寫Java代碼而不用寫JNI或本地代碼。功能相對於Windows的Platform/Invoke和Python的ctypes。app

 

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官方給出的默認類型映射表以下:

還有不少其它的類型映射,須要的請到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編程詳解

相關文章
相關標籤/搜索