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);
После
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);
Выводы
Вызовы методов превратились в удобную вещь и не надобно запоминать сигнатуры и конвертировать значения и удалять ссылки. Достаточно передавать название класса, метода и аргументы.
Также получилась интересная задачка, благодаря которой немного поразбирался с новыми плюшками языка (которые мне ну очень понравились) и вспомнил шаблоны.
Благодарю за прочтение. Ну или за внимание, если вы не все прочитали. С радостью прочитаю предложения по улучшению и критику работы. А также отвечу на вопросы.
Комментарии (15)
valfrom
25.05.2015 14:51+1Ссылку на gihub хочется…
Jester92 Автор
25.05.2015 14:54+3Возможно, там некоторое недоработано и код может быть некрасивый, так что не обессудьте. Тыц
domix32
25.05.2015 16:05+1Впервые вижу, чтобы кто-то посетовал на некрасивый код и все равно ВЫЛОЖИЛ ссылку.
А разве для вызова метода из C++ он не должен отмечаться ещё и ключевым словомnative
?Hertz
25.05.2015 16:11native методы должны быть реализованы в динамической библиотеке, экспортирующей соответствующую c-функцию. Из нативного кода можно вызывать любые методы jvm.
Jester92 Автор
25.05.2015 16:14native нужно отмечать методы, которые вызываются из Java и определяются в C++. Если не ошибаюсь, то
package com.mypackage; class Test { private static native void nativeMyMethod(); public void JavaMethod() { nativeMyMethod(); } }
А в С++ определение будет
extern "C" void Java_com_mypackage_Test_nativeMyMethod() {...}
А код мой мне всегда не нравиться и всегда хочется переделать, правда не всегда знаю как.Hertz
25.05.2015 16:17Не обязательно C++, любой язык, компилятор которого способен собирать динамические библиотеки, экспортирующие си-функции.
Фортран можно, я на D реализовывал нативные методы еще.
Hertz
25.05.2015 16:05+2Я использовал подобную идею, но предпочитаю задавать сигнатуру функций, а не полагаться на правильную передачу аргументов.
Использование выглядит примерно так:
static const jni::instance_method<jint(jint, jint)> add{"add", jni_env, instance}; const auto sum = add(1, 1); assert(sum == 2);
Monnoroch
25.05.2015 16:26+4Делал для себя несколько лет назад. С тех пор ушел от нативного кодинга на андроиде и запросил развитие. Уверен, что за несколько правок можно заставить работать.
github.com/Monnoroch/CppJni
Там есть буквально все: даже возможность маппинга С++ классов на джава-классы.monah_tuk
26.05.2015 12:23Ещё бы какую-нибудь документацию к этому делу…
Monnoroch
26.05.2015 16:12К сожалению, я давным давно не занимаюсь проектом, и плохо подготовил его для open-source. Однако, там есть некое описание вот тут: github.com/Monnoroch/CppJni/blob/master/JniForwards.h.
И я вижу, там наплюсовали проект мне, кому-то, видимо, понравился, так что скажу, что буду очень рад пуллреквестам и все такое. В свое время мне очень облегчило жизнь решение сделать либу, а не ковыряться самому в jni.
PavelOsipov
26.05.2015 10:10Dropbox в теме написания мостов между C++ и Java пошел по пути кодогенерации. Djinni получился действительно неплох.
VioletGiraffe
Нужная вещь, надо себе вкрутить :)