用途
JNA(Java Native Access)在运行时把 Java/Kotlin 接口映射到已编译的 C 库上,不做任何链接期检查。字段顺序写错、open 标志写错、回调被回收等错误在链接时都是静默的,往往很久之后才在别处暴露。该技能用于指导如何防御性地编写 JNA 绑定,并记录足够信息以区分正常与损坏的配置。
主要陷阱与应对
-
open-flags 选项仅适用于 POSIX:Library.OPTION_OPEN_FLAGS 会原样传给平台的加载调用,JNA 不做翻译。在 POSIX 上 2 表示立即解析全部符号;在 Windows 上同一个 2 会让加载器把文件当普通数据映射,看起来加载成功,但所有符号查找都会失败,报错点名函数却归咎于缺少模块。示例在 Windows 分支使用空 map,在 POSIX 分支才设置该选项。
-
记录实际打开的文件:应记录 NativeLibrary.getInstance(name).file?.absolutePath,而非请求的名称。在装有系统级副本的机器上,正常与损坏的打包都能加载并工作,解析路径是唯一区分依据。
-
裸库名不是文件名:JNA 会把 "foo" 映射为平台约定(libfoo.so、libfoo.dylib、foo.dll)。打包通常只含带版本的文件而无无版本符号链接,导致裸名查找失败并静默回退到系统副本。应优先尝试实际打包文件的绝对路径,裸名仅作后备。
-
jna.library.path 的读取时机应以字节码为准:反汇编固定 jar 可见该属性在每次加载调用时重新读取,而静态初始化器自行从其他来源填充搜索集,仅作为首次打开失败后的后备。应针对属性名而非字段名验证,且结论随 JNA 版本变化,每次升级都需重跑。NativeLibrary.addSearchPath(name, dir) 仍是按库名显式设置搜索路径的 API。JVM 自身的 System.loadLibrary 不遵循 JNA 属性。
-
结构体按原始偏移读取:上游增删或重排字段会直接读到错误字节而非链接错误。只映射稳定前缀(自所支持最旧 API 版本起就存在的字段)适用于只读结构体;需要写回的结构体必须完整。应比较运行时 API 版本与编写布局时的版本,主版本不同时大声记录日志,并注明布局对应的库版本与头文件。
-
回调被弱引用持有:CallbackReference 继承 WeakReference<Callback>,回调被回收后原生代码持有指向空处的函数指针,失败与注册代码无关联。注册期间应把回调保存在字段中,释放其指向对象前先注销。
-
数组参数没有终止符:接受以 NULL 结尾的 char ** 的 C 函数需要调用方自行补末尾 null 元素,JNA 不会追加。
-
返回调用方拥有的缓冲区应映射为 Pointer:C 契约要求调用方归还结果时,映射为 String 会丢失地址,导致内存永不回收。应映射为 Pointer、读出值后调用库自身的释放函数。
验证方式
验证命令针对实际发布的固定 jar 运行(示例为 jna-5.19.1.jar,需排除 sources 包),部分步骤还需从仓库根目录运行。使用 javap 检查三项字节码结论:选项键的值流入 Native.open(String, int);每处 jna.library.path 命中归属 loadLibrary(...) 而非 static {};CallbackReference 继承 WeakReference<Callback>。另有 grep 检查真实使用者是否把 OPTION_OPEN_FLAGS 放在 Platform.isWindows() 的 else 分支中。