Using Slf4j from C++ JNI
Lately I've been gluing some Java+Netty code with a C++ native library, and I wanted the native C++ code to use Java's logging too (in this case the ubiquitous slf4j framework).
So, let's say for example you have the following Java class:
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class NativeGlue {
public native void doNativeThings();
}
it would be then great to log from JNI using the static log
Slf4j field (gratiously provided by lombok in this case)!
That's easy! Let's just get retrieve the value from the static log
field:
auto class_ = env->GetObjectClass(source);
auto logField = env->GetStaticFieldID(class_, "log", "Lorg/slf4j/Logger;");
auto logObject = env->GetStaticObjectField(class_, logField);
But now comes the tricky part: how do we log? Of course we'd love the log methods to be available in C++ as they would be in Java, so that one can use the varags methods and do things like log.info("Hello {} {} times!", "world", 2);
.
And here comes the trick! We glue the varargs public abstract void info(java.lang.String, java.lang.Object...);
method and bind it to a C++ variadic template function that will take care of transforming whatever we pass into its java counterpart, so that we can safely pass strings, primitive types and other Java objects to the underlying Slf4j method.
As we know the ellipsis in Java is just syntactic sugar to get an object array: this is well visible in the JNI signature of the info
(pick your preferred loglevel here) varargs method:
(Ljava/lang/String;[Ljava/lang/Object;)V
(To get the JNI signatures, use javap
. The one above was obtained using javap -s org/slf4j/Logger.class
.)
Starting from the logObject
retrieved before, we can easily get the info
method id by signature as follows:
auto methodId = env->GetMethodID(env->GetObjectClass(logObject), "info", "(Ljava/lang/String;[Ljava/lang/Object;)V");
And then use it in a variadic template function that unrolls its arguments and collects them into a jobjectArray
to be provided as second argument to the info
method:
template<typename... Ts>
void Log::info(const std::string &format, Ts &&... args) {
auto argArray = env->NewObjectArray(sizeof...(args), env->FindClass("java/lang/Object"), nullptr);
toArgArray(env, argArray, 0, std::forward<Ts>(args)...);
env->CallVoidMethod(object, infoMethod, toJava(format), argArray);
}
void Log::toArgArray(JNIEnv *env, jobjectArray &array, int) {
}
template<typename T, typename... Ts>
void Log::toArgArray(JNIEnv *env, jobjectArray &array, int position, T &¤t, Ts &&... args) {
env->SetObjectArrayElement(array, position, toJava(std::forward<T>(current)));
toArgArray(env, array, position + 1, std::forward<Ts>(args)...);
}
The empty toArgArray
overload represents the base of the template (inductive) loop. As you can see, each time an element is set into the argument array, a toJava
function is called to wrap the C++ value current
into the proper Java equivalent.
For asciiz strings that would be a jstring
:
jstring Log::toJava(const char *value) {
return env->NewStringUTF(value);
}
For native types, we need to take care and autobox them into their equivalent jobject
s:
jobject Log::toJava(int value) {
auto class_ = env->FindClass("java/lang/Integer");
auto ctor = env->GetMethodID(class_, "<init>", "(I)V");
return env->NewObject(class_, ctor, value);
};
And that's enough to log! To make it easy I provide the whole example class here:
log.hh
#ifndef LOG_HH
#define LOG_HH
#include <jni.h>
#include <string>
/**
* A JNI wrapper to use Slf4j facility.
*
* @author Andrea Leofreddi
*/
class Log {
public:
template<typename... Ts>
void info(const std::string &string, Ts &&...args);
Log(JNIEnv *env, jobject source);
private:
JNIEnv *env;
const jobject object;
const jmethodID infoMethod;
inline jobject &toJava(jobject &value);
inline jstring toJava(const char *value);
inline jstring toJava(const std::string &value);
inline jobject toJava(int value);
inline void toArgArray(JNIEnv *env, jobjectArray &array, int position);
template<typename T, typename... Ts>
inline void toArgArray(JNIEnv *env, jobjectArray &array, int position, T &¤t, Ts &&... args);
};
template<typename... Ts>
void Log::info(const std::string &format, Ts &&... args) {
auto argArray = env->NewObjectArray(sizeof...(args), env->FindClass("java/lang/Object"), nullptr);
toArgArray(env, argArray, 0, std::forward<Ts>(args)...);
env->CallVoidMethod(object, infoMethod, toJava(format), argArray);
}
void Log::toArgArray(JNIEnv *env, jobjectArray &array, int) {
}
template<typename T, typename... Ts>
void Log::toArgArray(JNIEnv *env, jobjectArray &array, int position, T &¤t, Ts &&... args) {
env->SetObjectArrayElement(array, position, toJava(std::forward<T>(current)));
toArgArray(env, array, position + 1, std::forward<Ts>(args)...);
}
jobject &Log::toJava(jobject &value) {
return value;
}
jstring Log::toJava(const char *value) {
return env->NewStringUTF(value);
}
jstring Log::toJava(const std::string &value) {
return env->NewStringUTF(value.c_str());
}
jobject Log::toJava(int value) {
auto class_ = env->FindClass("java/lang/Integer");
assert(class_);
auto ctor = env->GetMethodID(class_, "<init>", "(I)V");
assert(ctor);
return env->NewObject(class_, ctor, value);
};
#endif //LOG_HH
log.cc
#include "log.hh"
static jobject getLogObject(JNIEnv *env, jobject source) {
assert(source);
auto class_ = env->GetObjectClass(source);
assert(class_);
auto logField = env->GetStaticFieldID(class_, "log", "Lorg/slf4j/Logger;");
assert(logField);
auto logObject = env->GetStaticObjectField(class_, logField);
assert(logObject);
return logObject;
}
static jmethodID getMethod(JNIEnv *env, jobject log, std::string method) {
auto methodId = env->GetMethodID(env->GetObjectClass(log), "info", "(Ljava/lang/String;[Ljava/lang/Object;)V");
assert(methodId);
return methodId;
}
Log::Log(JNIEnv *env, jobject source) : env(env), object(getLogObject(env, source)), infoMethod(getMethod(env, object, "info")) {
}
And now let's use the Log class from the initial example:
JNIEXPORT jint JNICALL Java_NativeGlue_doNativeThings(JNIEnv *env, jobject object) {
Log log(env, object);
log.info("Hello {} {} times!", "world", 2);
}
Enjoy :)