[Из песочницы] Удобоваримый вызов Java методов из нативного кода

Существует довольно много приложений под Android, которые совмещают C++ и Java код. Где Java выступает оберткой/прослойкой, а C++ выполняет всю грязную работу. Пожалуй, ярким примером могут служить игры. В связи с этим часто приходится вызывать Java код из нативного для доступа к системным свойствам и плюшкам, которые предоставляет система (переключится на другую активность, послать или скачать что-либо из интернета). Причин много, а проблема одна: каждый раз приходится писать в лучшем случае 5 строчек кода и помнить, какую сигнатуру функции нужно запихнуть в параметр. Потом еще нужно перевести эти параметры в нужный тип. Стандартный пример из туториалов: long f (int n, String s, float g); Строка-сигнатура для данного метода будет (ILjava/lang/String; F)J.Вам удобно это все запоминать? А переводить С-строки в jstring? Мне — нет. Мне хочется писать:

CallStaticMethod(className, «f», 1, 1.2f); Постановка задачи Для начала поймем, что нам нужно. В сущности, это четыре вещи: Вызвать метод; Из параметров нужно вытянуть строку сигнатуры. Да, да, вот эту (ILjava/lang/String; F)J; Сконвертировать параметры в нужный тип; Возвратить тип данных, который хочет видеть пользователь нашего класса. Собственно, это все. Вроде бы просто. Приступим? Вызов метода Теперь стоит отметить, как мы будем вызывать нашу функцию-оболочку. Так как параметров может разное количество (от нуля и больше), то нужна функция вроде print`а в стандартной библиотеке, но с тем, чтобы было удобно вытягивать тип параметра и сам параметр. В С++11 появились вариадические шаблоны. Ими и воспользуемся. template MethodType CallStaticMethod (Args… args); Составляем сигнатуру Для начала нам нужно получить строку, которая числится в документации для данного типа. Тут два варианта: Используем typeid и цепочку if… else. Должно получится что-то вроде: if (typeid (arg) == typeid (int)) return «I»; else if (typeid (arg) == typeid (float)) return «F»; И так для всех типов, которые вам нужны. Используем шаблоны и их частичные типизации. Метод интересен тем, что у вас будут функции в одну строку и не будет лишних сравнений типов. Более того все это будет на стадии инстанциации шаблонов. Выглядеть все будет примерно так: template std: string GetTypeName ();

// int template <> std: string GetTypeName() { return «I»; }

// string template <> std: string GetTypeName() { return «Ljava/lang/String;»; } Для составления строки-сигнатуры в нашем существует два способа: рекурсивный и через массив. Сначала рассмотрим рекурсивный вызов. void GetTypeRecursive (std: string&) { }

template void GetTypeRecursive (std: string& signatureString, T value, Args… args) { signatureString += GetTypeName(); GetTypeRecursive (signatureString, args…); } Вызов всего этого непотребства: template MethodType CallStaticMethod (const char* className, const char* mname, Args… args) { std: string signature_string = »(»; GetTypeRecursive (signature_string, args…); signature_string += »)»; signature_string += GetTypeName(); return MethodType (); // пока здесь заглушка } Рекурсия — это хорошо в воспитательно-образовательных целях, но предпочитаю ее обходить при возможности. Тут такая возможность есть. Так как аргументы идут последовательно и мы можем узнать количество аргументов можно использовать удобство предоставленное стандартом С++11. Код преобразуется в: template 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(); return MethodType (); // пока здесь заглушка } Кода вроде бы и больше, но работает оно быстрее. Хотя бы за счет того, что не вызываем функций больше, чем нам это нужно.Конвертация типа данных Есть несколько вариантов вызова CallStaticMethod: NativeType CallStaticMethod (JNIEnv *env, jclass clazz, jmethodID methodID, …);

NativeType CallStaticMethodA (JNIEnv *env, jclass clazz, jmethodID methodID, jvalue *args);

NativeType CallStaticMethodV (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(JniHolder (env, 10)); Но есть одно но. Когда преобразования происходит мы возвращаем jvalue, но у нас удаляется jObject и val.l указывает на невалидный адрес. Поэтому приходится сохранять холдеры во время вызова функции java. JniHolder holder (env, 10) jvalue val = static_cast(holder); В случае передачи нескольких параметров используем список инициализации: JniHolder holders[size] = { std: move (JniHolder (env, args))… }; jvalue vals[size]; for (size_t i = 0; i < size; ++i) vals[i] = static_cast(holders[i]); Возвращение нужного типа данных Хотелось бы написать какой-то один метод, который разруливал ситуацию и выглядел: template MethodType CallStaticMethod (Args… args) { MethodType result = …; …. return reesult; }

Но есть неприятная особенность JNI: для каждого возвращаемого типа есть свой конкретный метод. То есть, для int вам нужен CallStaticIntMethod, для float — CallStaticFloatMethod и так далее. Пришел к частичным типизациям шаблонов. Сначала объявляем нужный нам интерфейс: template struct Impl { template static MethodType CallMethod (JNIEnv* env, jclass clazz, jmethodID method, Args… args); }; Потом для каждого типа пишем реализацию. Для целых чисел (int) будет выглядеть: template <> struct Impl { template static int CallStaticMethod (JNIEnv* env, jclass clazz, jmethodID method, Args… args) { const int size = sizeof…(args); if (size!= 0) { jvalue vals[size] = { static_cast(JniHolder (env, args))… }; return env→CallStaticIntMethodA (clazz, method, vals); }

return env→CallStaticIntMethod (clazz, method); } }; Если у нас ноль параметров, то нужно вызывать CallStaticMethod, а не CallStaticMetodA. Ну и если пытаться создать массив размерностью ноль, компилятор сообщит вам все, что думает по этому поводу.Финал Сам метод вызова выглядит: template 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();

JNIEnv *env = getEnv (); JniClass clazz (env, className); jmethodID method = env→GetStaticMethodID (clazz.get (), mname, signature_string.c_str ()); return Impl:: 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(«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); Выводы Вызовы методов превратились в удобную вещь и не надобно запоминать сигнатуры и конвертировать значения и удалять ссылки. Достаточно передавать название класса, метода и аргументы.Так же получилась интересная задачка, благодаря которой немного поразбирался с новыми плюшками языка (которые мне ну очень понравились) и вспомнил шаблоны.

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

© Habrahabr.ru