Send C++ string to Java via JNI

25,851

Solution 1

In the function you shared, in your c++ code you are creating an object array with NewObjectArray. Then in your for-loop you are creating a string with NewStringUTF and storing it at an index in your array using SetObjectArrayElement. Until now, your object array is only known to your c++ code and not to your java code. Only when you return it will your java app get access to it.
I can think of a couple of ways to send the string to java from c++, though it may not be exactly what you intended.

  1. Pass a String array to your native function. In you native code you can access each element using GetObjectArrayElement and update it using SetObjectArrayElement. This will probably be pointless since you end up having to call a function which I persume you do not want.

  2. If you already have a string defined as a field in your java code, from your native get access to it using GetFieldID and GetObjectField, and you can update it using SetObjectField. I dont know how you will signal your java code that the field has been updated though ( if you need it)

EDIT
The updated function which you have written is meant to be called from the java layer. The clue for this is name of the function Java_the_package_MainActivity_getJniString. To call java code from a native context, you will need references to the env and obj from java. Have a look at How do I load my own Java class in C on Android? for an approach to get this. You will also probably have to look up how to use global references in JNI

Solution 2

At the request of @Sam, here is a method that avoids using modified UTF-8 because we don't know that it is safe to do so.

NewStringUTF creates a string from its modified UTF-8 encoding. It is not correct to use it with user data--it's unlikely to be encoded with modified UTF-8. We could just hope that the characters in the data are restricted to keep it compatible. Instead, we can convert it properly.

JNI uses modified UTF-8 strings throughout its API. We can use strings we know are compatible, particularly literals for Java identifiers (except not all currency symbols).

Below are two native method implementations. The second is better in most ways.

For this native method:

private static native String getJniString();

Here is an implementation:

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 is awkward. so, if we can move the knowledge that the native string is "UTF-8" to the Java side, we can do this:

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

And the much simpler implementation:

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;
}

Solution 3

You can convert a c-string into a jstring and return it. An example would look something along the lines of:

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));
}

Solution 4

Typically with JNI the calls go from the JVM into the C code. The normal paradigm would be:

  1. Java programmers make a Java class with several methods declared as native (no implementation)
  2. Java programmers compile the class with javac
  3. Java programmers run javah against the compiled .class file, this produces a .h header file
  4. C programmer #include the new header file and implement the interface

The only examples I have seen of doing this in the reverse direction (C code initiating contact with Java) involves having the C code actually create a JVM.

To answer your question about the code sample, the Java Strings being created are returned with the return statement at the end of code execution, logically, this is when program flow for that thread of execution is returned back to the JVM.

Share:
25,851
AeroBuffalo
Author by

AeroBuffalo

Updated on July 09, 2022

Comments

  • AeroBuffalo
    AeroBuffalo almost 2 years

    I am working on the C++ side of a project that is building an Android application. There is some information (via strings and string arrays) that I need to pass to the Java application (via JNI). I have never done this before, and the people working in the reverse direction have no experience with C++ and admit that they cannot really help.

    I did find the following code (from here)

     #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);  
        }  
    

    But this makes no sense to me. Mostly, I am not sure how I am supposed to incorporate this into the C++ side of the program and I am failing to understand exactly how this works. Is the code sending out the message upon execution of the return(ret); line? Or during the execution of the line within for loop?

    Ideally, I would like the string/string array to be sent out "live" in line and not at the end of a function so that I do not have to incorporate a new function.

    Will the code I found work for what I want (with some adaptation)? Is what I am looking for even possible? If so, how can I do it?

    EDIT/UPDATE: Having spent the day looking into JNI and the terminology, I think I have failed to correctly communicate what I am looking to achieve both here and as a comment to @jogabonito's answer/reply.

    That being said. The code I am working on is for an IM client that will need to push message and presence updates to the Android java application (via JNI) so that the Android application does not poll for updates. I have managed to learn how to setup the functions for the java code to call to requrest information. However, I do not have any idea how to push new message or presence information (jabber stanza strings) to the java code when it comes in. All the code that I have seen on how to do this (see below for example) seems to require getting information from the java code (env, class, methodid, etc).

    It does not make sense to me how this is supposed to be possible when it is not the java code calling the function, but my c++ code. Any explanation/help would be very much appreciated.

    #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);
    }
    
  • AeroBuffalo
    AeroBuffalo almost 12 years
    First off, thanks for the input. I just got done talking with the team handling the java/android end. It turns out that the way they have done other interactions like this in the past was for either end (c++/java) to call functions within those programs to execute and return information, which I think is in line with your #1. I am going to get the code they did for the previous work, but do you know of any more general/outline information that I could learn from? Thanks again.
  • AeroBuffalo
    AeroBuffalo almost 12 years
    After spending the day learning about JNI, I have updated my original question to (hopefully) better define what I am seeking help on.
  • Tom Blodget
    Tom Blodget about 10 years
    GetStringUTFChars and NewStringUTF are crutches. They use the modified UTF-8 encoding. When converting strings, it's better to use standard Java methods with well-known behaviors. String.GetByte(Charset) or new String(byte[], Charset) or their overloads. Sometimes the most appropriate overloads are those that use the default encoding rather than an explicit one. Modified UTF-8 is almost never the one that the C-side is using. It's usually compatible over only a subset of the character set.
  • Sam
    Sam almost 10 years
    @TomBlodget could you give an example please?
  • Tom Blodget
    Tom Blodget almost 10 years
    @Sam I created an answer to explain.
  • Sam
    Sam almost 10 years
    This is great, thanks Tom. I did end up with something similar but didn't use a reinterpret cast (just (jbyte*) did it for me. I did also have a check for bytes being null if there is not enough memory to alloc the byte array (I mostly copied from elsewhere)