Continuing a little bit from my previous post. I was working on this Android thing that needs Java to be called from C++ and vice-versa, so I had to use JNI and as some might know JNI isn’t that beautiful to work with out of the box, so I was creating nice abstractions – like wrapping jobject in RAII class and make it possible to call methods on that object without always passing the JNIEnv pointer and also creating global references to use the object out of the function call scope etc.
As some might know calling a JNI method requires to pass the method signature which is this string that looks like this for an example: "(IJLjava/lang/String;)Z"
where:
- I stands for integer;
- J stands for long;
- Ljava/langString; stands for
java.lang.String
object; - and Z stands for boolean;
- more on that here.
So every time you call a method you have to think of this string and also pass the apropriate arguments, but why not deduct the signature directly from the argument types passed in the method – of course it wouldn’t work for all the custom Java classes, but something could still be done about it … and so I got carried away.
First take – try to concatenate string by using recursive template argument pack unwrapping.
Basically this:
inline std::string get_signature()
{
return {};
}
template<typename T, typename... TArgs>
inline std::string get_signature(const T& arg, const TArgs&... args)
{
return "x" + get_signature(std::forward<const TArgs&>(args)...);
}
All good – created a lot of special cases for each known JNI type and one special case for std::string
-> java.lang.String
.
The problem – Godbolt showed me a test case to be around 500 instructions – a lot of things to happening at run-time – mostly string construction and concatenation – which is not what I’d like to happen on every method call.
Take two – reuse single std::string with pre-reserved memory.
inline void get_signature(const std::string&)
{}
template<typename T, typename... TArgs>
inline void get_signature(std::string& out, const T& arg, const TArgs&... args)
{
out.append("x");
get_signature(out, std::forward<const TArgs&>(args)...);
}
Things start to look better – Godbolt shows reduced instruction count to 200 – still a lot of run-time calculations are happening, but at least there’s no unnecessary re-allocations.
Still I thought to myself – I can do better.
Take three – the dark arts of constexpr.
I thought – could it be possible to run all these concatenations at compile-time? I reverted back to first solution and switched to C++20 in hopes to invoke std::string constexpr constructor and hoped the concatenation operator would work too – turns out compilers are not all there yet.
Then I found this wonderful C++17 example on StackOverflow and that helped me achieve this:
inline constexpr auto get_signature()
{
return concat("");
}
template<typename T, typename... TArgs>
inline constexpr auto get_signature(const T& arg, const TArgs&... args)
{
return concat("x", get_signature(std::forward<const TArgs&>(args)...).str);
}
This reduces everything down to near zero instructions as the string get’s concatenated at compile time. How cool is that?
So in the end I can have this abstraction for JNI method call:
template<typename... TArgs>
inline void invokeMethod(const std::string& method, const TArgs&... args)
{
constexpr auto signature = get_signature(std::forward<const TArgs&>(args)...);
jniEnv->CallVoidMethod(method, signature.str, std::forward<const TArgs&>(args)...);
}
which reduces this:
javaObject->invokeMethod("doSomething", "(IJLjava/lang/String;)V", 1234, 987654321L, "asdf");
into this:
javaObject->invokeMethod("doSomething", 1234, 987654321L, "asdf");
Not much, but dope AF!