Существует довольно много приложений под 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);
Постановка задачи
Для начала поймем, что нам нужно. В сущности, это четыре вещи:
- Вызвать метод;
- Из параметров нужно вытянуть строку сигнатуры. Да, да, вот эту (ILjava/lang/String;F)J;
- Сконвертировать параметры в нужный тип;
- Возвратить тип данных, который хочет видеть пользователь нашего класса.
Собственно, это все. Вроде бы просто. Приступим?
Вызов метода
Теперь стоит отметить, как мы будем вызывать нашу функцию-оболочку. Так как параметров может разное количество (от нуля и больше), то нужна функция вроде print`а в стандартной библиотеке, но с тем, чтобы было удобно вытягивать тип параметра и сам параметр. В С++11 появились вариадические шаблоны. Ими и воспользуемся.
template <typename MethodType, typename... Args> MethodType CallStaticMethod(Args... args);
Составляем сигнатуру
Для начала нам нужно получить строку, которая числится в документации для данного типа. Тут два варианта:
- Используем typeid и цепочку if … else. Должно получится что-то вроде:
if (typeid(arg) == typeid(int)) return “I”; else if (typeid(arg) == typeid(float)) return “F”;
И так для всех типов, которые вам нужны.
- Используем шаблоны и их частичные типизации. Метод интересен тем, что у вас будут функции в одну строку и не будет лишних сравнений типов. Более того все это будет на стадии инстанциации шаблонов. Выглядеть все будет примерно так:
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 с конструкторами нужных типов.
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`а.
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:
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/
Добавить комментарий