6
\$\begingroup\$

I am working on a project which uses JNI. More than once I fell over the absence of type safety working with the variardic function calls. So I came up with this module. It wraps the variardic JNI functions using C++ variardc templates to offer a type-safe alternative. It also features integrated error checking for more compact code. This could be of use to others. I want to flesh it out and then publish it so others may base their work on it. This is the first time I really used templates in C++. I know it works well for me, but I also want to know whether the design is good. I am grateful for your opinions.

In the code below, I omitted the repetitive implementations which only differ in the type used. You can find the complete code here.

Use-Case

Instead of the lengthy and error-prone

jclass jcls = (*env)->FindClass(env, "SomeClass");
if (jcls == NULL) {
    return; // handle error
}
jmethodID constructor = (*env)->GetMethodID(env, jcls, "<init>", "(JLjava/lang/String;)V");
if (constructor == NULL) {
    return; // handle error
}
jstring jsomestring = (*env)->NewStringUTF(env, somestring);
if (jsomestring == NULL) {
    return; // handle error
}
jlong jsomelong = somelong; // explicitly convert
jobject jobj = (*env)->NewObject(env, jcls, constructor, jsomelong, jsomestring);
if (jobj == NULL) {
    return; // handle error
}
jmethodID method = (*env)->GetMethodID(env, jcls, "someMethod", "()V");
if (method == NULL) {
    return; // handle error
}
(*env)->CallVoidMethod(env, jobj, method);
(*env)->DeleteLocalRef(jsomestring); // late cleanup
(*env)->DeleteLocalRef(jobj);

you can now write

try {
    tenv.find_class("SomeClass").
        GetConstructor<jlong,jstring>()(
            some_number, // implicit conversion where possible
            tenv.make_jstring(some_string) // explicit conversion with automated clean-up
        ).
        GetMethod<void()>("someMethod")();
} catch (std::exception & e) {
    // handle error
}

.

Header

#pragma once

#include <jni.h>
#include <string>
#include <stdexcept>
#include <functional>
#include <memory>

/**
 * Exception class for all runtime errors regarding TypedJNI.
 */
class TypedJNIError : public std::runtime_error 
{
    public:
    TypedJNIError(const std::string& what_arg);
};

namespace TypedJNI { // namespace for internal helper functions
/**
 * GetTypeString maps the type arguments to the corresponding JNI type signature.
 * 
 * * GetTypeString<jboolean>() → "Z"
 * * GetTypeString<jbyte>() → "B"
 * * and so on
 * 
 * The full list is at https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/types.html#type_signatures.
 * 
 * Can handle an arbitrary number of arguments: `GetTypeString<jboolean,jbyte>()` → "ZB"
 */
template <typename T>
std::string GetTypeString(){
    // this declares the templated function, but the default template may never actually be used
    static_assert(std::is_same<T,void>::value, "Cannot handle this type.");
    return "This actually never gets compiled.";
};
template <> std::string GetTypeString<void>();
template <> std::string GetTypeString<jboolean>();
template <> std::string GetTypeString<jint>();
template <> std::string GetTypeString<jlong>();
template <> std::string GetTypeString<jstring>();
template<typename T, typename... Args>
typename std::enable_if<sizeof...(Args) != 0, std::string>::type
GetTypeString() {
    // recursively expand type string
    return GetTypeString<T>() + GetTypeString<Args...>();
};
template<typename... Args>
typename std::enable_if<sizeof...(Args) == 0, std::string>::type
GetTypeString() {
    // end of recursion
    return "";
};

/**
 * Wrapper around GetStaticMethodID(…)
 * 
 * Performs error-checking. Throws an exception on error.
 */
jmethodID GetStaticMethodID(JNIEnv *env, const jclass cls, const std::string name, const std::string & signature);

/**
 * Wrapper around GetMethodID(…)
 * 
 * Performs error-checking. Throws an exception on error.
 */
jmethodID GetMethodID(JNIEnv *env, const jclass cls, const std::string name, const std::string & signature);
} // namespace TypedJNI

/**
 * TypedJNIStaticMethod selects the corresponding JNI function for static methods based on the requested return type.
 * 
 * * TypedJNIStaticMethod<void>::get(…) → CallStaticVoidMethod
 * * TypedJNIStaticMethod<jint>::get(…) → CallStaticIntMethod
 * * and so on
 * 
 * The full list is at https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/functions.html#calling_static_methods.
 * 
 * For convenience, jstring is handled explicitly. All other objects are jobject. 
 * 
 * Based on https://stackoverflow.com/questions/9065081/.
 */
template<typename T> 
class TypedJNIStaticMethod;
template<typename ...Args> 
class TypedJNIStaticMethod<void(Args...)>
{
    public:
    static std::function<void(Args...)> get(JNIEnv *env, const jclass cls, const std::string & name) {
        jmethodID mid = TypedJNI::GetStaticMethodID(env, cls, name, "("+TypedJNI::GetTypeString<Args...>()+")"+TypedJNI::GetTypeString<void>());
        return [env, cls, mid](Args... args) {
            env->CallStaticVoidMethod(cls, mid, args...);
        };
    }
};
// other types omitted for less code to review

/**
 * Like TypedJNIStaticMethod, but for object methods.
 */
template<typename T> 
class TypedJNIMethod;
template<typename ...Args> 
class TypedJNIMethod<void(Args...)>
{
    public:
    static std::function<void(Args...)> get(JNIEnv *env, const jclass cls, const jobject obj, const std::string & name) {
        jmethodID mid = TypedJNI::GetMethodID(env, cls, name, "("+TypedJNI::GetTypeString<Args...>()+")"+TypedJNI::GetTypeString<void>());
        return [env, obj, mid](Args... args) {
            env->CallVoidMethod(obj, mid, args...);
        };
    }
};
// other types omitted for less code to review

/**
 * Class for a proxy object referencing a Java object.
 * 
 * The local reference to the Java object is deleted when the last copy of the proxy object is destroyed.
 */
class TypedJNIObject {
    private:
    JNIEnv *env = nullptr;
    jclass cls = nullptr;
    std::shared_ptr<_jobject> obj = nullptr;
    public:
    TypedJNIObject(JNIEnv *env, jclass cls, jobject obj);
    template<typename... Args>
    std::function<Args...> GetMethod(const std::string name) {
        return TypedJNIMethod<Args...>::get(env, cls, obj.get(), name);
    }
};

/**
 * Special case of TypedJNIMethod for accessing a Java constructor.
 */
template<typename ...Args> 
class TypedJNIConstructor
{
    public:
    static std::function<TypedJNIObject(Args...)> get(JNIEnv *env, const jclass cls) {
        // yes indeed GetMethodID as illustrated at https://stackoverflow.com/questions/7260376/ (not get GetStaticMethodID)
        const jmethodID mid = TypedJNI::GetMethodID(env, cls, "<init>", "("+TypedJNI::GetTypeString<Args...>()+")"+TypedJNI::GetTypeString<void>());
        return [env, cls, mid](Args... args) -> TypedJNIObject {
            return TypedJNIObject(env, cls, env->NewObject(cls, mid, args...));
        };
    }
};

/**
 * Special case of TypedJNIObject for convenient string conversion.
 * 
 * Uses NewStringUTF (expects Java modified Unicode, see https://stackoverflow.com/questions/7921016).
 */
class TypedJNIString {
    private:
    std::shared_ptr<_jstring> jstrptr = nullptr;
    public:
    TypedJNIString(JNIEnv *env, const std::string & str);
    operator jstring() const;
};

/**
 * Class for a proxy object referencing a Java class.
 * 
 * Static methods can be accessed as well as the constructors.
 */
class TypedJNIClass {
    private:
    JNIEnv *env = nullptr;
    public:
    const jclass cls = nullptr;
    TypedJNIClass(JNIEnv *env, jclass cls);
    template<typename... Args>
    std::function<Args...> GetStaticMethod(const std::string name) {
        return TypedJNIStaticMethod<Args...>::get(env, cls, name);
    }
    template<typename... Args>
    std::function<TypedJNIObject(Args...)> GetConstructor() {
        return TypedJNIConstructor<Args...>::get(env, cls);
    }
};

/**
 * Class for creating a Java VM and Environment.
 * 
 * Wrapper around JNI_CreateJavaVM.
 * 
 * Performs error-checking. Throws an exception on error.
 * 
 * Provides access to Java classes and utility methods.
 * 
 * The VM is destroyed with the TypedJNIEnv object.
 */
class TypedJNIEnv {
    private:
    JavaVM *vm = nullptr;
    public:
    /**
     * The environment is provided by a naked pointer.
     * 
     * This is public so functionality not wrapped by TypedJNI can use direct access.
     */
    JNIEnv *env = nullptr;
    TypedJNIEnv(const TypedJNIEnv&) = delete;
    TypedJNIEnv& operator=(const TypedJNIEnv&) = delete;
    TypedJNIEnv(JavaVMInitArgs vm_args);
    virtual ~TypedJNIEnv();
    
    /**
     * Find a Java class from the Java runtime.
     */
    TypedJNIClass find_class(std::string name);
    
    /**
     * Create a Java String from a std::string.
     */
    TypedJNIString make_jstring(const std::string & str);
};

Implementation

#include "typedjni.hpp"

TypedJNIError::TypedJNIError(const std::string& what_arg) : std::runtime_error(what_arg) {};

template <> std::string TypedJNI::GetTypeString<void>(){return "V";};
template <> std::string TypedJNI::GetTypeString<jboolean>(){return "Z";};
template <> std::string TypedJNI::GetTypeString<jint>(){return "I";};
template <> std::string TypedJNI::GetTypeString<jlong>(){return "J";};
template <> std::string TypedJNI::GetTypeString<jstring>(){return "Ljava/lang/String;";};

jmethodID TypedJNI::GetStaticMethodID(JNIEnv *env, const jclass cls, const std::string name, const std::string & signature) {
    jmethodID mid = env->GetStaticMethodID(cls, name.c_str(), signature.c_str());
    if (mid == NULL) {
        throw TypedJNIError("Failed to find static method '"+name+"' "+signature+".");
    }
    return mid;
}

jmethodID TypedJNI::GetMethodID(JNIEnv *env, const jclass cls, const std::string name, const std::string & signature) {
    jmethodID mid = env->GetMethodID(cls, name.c_str(), signature.c_str());
    if (mid == NULL) {
        throw TypedJNIError("Failed to find method '"+name+"' "+signature+".");
    }
    return mid;
}

TypedJNIObject::TypedJNIObject(JNIEnv *env, jclass cls, jobject obj) : 
    env(env), 
    cls(cls),
    obj(std::shared_ptr<_jobject>(obj, [env](jobject o){env->DeleteLocalRef(o);})) {};

TypedJNIString::TypedJNIString(JNIEnv *env, const std::string & str) {
    jstring jstr = env->NewStringUTF(str.c_str());
    if (jstr == NULL) {
        throw TypedJNIError("NewStringUTF failed for string '"+str+"'.");
    }
    jstrptr = std::shared_ptr<_jstring>(jstr, [env](jstring s){env->DeleteLocalRef(s);});
}

TypedJNIString::operator jstring() const {
    return jstrptr.get();
}

TypedJNIClass::TypedJNIClass(JNIEnv *env, jclass cls) : env(env), cls(cls) {
    if (!cls) {
        throw TypedJNIError("Tried to create TypedJNIClass from nullptr.");
    }
};

TypedJNIEnv::TypedJNIEnv(JavaVMInitArgs vm_args) {
    jint res = JNI_CreateJavaVM(&vm, (void **)&env, &vm_args);
    if (res != JNI_OK) {
        throw TypedJNIError(std::string("Failed to create Java VM (error ")+std::to_string(res)+").");
    }
}

TypedJNIEnv::~TypedJNIEnv() {
    vm->DestroyJavaVM();
}

TypedJNIClass TypedJNIEnv::find_class(std::string name) {
    jclass cls = env->FindClass(name.c_str());
    if (cls == NULL) {
        throw TypedJNIError("Failed to find class '"+name+"'.");
    }
    return TypedJNIClass(env, cls);
}

TypedJNIString TypedJNIEnv::make_jstring(const std::string & str) {
    return TypedJNIString(env, str);
}
\$\endgroup\$
1
  • \$\begingroup\$ For further reference: I found a flaw in the design regarding the deleter in TypedJNIObject. If the last C++ instance goes out of scope, the deleter is called and the Java object is garbage collected. To compensate, TypedJNIMethod::get now captures the TypedJNIObject instance rather than the raw and potentially dangling pointers. \$\endgroup\$
    – Hermann
    Jan 26 at 21:03
2
\$\begingroup\$

Consider changing the interface

I'm not very familiar with Java, but I assume you want the interface to the Java classes to feel as native C++ as possible. Your example code does not really look like native C++:

    tenv.find_class("SomeClass").
        GetConstructor<jlong,jstring>()(
            some_number, // implicit conversion where possible
            tenv.make_jstring(some_string) // explicit conversion with automated clean-up
        ).
        GetMethod<void()>("someMethod")();

Part of it is the function names: I would expect GetConstructor() to return a constructor, but not actually construct the object. Perhaps it should just be renamed Construct(), but that still doesn't feel right; you never have to write std::string foo = std::string::GetConstructor()("Hello") in C++.

Let's start with how I would want the interface to work:

auto psclass = tenv.find_class("SomeClass");
auto object = psclass(some_number, tenv.make_jstring(some_string));
object.call<void()>("someMethod")();

In the above example, creating a class object would work the same as you already have. But then to construct it, I want to feel it as much C++ as possible. Overloading operator() of TypedJNIClass would allow you to do that to a certain extent (you can write std::string = std::string("Hello") after all). Also, I wouldn't want to have to specify the template parameters, so let the compiler deduce them automatically. Here's a possible implementation:

class TypedJNIClass {
    ...
    template<typename... Args>
    TypedJNIObject operator()(Args... &&args) {
        const jmethodID mid = TypedJNI::GetMethodID(env, cls, "<init>", "(" + TypedJNI::GetTypeString<Args...>() + ")" + TypedJNI::GetTypeString<void>());
        return TypedJNIObject(env, cls, env->NewObject(cls, mid, args...));
    }
};

And then for calling methods, I would want to avoid the two-step process, and not first get a lambda and then pass the parameters to the lambda:

class TypedJNIObject {
    ...
    template<typename... Args>
    auto call(const std::string &name, Args... &&args) {
        return TypedJNIMethod<Args...>::get(env, cls, obj.get(), name)(args);
    }
};

Of course, the latter might be inefficient if you repeatedly want to call the same method.

What about functions that return something?

You only handle functions that return void. I'm sure someone would want to use this to get some data back from the Java methods they are calling.

Enforce copy semantics for objects

C++ programmers are used to copy semantics. However, it looks like you wrote TypedJNIObject in such a way that if you write:

auto object1 = tenv.find_class("SomeClass")(...);
auto object2 = object1;

Then both object1 and object2 refer to the same Java object. This could be very confusing. I recommend that you change the type of TypedJNIObject::obj from std::shared_ptr to std::unique_ptr, and write a copy constructor for it that actually creates a deep copy of the object, so it behaves like a regular C++ object. If a C++ programmer wants to avoid a deep copy, they'd just use references explicitly:

auto object1 = tenv.find_class("SomeClass")(...);
auto &object2 = object1;

Avoid repeating the name of a namespace in its members

Many things inside namespace TypedJNI are prefixed with TypedJNI, but that is unnecessary duplication. You can remove that prefix from all its member functions and classes. Code using your classes can just write:

TypedJNI::Class psclass = tenv.find_class("SomeClass");
\$\endgroup\$
2
  • \$\begingroup\$ Thank you for your review. I was sure about many of the points you mentioned. I just updated the interface according to your suggestion. I definitely want to look if I can make the automatic template parameter deduction work. Many (but not all) other return types are actually implemented – I just left them out of the review because it is all the same (except for the type, of course). I was unsure whether a prefix or a namespace would be better (you can tell from the code I was undecided). Your answer made me lean more towards namespaces. \$\endgroup\$
    – Hermann
    Dec 4 '20 at 21:05
  • \$\begingroup\$ The C++ copy semantics are another good point. I am doubtful I can implement that as Java objects – as far as I know – are not guaranteed to provide a mechanism for creating a deep copy. Users will need to keep it in mind. But I want to add it to the comment for clarification. \$\endgroup\$
    – Hermann
    Dec 4 '20 at 21:16

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.