为了正确解释问题,这将是一篇很长的文章,所以请耐心等待。它还可能需要一些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)
0
和4
,它们是通过readField
方法检索的:
function1
-> 402397356
+ 0
=402397356 (0x17FC18AC)
interface3Pointer.function1
)= _getPointer(402397356
)然后返回402087408 (0x17F75DF0)
function2
-> 402397356
+ 4
=402397360 (0x17FC18B0)
interface3Pointer.function2
)= _getPointer(402397360
)然后返回401814304 (0x17F33320)
(!)如您所见,interface3Pointer.function2
分配了与相同的指针interface2Pointer.function10
。
现在,CallbackReference.java
内部使用弱哈希映射来跟踪已经分配给Java表示形式的回调指针。IllegalStateException
之所以抛出该异常,是因为该映射仍具有对已匹配指针(interface2Pointer.function10
@ 401814304
)的引用,因此无法再次插入并将其映射到另一个接口。
从这一点来看,我可以观察到三个问题:
stub.dll
两个操作都使用相同的回调?这是相当令人惊讶的,因为interface2Pointer.function10
它的签名不同于interface3Pointer.function2
。上述观察结果与重新启动进程和主机OS之后的后续重试一致。我们甚至在后续执行中得到的地址指针与此处提到的地址指针相同。
更糟糕的是,第三方工具制造商声称有没有问题,无论是interop.dll
和stub.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流程的方法:
stub.dll
IllegalStateException
用stub.dll
。interface2
和interface3
绑定。事情工作正常我们用于在步骤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完全有能力在内初始化大量函数指针(即,回调)。struct
Structure
Structure
更新
鉴于function10
并function2
具有有效的相同签名,((*)(), void*)
您的存根库很可能正在使用占位符功能(例如“ _not_implemented”)。如果您没有积极使用这些功能,则只需将它们都更改为具有相同的接口即可(既可以使用现有接口,也可以编写一个接口)。这样可以绕开JNA的限制。
可以说,JNA可以放弃此限制,或者提供解决方法,但这需要在JNA内更改代码。即使是本机代码在稍后(及时)上下文中重用函数指针的问题,您也需要调整JNA以便能够有目的地刷新较早的映射(假设它确实不再使用)。
本文收集自互联网,转载请注明来源。
如有侵权,请联系 [email protected] 删除。
我来说两句