使用 JNA 将本机 C 函数映射到 Java 接口时的指针问题

Posted

技术标签:

【中文标题】使用 JNA 将本机 C 函数映射到 Java 接口时的指针问题【英文标题】:Pointer issues when mapping native C functions to Java interface with JNA 【发布时间】:2014-12-03 10:53:55 【问题描述】:

为了正确解释这个问题,这将是一个很长的帖子,所以请多多包涵。它还可能需要了解 JNA 库 (v 4.1.0) 的内部知识,或检查其源代码的能力。

简而言之,我们在从用 C 编写的第 3 方组件获取指向本机函数的指针时遇到问题。有问题的指针似乎破坏了 JNA 功能,因为重复的指针值。当我们在另一个 JVM 进程中将 JNA 绑定作为子 JVM 进程的一部分执行时,会反复观察到该问题。

背景

我们正在与用 C 编写的用于 Windows 的第 3 方工具集成。工具制造商为我们提供了 C 头文件和一个 dll,我们必须通过我们的 Java 代码进行互操作。 dll包含暴露函数指针的结构,我们通过JNAerator映射到Java接口,我将其称为interop.dll

interop.dll 与 3rd 方工具(即预装在系统上)进行通信,因此它是一种通信 sdk。出于测试目的,我们最近收到了stub.dll(同样来自该制造商),它不需要运行或安装第 3 方工具。 interop.dll 负责决定是使用存根还是真正的 3rd 方工具,如果存根在 bin 目录中,则自动选择存根。

因此,无论如何,我们必须映射由interop.dll 公开的固定数量的函数。 为此,interop.dll 将包含以下功能:

void* (__cdecl *ObtainInterface)( const char* interfaceName );

我们会像这样在 Java 中映射它:

public interface ObtainInterface_callback extends Callback 
    Pointer apply(String interfaceName);
;
public ObtainInterface_callback ObtainInterface;

此函数用于从第 3 方工具或stub.dll 中“提取”另一个函数,然后使用其指针值将其导出到 Java 接口。换句话说,我们使用它来挖掘目标 dll 的 API,并将我们需要的其他 C 函数映射到 Java 接口。我们提取的函数在各自的 C 结构中声明,并以下列方式声明

void (__cdecl *SomeName)(Params.....)

后者由JNAerator 以类似于上述ObtainInterface 的方式自动映射。

所以,下面是我们在 Java 代码中获取接口的方法:

Pointer interface1Pointer = ObtainInterface_callback.apply("Interface1");
Interface1 interface1 = new Interface1(interface1Pointer);

Pointer interface2Pointer = ObtainInterface_callback.apply("Interface2");
Interface2 interface2 = new Interface2(interface2Pointer);

Pointer interface3Pointer = ObtainInterface_callback.apply("Interface3");
Interface3 interface3 = new Interface3(interface3Pointer);

Interface1 的构造函数如下所示(Interface2Interface3 相同):

public Interface1(Pointer peer) 
    super(peer);
    read();

注意:(对technomage's answer的回应)上述Interface1、2和3的代码是由JNAerator自动生成的,试图将带有函数的C结构映射到Java带有回调的对象。

我们已成功与interop.dll 和第 3 方工具集成。


问题

当我们切换到使用 stub dll 时,我们会收到一些来自 JNA 代码的 IllegalStateExceptionCallbackReference.java @ 第 122 行)。当我们尝试获取第三个接口Interface3 interface3 = new Interface3(interface3Pointer);时出现问题

我们下载了 JNA 的源代码并开始通过代码进行调试,以查看究竟是什么导致了问题。

read() 方法(参见上面Interface1 的构造函数)在内部为映射结构的所有成员调用readField() 方法。因为所有结构成员都是函数指针,所以readField 产生一个Callback 实例(如Pointer.java@line 419),后者导致调用本机方法long _getPointer(long addr)。对于那些感兴趣的人,本机方法看起来像这样(我不确定这是否足够相关):

dispatch.c,@line 2359

/*
 * Class:     Native
 * Method:    _getPointer
 * Signature: (J)Lcom/sun/jna/Pointer;
 */
JNIEXPORT jlong JNICALL Java_com_sun_jna_Native__1getPointer
    (JNIEnv *env, jclass UNUSED(cls), jlong addr)

    void *ptr = NULL;
    MEMCPY(env, &ptr, L2A(addr), sizeof(ptr));
    return A2L(ptr);

我们发现上述_getPointer 调用返回的地址存在问题,同时使用stub.dll 运行。以下是我们在调试时捕获的详细信息:

interface2Pointer 的值为 402394304 (0x17FC0CC0),(C 结构的指针) readField 方法在该结构中发现了 10 个函数指针,最后一个位于偏移量 36 function10 -> interface2Pointer + offset = 402394304 + 36 = 402394340 (0x17FC0CE4)。 最后,调用_getPointer(interface2Pointer.function10) = _getPointer(402394340) 将返回结构内回调的地址,当前为401814304 (0x17F33320)

interface3Pointer 重复同样的操作

interface3Pointer -> 402397356 (0x17FC18AC) 有两个带偏移量的内部函数,分别是04,通过readField方法获取: function1 -> 402397356 + 0 = 402397356 (0x17FC18AC) _getPointer(interface3Pointer.function1) = _getPointer(402397356) 然后返回402087408 (0x17F75DF0) function2 -> 402397356 + 4 = 402397360 (0x17FC18B0) _getPointer(interface3Pointer.function2) = _getPointer(402397360) 然后返回401814304 (0x17F33320) (!)

如您所见,interface3Pointer.function2 被分配了与interface2Pointer.function10 相同的指针。

现在,CallbackReference.java 在内部使用弱哈希映射来跟踪已分配给 Java 表示的回调指针,IllegalStateException 被抛出,因为该映射仍然具有对已匹配指针的引用(interface2Pointer.function10@401814304),因此无法再次插入并映射到另一个接口。

从这一点上我可以观察到三个问题:

    不同的函数产生相同的指针是否正常?也许stub.dll 对这两个操作使用相同的回调?这相当令人惊讶,因为interface2Pointer.function10 的签名与interface3Pointer.function2 不同。 弱哈希映射的使用给上面的代码带来了很大的不确定性。如果我们暂停调试器足够长的时间以发生 GC 调用,我们可以绕过异常,因此该行为可能并不总是可重现的。 我无法确定是否确实发生了 GC,我们是否会得到所需的行为。如果同一个指针首先是错误的怎么办?如果分配成功,我担心我们最终可能会调用错误的回调。

上述观察结果一致,在重新启动进程和主机操作系统之后的后续重试。我们甚至在后续执行中获得了与此处提到的相同的地址指针。

更糟糕的是,第 3 方工具制造商声称 interop.dllstub.dll 都没有可能导致上述行为的问题。

更新 为了响应 cmets,我在这里添加了原生函数的签名:

interface2.function10:

void (__cdecl *function10)( CallbackWithFunction10EventInfo cb, void* userData );

interface3.function1:

void (__cdecl *function1)(CallbackWithNoData cb, void* userData, int value );

interface3.function2:

void (__cdecl *function2)(CallbackWithNoData cb, void* userData);

签名说明

虽然这两种方法的第一个参数cb 显然具有不同的类型,但CallbackWithFunction10EventInfoCallbackWithNoData 是“分层”相关的并非不可能(就像某种伪造的继承,这在某些情况下是可能的C) 中的情况。这样的事情会影响返回的指针值吗?


一些断言

我们还通过interop.dll 和真实工具调试了在删除存根 dll 并使用工作集成时返回的指针值。我们的java代码还是一样的。

interface2Pointer -> 401508620 (0x17EE890C)

function10 -> interface2Pointer + offset = 401508620 + 36 = 401508656 (0x17EE8930)

_getPointer(interface2Pointer.function10) = _getPointer(401508656) = 400857536 (0x17E499C0)

interface3Pointer -> 401508920 (0x17EE8A38)

function1 -> interface3Pointer + offset1 = 401508920 + 0 = 401508920 (0x17EE8A38)

_getPointer(interface3Pointer.function1) = _getPointer(401508920) = 401018032 (0x17E70CB0)

function2 -> interface3Pointer + offset2 = 401508920 + 4 = 401508924 (0x17EE8A3C)

_getPointer(interface3Pointer.function2) = _getPointer(401508924) = 401017424 (0x17E70A50)

显然,非存根地址是唯一的,我们可以进行互操作。


我们的设置

代码正在使用 Microsoft Windows XP 的虚拟机上执行,并驻留在 阴影 jar 中。我们使用 JDK/JRE 1.6 和 JNA 4.1.0 版。

我们的测试和执行场景提供了 3 种方式来执行执行互操作绑定的 Java 进程:

    独立进程 - 与真实工具配合良好,在 stub.dll 下静默失败 另一个 JVM 进程的子进程 - 与真实工具配合良好,将讨论的 IllegalStateExceptionstub.dll 一起抛出。 另一个 JVM 进程的子进程,但是我们注释掉了 interface2interface3 绑定。事情正常

我们在第 2 步和第 3 步中用于启动子 Java 进程的命令行是:

java -cp our-shaded.jar main.class.package.Application

在调试的时候,我们加上-Xdebug -Xrunjdwp:transport=dt_socket,address=8998,server=y

更新

虽然只是执行一些额外的断言,但在独立进程执行的情况下检查stub.dll 返回的指针是值得的(如上面的第 1 点)。结果既令人困惑,又给了我们一些方向。 Standalone 进程获得了唯一的指针,其方式与使用真实工具的方式类似。因此,原因可能是子进程和一些共享内存或本地代码和子 Java 进程之间暴露的内存限制......


问题

如果问题是由我们的使用或存根 dll 本身引起的,我将不胜感激(我会责怪后者)。如果他们的代码确实存在问题,我们可能需要说服第三方制造商,否则我们可能没有机会获得新版本的存根,这意味着我们应该寻找解决方法。因此,欢迎您在这方面提供任何帮助或解决方法提示。

【问题讨论】:

什么冲突函数的本机签名(假设它们始终是相同的函数冲突)? @technomage,好电话,我在帖子中添加了签名 我还在断言部分添加了一个更新,现在我们认为问题可能在于子 JVM 进程没有正确处理本机代码。 将内存地址表示为十六进制值而不是十进制值很有用(例如 0xFFFE8012)。这通常会暴露被十进制表示隐藏的模式。 @technomage,你去吧,我在十进制指针的括号中添加了十六进制值 【参考方案1】:

将函数指针唯一映射到回调引用的目的是在回调映射中暴露程序错误,并提供一种在本机指针超出范围时自动释放内存的方法。通常,C 函数指针具有单个可接受的签名(可变参数语义和强制转换除外)。如果单个本机指针映射到多个 Java 对象,清理也会变得更加复杂。

您的本机代码可能会动态分配函数指针,在这种情况下,特定的指针可能最终会被重用(尤其是在本机代码使用显式内存池的情况下)。如果是这种情况,您可能只需要清除弱哈希映射(JNA 不会公开这一点,但使用一些自定义代码在映射上调用 .size() 将是微不足道的)。

本机代码也可能使用占位符函数,其中占位符或通用函数被重用(通常方法签名相同)。如果是这种情况,错误将是确定性的(这似乎不是您的情况)。

或者,本机代码可能使用单个调度函数(这听起来不像,否则您会在第一个函数指针之后看到错误)。

我想指出,如果您实际上将本机 struct 映射到 JNA Structure,它可能会更加容易。这将为您避免手动提取和初始化接口指针。 JNA 完全能够在 Structure 中初始化大量函数指针(即回调)。

更新

鉴于function10function2 实际上具有相同的签名((*)(), void*),您的存根库很可能正在使用占位符函数(例如“_not_implemented”)。如果您没有积极使用这些功能,您可以简单地将它们更改为具有相同的界面(现有的或您编写的)。这将绕过 JNA 限制。

可以说 JNA 可以取消此限制,或提供一种解决方法,但这需要在 JNA 中更改代码。即使这是本机代码在以后(及时)上下文中重新使用函数指针的问题,您也需要调整 JNA 以便能够有目的地刷新旧映射(假设它确实不再使用)。

【讨论】:

事实上Interface1,2,3 是由JNAerator 作为JNA 扩展结构生成的。当 C 结构的 Java 对象对应物调用生成的 read 方法时,会发生错误,同时分配包含的回调。我将在帖子编辑中澄清这一点 重新阅读您的帖子似乎支持存根 dll 正在使用占位符函数,或者我们应该使用弱哈希映射以使 JNA 重用指针。当我们使用子进程时,行为的差异支持后者,因为本机进程可能会重用指针地址,以防在内存受限的情况下执行。我们决定通过忽略存根而不深入挖掘 JNA(进行修改)来更快地完成我们的工作。再次感谢您的所有帮助,我明白即使有了这些信息,也很难诊断出问题

以上是关于使用 JNA 将本机 C 函数映射到 Java 接口时的指针问题的主要内容,如果未能解决你的问题,请参考以下文章

无法使用 JNA 找出本机类型的映射

JNA 结构和指针映射

Java JNA 本机库调用 NoClassDefFoundError

在 Windows 上使用 JNA 调用 __cpuid 函数

JNA C语言与Java类型转换(不定期更新)

使用 JNA 本机等待调用检测线程中断 (Windows)