Pull to refresh

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

Reading time 7 min
Views 18K
Существует довольно много приложений под 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);

Выводы


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

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

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

Articles