先說(shuō)JNI(Java Native Interface)吧,有過(guò)不同語(yǔ)言間通信經(jīng)歷的一般都知道,它允許Java代碼和其他語(yǔ)言(尤其C/C++)寫(xiě)的代碼進(jìn)行交互,只要遵守調用約定即可。首先看下JNI調用C/C++的過(guò)程,注意寫(xiě)程序時(shí)自下而上,調用時(shí)自上而下。

可 見(jiàn)步驟非常的多,很麻煩,使用JNI調用.dll/.so共享庫都能體會(huì )到這個(gè)痛苦的過(guò)程。如果已有一個(gè)編譯好的.dll/.so文件,如果使用JNI技 術(shù)調用,我們首先需要使用C語(yǔ)言另外寫(xiě)一個(gè).dll/.so共享庫,使用SUN規定的數據結構替代C語(yǔ)言的數據結構,調用已有的 dll/so中公布的函 數。然后再在Java中載入這個(gè)庫dll/so,最后編寫(xiě)Java native函數作為鏈接庫中函數的代理。經(jīng)過(guò)這些繁瑣的步驟才能在Java中調用 本地代碼。因此,很少有Java程序員愿意編寫(xiě)調用dll/.so庫中原生函數的java程序。這也使Java語(yǔ)言在客戶(hù)端上乏善可陳,可以說(shuō)JNI是 Java的一大弱點(diǎn)!
那么JNA是什么呢?
JNA(Java Native Access)是一個(gè)開(kāi)源的Java框架,是Sun公司推出的一種調用本地方法的技術(shù),是建立在經(jīng)典的JNI基礎之上的一個(gè)框架。之所以說(shuō)它是JNI的替 代者,是因為JNA大大簡(jiǎn)化了調用本地方法的過(guò)程,使用很方便,基本上不需要脫離Java環(huán)境就可以完成。
如果要和上圖做個(gè)比較,那么JNA調用C/C++的過(guò)程大致如下:

可以看到步驟減少了很多,最重要的是我們不需要重寫(xiě)我們的動(dòng)態(tài)鏈接庫文件,而是有直接調用的API,大大簡(jiǎn)化了我們的工作量。
JNA只需要我們寫(xiě)Java代碼而不用寫(xiě)JNI或本地代碼。功能相對于Windows的Platform/Invoke和Python的ctypes。
JNA使用一個(gè)小型的JNI庫插樁程序來(lái)動(dòng)態(tài)調用本地代碼。開(kāi)發(fā)者使用Java接口描述目標本地庫的功能和結構,這使得它很容易利用本機平臺的功能,而不會(huì )產(chǎn)生多平臺配置和生成JNI代碼的高開(kāi)銷(xiāo)。這樣的性能、準確性和易用性顯然受到很大的重視。
此外,JNA包括一個(gè)已與許多本地函數映射的平臺庫,以及一組簡(jiǎn)化本地訪(fǎng)問(wèn)的公用接口。
注意:
JNA是建立在JNI技術(shù)基礎之上的一個(gè)Java類(lèi)庫,它使您可以方便地使用java直接訪(fǎng)問(wèn)動(dòng)態(tài)鏈接庫中的函數。
原來(lái)使用JNI,你必須手工用C寫(xiě)一個(gè)動(dòng)態(tài)鏈接庫,在C語(yǔ)言中映射Java的數據類(lèi)型。
JNA中,它提供了一個(gè)動(dòng)態(tài)的C語(yǔ)言編寫(xiě)的轉發(fā)器,可以自動(dòng)實(shí)現Java和C的數據類(lèi)型映射,你不再需要編寫(xiě)C動(dòng)態(tài)鏈接庫。
也許這也意味著(zhù),使用JNA技術(shù)比使用JNI技術(shù)調用動(dòng)態(tài)鏈接庫會(huì )有些微的性能損失。但總體影響不大,因為JNA也避免了JNI的一些平臺配置的開(kāi)銷(xiāo)。
JNA的項目已遷移至Github,目前最新版本是4.1.0,已有打包好的jar文件可供下載。
JNA把一個(gè).dll/.so文件看做是一個(gè)Java接口,下面以一個(gè)簡(jiǎn)單的實(shí)例來(lái)說(shuō)明怎么使用。
當然要從最經(jīng)典的HelloWorld開(kāi)始,我們調用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]); } }}
運行程序,如果沒(méi)有帶參數則只打印出“Hello, World”,如果帶了參數,則會(huì )打印出所有的參數。
很簡(jiǎn)單,不需要寫(xiě)一行C代碼,就可以直接在Java中調用外部動(dòng)態(tài)鏈接庫中的函數!
下面來(lái)解釋下這個(gè)程序。
Library 或StdCallLibrary默認的是繼承Library ,如果動(dòng)態(tài)鏈接庫里的函數是以stdcall方式輸出的,那么就繼承StdCallLibrary,比如眾所周知的kernel32庫。比如上例中的接口定義:
public interface CLibrary extends Library {}
接口內部需要一個(gè)公共靜態(tài)常量:INSTANCE,通過(guò)這個(gè)常量,就可以獲得這個(gè)接口的實(shí)例,從而使用接口的方法,也就是調用外部dll/so的函數。
該常量通過(guò)Native.loadLibrary()這個(gè)API函數獲得,該函數有2個(gè)參數:
CLibrary INSTANCE = (CLibrary) Native.loadLibrary((Platform.isWindows() ? "msvcrt" : "c"), CLibrary.class);接口中只需要定義你要用到的函數或者公共變量,不需要的可以不定義,如上例只定義printf函數:
void printf(String format, Object... args);
注意參數和返回值的類(lèi)型,應該和鏈接庫中的函數類(lèi)型保持一致。
定義好接口后,就可以使用接口中的函數即相應dll/so中的函數了,前面說(shuō)過(guò)調用方法就是通過(guò)接口中的實(shí)例進(jìn)行調用,非常簡(jiǎn)單,如上例中:
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使用的簡(jiǎn)單例子,可能有人認為這個(gè)例子太簡(jiǎn)單了,因為使用的是系統自帶的動(dòng)態(tài)鏈接庫,應該還給出一個(gè)自己實(shí)現的庫函數例子。其實(shí)我覺(jué)得這個(gè)完全沒(méi)有必要,這也是JNA的方便之處,不像JNI使用用戶(hù)自定義庫時(shí)還得定義一大堆配置信息,對于JNA來(lái)說(shuō),使用用戶(hù)自定義庫與使用系統自帶的庫是完全一樣的方法,不需要額外配置什么信息。比如我在Windows下建立一個(gè)動(dòng)態(tài)庫程序:
#include "stdafx.h"extern "C"_declspec(dllexport) int add(int a, int b);int add(int a, int b) { return a + b;}
然后編譯成一個(gè)dll文件(比如CDLL.dll),放到當前目錄下,然后編寫(xiě)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); }}
有過(guò)跨語(yǔ)言、跨平臺開(kāi)發(fā)的程序員都知道,跨平臺、語(yǔ)言調用的難點(diǎn),就是不同語(yǔ)言之間數據類(lèi)型不一致造成的問(wèn)題。絕大部分跨平臺調用的失敗,都是這個(gè)問(wèn)題造成的。關(guān)于這一點(diǎn),不論何種語(yǔ)言,何種技術(shù)方案,都無(wú)法解決這個(gè)問(wèn)題。JNA也不例外。
上面說(shuō)到接口中使用的函數必須與鏈接庫中的函數原型保持一致,這是JNA甚至所有跨平臺調用的難點(diǎn),因為C/C++的類(lèi)型與Java的類(lèi)型是不一樣的,你必須轉換類(lèi)型讓它們保持一致,比如printf函數在C中的原型為:
void printf(const char *format, [argument]);
你不可能在Java中也這么寫(xiě),Java中是沒(méi)有char *指針類(lèi)型的,因此const char *轉到Java下就是String類(lèi)型了。
這就是類(lèi)型映射(Type Mappings),JNA官方給出的默認類(lèi)型映射表如下:

還有很多其它的類(lèi)型映射,需要的請到JNA官網(wǎng)查看。
另外,JNA還支持類(lèi)型映射定制,比如有的Java中可能找不到對應的類(lèi)型(在Windows API中可能會(huì )有很多類(lèi)型,在Java中找不到其對應的類(lèi)型),JNA中TypeMapper類(lèi)和相關(guān)的接口就提供了這樣的功能。
這可能是大家比較關(guān)心的問(wèn)題,但是遺憾的是,JNA是不能完全替代JNI的,因為有些需求還是必須求助于JNI。
使用JNI技術(shù),不僅可以實(shí)現Java訪(fǎng)問(wèn)C函數,也可以實(shí)現C語(yǔ)言調用Java代碼。
而JNA只能實(shí)現Java訪(fǎng)問(wèn)C函數,作為一個(gè)Java框架,自然不能實(shí)現C語(yǔ)言調用Java代碼。此時(shí),你還是需要使用JNI技術(shù)。
JNI是JNA的基礎,是Java和C互操作的技術(shù)基礎。有時(shí)候,你必須回歸到基礎上來(lái)。
(1)JNA—JNI終結者
(2)C++DLL編程詳解
聯(lián)系客服