Using Slf4j from C++ JNI

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 &&current, 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 jobjects:

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 &&current, 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 &&current, 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 :)