Java中使用JNA调用C函数

引言

在Java混沌初开的年代,如果想跨语言调用C语言或其他语言编译生成的动态库,则需要通过JNI(Java Native Interface)来实现。
而本文所讲述的JNA(Java Native Access),是个开源的Java调用本地(native)方法的框架,由Sun公司在经典JNI的基础上开发。
JNA与JNI相比,最大的优点是,在已有.dll/.so动态库的情況下,不需要编写任何除Java以外的代码,所有本地调用的代码全部由Java编写,基本上不需要安装任何额外的C语言编译环境。
它的出现大大简化了 Java调用本地方法的过程,使用起来非常方便,可以说是JNI的颠覆者。

技术原理

JNA使用一个小型的JNI库插桩(Stub)程序来动态调用本地代码。开发者使用Java接口来描述目标动态库的功能和结构,这使得它很容易使用当前系统的功能函数,而不会产生多平台配置和生成JNI代码的高开销。
此外,JNA包括一个已与许多本地函数映射的平台库,以及一组简化本地访冋的公用接口。
在JNI中,开发者必须手动用C/C++语言编写一个动态库,以映射Java的数据结构。而在JNA中,它提供了一个动态的C语言编写的转发器,可以自动实现Java和C的数据类型映射,不再需要手动编写C动态库。
JNA支持大多数的常见系统平台,包括Win32(x86/x64)、Linux(x86/x64/arm)、FreeBSD(x86/x64)和OpenBSD(x86/x64)等。

简单示例

首先需要添加JNA的依赖,可以通过Maven方便地添加:

<dependency>
      <groupId>net.java.dev.jna</groupId>
      <artifactId>jna</artifactId>
      <version>4.5.1</version>
</dependency>

以下是几种常见的目标本地函数/C函数,包括C标准函数、Win32函数和C动态库函数。

C标准库函数

我们现在需要在Java中调用C标准函数的printf()函数:

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

public class CStdLibrary {

  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, %s!\n", "World");
  }
}

Win32函数

在Windows平台,微软提供了丰富、强大的Win32函数,利用JNA可方便的调用。我们以该函数在kernel32.dll库中GetSystemTime()函数为例,该函数的声明为void WINAPI GetSystemTime( _Out_ LPSYSTEMTIME lpSystemTime ),其中LPSYSTEMTIME结构的定义如下:

typedef struct _SYSTEMTIME {
  WORD wYear;
  WORD wMonth;
  WORD wDayOfWeek;
  WORD wDay;
  WORD wHour;
  WORD wMinute;
  WORD wSecond;
  WORD wMilliseconds;
} SYSTEMTIME, *PSYSTEMTIME;

如果我们想在Java中直接调用GetSystemTime()函数,就必须先把LPSYSTEMTIME结构给映射出来:

public static class SystemTime extends Structure {

    public short year;
    public short month;
    public short dayOfWeek;
    public short day;
    public short hour;
    public short minute;
    public short second;
    public short millisecond;

    protected List<String> getFieldOrder() {
      return Arrays.asList(
          "year", "month", "dayOfWeek", "day", "hour", "minute", "second", "millisecond");
    }
  }

上述代码展示了一个内部的静态类,继承了JNA的Structure基类。由于结构体映射过程中成员的顺序必须保持一致,因此我们还要实现getFieldOrder()方法来手动指定成员的顺序。
请注意,Win32 C结构体中的WORD类型,实质为基本类型中的unsigned short,它被映射为Java中的short类型。详细的类型映射对照表如下:

C基本类型 长度 Java类型 Windows类型
char 8位整型 byte BYTE、TCHAR
short 16位整型 short WORD
wchar_t 16/32位字符 char TCHAR
int 32位整型 int DWORD
int 布尔值 boolean BOOL
long 32/64位整型 NativeLong LONG
long long 64位整型 long __int64
float 32位浮点型 float
double 64位浮点型 double
char* C字符串 String LPTCSTR
void* 指针 Pointer LPVOID、HANDLE、LPXXX

无符号类型(unsigned)同有符号类型一致。
我们回到刚刚的GetSystemTime()函数。在构造好参数结构后,就可以编写映射接口并调用了:

public interface Kernel32 extends StdCallCallback, Library {
    Kernel32 INSTANCE = (Kernel32) Native.loadLibrary("kernel32", Kernel32.class);
    Kernel32 SYNC_INSTANCE = (Kernel32) Native.synchronizedLibrary(INSTANCE);

    void GetSystemTime(SystemTime result);
  }

  public static void main(String[] args) {
    Kernel32 lib = Kernel32.INSTANCE;
    SystemTime time = new SystemTime();
    lib.GetSystemTime(time);

    System.out.println("Today's integer value is " + time.day);
  }

该函数在kernel32.dll中,所以我们需要载入名为kernel的本地库。请注意,映射被__stdcall修饰的Win32函数需要继承StdCallLibrary,而不是Library(大多数C标准库和动态库只需要继承Library)。
上面代码中除了有Kernel32 INTSANCE的实例之外,还有一个Kernel32 SYNC_INSTANCE实例,后者可以保证同时只有一个对该对象的访问。
映射后GetSystemTime()的方法签名为void GetSystemTime(SYSTEMTIME result),这里我们只需要传递对象引用即可。

C动态库

接下来就是我们自己编写C动态库并利用JNA调用它了。用C++编写DLL动态库,导出为C格式。
MyCFunc.h

#define DllExport _declspec(dllexport)
#ifdef __cplusplus
extern "C" {
#endif
    DllExport void HelloWorld();
    DllExport int IntSquare(int x);
#ifdef __cplusplus
}
#endif

MyCFunc.cpp

#include "stdafx.h"
#include "MyCFunc.h"
void HelloWorld() {
    std::cout << "Hello World!" << std::endl;
}

int IntSquare(int x) {
    return x * x;
}

将编译生成的dll放进Classpath,然后编写Java接口来映射该动态库并调用:

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

public class MyCLibrary {

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

    void HelloWorld();
    int IntSquare(int x);
  }

  public static void main(String[] args) {
    DynamicLibrary lib = DynamicLibrary.INSTANCE;

    lib.HelloWorld();
    System.out.println(lib.IntSquare(5));
  }
}

可以看到JNA调用过程与之前并无太大差别,十分简便,不需要编写任何额外的非Java代码。

直接映射

在使用JNA的时候,我们除了可以把本地库映射为Java接口,也可以像JNI那样映射到具体的native方法,这种技术被称为直接映射(Direct Mapping)。请注意,使用该方法调用本地库时需要特别注意对象的生命周期,在实践中它很快就会被垃圾回收。
用直接映射的方式调用C标准库:

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

public class DirectMapping {

  public static native double sin(double x);
  public static native double cos(double x);

  static {
    Native.register(Platform.C_LIBRARY_NAME);
  }

  public static void main(String[] args) {
    System.out.println("sin(0)=" + sin(0));
    System.out.println("cos(0)=" + cos(0));
  }
}

可以看到这里的代码和经典的JNI代码已经很像了,本地方法同样需要标注为native。
以之前那个自己编写的MyCFunc.dll动态库为例:

import com.sun.jna.Native;

public class DirectMapping {

  static {
    Native.register(Platform.C_LIBRARY_NAME);
  }

  public static native void HelloWorld();
  public static native int IntSquare(int x);

  static {
    Native.register("MyCFunc");
  }

  public static void main(String[] args) {
    HelloWorld();
    System.out.println(IntSquare(9));
  }
}

简单吧!

高级示例

本节中,我们使用以下3个C函数来演示回调等高级用法:

int SentryInitV2(SocketType iSocketType);
int SentryStartLocalListen(int port, fDeviceStatusChanged pFun, void *pContext);
int SentrySetMultiAlarmLight(LONG lDevHandle, const SentryMultiAlarmLight *pMultiLightStatus);

首先来映射第一个函数。该函数和以下几个函数都采用标准的接口映射。
第一个函数十分简单,形参中的SocketType实质就是int,如下映射、调用就好:

public static final int SocketType = 0;

int SentryInitV2(int iSocketType);

int ret = SentryInitV2(SocketType);

第二个函数涉及到了回调,所以会比较麻烦。我们先来看看回调函数的定义:

typedef int (CALLBACK *fDeviceStatusChanged)(LONG lDevHandle, int iDeviceVendor, char *pszAddress, int iPort, BOOL bOnline, void *pContext);

要把回调函数映射到Java,需要定义一个内部的、继承JNA的Callback的接口,并声明invoke(),该invoke()的方法签名就是原回调函数的映射:

interface fDeviceStatusChanged extends Callback {

  int invoke(NativeLong lDevHandle, int iDeviceVendor, String pszAddress, int iPort,
      boolean bOnline, Pointer pContext);
}

回调函数的接口定义完毕后,接下来就可以按照自己的需求实现该回调函数接口,然后就是逐一映射原函数的参数即可:

int SentryStartLocalListen(int port, fDeviceStatusChanged pFun, Pointer pContext);

第三个函数涉及到了结构体,原结构体定义如下:

typedef struct tagSentryMultiAlarmLight
{
  int lightStatus[32];
} SentryMultiAlarmLight;

按照本文前述的结构体映射方法,继承JNA的Structure类,将SentryMultiAlarmLight结构体映射为Java的内部类:

  public static class SentryMultiAlarmLight extends Structure {

    public int[] lightStatus = new int[32];

    @Override
    protected List<String> getFieldOrder() {
      return Collections.singletonList("lightStatus");
    }
  }

最后声明该函数的映射方法即可:

int SentrySetMultiAlarmLight(NativeLong lDevHandle, SentryMultiAlarmLight pMultiLightStatus);