📜 ⬆️ ⬇️

Digestible call of Java methods from native code

There are quite a few Android applications that combine C ++ and Java code. Where Java acts as a wrapper / interlayer, and C ++ does all the dirty work. Perhaps a prime example is the game. In this regard, it is often necessary to call Java code from the native access to system properties and the buns that the system provides (switch to another activity, send or download something from the Internet). There are many reasons, but one problem: every time, at best, you have to write 5 lines of code and remember which function signature to push into the parameter. Then you still need to translate these parameters to the desired type. Standard example from tutorials:

long f (int n, String s, float g); 

The signature string for this method will be (ILjava / lang / String; F) J.

Is it convenient for you to memorize everything? And translate C strings to jstring? I do not. I want to write:
')
 CallStaticMethod<long>(className, “f”, 1, 1.2f); 


Formulation of the problem


First, let's understand what we need. In essence, these are four things:

  1. Call a method;
  2. From the parameters you need to pull the signature line. Yes, yes, this one (ILjava / lang / String; F) J;
  3. Convert parameters to the desired type;
  4. Return the data type that the user of our class wants to see.

Actually, that's all. It seems to be simple. Let's start?

Method call


Now it's worth noting how we will call our wrapper function. Since there are different numbers of parameters (from zero and more), we need a function like print in the standard library, but in order to make it convenient to pull the parameter type and the parameter itself. In C ++ 11, variadic templates appeared. And we will use them.

 template <typename MethodType, typename... Args> MethodType CallStaticMethod(Args... args); 

Make a signature


First we need to get the string that is listed in the documentation for this type. There are two options:
  1. Use typeid and if ... else chain. You should get something like:
     if (typeid(arg) == typeid(int)) return “I”; else if (typeid(arg) == typeid(float)) return “F”; 

    And so for all the types that you need.
  2. We use templates and their partial typifications. The method is interesting because you will have functions in one line and there will be no unnecessary type comparisons. Moreover, all this will be at the stage of instantiation of templates. Everything will look like this:
     template <typename T> std::string GetTypeName(); // int template <> std::string GetTypeName<int>() { return “I”; } // string template <> std::string GetTypeName<const char*>() { return “Ljava/lang/String;”; } 


There are two ways to create signature strings in ours: recursive and through an array. First consider the recursive call.

 void GetTypeRecursive(std::string&) { } template <typename T, typename... Args> void GetTypeRecursive(std::string& signatureString, T value, Args... args) { signatureString += GetTypeName<T>(); GetTypeRecursive(signatureString, args...); } 

The challenge of all this lewdness:

 template <typename MethodType, typename... Args> MethodType CallStaticMethod(const char* className, const char* mname, Args... args) { std::string signature_string = "("; GetTypeRecursive(signature_string, args...); signature_string += ")"; signature_string += GetTypeName<MethodType>(); return MethodType(); //    } 

Recursion is good for educational purposes, but I prefer to bypass it whenever possible. There is such an opportunity. Since the arguments go sequentially and we can find out the number of arguments, you can use the convenience provided by the C ++ 11 standard . The code is converted to:

 template <typename MethodType, typename... Args> MethodType CallStaticMethod(const char* className, const char* mname, Args... args) { const size_t arg_num = sizeof...(Args); std::string signatures[arg_num] = { GetType(args)... }; std::string signature_string; signature_string.reserve(15); signature_string += "("; for (size_t i = 0; i < arg_num; ++i) signature_string += signatures[i]; signature_string += ")"; signature_string += GetTypeName<MethodType>(); return MethodType(); //    } 

Code seems to be more, but it works faster. At least due to the fact that we do not call more functions than we need it.

Data type conversion


There are several options for calling CallStaticMethod:

 NativeType CallStatic<type>Method(JNIEnv *env, jclass clazz, jmethodID methodID, ...); NativeType CallStatic<type>MethodA(JNIEnv *env, jclass clazz, jmethodID methodID, jvalue *args); NativeType CallStatic<type>MethodV(JNIEnv *env, jclass clazz, jmethodID methodID, va_list args); 

After torture attempts and tricks, it was decided to use CallStaticMethodA (JNIEnv *, jclass, jmethodID, jvalue *). Now you just need to bring all the parameters to jvalue. The jvalue itself is a union in which you need to set the required field depending on the type of data that your favorite users gave you. We will not be wise and create a structure (or class; matter of taste) JniHolder with constructors of the necessary types.

Jniholder
 struct JniHolder { jvalue val; JObjectHolder jObject; // bool explicit JniHolder(JNIEnv *env, bool arg) : jObject(env, jobject()) { val.z = arg; } // byte explicit JniHolder(JNIEnv *env, unsigned char arg) : jObject(env, jobject()) { val.b = arg; } // char explicit JniHolder(JNIEnv *env, char arg) : jObject(env, jobject()) { val.c = arg; } // short explicit JniHolder(JNIEnv *env, short arg) : jObject(env, jobject()) { val.s = arg; } // int explicit JniHolder(JNIEnv *env, int arg) : jObject(env, jobject()) { val.i = arg; } // long explicit JniHolder(JNIEnv *env, long arg) : jObject(env, jobject()) { val.j = arg; } // float explicit JniHolder(JNIEnv *env, float arg) : jObject(env, jobject()) { val.f = arg; } // double explicit JniHolder(JNIEnv *env, double arg) : jObject(env, jobject()) { val.d = arg; } // string explicit JniHolder(JNIEnv *env, const char* arg) : jObject(env, env->NewStringUTF(arg)) { val.l = jObject.get(); } // object explicit JniHolder(JNIEnv *env, jobject arg) : jObject(env, arg) { val.l = jObject.get(); } //////////////////////////////////////////////////////// operator jvalue() { return val; } jvalue get() { return val; } }; 


Where JObjectHolder is a wrapper for holding and deleting a jobject.

JObjectHolder
 struct JObjectHolder { jobject jObject; JNIEnv* m_env; JObjectHolder() : m_env(nullptr) {} JObjectHolder(JNIEnv* env, jobject obj) : jObject(obj) , m_env(env) {} ~JObjectHolder() { if (jObject && m_env != nullptr) m_env->DeleteLocalRef(jObject); } jobject get() { return jObject; } }; 


A JniHolder object is created, to which the JNIEnv * and value are passed. In the constructor, we know which field needs to be set in jvalue. In order to avoid the temptation of the compiler to type types imperceptibly, we make all constructors explicit. The whole chain takes one line:

 jvalue val = static_cast<jvalue>(JniHolder(env, 10)); 

But there is one thing. When conversion occurs, we return jvalue, but jObject is deleted and val.l points to an invalid address. Therefore, you have to save the holders during the java function call.

 JniHolder holder(env, 10) jvalue val = static_cast<jvalue>(holder); 

In the case of the transfer of several parameters, we use the initialization list:

 JniHolder holders[size] = { std::move(JniHolder(env, args))... }; jvalue vals[size]; for (size_t i = 0; i < size; ++i) vals[i] = static_cast<jvalue>(holders[i]); 

Return the desired data type


I would like to write some one method that resolved the situation and looked like:

 template <typename MethodType, typename... Args> MethodType CallStaticMethod(Args... args) { MethodType result = ...; …. return reesult; } 

But there is an unpleasant feature of JNI: for each return type there is a specific method. That is, for int you need CallStaticIntMethod, for float, CallStaticFloatMethod and so on. Came to partial typing patterns. First, we declare the interface we need:

 template <typename MethodType> struct Impl { template <typename... Args> static MethodType CallMethod(JNIEnv* env, jclass clazz, jmethodID method, Args... args); }; 

Then for each type we write an implementation. For integers (int) will look like:

 template <> struct Impl <int> { template <typename... Args> static int CallStaticMethod(JNIEnv* env, jclass clazz, jmethodID method, Args... args) { const int size = sizeof...(args); if (size != 0) { jvalue vals[size] = { static_cast<jvalue>(JniHolder(env, args))... }; return env->CallStaticIntMethodA(clazz, method, vals); } return env->CallStaticIntMethod(clazz, method); } }; 

If we have zero parameters, then CallStaticMethod should be called, not CallStaticMetodA. Well, if you try to create an array of dimension zero, the compiler will tell you everything you think about it.

The final


The calling method itself looks like:

 template <typename MethodType, typename... Args> MethodType CallStaticMethod(const char* className, const char* mname, Args... args) { const size_t arg_num = sizeof...(Args); std::string signatures[arg_num] = { GetType(args)... }; std::string signature_string; signature_string.reserve(15); signature_string += "("; for (size_t i = 0; i < arg_num; ++i) signature_string += signatures[i]; signature_string += ")"; signature_string += GetTypeName<MethodType>(); JNIEnv *env = getEnv(); JniClass clazz(env, className); jmethodID method = env->GetStaticMethodID(clazz.get(), mname, signature_string.c_str()); return Impl<MethodType>::CallStaticMethod(env, clazz.get(), method, args...); } 

Now call the method from java:

Java code
 class Test { public static float TestMethod(String par, float x) { mOutString += "float String: " + par + " float=" + x + "\n"; return x; } }; 


Somewhere in the native code:

 float fRes = CallStaticMethod<float>("Test", "TestMethod", "TestString", 4.2f); 

Previously, the code looked like
 JNIEnv* env = getEnv(); // -     jclass clazz = env->FindClass(“Test”); jmethodID method = env->GetStaticMethodID(“Test”, “TestMethod”, “(Ljava/lang/String;F)Ljava/lang/String;); jstring str = env->NewStringUTF(“TestString”); float fRes = env->CallStaticFloatMethod(clazz, method, str, 4.2f); env->DeleteLocalRef(clazz); env->DeleteLocalRef(str); 

findings


Calls to methods have become a convenient thing, and you don’t need to memorize signatures and convert values ​​and delete references. It is enough to pass the name of the class, method and arguments.

It also turned out to be an interesting task, thanks to which I had a little understanding with the new buns of the language (which I really liked) and remembered the patterns.

Thanks for reading. Well, or for attention, if you have not read everything. I am pleased to read suggestions for improvement and criticism of the work. And also answer questions.

Source: https://habr.com/ru/post/258707/


All Articles