为了正确解释这个问题,这将是一个很长的帖子,所以请耐心等待。它还可能需要一些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
的构造函数看起来像这样(Interface2
和Interface3
相同):
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)
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
不同。上述观察结果一致,并且在重新启动进程和主机操作系统之后进行后续重试。我们甚至得到与后续执行中提到的相同的地址指针。
更糟糕的是,第三方工具制造商声称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点所述)。结果既困惑又给了我们一些方向。 Standalone进程获得了独特的指针,就像它使用真实工具一样。因此,原因可能是子进程和一些共享内存或对本机代码和子Java进程之间暴露的内存的限制......
我很欣赏这个问题是由我们的使用还是存根dll本身引起的(我会责怪后者)。我们可能需要说服第三方制造商是否确实存在代码问题,否则我们可能无法获得新版本的存根,这意味着我们应该寻找解决方法。因此,欢迎任何有关该方向的帮助或解决方法提示。
答案 0 :(得分:1)
将函数指针唯一映射到回调引用的目的是在回调映射中公开编程错误,并提供一种在本机指针超出范围时自动处理内存的方法。通常,C函数指针具有单个可接受的签名(varargs语义和转换除外)。如果单个本机指针映射到多个Java对象,清理也会变得复杂一些。
您的本机代码可能会动态分配函数指针,在这种情况下,特定指针最终可能会被重用(特别是如果本机代码使用显式内存池)。如果是这种情况,你可能只需要清除弱哈希映射(JNA不公开这个,但是在一些自定义代码中调用地图上的.size()
是微不足道的)。
本机代码也可能使用占位符函数,其中重用占位符或公共函数(通常在方法签名相同的情况下)。如果是这种情况,则错误将是确定性的(这似乎不是您的情况)。
或者,本机代码可能正在使用单个调度函数(这听起来不像是情况,或者您在第一个函数指针之后看到错误)。
我想要注意的是,如果您实际将原生struct
映射到JNA Structure
,那么它可能会更容易 。这样可以避免手动提取和初始化接口指针。 JNA完全能够在Structure
内初始化一系列函数指针(即回调函数)。
<强>更新强>
鉴于function10
和function2
实际上具有相同的签名((*)(), void*)
,您的存根库可能正在使用占位符函数(例如&#34; _not_implemented&#34;)。如果您没有主动使用这些功能,您可以简单地将它们更改为具有相同的界面(现有的或您编写的界面)。这将绕过JNA限制。
可以说JNA可以放弃这个限制,或提供解决方法,但这需要在JNA中进行代码更改。即使本机代码在稍后(在时间)上下文中重用函数指针,你也需要调整JNA以便能够故意刷新旧的映射(假设它&#39 ;真的不再使用了。)