From b2e1291973249aaba49186d1ccbfd2b730d421d9 Mon Sep 17 00:00:00 2001 From: Peter Johnson Date: Sun, 25 Sep 2016 19:29:17 -0700 Subject: [PATCH] Refactor JNI helpers into wpiutil (support/jni_util.h). (#105) --- include/support/jni_util.h | 439 +++++++++++++++++++++++++++++++++++++ 1 file changed, 439 insertions(+) create mode 100644 include/support/jni_util.h diff --git a/include/support/jni_util.h b/include/support/jni_util.h new file mode 100644 index 0000000000..32860a5a68 --- /dev/null +++ b/include/support/jni_util.h @@ -0,0 +1,439 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) FIRST 2016. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#ifndef WPIUTIL_SUPPORT_JNI_UTIL_H_ +#define WPIUTIL_SUPPORT_JNI_UTIL_H_ + +#include +#include +#include +#include +#include + +#include + +#include "llvm/ArrayRef.h" +#include "llvm/ConvertUTF.h" +#include "llvm/SmallString.h" +#include "llvm/SmallVector.h" +#include "llvm/StringRef.h" +#include "support/atomic_static.h" +#include "support/SafeThread.h" + +namespace wpi { +namespace java { + +// Finds a class and keep it as a global reference. +// Use with caution, as the destructor does NOT call DeleteGlobalRef due +// to potential shutdown issues with doing so. +class JClass { + public: + JClass(JNIEnv* env, const char* name) : m_cls(nullptr) { + jclass local = env->FindClass(name); + if (!local) return; + m_cls = static_cast(env->NewGlobalRef(local)); + env->DeleteLocalRef(local); + } + + void free(JNIEnv *env) { + env->DeleteGlobalRef(m_cls); + m_cls = nullptr; + } + + explicit operator bool() const { return m_cls; } + + operator jclass() const { return m_cls; } + + private: + jclass m_cls; +}; + +// Container class for cleaning up Java local references. +// The destructor calls DeleteLocalRef. +template +class JLocal { + public: + JLocal(JNIEnv *env, T obj) : m_env(env), m_obj(obj) {} + ~JLocal() { + if (m_obj) m_env->DeleteLocalRef(m_obj); + } + operator T() { return m_obj; } + T obj() { return m_obj; } + + private: + JNIEnv *m_env; + T m_obj; +}; + +// +// Conversions from Java objects to C++ +// + +// Java string (jstring) reference. The string is provided as UTF8. +// This is not actually a reference, as it makes a copy of the string +// characters, but it's named this way for consistency. +class JStringRef { + public: + JStringRef(JNIEnv *env, jstring str) { + jsize size = env->GetStringLength(str); + const jchar *chars = env->GetStringCritical(str, nullptr); + if (chars) { + llvm::convertUTF16ToUTF8String(llvm::makeArrayRef(chars, size), m_str); + env->ReleaseStringCritical(str, chars); + } + } + + operator llvm::StringRef() const { return m_str; } + llvm::StringRef str() const { return m_str; } + const char* c_str() const { return m_str.data(); } + + private: + llvm::SmallString<128> m_str; +}; + +// Details for J*ArrayRef and CriticalJ*ArrayRef +namespace detail { + +template +class JArrayRefInner {}; + +// Specialization of JArrayRefBase to provide StringRef conversion. +template +class JArrayRefInner { + public: + operator llvm::StringRef() const { return str(); } + + llvm::StringRef str() const { + auto arr = static_cast(this)->array(); + if (arr.empty()) return llvm::StringRef{}; + return llvm::StringRef{reinterpret_cast(arr.data()), + arr.size()}; + } +}; + +// Base class for J*ArrayRef and CriticalJ*ArrayRef +template +class JArrayRefBase : public JArrayRefInner, T> { + public: + explicit operator bool() const { return this->m_elements != nullptr; } + + operator llvm::ArrayRef() const { return array(); } + + llvm::ArrayRef array() const { + if (!this->m_elements) return llvm::ArrayRef{}; + return llvm::ArrayRef{this->m_elements, this->m_size}; + } + + protected: + JArrayRefBase(JNIEnv *env, T* elements, size_t size) { + this->m_env = env; + this->m_jarr = nullptr; + this->m_size = size; + this->m_elements = elements; + } + + JArrayRefBase(JNIEnv *env, jarray jarr) { + this->m_env = env; + this->m_jarr = jarr; + this->m_size = env->GetArrayLength(jarr); + this->m_elements = nullptr; + } + + JNIEnv *m_env; + jarray m_jarr = nullptr; + size_t m_size; + T *m_elements; +}; + +} // namespace detail + +// Java array / DirectBuffer reference. + +#define WPI_JNI_JARRAYREF(T, F) \ + class J##F##ArrayRef : public detail::JArrayRefBase { \ + public: \ + J##F##ArrayRef(JNIEnv *env, jobject bb, int len) \ + : detail::JArrayRefBase( \ + env, static_cast(env->GetDirectBufferAddress(bb)), len) {} \ + J##F##ArrayRef(JNIEnv *env, T##Array jarr) \ + : detail::JArrayRefBase(env, jarr) { \ + m_elements = env->Get##F##ArrayElements(jarr, nullptr); \ + } \ + ~J##F##ArrayRef() { \ + if (m_elements) \ + m_env->Release##F##ArrayElements(static_cast(m_jarr), \ + m_elements, JNI_ABORT); \ + } \ + }; \ + \ + class CriticalJ##F##ArrayRef : public detail::JArrayRefBase { \ + public: \ + CriticalJ##F##ArrayRef(JNIEnv *env, T##Array jarr) \ + : detail::JArrayRefBase(env, jarr) { \ + m_elements = \ + static_cast(env->GetPrimitiveArrayCritical(jarr, nullptr)); \ + } \ + ~CriticalJ##F##ArrayRef() { \ + if (m_elements) \ + m_env->ReleasePrimitiveArrayCritical(m_jarr, m_elements, JNI_ABORT); \ + } \ + }; + +WPI_JNI_JARRAYREF(jboolean, Boolean) +WPI_JNI_JARRAYREF(jbyte, Byte) +WPI_JNI_JARRAYREF(jshort, Short) +WPI_JNI_JARRAYREF(jint, Int) +WPI_JNI_JARRAYREF(jlong, Long) +WPI_JNI_JARRAYREF(jfloat, Float) +WPI_JNI_JARRAYREF(jdouble, Double) + +#undef WPI_JNI_JARRAYREF + +// +// Conversions from C++ to Java objects +// + +// Convert a UTF8 string into a jstring. +inline jstring MakeJString(JNIEnv *env, llvm::StringRef str) { + llvm::SmallVector chars; + llvm::convertUTF8ToUTF16String(str, chars); + return env->NewString(chars.begin(), chars.size()); +} + +// details for MakeJIntArray +namespace detail { + +// Slow path (get primitive array and set individual elements). This +// is used if the input type is not an integer of the same size (note +// signed/unsigned is ignored). +template ::value && sizeof(jint) == sizeof(T))> +struct ConvertIntArray { + static jintArray ToJava(JNIEnv *env, llvm::ArrayRef arr) { + jintArray jarr = env->NewIntArray(arr.size()); + if (!jarr) return nullptr; + jint *elements = + static_cast(env->GetPrimitiveArrayCritical(jarr, nullptr)); + if (!elements) return nullptr; + for (size_t i = 0; i < arr.size(); ++i) + elements[i] = static_cast(arr[i]); + env->ReleasePrimitiveArrayCritical(jarr, elements, 0); + return jarr; + } +}; + +// Fast path (use SetIntArrayRegion) +template +struct ConvertIntArray { + static jintArray ToJava(JNIEnv *env, llvm::ArrayRef arr) { + jintArray jarr = env->NewIntArray(arr.size()); + if (!jarr) return nullptr; + env->SetIntArrayRegion(jarr, 0, arr.size(), arr.data()); + return jarr; + } +}; + +} // namespace detail + +// Convert an ArrayRef to a jintArray. +template +inline jintArray MakeJIntArray(JNIEnv *env, llvm::ArrayRef arr) { + return detail::ConvertIntArray::ToJava(env, arr); +} + +// Convert a SmallVector to a jintArray. This is required in addition to +// ArrayRef because template resolution occurs prior to implicit conversions. +template +inline jintArray MakeJIntArray(JNIEnv *env, + const llvm::SmallVectorImpl &arr) { + return detail::ConvertIntArray::ToJava(env, arr); +} + +// Convert a std::vector to a jintArray. This is required in addition to +// ArrayRef because template resolution occurs prior to implicit conversions. +template +inline jintArray MakeJIntArray(JNIEnv *env, const std::vector &arr) { + return detail::ConvertIntArray::ToJava(env, arr); +} + +// Convert a StringRef into a jbyteArray. +inline jbyteArray MakeJByteArray(JNIEnv *env, llvm::StringRef str) { + jbyteArray jarr = env->NewByteArray(str.size()); + if (!jarr) return nullptr; + env->SetByteArrayRegion(jarr, 0, str.size(), + reinterpret_cast(str.data())); + return jarr; +} + +// Convert an array of integers into a jbooleanArray. +inline jbooleanArray MakeJBooleanArray(JNIEnv *env, llvm::ArrayRef arr) +{ + jbooleanArray jarr = env->NewBooleanArray(arr.size()); + if (!jarr) return nullptr; + jboolean *elements = + static_cast(env->GetPrimitiveArrayCritical(jarr, nullptr)); + if (!elements) return nullptr; + for (size_t i = 0; i < arr.size(); ++i) + elements[i] = arr[i] ? JNI_TRUE : JNI_FALSE; + env->ReleasePrimitiveArrayCritical(jarr, elements, 0); + return jarr; +} + +// Convert an array of booleans into a jbooleanArray. +inline jbooleanArray MakeJBooleanArray(JNIEnv *env, llvm::ArrayRef arr) +{ + jbooleanArray jarr = env->NewBooleanArray(arr.size()); + if (!jarr) return nullptr; + jboolean *elements = + static_cast(env->GetPrimitiveArrayCritical(jarr, nullptr)); + if (!elements) return nullptr; + for (size_t i = 0; i < arr.size(); ++i) + elements[i] = arr[i] ? JNI_TRUE : JNI_FALSE; + env->ReleasePrimitiveArrayCritical(jarr, elements, 0); + return jarr; +} + +// Other MakeJ*Array conversions. + +#define WPI_JNI_MAKEJARRAY(T, F) \ + inline T##Array MakeJ##F##Array(JNIEnv *env, llvm::ArrayRef arr) { \ + T##Array jarr = env->New##F##Array(arr.size()); \ + if (!jarr) return nullptr; \ + env->Set##F##ArrayRegion(jarr, 0, arr.size(), arr.data()); \ + return jarr; \ + } + +WPI_JNI_MAKEJARRAY(jboolean, Boolean) +WPI_JNI_MAKEJARRAY(jbyte, Byte) +WPI_JNI_MAKEJARRAY(jshort, Short) +WPI_JNI_MAKEJARRAY(jlong, Long) +WPI_JNI_MAKEJARRAY(jfloat, Float) +WPI_JNI_MAKEJARRAY(jdouble, Double) + +#undef WPI_JNI_MAKEJARRAY + +// Convert an array of std::string into a jarray of jstring. +inline jobjectArray MakeJStringArray(JNIEnv *env, + llvm::ArrayRef arr) { + static JClass stringCls{env, "java/lang/String"}; + if (!stringCls) return nullptr; + jobjectArray jarr = env->NewObjectArray(arr.size(), stringCls, nullptr); + if (!jarr) return nullptr; + for (std::size_t i = 0; i < arr.size(); ++i) { + JLocal elem{env, MakeJString(env, arr[i])}; + env->SetObjectArrayElement(jarr, i, elem.obj()); + } + return jarr; +} + +// Generic callback thread implementation. +// +// JNI's AttachCurrentThread() creates a Java Thread object on every +// invocation, which is both time inefficient and causes issues with Eclipse +// (which tries to keep a thread list up-to-date and thus gets swamped). +// +// Instead, this class attaches just once. When a hardware notification +// occurs, a condition variable wakes up this thread and this thread actually +// makes the call into Java. +// +// The template parameter T is the message being passed to the callback, but +// also needs to provide the following functions: +// static JavaVM* GetJVM(); +// static const char* GetName(); +// void CallJava(JNIEnv *env, jobject func, jmethodID mid); +// +// When creating this, ATOMIC_STATIC_INIT() needs to be performed on the +// templated class as well. +template +class JCallbackThread : public SafeThread { + public: + void Main(); + + std::queue m_queue; + jobject m_func = nullptr; + jmethodID m_mid; +}; + +template +class JCallbackManager : public SafeThreadOwner> { + public: + void SetFunc(JNIEnv* env, jobject func, jmethodID mid); + + template + void Send(Args&&... args); +}; + +template +void JCallbackManager::SetFunc(JNIEnv* env, jobject func, jmethodID mid) { + auto thr = this->GetThread(); + if (!thr) return; + // free global reference + if (thr->m_func) env->DeleteGlobalRef(thr->m_func); + // create global reference + thr->m_func = env->NewGlobalRef(func); + thr->m_mid = mid; +} + +template +template +void JCallbackManager::Send(Args&&... args) { + auto thr = this->GetThread(); + if (!thr) return; + thr->m_queue.emplace(std::forward(args)...); + thr->m_cond.notify_one(); +} + +template +void JCallbackThread::Main() { + JNIEnv *env; + JavaVMAttachArgs args; + args.version = JNI_VERSION_1_2; + args.name = const_cast(T::GetName()); + args.group = nullptr; + jint rs = T::GetJVM()->AttachCurrentThreadAsDaemon((void**)&env, &args); + if (rs != JNI_OK) return; + + std::unique_lock lock(m_mutex); + while (m_active) { + m_cond.wait(lock, [&] { return !(m_active && m_queue.empty()); }); + if (!m_active) break; + while (!m_queue.empty()) { + if (!m_active) break; + auto item = std::move(m_queue.front()); + m_queue.pop(); + auto func = m_func; + auto mid = m_mid; + lock.unlock(); // don't hold mutex during callback execution + item.CallJava(env, func, mid); + if (env->ExceptionCheck()) { + env->ExceptionDescribe(); + env->ExceptionClear(); + } + lock.lock(); + } + } + + JavaVM* jvm = T::GetJVM(); + if (jvm) jvm->DetachCurrentThread(); +} + +template +class JSingletonCallbackManager : public JCallbackManager { + public: + static JSingletonCallbackManager& GetInstance() { + ATOMIC_STATIC(JSingletonCallbackManager, instance); + return instance; + } + + private: + ATOMIC_STATIC_DECL(JSingletonCallbackManager) +}; + +} // namespace java +} // namespace wpi + +#endif // WPIUTIL_SUPPORT_JNI_UTIL_H_