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

伊瓦伊洛·斯拉沃夫(Ivaylo Slavov)

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

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

背景

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

interop.dll与第三方工具通信(在系统上是预安装),因此它是一种通信SDK的。出于测试目的,我们最近stub.dll(又从该制造商处)获得了一个工具,它不需要运行或完全安装第3方工具。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如下所示(与Interface2相同Interface3):

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

注意:(作为对技术答案的回应Interface1,,2和3的上述代码由JNAerator自动生成,试图将具有函数的C结构映射到具有回调的Java对象。

我们已经成功地与interop.dll和第三方工具集成


问题

当我们切换到使用时stub dll,我们IllegalStateException从JNA代码(CallbackReference.java@第122行)中获得了一些东西当我们尝试获取第三个接口时,会出现问题Interface3 interface3 = new Interface3(interface3Pointer);

我们下载了JNA的源代码,并开始通过代码进行调试以查看到底是什么引起了该问题。

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

dispatch.c,@第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具有value 402394304 (0x17FC0CC0),(C结构的指针)
  • readField方法在该结构中发现10个函数指针,最后一个位于offset处36
    • function10-> interface2Pointer+ offset= 402394304+ 36= 402394340 (0x17FC0CE4)
    • 最后,有一个_getPointer(interface2Pointer.function10)=的调用_getPointer(402394340),它将返回struct中当前回调的地址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,我们将获得所需的行为。如果相同的指针首先出现错误怎么办?如果分配成功,我担心我们最终可能会调用错误的回调。

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

更糟糕的是,第三方工具制造商声称有没有问题,无论是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. 另一个JVM进程的子进程-与真正的工具运行良好,抛出讨论IllegalStateExceptionstub.dll
  3. 另一个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点所示)由返回的指针结果既令人困惑,又给了我们一些指导。独立进程以与实际工具类似的方式获得了唯一的指针。因此,原因可能是子进程和某些共享内存或本机代码与子Java进程之间公开的内存限制...


问题

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

技术

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

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

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

另外,本机代码可能只使用一个调度函数(听起来不像是这种情况,否则您会在第一个函数指针之后看到每个函数指针上的错误)。

我想指出的是,如果您将本机实际映射到JNA,则对您来说可能会容易得多这将为您避免手动提取和初始化接口指针。JNA完全有能力在内初始化大量函数指针(即,回调)structStructureStructure

更新

鉴于function10function2具有有效的相同签名,((*)(), void*)您的存根库很可能正在使用占位符功能(例如“ _not_implemented”)。如果您没有积极使用这些功能,则只需将它们都更改为具有相同的接口即可(既可以使用现有接口,也可以编写一个接口)。这样可以绕开JNA的限制。

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

本文收集自互联网,转载请注明来源。

如有侵权,请联系 [email protected] 删除。

编辑于
0

我来说两句

0 条评论
登录 后参与评论

相关文章