我知道编辑这个网站吗?

使用本机库进行开发

本节内容已经过时,可能具有误导性或无效。请谨慎对待这里的任何说明。如有疑问,请向社区求助

简介

有时,您可能希望无法作为 Java 组件提供的第三方库使用,而只能作为本机库使用。

有选项可用于使用本机库:两个JNA 和 JNI(详细说明见下文)。

利用重建库的缺点

尽管 JNA 和 JNI 为使用本机库提供了一些便利,但使用本机库也有很大的缺点:

  • 使用本机库与Java的平台无关背道而驰

即使 ImageJ 只支持 32 位/64 位 Windows、macOS 和 Linux(暂时还支持 macOS 的 PowerPC),维护起来也是一大麻烦。这也意味着你会遇到成功 C/C++ 的问题,其中在一个平台上编译的库并不意味着可以在其他平台上编译。甚至有一些 C 代码在同一平台上可以很好地编译 32 位但不能编译 64 位 CPU 的示例。

  • 没有“编译一次,到处运行”

Java 的最大优势之一是,您可以为您的计划(或任何其他人计划)运行的每个平台进行编译。重建库扩大了这一优势。

  • 通过使用本机库,更容易产生导致整个Java虚拟机崩溃的致命错误。因此,使用本机库时调试可能非常困难。

有关使用 Java 而不是本机库的更多参数,请参阅我们的rationale for using Java

JNA 与 JNI

JNA相对于JNI的优点是:

  • 可编写脚本 -你不需要C编译器
  • 可以将本机库投入附加平台,而消耗任何重新编译

JNI相对于JNA的优点是:

  • Java的官方部分
  • 更快
  • JNI 是“类型更安全”(即 Java 和本机代码通过相同的类型定义访问数据)

JNA

JNA project (Java Native Access)试图提供一种简单、纯Java的方式来使用其本机接口访问本机库。

函数

为此,开发人员必须定义一个接口,用 Java 术语描述该库提供的功能。例如:

import com.sun.jna.Library;

public interface C extends Library {
        public int symlink(String oldpath, String newpath);
}

注意:

  • 即使包名称为com.sun.jna,JNA也不是Java的官方部分。因此,JNA支持的平台少于Java本身。
  • 您不需要声明本机库提供的所有功能。在此示例中,仅声明了 symlink
  • Java 类型String映射到 C 层中的const char *。对于其他原始 Java 类型也发生同样的情况。
  • 由于参数需要在调用本机函数进行映射,并在返回后映射回来,并且所有这些映射都是使用引用完成的,因此与 JNI 相比,JNA 相对较慢(除非复制数据所耗费的时间超过了本机库中所耗费的处理时间)。
  • 编译器不保证接口正确。事实上,您很容易错误地声明函数,从而导致Java虚拟机严重崩溃。
  • 注意:要在BeanShell中定义此接口,您需要使用语法interface C implements Library,而不是使用关键字extends

以这种方式使用该库:

import com.sun.jna.Native;

C c = (C)Native.loadLibrary("c", C.class);
int result = c.symlink(source, target);

指定库搜索路径

如果您想要使用未安装的库在您的平台默认查找库的位置之一,您可能需要找到告诉 JNA 在哪里可以该库:

NativeLibrary.addSearchPath("opencv", "C:\\opencv");
OpenCV openCV = (OpenCV)NativeLibrary.loadLibrary("opencv", OpenCV.class);

注意:某些库依赖于其他库。如果依赖项不在默认库路径中,则可能无法在不重新启动虚拟机的情况下从 Java 中加载它们,具体取决于平台。在 Linux 上,例如您需要相应的环境变量LD_LIBRARY_PATH(在 Java 进程中设置它们没有帮助,因为动态加载器已经初始化并且在初始化后不该变量的更改)。

常量/枚举

如果 C 头文件使用 #define 语句定义了常量,则在编译的本机库中找不到该常量。同样,C 编译器优化了枚举的名称。因此,常量和枚举都需要在接口中定义。

例如,这个C头文件:

#ifndef MY_HEADER_H
#define MY_HEADER_H

#define OFF 0
#define ON 0xff

enum counter_t {
        ZERO,
        ONE,
        TWO,
        THREE
};

extern counter_t get_counter(void);
#endif

需要使用基于JNA的接口进行处理,如下所示:

public interface MyLibrary extends Library {
        public final int OFF = 0;
        public final int ON = 0xff;
        public final int ZERO = 0;
        public final int ONE = 1;
        public final int TWO = 2;
        public final int THREE = 3;

        public int get_counter();
}

结构

有些函数不采用简单的数据类型作为参数,而是采用所谓的结构。这些必须被定义为接口的内部静态类,并且它们需要扩展类com.sun.jna.Structure

import com.sun.jna.Library;
import com.sun.jna.Structure;

public interface C extends Library {
        public static class timeval implements Structure {
                long tv_sec, tv_usec;
        }
        public static class timezeone implements Structure {
                int tz_minuteswest, tz_dsttime;
        }
        public int gettimeofday(timeval timeval, timezone timezone);
}

结构的某些字段可能是固定大小的吞吐量(例如unsigned char path[1024])。这些字段应使用 Java 中的默认初始值设定项进行声明(例如byte[] path = new byte[1024];)。

####通过指针访问结构

您调用的函数可能会返回指向结构的指针。要初始化此类结构的 Java 版本的字段,您可以使用 useMemory(Pointer)read() 方法:

...
        public static class MyStruct {
                public MyStruct(Pointer p) {
                        // cannot use super(p) because of fixed-size array fields
                        super();
                        useMemory(p); // set pointer
                        read(); // initialize fields
                }

                public MyStruct() {
                        super();
                        // handle fixed-size array fields correctly
                        ensureAllocated();
                }
        }
...

注意:当调用超类的构造函数时,固定大小的读写字段尚未初始化。因此,超类的构造函数无法正确处理它们:它们的大小未知。这就是为什么你必须调用ensureAllocated()以及为什么你不能使用超类的super(Pointer)构造函数的原因。从技术上讲,当你的结构体不固定包含大小的读写字段时,你可以,但是习惯于总是避免使用super(Pointer)构造函数将帮助您避免麻烦。

按值传递结构

将结构实例传递给函数时,会自动分配和加载内存,并传递指针。

如果您想按值传递结构,则必须其进行任何子类化并实现 Structure.ByValue 接口。该接口纯粹是一个标签,不需要定义额外的函数。

例子:

public class Timespec extends Structure {
    long tv_sec;
    long tv_usec;
}

public class Stat extends Structure {
    long /* dev_t */      st_dev;     /* ID of device containing file */
    long /* ino_t */      st_ino;     /* inode number */
    long /* nlink_t */    st_nlink;   /* number of hard links */
    int /* mode_t */      st_mode;    /* protection */
    int /* uid_t */       st_uid;     /* user ID of owner */
    int /* gid_t */       st_gid;     /* group ID of owner */
    int __pad0;
    long /* dev_t */      st_rdev;    /* device ID (if special file) */
    long /* off_t */      st_size;    /* total size, in bytes */
    long /* blksize_t */  st_blksize; /* blocksize for file system I/O */
    int /* blkcnt_t */    st_blocks;  /* number of 512B blocks allocated */
    Timespec /* time_t */ st_atime;   /* time of last access */
    Timespec /* time_t */ st_mtime;   /* time of last modification */
    Timespec /* time_t */ st_ctime;   /* time of last status change */
}

public class StatByValue extends Stat implements Structure.ByValue {
    public StatByValue(Stat stat) {
        super();
        ensureAllocated();
        byte[] buffer = new byte[size()];
        stat.getPointer().read(0, buffer, 0, buffer.length);
        getPointer().write(0, buffer, 0, buffer.length);
        read();
    }
}

编写 JNA 脚本

BeanShell中,不可能扩展接口因此,不可能修改普通的Java方式来使用JNA。就JNA而言,其他脚本语言也存在类似的问题。

但是你可以使用NativeLibrarygetFunction(String)方法来获取一个函数对象,它的方法invokeInt(Object[])invokePointer(Object[])和朋友将允许你调用该函数。

如果结果不是基本类型,您可以使用Pointer的方法来访问数据。 BeanShell 示例:

import com.sun.jna.NativeLibrary;

// get the C runtime library
c = NativeLibrary.getInstance("c");

// retrieve the getenv() function and call it
getenv = c.getFunction("getenv");
print(getenv.invokePointer(new Object[] { "HELLO" }).getString(0));

// retrieve and use the setenv() function
setenv = c.getFunction("setenv");
print(setenv.invokeInt(new Object[] { "HELLO", "world", new Integer(1) }));

// show that it did something
print(getenv.invokePointer(new Object[] { "HELLO" }).getString(0));

// note that System.getenv() remains oblivious
print(System.getenv("HELLO"));

可能会出现严重的复杂情况:某些函数名称实际上并不是引用本机库中的函数,而是由赋值器指向另一个函数。示例:至少在 Linux 上,lstat()标记调用标注附加参数的__lxstat()

另外,您选择的脚本语言中定义的类可能不适用于 JNA。其一,BeanShell 添加了两个 JNA 无法(也不应该)处理的字段。作为解决方法,您可以使用 Pointerget 系列方法。

例子:

import com.sun.jna.Memory;
import com.sun.jna.NativeLibrary;

import java.util.Date;

c = NativeLibrary.getInstance("c");
lstat = c.getFunction("__lxstat");
errno = c.getFunction("errno");

path = System.getProperty("imagej.dir");
print(path);
stat = new Memory(144);
result = lstat.invokeInt(new Object[] { new Integer(0), path, stat });

print("result: " + result);
if (result < 0) {
    strerror = c.getFunction("strerror");
    err = errno.getInt(0);
    error = strerror.invokePointer(new Object[] { new Integer(err) }).getString(0);
    print("errno: " + error + " (" + err + ")");
}
print("blocks: " + stat.getInt(64));
print("atime: " + new Date(stat.getLong(72) * 1000));
print("mtime: " + new Date(stat.getLong(88) * 1000));
print("ctime: " + new Date(stat.getLong(104) * 1000));

JNI

缩写 JNI 代表 Java Native Interface。它是从 Java 内部访问本机库的原始且最受支持的方式。因此,它很强大,但使用起来也有点麻烦。

第一步

在实现任何本机(C 或 C++)代码之前,您需要声明本机函数。例子:

public class Hello_World_JNI {
        public native void helloWorld();
}

这告诉JVM方法helloWorld()是在本机库中实现的(必须单独加载)。

接下来是生成一个C标头,其中包含实现该方法的C函数的声明:

javac Hello_World_JNI.java
javah -classpath . Hello_World_JNI

请注意,执行文件javah仅适用于.class文件,因此我们必须首先编译.java源文件。输出是.h文件。

此后,执行实际工作的需要在单独的.c文件中代码实现。这包括由javah生成的头文件,并编译为共享库:Windows上的.dll,macOS上的.dylib(Apple的Java也使用扩展.jnilib),以及其他地方的.so

在调用本机方法,JVM需要加载本机库。基本上有两种不同的方法可以做到这一点,System.loadLibrary()System.load()之前。之前在系统范围的库搜索路径因为中查找库,而稍后则希望动态链接库文件的绝对路径。就ImageJ而言,通常首选,我们希望避免需要用户的管理员权限。

ImageJ 中的支持

本机库位于:

<ImageJ-directory>/lib/<platform>/

将自动添加到java.library.path,将它们引入到Java区域-它们仍然需要加载。加载的一种是选择创建一个Service,在其initialize()方法中加载本机库。例如,参见ITK compatibility layer

Fiji 构建系统名称还通过使用 gcc 编译以 .c 结尾的源文件来支持本机目标。如果找到 C 源代码,将调用 javah,并使用 GCC 编译本机库,然后生成的共享库存放 <fiji-directory>/lib/<platform>/ 目录中。

最后,fiji-lib.jar中的fiji.JNI类提供了加载本机库的便捷方法。例子:

static {
        JNI.loadLibrary("hello-world");
}

使用 C 中的 Java 本机接口

从C访问Java类、实例和方法时需要牢记以下几点。

最重要的是:Java有自己的内存管理。与C不同,它在需要时移动事物。在C中,一旦获得了某些数据的地址,程序员就必须负责确保内存范围有效,直到没有变量再保留该范围的任何引用(地址)。要访问存储在Java虚拟机内存范围(“堆”)中的数据,必须将这些数据“固定”到某个内存位置,并告诉JVM何时可以重新移动数据。

使用 JNI 时,此类固定问题是最糟糕的事情,因为很容易在测试阶段作业的草率代码中产生。

Java对本机函数的每次调用都会传递JNI环境的引用作为第一个参数。这是一个指向内部状态变量的不透明指针,每次与Java对都需要这些变量。例子:

JNIEXPORT void JNICALL Java_Hello_1World_helloWorld
    (JNIEnv *env, jclass object, jstring message);

通常,您将大量使用 jbytejshortjintjlong 等提供的类型。这些类型通常与本机 C 类型 charshortintlong相同(但并非总是;本质,问题涉及跨平台细节)。

一个有意义的例外是jchar,它与char不同。Unix的作者以他们无限的智慧,只需要7位或最多决定8位来编码文本。Java的作者知道错误的,因此jchar指的是16位Unicode字符。在C中,您通常使用UTF-8(为了和节省内存),因此请确保使用JNI API的*UTF函数(例如,NewStringUTF()而不是NewString())。

###调用JNI API

JNI API 中有很多函数,几乎所有函数都以函数指针的形式存储在 JNIEnv 实例中。由于其中许多需要访问环境来与用户的隐藏状态变量进行交互,因此大多数调用如下所示,将环境作为第一个参数传递回函数:

(*env)->NameOfTheFunction(env, ...);

调用方法和访问字段

如果需要从 C 调用 Java 方法,首先需要该类的引用。注意:类名必须以 UTF-8 格式和斜杠格式提交,而不是点分形式。例子:

jclass image_plus_class = (*env)->FindClass(env, "ij/ImagePlus");

原始类型没有相应的 jclass 实例,每个原始类型都有单独的访问器(如果适用)。

如果您需要访问仓库,请在类名前面加上左方括号,例如“[ij/process/ImageProcessor”;

基本类型引用很特殊:“类名称”是大写字母,例如 B 代表 byteI 有趣代表 int,(的)Z 代表 boolean。相应的类名称为 [B[I[Z

确定类名的最简单方法是在Java中实例化该类并在实例上调用instance.getClass().getName()

要调用方法,您需要首先获取方法id:

jmethodID get_title_method = (*env)->GetMethodID(env,
    image_plus_class, "getTitle", "Ljava/lang/String;()");

第四个参数是签名,指定输入参数和返回值的类型。精确签名的最简单方法是在类上调用javap,传递-s选项以显示签名以及人类可消化的信息。

获得方法id后,您可以调用该方法,并提交相应类的实例:

(*env)->CallVoidMethod(env, instance, image, get_title_method);

Call<return-type>Method() 函数族采用可变数量的参数。请务必小心输入正确的数量和类型的参数!

###一些技巧

  • 永远不要假设对方法的引用,甚至JNIEnv参数在本机函数之间是持续的调用。很可能导致整个 JVM 瘫痪的分段错误/访问冲突的根源。

  • 最终确保通过 JNI 访问的内存大幅释放。这不仅可以避免内存泄漏,广东省由于阻止 JVM 的内存管理执行其任务而导致性能问题。

  • 为了性能,您应该尽量减少 FindClass()GetMethodID() 的使用:虽然 Java 7 承诺这些函数有更好的性能,但仍然有很多人使用 Java 5 或 Java 6,这些调用速度很慢。

  • 注意编译器的警告。他们不只是为了好玩。

注意事项

最重要的许多问题是您应该知道您需要支持哪些平台(操作系统和系统结构的组合,即i386 Windowsx86_64 Windows是不同的)并确保本机库。否则,您的用户将看到无用的UnsatisfiedLinkError异常存在。

通常,您不需要担心这些问题,因为斐济可以方便地向您隐藏这些问题(这是斐济的使命之一,隐藏不必要且烦人的繁琐细节)。

  • C 源代码需要使用定义的 _JNI_IMPLEMENTATION_ 符号进行编译。这是由于 Windows 要求 .dll 文件明确标记哪些符号将由库提供以及哪些符号需要从另一个库导入。

  • 在 Windows 上,默认情况下符号会自动版本控制。这会干扰 JNI 所需的常量名称。因此,您需要使用 GCC 链接器选项-kill-at编译 C 源代码。如果您要求 GCC 为链接,则需要将选项设置为 -Wl,-kill-at 传递。

  • java 属性java.library.path 需要设置为可以找到.so.dylib/.jnilib.dll 文件的路径(分别适用于 Linux/BSD/Haiku/etc、macOS 和 Windows)。

  • java.library.path 属性之外,环境变量 LD_LIBRARY_PATHDYLD_LIBRARY_PATHPATH 分别应在 Linux/BSD/Haiku/etc、macOS 和 Windows 上进行相应调整。

  • 如果您加载一个库,并且该库还需要加载另一个库,则应设置搜索路径,以便动态链接器在与原始库相同的目录中查找,小区必须调整系统范围的搜索路径(本管理员权限)。GCC链接器选项名为§§§0§§需要§(请注意,您必须阻止命令行扩展$字符)。

进一步阅读

有关 JNI 的完整信息,请参阅Sun’s/Oracle’s programmer guide on JNI

快速参考

本页推出了有关在 ImageJ 支持的不同环境中使用本机库的一些提示。

行动 Linux macOS 窗口
上市库的依赖项 ldd <library-file> otool -L <library-file> objdump -p <library-file> | grep "DLL Name:"
跟踪系统调用 strace -Ffo syscall.log ./fiji <args> dtruss ./fiji <args> 使用Sysinternal’s Process Monitor