如何在JNI中传递C结构体到Java代码并返回?

72

我有一些通过JNI调用的C函数,这些函数需要传递一个指向结构体的指针,还有一些函数会分配/释放相同类型结构体的指针,以便于使用我的包装器。令人惊讶的是,JNI文档对如何处理C结构几乎没有提及。

我的C头文件如下:

typedef struct _MyStruct {
  float member;
} MyStruct;

MyStruct* createNewMyStruct();
void processData(int *data, int numObjects, MyStruct *arguments);

对应的JNI C包装文件包含:

JNIEXPORT jobject JNICALL
Java_com_myorg_MyJavaClass_createNewMyStruct(JNIEnv *env, jobject this) {
  return createNewMyStruct();
}

JNIEXPORT void JNICALL
Java_com_myorg_MyJavaClass_processData(JNIEnv *env, jobject this, jintArray data,
                                       jint numObjects, jobject arguments) {
  int *actualData = (*env)->GetIntArrayElements(env, data, NULL);
  processData(actualData, numObjects, arguments);
  (*env)->ReleaseIntArrayElements(env, data, actualData, NULL);
}

...最后,对应的Java类:

public class MyJavaClass {
  static { System.loadLibrary("MyJniLibrary"); }

  private native MyStruct createNewMyStruct();
  private native void processData(int[] data, int numObjects, MyStruct arguments);

  private class MyStruct {
    float member;
  }

  public void test() {
    MyStruct foo = createNewMyStruct();
    foo.member = 3.14159f;
    int[] testData = new int[10];
    processData(testData, 10, foo);
  }
}

很不幸,这段代码在调用createNewMyStruct()后会导致JVM崩溃。我对JNI比较新手,不知道问题出在哪里。

编辑:我应该指出C代码非常基础,经过了充分测试,并从一个正常运作的iPhone项目移植过来。此外,该项目使用Android NDK框架,可以在JNI内从Android项目运行本机C代码。但是,我不认为这是严格意义上的NDK问题...看起来像是我的JNI设置/初始化错误。


有关错误的更具体信息是什么?有任何错误消息吗? - Petar Minchev
不行,它只会导致JRE崩溃。这就是处理JNI时的一个缺陷... - Nik Reiman
1
在出现Heisenbugs之前,我们添加了CheckJNI功能来查找常见的代码错误。它在模拟器中默认启用。要了解如何在设备上启用该功能,请参阅http://android.git.kernel.org/?p=platform/dalvik.git;a=tree;f=docs;h=b0d3023109f548626cce1a9a586c95e82fb012ac;hb=HEAD上的jni-tips和embedded-vm-control文档。 - fadden
请查看JNA(以及参见部分)。 - dma_k
您还可以通过JNI传递nio.DirectByteBuffers,并在Java端使用例如https://github.com/marc-christian-schulze/structs4java解析内存。 - Marc-Christian Schulze
4个回答

51

你需要创建一个Java类,其中的成员和C结构体一样,并通过env->GetIntField、env->SetIntField、env->GetFloatField、env->SetFloatField等方法在C代码中进行“映射”-简而言之,需要大量手动劳动。希望已经有自动完成此过程的程序存在:JNAerator(http://code.google.com/p/jnaerator)和SWIG(http://www.swig.org/)。两者都有优缺点,选择权在于你。


17
你能提供一些集成的简单代码示例吗? - Centurion
5
@Centurion 我知道URL通常不被认可,但这是一条评论,我发现这个网页在理解这些内容方面非常有帮助:http://www3.ntu.edu.sg/home/ehchua/programming/java/JavaNativeInterface.html#zz-5.1 - James
@James...它们被人们所不赞成的原因是因为当有人多年后寻找相同的数据时,发现一个又一个的404错误页面会让人感到非常恼火。 - NerdyDeeds

11

这个程序崩溃是因为Java_com_myorg_MyJavaClass_createNewMyStruct被声明为返回jobject,但实际上返回的是结构体MyStruct。如果您启用了CheckJNI,虚拟机会大声抱怨并中止运行。您的processData()函数也会对传入的arguments感到不满。

jobject是托管堆上的对象。它可以在声明字段之前或之后有额外的内容,并且字段在内存中不必按任何特定顺序布局。因此,您无法将C结构映射到Java类上。

最直接的处理方式已在早期回答中确定:使用JNI函数操作jobject。从Java或使用NewObject分配对象,使用适当的调用Get/Set对象字段。

这里有各种“作弊”的方法。例如,您可以在Java对象中包含一个byte[],其中包含sizeof(struct MyStruct)字节,然后使用GetByteArrayElements获取指向它的指针。有点丑陋,特别是如果您还想从Java端访问字段。


如果结构布局与Java对象布局匹配,使用byte[]才能发挥作用,不是吗?这似乎不太可能。 - barneypitt
@barneypitt:它们不重叠。这个想法是创建一个适当大小的Java byte[],并将C数据存储在那里,有效地将C结构序列化到Java可以看到的内存中。这要求Java代码知道结构体的大小、字节顺序和字段对齐方式,如果你不固定数组,那么C代码必须小心地访问它。在被接受的答案中使用的方法是,让Java“拥有”数据,而C通过JNI访问字段,通常比让C在jobject中“拥有”数据,并让Java从字节中提取数据更好...但我喜欢展示所有选项。 - fadden
好的,我误解了。实际上我正在研究如何正确匹配Java类和C++类的布局,最终目标是拥有共享内存对象。我认为只有在C是由Java生成时才可能实现...因为Java不保证布局,而C确保它们按照声明顺序保持不变。因此,您可以使用“jol”库来确定Java类的布局,然后确保C结构体的顺序与jol中的顺序相匹配。为了确保填充匹配,您可以插入虚拟字节变量和#pragma pack 4。不过我还没有将其付诸实践! - barneypitt

7

C结构是变量(包括一些函数指针)的集合。将其传递给Java不是一个好主意。通常,问题在于如何将更复杂的类型,如指针,传递给Java。

JNI书中推荐将指针/结构保留在本地,并导出操作到Java。您可以阅读一些有用的文章。我已经阅读了JavaTM本地接口程序员指南和规范9.5对等类提供了一个解决方案来处理这个问题。


5
链接失效 -- 看起来Oracle已经删除了这些内容。有几个网站提供了备份,或者你可以使用archive.org: http://web.archive.org/web/20070101174413/http://java.sun.com/docs/books/jni/ - fadden

0
  1. 在Java和C++两端都创建类,只需放入成员变量。C++结构体实际上就是具有公共数据成员的类。如果您真的在使用纯C,请立即停止阅读。
  2. 使用您的IDE自动为成员变量生成setter和getter。
  3. 使用javah从Java类生成C头文件。
  4. 在C++端进行一些编辑,使setter和getter与生成的头文件匹配。
  5. 加入JNI代码。

这不是一个理想的解决方案,但它可能会为您节省一些时间,并且至少会给您一个可以编辑的框架。这个功能可以添加到IDE中,但如果没有大量需求,它可能不会发生。大多数IDE甚至不支持混合语言项目,更不用说让它们相互通信了。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接