Удобоваримый вызов Java методов из нативного кода

от автора

Существует довольно много приложений под Android, которые совмещают C++ и Java код. Где Java выступает оберткой/прослойкой, а C++ выполняет всю грязную работу. Пожалуй, ярким примером могут служить игры. В связи с этим часто приходится вызывать Java код из нативного для доступа к системным свойствам и плюшкам, которые предоставляет система (переключится на другую активность, послать или скачать что-либо из интернета). Причин много, а проблема одна: каждый раз приходится писать в лучшем случае 5 строчек кода и помнить, какую сигнатуру функции нужно запихнуть в параметр. Потом еще нужно перевести эти параметры в нужный тип. Стандартный пример из туториалов:

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

Строка-сигнатура для данного метода будет (ILjava/lang/String;F)J.

Вам удобно это все запоминать? А переводить С-строки в jstring? Мне — нет. Мне хочется писать:

CallStaticMethod<long>(className, “f”, 1, 1.2f);  

Постановка задачи

Для начала поймем, что нам нужно. В сущности, это четыре вещи:

  1. Вызвать метод;
  2. Из параметров нужно вытянуть строку сигнатуры. Да, да, вот эту (ILjava/lang/String;F)J;
  3. Сконвертировать параметры в нужный тип;
  4. Возвратить тип данных, который хочет видеть пользователь нашего класса.

Собственно, это все. Вроде бы просто. Приступим?

Вызов метода

Теперь стоит отметить, как мы будем вызывать нашу функцию-оболочку. Так как параметров может разное количество (от нуля и больше), то нужна функция вроде print`а в стандартной библиотеке, но с тем, чтобы было удобно вытягивать тип параметра и сам параметр. В С++11 появились вариадические шаблоны. Ими и воспользуемся.

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

Составляем сигнатуру

Для начала нам нужно получить строку, которая числится в документации для данного типа. Тут два варианта:

  1. Используем typeid и цепочку if … else. Должно получится что-то вроде:
    if (typeid(arg) == typeid(int)) return “I”; else if (typeid(arg) == typeid(float)) return “F”; 

    И так для всех типов, которые вам нужны.

  2. Используем шаблоны и их частичные типизации. Метод интересен тем, что у вас будут функции в одну строку и не будет лишних сравнений типов. Более того все это будет на стадии инстанциации шаблонов. Выглядеть все будет примерно так:
    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;”; } 

Для составления строки-сигнатуры в нашем существует два способа: рекурсивный и через массив. Сначала рассмотрим рекурсивный вызов.

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

Вызов всего этого непотребства:

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(); // пока здесь заглушка } 

Рекурсия — это хорошо в воспитательно-образовательных целях, но предпочитаю ее обходить при возможности. Тут такая возможность есть. Так как аргументы идут последовательно и мы можем узнать количество аргументов можно использовать удобство предоставленное стандартом С++11. Код преобразуется в:

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(); // пока здесь заглушка } 

Кода вроде бы и больше, но работает оно быстрее. Хотя бы за счет того, что не вызываем функций больше, чем нам это нужно.

Конвертация типа данных

Есть несколько вариантов вызова 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); 

После пыток попыток и ухищрений было решено использовать CallStaticMethodA(JNIEnv*, jclass, jmethodID, jvalue*). Теперь только нужно привести все параметры к jvalue. Сам jvalue это union, в котором нужно установить нужное поле в зависимости от типа данных, которые вам передали любимые пользователи. Мудрить не будем и создаем структуру (или класс; дело вкуса) JniHolder с конструкторами нужных типов.

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

Где JObjectHolder — обертка для удержания и удаления 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; } 	}; 

Создается объект JniHolder, куда передаются JNIEnv* и значение. В конструкторе мы знаем какое поле нужно выставить в jvalue. Чтобы не было соблазна у компилятора приводить типы незаметно, все конструкторы делаем explicit. Вся цепочка занимает одну строчку:

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

Но есть одно но. Когда преобразования происходит мы возвращаем jvalue, но у нас удаляется jObject и val.l указывает на невалидный адрес. Поэтому приходится сохранять холдеры во время вызова функции java.

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

В случае передачи нескольких параметров используем список инициализации:

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

Возвращение нужного типа данных

Хотелось бы написать какой-то один метод, который разруливал ситуацию и выглядел:

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

Но есть неприятная особенность JNI: для каждого возвращаемого типа есть свой конкретный метод. То есть, для int вам нужен CallStaticIntMethod, для float – CallStaticFloatMethod и так далее. Пришел к частичным типизациям шаблонов. Сначала объявляем нужный нам интерфейс:

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

Потом для каждого типа пишем реализацию. Для целых чисел (int) будет выглядеть:

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

Если у нас ноль параметров, то нужно вызывать CallStaticMethod, а не CallStaticMetodA. Ну и если пытаться создать массив размерностью ноль, компилятор сообщит вам все, что думает по этому поводу.

Финал

Сам метод вызова выглядит:

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

Теперь вызов метода из java:

Java код

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

Где-то в нативном коде:

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

Ранее код выглядел

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

Выводы

Вызовы методов превратились в удобную вещь и не надобно запоминать сигнатуры и конвертировать значения и удалять ссылки. Достаточно передавать название класса, метода и аргументы.

Так же получилась интересная задачка, благодаря которой немного поразбирался с новыми плюшками языка (которые мне ну очень понравились) и вспомнил шаблоны.

Благодарю за прочтение. Ну или за внимание, если вы не все прочитали. С радостью прочитаю предложения по улучшению и критику работы. А так же отвечу на вопросы.

ссылка на оригинал статьи http://habrahabr.ru/post/258707/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *