通过JNI将C++字符串发送到Java

20
我正在参与一个构建Android应用程序的项目,负责C++方面的工作。有一些信息(通过字符串和字符串数组)需要传递给Java应用程序(通过JNI)。我以前从未做过这个,而反向工作的人没有C ++经验,并承认他们无法提供真正的帮助。
我找到了以下代码(来自这里
 #include <jni.h>  
    #include "ArrayHandler.h"  

    JNIEXPORT jobjectArray JNICALL Java_ArrayHandler_returnArray (JNIEnv *env, jobject jobj){        
      jobjectArray ret;  
      int i;  
      char *message[5]= {"first","second","third","fourth","fifth"};  
      ret= (jobjectArray)env->NewObjectArray(5,env->FindClass("java/lang/String"),env->NewStringUTF(""));  

      for(i=0;i<5;i++) {  
        env->SetObjectArrayElement(ret,i,env->NewStringUTF(message[i]));  
      }  
      return(ret);  
    }  

但对我来说这毫无意义。我不确定应该如何将其纳入程序的C++部分,也无法理解它的工作原理。代码是在return(ret);一行执行时发送消息吗?还是在for循环内执行的那一行?
理想情况下,我希望字符串/字符串数组能够实时地以行形式发送,而不是在函数结尾处发送,以便我无需添加新函数。
我找到的代码是否可以实现我想要的功能(稍加改编)?我想要的东西是否可能?如果是,我该怎么做呢?
编辑/更新: 今天整天我一直在研究JNI和术语,我认为我没有正确地传达我在此处和在@jogabonito的答案/回复中所要实现的目标。
话虽如此,我正在开发的代码是用于IM客户端的,它需要将消息和存在更新推送到Android Java应用程序(通过JNI),以便 Android 应用程序不需要轮询来获取更新。我已经学会了如何设置Java代码调用请求信息的函数。但是,当信息到达时,我不知道如何将新的消息或存在信息(jabber stanza字符串)推送到Java代码中去。所有我看到的关于如何做到这一点的代码(例如下面的代码)似乎都需要从Java代码(env、class、methodid等)中获取信息。
当不是Java代码调用函数而是我的C ++代码时,这应该如何实现并不合理。任何解释或帮助都将不胜感激。
#include <string.h>
#include <stdio.h>
#include <jni.h>

jstring Java_the_package_MainActivity_getJniString( JNIEnv* env, jobject obj){

    jstring jstr = (*env)->NewStringUTF(env, "This comes from jni.");
    jclass clazz = (*env)->FindClass(env, "com/inceptix/android/t3d/MainActivity");
    jmethodID messageMe = (*env)->GetMethodID(env, clazz, "messageMe", "(Ljava/lang/String;)Ljava/lang/String;");
    jobject result = (*env)->CallObjectMethod(env, obj, messageMe, jstr);

    const char* str = (*env)->GetStringUTFChars(env,(jstring) result, NULL); // should be released but what a heck, it's a tutorial :)
    printf("%s\n", str);

    return (*env)->NewStringUTF(env, str);
}

添加 androidandroid-ndk 标签,因为我认为这将帮助您更好地了解如何在 Android 上进行本地开发。 - Tim Bender
4个回答

18

应 @Sam 要求,这里有一种避免使用修改过的 UTF-8 的方法,因为我们不确定这样做是否安全。

NewStringUTF 从其修改过的 UTF-8 编码创建字符串。不能用它处理用户数据--这些数据很可能没有用修改过的 UTF-8 进行编码。我们只能希望数据中的字符受到限制以保持兼容性,但这并不可靠。相反,我们可以进行适当的转换。

JNI 在其 API 中始终使用修改过的 UTF-8 字符串。我们可以使用已知兼容的字符串,特别是 Java 标识符的字面量(除了某些货币符号)。

以下是两个本地方法实现。第二种在大多数情况下更好。

对于此本地方法:

private static native String getJniString();

这是一个实现:

JNIEXPORT jstring JNICALL 
Java_the_Package_MainActivity_getJniString(JNIEnv *env, jclass)
{   
    std::string message = "Would you prefer €20 once "
                          "or ₹10 every day for a year?";

    int byteCount = message.length();
    jbyte* pNativeMessage = reinterpret_cast<const jbyte*>(message.c_str());
    jbyteArray bytes = env->NewByteArray(byteCount);
    env->SetByteArrayRegion(bytes, 0, byteCount, pNativeMessage);

    // find the Charset.forName method:
    //   javap -s java.nio.charset.Charset | egrep -A2 "forName"
    jclass charsetClass = env->FindClass("java/nio/charset/Charset");
    jmethodID forName = env->GetStaticMethodID(
      charsetClass, "forName", "(Ljava/lang/String;)Ljava/nio/charset/Charset;");
    jstring utf8 = env->NewStringUTF("UTF-8");
    jobject charset = env->CallStaticObjectMethod(charsetClass, forName, utf8);

    // find a String constructor that takes a Charset:
    //   javap -s java.lang.String | egrep -A2 "String\(.*charset"
    jclass stringClass = env->FindClass("java/lang/String");
    jmethodID ctor = env->GetMethodID(
       stringClass, "<init>", "([BLjava/nio/charset/Charset;)V");

    jstring jMessage = reinterpret_cast<jstring>(
      env->NewObject(stringClass, ctor, bytes, charset));

    return jMessage;
}

JNI很笨拙。因此,如果我们能将本地字符串是“UTF-8”这一知识移动到Java端,我们就可以这样做:

private static String getJniString2()
{
    return new String(getJniStringBytes(), Charset.forName("UTF-8"));
}
private static native byte[] getJniStringBytes();

而且实现起来也简单得多:

JNIEXPORT jbyteArray JNICALL Java_the_Package_MainActivity_getJniStringBytes(JNIEnv *env, jclass)
{   
    std::string message = "Would you prefer €20 once "
                          "or ₹10 every day for a year?";

    int byteCount = message.length();
    jbyte* pNativeMessage = reinterpret_cast<const jbyte*>(message.c_str());
    jbyteArray bytes = env->NewByteArray(byteCount);
    env->SetByteArrayRegion(bytes, 0, byteCount, pNativeMessage);

    return bytes;
}

1
太好了,谢谢Tom。我最终得到了类似的东西,但没有使用reinterpret_cast(只是(jbyte*)对我来说就可以了)。如果没有足够的内存来分配字节数组,我也会检查bytes是否为空(我大部分都是从别处复制过来的)。 - Sam
这真是一个很棒的答案。第二种方法帮助我非常容易地将cpp字符串转换为Java字符串。感谢@Tom Blodget提供的绝妙解决方案。 - Bhavnik

4
在您分享的函数中,在您的C++代码中,您使用NewObjectArray创建了一个对象数组。然后在for循环中,您使用NewStringUTF创建一个字符串,并使用SetObjectArrayElement将其存储在数组的索引中。到目前为止,您的对象数组仅为C++代码所知,而不是Java代码所知。只有当您返回它时,您的Java应用程序才能访问它。
我可以想到几种从C++向Java发送字符串的方法,尽管可能不完全符合您的意图。
  1. 将String数组传递给本机函数。在本机代码中,您可以使用GetObjectArrayElement访问每个元素,并使用SetObjectArrayElement更新它。这可能是无意义的,因为您最终不得不调用一个函数,我认为您不想要。
  2. 如果您已经在Java代码中定义了一个作为字段的字符串,则可以使用GetFieldIDGetObjectField从本机获取对它的访问权限,并使用SetObjectField进行更新。但是,我不知道您如何向Java代码发出信号,表明该字段已被更新(如果需要)。
编辑
您编写的更新后的函数旨在从Java层调用。这是函数名称Java_the_package_MainActivity_getJniString的线索。要从本机上下文调用Java代码,您需要引用来自Java的envobj。请参阅如何在Android上使用C ++加载自己的Java类?以获取一个获取此类引用的方法。您还可能需要查找如何在JNI中使用全局引用。

首先,感谢您的回复。我刚刚与处理Java/Android端的团队交谈完毕。事实证明,他们过去处理类似交互的方式是让任一端(C++/Java)调用这些程序中的函数来执行并返回信息,我认为这符合您提出的第一个建议。我将获取之前工作所用的代码,但您是否了解可以学习更多通用/概述信息的资源?再次感谢。 - AeroBuffalo
在学习了一整天 JNI 后,我已经更新了我的原始问题,以更好地定义我需要帮助的内容(希望如此)。 - AeroBuffalo

2
您可以将C-字符串转换为jstring并返回。一个例子可能如下所示:
JNIEXPORT jstring JNICALL Java_Class_Method(jstring data) 
{

    // jstring to char *
    const char *cStr = (*env)->GetStringUTFChars(env, data, NULL);

    // convert char * to jstring and return it
    return ((*env)->NewStringUTF(env, cStr));
}

GetStringUTFCharsNewStringUTF是一种权宜之计。它们使用了“修改过的UTF-8”编码。在转换字符串时,最好使用具有众所周知行为的标准Java方法。例如String.GetByte(Charset)new String(byte[], Charset)及其重载方法。有时,最合适的重载方法是使用默认编码而不是显式指定编码的方法。 “修改过的UTF-8”几乎从不是C端正在使用的编码。它通常仅兼容字符集的子集。 - Tom Blodget
@TomBlodget,你能举个例子吗? - Sam
1
@Sam,我创建了一个答案来解释。 - Tom Blodget

1
通常情况下,JNI的调用是从JVM到C代码的。正常的范例应该是:
1. Java程序员创建一个Java类,并将几个方法声明为native(没有实现)。 2. Java程序员使用javac编译类。 3. Java程序员对编译后的.class文件运行javah,这会生成一个.h头文件。 4. C程序员#include新的头文件并实现接口。
我所见过的唯一反向操作的例子(C代码启动与Java的联系)涉及到C代码实际上创建了一个JVM。
回答你关于代码示例的问题,创建的Java字符串是在代码执行结束时通过返回语句返回的,逻辑上,这是当线程执行流程返回到JVM时。

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