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

时间:2014-12-03 10:53:55

标签: java c dll interop jna

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

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

背景

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

interop.dll与第三方工具(预先安装在系统上)进行通信,因此它是一种通信sdk。出于测试目的,我们最近提供了stub.dll(再次来自该制造商),它不需要运行或安装第三方工具。 interop.dll负责决定是使用存根还是真正的第三方工具,并自动选择存根(如果它存在于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;

此功能用于"提取"来自第三方工具或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和第三方工具集成。


问题

当我们切换到使用stub dll时,我们会从JNA代码(IllegalStateException @ line 122)获得一些CallbackReference.java。当我们尝试获取第三个接口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),因此无法再次插入并将其映射到另一个界面。

从这一点来看,我可以发现三个问题:

  1. 不同的函数导致相同的指针是否正常?也许stub.dll对两个操作使用相同的回调?这是相当令人惊讶的,因为interface2Pointer.function10的签名与interface3Pointer.function2不同。
  2. 弱哈希映射的使用在上面的代码中带来了很大的不确定性。如果我们暂停调试器足够长时间以进行GC调用,我们可以绕过异常,因此行为可能并不总是可重现。
  3. 我无法确定GC是否确实发生,我们将获得所需的行为。如果同一个指针首先出错了怎么办?如果成功分配,我担心我们最终可能会调用错误的回调。
  4. 上述观察结果一致,并且在重新启动进程和主机操作系统之后进行后续重试。我们甚至得到与后续执行中提到的相同的地址指针

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

    更新 在回复评论时,我在这里添加了原生函数的签名:

    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显然有不同的类型,但CallbackWithFunction10EventInfo并不是分层次的"与CallbackWithNoData相关(就像某种伪造的继承,在某些情况下可能在C中)。这样的事情会影响返回的指针值吗?


    一些断言

    我们还调试了在我们删除存根dll并使用interop.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进程的方法:

    1. 独立流程 - 适用于真实工具,使用stub.dll
    2. 静默失败
    3. 另一个JVM进程的子进程 - 与真实工具配合良好,将IllegalStateExceptionstub.dll一起抛出。
    4. 另一个JVM进程的子进程,但我们注释掉interface2interface3绑定。事情是正常的
    5. 我们用于在步骤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本身引起的(我会责怪后者)。我们可能需要说服第三方制造商是否确实存在代码问题,否则我们可能无法获得新版本的存根,这意味着我们应该寻找解决方法。因此,欢迎任何有关该方向的帮助或解决方法提示。

1 个答案:

答案 0 :(得分:1)

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

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

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

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

我想要注意的是,如果您实际将原生struct映射到JNA Structure,那么它可能会更容易 。这样可以避免手动提取和初始化接口指针。 JNA完全能够在Structure内初始化一系列函数指针(即回调函数)。

<强>更新

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

可以说JNA可以放弃这个限制,或提供解决方法,但这需要在JNA中进行代码更改。即使本机代码在稍后(在时间)上下文中重用函数指针,你也需要调整JNA以便能够故意刷新旧的映射(假设它&#39 ;真的不再使用了。)