使用JNA调用C/C++动态链接库

在编写Java程序时,我们偶尔会调用一些其他语言(主要是C和C++)写成的第三方库。它们多以.dll或.so文件的形式存在,称为动态链接库(dynamic link library),也经常称为本地库(native library)。最近工作中遇到了需要调本地库的需求,做个简单记录。

传统方法自然是使用大名鼎鼎的JNI(Java Native Interface),步骤如下:

  1. 在Java代码中定义native方法的签名,并用javah命令生成对应的头文件;
  2. 将生成的头文件和本地库一起导入一个中间库项目,编写C/C++代码调用本地库的逻辑;
  3. 把编译好的中间库放入Java项目,调用System.loadLibrary()方法载入,再调用第1步定义的native方法。

以最简单的Hello World为例(这里的“本地库”就是C标准库),代码最终长这样:

  • HelloWorld.java
class HelloWorld {
  private native void print();
  static {
    System.loadLibrary("HelloWorld");
  }
  public static void main(String[] args) {
    new HelloWorld().print();
  }
}
  • HelloWorld.h
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class HelloWorld */

#ifndef _Included_HelloWorld
#define _Included_HelloWorld
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     HelloWorld
 * Method:    print
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_HelloWorld_print
  (JNIEnv *, jobject);

#ifdef __cplusplus
}
#endif
#endif
  • libHelloWorld.c
#include <stdio.h>
#include "HelloWorld.h"

JNIEXPORT void JNICALL
Java_HelloWorld_print(JNIEnv *env, jobject obj) {
  printf("Hello World!\n");
  return;
}

由此可见,使用JNI的过程还是非常繁琐的:

  • 不能直接调用,需要建立一个中间项目做桥接(bridge);
  • 会生成大量胶水代码(glue code);
  • 桥接项目调用第三方库时必须遵循JNI的固定模式(boilerplate)。

JNA(Java Native Access)的出现则大大降低了Java代码调用本地库的难度:不需要再建立额外的项目和写其他的C/C++代码,只需要在Java程序里添加一个接口,就能方便地直接代理本地库的方法了。

首先添加JNA的Maven依赖:

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

    <dependency>
      <groupId>net.java.dev.jna</groupId>
      <artifactId>jna-platform</artifactId>
      <version>4.5.2</version>
    </dependency>
  </dependencies>

以下是最简单的示例,代理C标准库的printf()函数:

public class HelloJNA {
  public interface CLibrary extends Library {
    CLibrary instance = (CLibrary) Native.loadLibrary("c", CLibrary.class);
    // int printf(const char *format, ...)
    // char *对应String,可变参数列表对应Object...
    int printf(String format, Object... args);
  }

  public static void main(String[] args) {
    CLibrary.instance.printf("Hello World! %d", 123456);
  }
}

比JNI清爽多了。究其原因,JNA(在JNI的基础上)多做了以下两件事:

  • 将本地库抽象为继承了Library接口的接口。由于.dll、.so文件本身就是对外提供C/C++方法的容器,所以抽象成接口更符合Java程序员的思维,用起来也方便。
  • 维护了Java数据类型与C/C++数据类型之间的动态映射。众所周知,跨平台调用时数据类型不兼容是老大难问题,JNI的烦人之处就在于我们必须手动处理。而JNA可以自动转换两个平台的数据类型,编写Java代码时只需要记住映射关系就可以了。

关于JNA的具体机制,在其官方文档中有简要的说明,看官可以参考,此处不再赘述。

简单数据类型的映射关系如下表所示,可以满足绝大多数需求了。

195230-0c0fce76459906c2.png

下面讨论一下两种比较特殊的情况,即指针和回调函数。

  • 指针类型
    假设本地库代码中,一个用来分配缓存空间的C函数声明如下:
void allocate_buffer(char **bufp, int *lenp);

Java没有显式指针的概念,只有引用,所以JNA内提供了一组ByReference类型用来表示各种数据类型的指针。我们在Library接口的实现类中,可以这样代理上述函数:

void allocate_buffer(PointerByReference bufp, IntByReference lenp);

可见,整形指针用IntByReference来表示,而字符串指针(即上大一时老师会说的“指向指针的指针”)可以用PointerByReference表示,很容易理解。调用该函数的示例:

PointerByReference bufp = new PointerByReference();
IntByReference lenp = new IntByReference();
SomeLibrary.instance.allocate_buffer(bufp, lenp);
// 取出缓存空间
Pointer p = bufp.getValue();
byte[] buffer = p.getByteArray(0, lenp.getValue());

当然,由于单个指针指向的是一个内存单元,所以我们也可以用只有一个元素的数组来代替它们,在语言层面是等价的。但是这样会对理解造成困扰,所以JNA才专门提供了ByReference类型进行区分。

  • 回调函数
    大家都知道回调函数是什么,就不废话了,贴张图吧。
195230-b5f41f3502b742fb.png

C语言的回调函数本质上是当做参数传递的函数指针,前面的ByReference无法处理这种情况,所以JNA又专门提供了Callback接口,使用起来也不难。举个例子,在Linux的信号(软中断)机制中,有如下定义:

#include <signal.h>
typedef void (*sighandler_t) (int);
sighandler_t signal(int signum, sighandler_t func);
int raise(int signum);

signal()函数接收两个参数,一是信号的值,二是收到信号后的回调函数。raise()函数则用来触发信号。下面给出用JNA调用它们的完整示例:

public class JNAWithCallbackExample {
  private static int SIGUSR1 = 30;

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

    interface sighandler_t extends Callback {
      void invoke(int signum);
    }

    sighandler_t signal(int signum, sighandler_t func);
    int raise(int signum);
  }

  public static void main(String[] args) {
    // 保持引用
    CLibrary.sighandler_t func = new sighandler_t() {
      public void invoke(int signum) {
        System.out.println("Signal " + signum + " raised");
      }
    };
    CLibrary.sighandler_t signal = CLibrary.instance.signal(SIGUSR1, func);
    int result = CLibrary.instance.raise(SIGUSR1);
  }
}

可见,我们可以通过定义与回调函数类型同名的、继承Callback的接口来实现回调。需要注意,在回调函数完成之前,我们必须要保持对回调对象的引用,以防止回调逻辑被GC掉出现异常。

最后一个问题,如果需要调用的本地库函数特别多,或者存在struct、union这样的复杂结构,难道还要看着文档手动将它们翻译成Java代码吗?简单的方法自然是有的,可以借助JNAerator工具,根据C/C++代码自动生成对应的JNA代码。具体用法请参考官方Wiki

民那晚安。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值