From b9b0217a07432d2901c21c6add8a8cf5ee0f1104 Mon Sep 17 00:00:00 2001 From: Sidharth Guglani Date: Tue, 8 Oct 2019 17:48:32 -0700 Subject: [PATCH] Add Scoped Local and Global Ref Summary: Add ScopedLocalRef, ScopedGlobalRef and some common methods to be used later. Reviewed By: amir-shalem Differential Revision: D17711284 fbshipit-source-id: be43d5e246bc2406765057783be11854877c41f1 --- java/CMakeLists.txt | 5 +- java/jni/ScopedGlobalRef.h | 140 +++++++++++++++++++++++++++++++++++++ java/jni/ScopedLocalRef.h | 140 +++++++++++++++++++++++++++++++++++++ java/jni/common.cpp | 100 ++++++++++++++++++++++++++ java/jni/common.h | 73 +++++++++++++++++++ java/jni/corefunctions.cpp | 74 ++++++++++++++++++++ java/jni/corefunctions.h | 51 ++++++++++++++ java/jni/macros.h | 22 ++++++ 8 files changed, 604 insertions(+), 1 deletion(-) create mode 100644 java/jni/ScopedGlobalRef.h create mode 100644 java/jni/ScopedLocalRef.h create mode 100644 java/jni/common.cpp create mode 100644 java/jni/common.h create mode 100644 java/jni/corefunctions.cpp create mode 100644 java/jni/corefunctions.h create mode 100644 java/jni/macros.h diff --git a/java/CMakeLists.txt b/java/CMakeLists.txt index c55c6bd9..30f96857 100644 --- a/java/CMakeLists.txt +++ b/java/CMakeLists.txt @@ -29,7 +29,10 @@ add_compile_options( -Wall -std=c++11) -add_library(yoga SHARED jni/YGJNI.cpp jni/YGJTypes.cpp jni/YGJNIVanilla.cpp) +file(GLOB jni_SRC + jni/*.cpp) + +add_library(yoga SHARED ${jni_SRC}) target_include_directories(yoga PRIVATE ${libfb_DIR}/include diff --git a/java/jni/ScopedGlobalRef.h b/java/jni/ScopedGlobalRef.h new file mode 100644 index 00000000..be434ce8 --- /dev/null +++ b/java/jni/ScopedGlobalRef.h @@ -0,0 +1,140 @@ +/* + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the LICENSE + * file in the root directory of this source tree. + */ +#pragma once + +#include +#include +#include +#include "corefunctions.h" + +namespace facebook { +namespace yoga { +namespace vanillajni { + +/** + * ScopedGlobalRef is a sort of smart reference that allows us to control the + * lifespan of a JNI global reference. + * + * This class is designed so that when a ScopedGlobalRef goes out of scoped, its + * destructor will delete -JNIEnv->DeleteGlobalRef()- the underlying JNI + * reference. + * + * This class should be used to wrap all the global references we create during + * normal JNI operations if we want reference to eventually go away (in JNI it + * is a common operation to cache some global references throughout the lifespan + * of a process, in which case using this class does not really make sense). The + * idea behind this is that in JNI we should be very explicit about the lifespan + * of global references. Global references can quickly get out of control if not + * freed properly, and the developer should always be very aware of the lifespan + * of each global reference that is created in JNI so that leaks are prevented. + * + * This class is very explicit in its behavior, and it does not allow to perform + * unexpected conversions or unexpected ownership transfer. In practice, this + * class acts as a unique pointer where the underying JNI reference can have one + * and just one owner. Transfering ownership is allowed but it is an explicit + * operation (implemneted via move semantics and also via explicity API calls). + * + * Note that this class doesn't receive an explicit JNIEnv at construction time. + * At destruction time it uses vanillajni::getCurrentEnv() to retrieve the + * JNIEnv. + * + * It is OK to cache a ScopedGlobalRef between different JNI native + * method calls. + */ +template +class ScopedGlobalRef { + static_assert( + std::is_same() || std::is_same() || + std::is_same() || std::is_same() || + std::is_same() || std::is_same() || + std::is_same() || std::is_same() || + std::is_same() || std::is_same() || + std::is_same() || std::is_same() || + std::is_same(), + "ScopedGlobalRef instantiated for invalid type"); + +public: + /** + * Constructs a ScopedGlobalRef with a JNI global reference. + * + * @param globalRef the global reference to wrap. Can be NULL. + */ + ScopedGlobalRef(T globalRef) : mGlobalRef(globalRef) {} + + /** + * Equivalent to ScopedGlobalRef(NULL) + */ + explicit ScopedGlobalRef() : mGlobalRef(NULL) {} + + /** + * Move construction is allowed. + */ + ScopedGlobalRef(ScopedGlobalRef&& s) : mGlobalRef(s.release()) {} + + /** + * Move assignment is allowed. + */ + ScopedGlobalRef& operator=(ScopedGlobalRef&& s) { + reset(s.release()); + return *this; + } + + ~ScopedGlobalRef() { + reset(); + } + + /** + * Deletes the currently held reference and reassigns a new one to the + * ScopedGlobalRef. + */ + void reset(T ptr = NULL) { + if (ptr != mGlobalRef) { + if (mGlobalRef != NULL) { + vanillajni::getCurrentEnv()->DeleteGlobalRef(mGlobalRef); + } + mGlobalRef = ptr; + } + } + + /** + * Makes this ScopedGlobalRef not own the underlying JNI global reference. + * After calling this method, the ScopedGlobalRef will not delete the JNI + * global reference when the ScopedGlobalRef goes out of scope. + */ + T release() { + T globalRef = mGlobalRef; + mGlobalRef = NULL; + return globalRef; + } + + /** + * Returns the underlying JNI global reference. + */ + T get() const { return mGlobalRef; } + + /** + * Returns true if the underlying JNI reference is not NULL. + */ + operator bool() const { + return mGlobalRef != NULL; + } + + ScopedGlobalRef(const ScopedGlobalRef& ref) = delete; + ScopedGlobalRef& operator=(const ScopedGlobalRef& other) = delete; + +private: + T mGlobalRef; +}; + +template +ScopedGlobalRef make_global_ref(T globalRef) { + return ScopedGlobalRef(globalRef); +} + +} // namespace vanillajni +} // namespace yoga +} // namespace facebook diff --git a/java/jni/ScopedLocalRef.h b/java/jni/ScopedLocalRef.h new file mode 100644 index 00000000..976daf2a --- /dev/null +++ b/java/jni/ScopedLocalRef.h @@ -0,0 +1,140 @@ +/* + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the LICENSE + * file in the root directory of this source tree. + */ +/** + * This is a modified version of Android's ScopedLocalRef class that can be + * found in the Android's JNI code. + */ +#pragma once + +#include +#include +#include + +namespace facebook { +namespace yoga { +namespace vanillajni { + +/** + * ScopedLocalRef is a sort of smart reference that allows us to control the + * lifespan of a JNI local reference. + * + * This class is designed so that when a ScopedLocalRef goes out of scope, its + * destructor will delete -JNIEnv->DeleteLocalRef()- the underlying JNI + * reference. + * + * This class should be used to wrap all the local references that JNI + * gives us other than those that are passed to native methods at + * invocation time. The idea behind this is that in JNI we should be very + * explicit about the lifespan of local references. Local references can quickly + * get out of control, and the developer should always be very aware of the + * lifespan of each local reference that is created in JNI so that leaks are + * prevented. + * + * This class is very explicit in its behavior, and it does not allow to perform + * unexpected conversions or unexpected ownership transfer. In practice, this + * class acts as a unique pointer where the underying JNI reference can have one + * and just one owner. Transfering ownership is allowed but it is an explicit + * operation (implemneted via move semantics and also via explicity API calls). + * + * As with standard JNI local references it is not a valid operation to keep a + * reference around between different native method calls. + */ +template +class ScopedLocalRef { + static_assert( + std::is_same() || std::is_same() || + std::is_same() || std::is_same() || + std::is_same() || std::is_same() || + std::is_same() || std::is_same() || + std::is_same() || std::is_same() || + std::is_same() || std::is_same() || + std::is_same(), + "ScopedLocalRef instantiated for invalid type"); + +public: + /** + * Constructs a ScopedLocalRef with a JNI local reference. + * + * @param localRef the local reference to wrap. Can be NULL. + */ + ScopedLocalRef(JNIEnv* env, T localRef) : mEnv(env), mLocalRef(localRef) {} + + /** + * Equivalent to ScopedLocalRef(env, NULL) + */ + explicit ScopedLocalRef(JNIEnv* env) : mEnv(env), mLocalRef(NULL) {} + + /** + * Move construction is allowed. + */ + ScopedLocalRef(ScopedLocalRef&& s) : mEnv(s.mEnv), mLocalRef(s.release()) {} + + /** + * Move assignment is allowed. + */ + ScopedLocalRef& operator=(ScopedLocalRef&& s) { + reset(s.release()); + mEnv = s.mEnv; + return *this; + } + + ~ScopedLocalRef() { + reset(); + } + + /** + * Deletes the currently held reference and reassigns a new one to the + * ScopedLocalRef. + */ + void reset(T ptr = NULL) { + if (ptr != mLocalRef) { + if (mLocalRef != NULL) { + mEnv->DeleteLocalRef(mLocalRef); + } + mLocalRef = ptr; + } + } + + /** + * Makes this ScopedLocalRef not own the underlying JNI local reference. After + * calling this method, the ScopedLocalRef will not delete the JNI local + * reference when the ScopedLocalRef goes out of scope. + */ + T release() { + T localRef = mLocalRef; + mLocalRef = NULL; + return localRef; + } + + /** + * Returns the underlying JNI local reference. + */ + T get() const { return mLocalRef; } + + /** + * Returns true if the underlying JNI reference is not NULL. + */ + operator bool() const { + return mLocalRef != NULL; + } + + ScopedLocalRef(const ScopedLocalRef& ref) = delete; + ScopedLocalRef& operator=(const ScopedLocalRef& other) = delete; + +private: + JNIEnv* mEnv; + T mLocalRef; +}; + +template +ScopedLocalRef make_local_ref(JNIEnv* env, T localRef) { + return ScopedLocalRef(env, localRef); +} + +} // namespace vanillajni +} // namespace yoga +} // namespace facebook diff --git a/java/jni/common.cpp b/java/jni/common.cpp new file mode 100644 index 00000000..1f5e99b2 --- /dev/null +++ b/java/jni/common.cpp @@ -0,0 +1,100 @@ +/* + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the LICENSE + * file in the root directory of this source tree. + */ +#include "common.h" + +namespace facebook { +namespace yoga { +namespace vanillajni { + +void registerNatives( + JNIEnv* env, + const char* className, + const JNINativeMethod methods[], + size_t numMethods) { + jclass clazz = env->FindClass(className); + + assertNoPendingJniException(env); + + env->RegisterNatives(clazz, methods, numMethods); + + assertNoPendingJniException(env); +} + +jmethodID getStaticMethodId( + JNIEnv* env, + jclass clazz, + const char* methodName, + const char* methodDescriptor) { + jmethodID methodId = + env->GetStaticMethodID(clazz, methodName, methodDescriptor); + assertNoPendingJniException(env); + return methodId; +} + +jmethodID getMethodId( + JNIEnv* env, + jclass clazz, + const char* methodName, + const char* methodDescriptor) { + jmethodID methodId = env->GetMethodID(clazz, methodName, methodDescriptor); + assertNoPendingJniException(env); + return methodId; +} + +jfieldID getFieldId( + JNIEnv* env, + jclass clazz, + const char* fieldName, + const char* fieldSignature) { + jfieldID fieldId = env->GetFieldID(clazz, fieldName, fieldSignature); + assertNoPendingJniException(env); + return fieldId; +} + +#define DEFINE_CALL_METHOD_FOR_PRIMITIVE_IMPLEMENTATION(jnitype, readableType) \ + DEFINE_CALL_METHOD_FOR_PRIMITIVE_INTERFACE(jnitype, readableType) { \ + va_list args; \ + va_start(args, methodId); \ + jnitype result = env->Call##readableType##MethodV(obj, methodId, args); \ + va_end(args); \ + assertNoPendingJniException(env); \ + return result; \ + } + +DEFINE_CALL_METHOD_FOR_PRIMITIVE_IMPLEMENTATION(jlong, Long); +DEFINE_CALL_METHOD_FOR_PRIMITIVE_IMPLEMENTATION(jfloat, Float); + +DEFINE_CALL_METHOD_FOR_PRIMITIVE_INTERFACE(void, Void) { + va_list args; + va_start(args, methodId); + env->CallVoidMethodV(obj, methodId, args); + va_end(args); + assertNoPendingJniException(env); +} + +ScopedLocalRef +callStaticObjectMethod(JNIEnv* env, jclass clazz, jmethodID methodId, ...) { + va_list args; + va_start(args, methodId); + jobject result = env->CallStaticObjectMethodV(clazz, methodId, args); + va_end(args); + assertNoPendingJniException(env); + return make_local_ref(env, result); +} + +ScopedGlobalRef newGlobalRef(JNIEnv* env, jobject obj) { + jobject result = env->NewGlobalRef(obj); + + if (!result) { + logErrorMessageAndDie("Could not obtain global reference from object"); + } + + return make_global_ref(result); +} +} // namespace vanillajni +} // namespace yoga +} // namespace facebook diff --git a/java/jni/common.h b/java/jni/common.h new file mode 100644 index 00000000..51ef4b35 --- /dev/null +++ b/java/jni/common.h @@ -0,0 +1,73 @@ +/* + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the LICENSE + * file in the root directory of this source tree. + */ +#pragma once +#include "ScopedGlobalRef.h" +#include "ScopedLocalRef.h" + +namespace facebook { +namespace yoga { +namespace vanillajni { + +/** + * Registers a set of methods for a JNI class. Aborts if registration fails. + */ +void registerNatives( + JNIEnv* env, + const char* className, + const JNINativeMethod methods[], + size_t numMethods); + +/** + * Returns a jmethodID for a class static method. Aborts if any error happens. + */ +jmethodID getStaticMethodId( + JNIEnv* env, + jclass clazz, + const char* methodName, + const char* methodDescriptor); + +/** + * Returns a jmethodID for a class non-static method. Aborts if any error + * happens. + */ +jmethodID getMethodId( + JNIEnv* env, + jclass clazz, + const char* methodName, + const char* methodDescriptor); + +/** + * Returns a class non-static field ID. Aborts if any error happens. + */ +jfieldID getFieldId( + JNIEnv* env, + jclass clazz, + const char* fieldName, + const char* fieldSignature); + +// Helper methods to call a non-static method on an object depending on the +// return type. Each method will abort the execution if an error +// (such as a Java pending exception) is detected after invoking the +// Java method. +#define DEFINE_CALL_METHOD_FOR_PRIMITIVE_INTERFACE(jnitype, readableType) \ + jnitype call##readableType##Method( \ + JNIEnv* env, jobject obj, jmethodID methodId, ...) +DEFINE_CALL_METHOD_FOR_PRIMITIVE_INTERFACE(void, Void); +DEFINE_CALL_METHOD_FOR_PRIMITIVE_INTERFACE(jlong, Long); +DEFINE_CALL_METHOD_FOR_PRIMITIVE_INTERFACE(jfloat, Float); + +ScopedLocalRef +callStaticObjectMethod(JNIEnv* env, jclass clazz, jmethodID methodId, ...); + +/** + * Given a local or a global reference, this method creates a new global + * reference out of it. If any error happens, aborts the process. + */ +ScopedGlobalRef newGlobalRef(JNIEnv* env, jobject obj); +} // namespace vanillajni +} // namespace yoga +} // namespace facebook diff --git a/java/jni/corefunctions.cpp b/java/jni/corefunctions.cpp new file mode 100644 index 00000000..34fc734e --- /dev/null +++ b/java/jni/corefunctions.cpp @@ -0,0 +1,74 @@ +/* + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the LICENSE + * file in the root directory of this source tree. + */ +#include "corefunctions.h" +#include "macros.h" + +namespace facebook { +namespace yoga { +namespace vanillajni { + +namespace { +JavaVM* globalVm = NULL; +struct JavaVMInitializer { + JavaVMInitializer(JavaVM* vm) { + if (!vm) { + logErrorMessageAndDie( + "You cannot pass a NULL JavaVM to ensureInitialized"); + } + globalVm = vm; + } +}; +} // namespace + +jint ensureInitialized(JNIEnv** env, JavaVM* vm) { + static JavaVMInitializer init(vm); + + if (!env) { + logErrorMessageAndDie( + "Need to pass a valid JNIEnv pointer to vanillajni initialization " + "routine"); + } + + if (vm->GetEnv(reinterpret_cast(env), JNI_VERSION_1_6) != JNI_OK) { + logErrorMessageAndDie( + "Error retrieving JNIEnv during initialization of vanillajni"); + } + + return JNI_VERSION_1_6; +} + +JNIEnv* getCurrentEnv() { + JNIEnv* env; + jint ret = globalVm->GetEnv((void**) &env, JNI_VERSION_1_6); + if (ret != JNI_OK) { + logErrorMessageAndDie( + "There was an error retrieving the current JNIEnv. Make sure the " + "current thread is attached"); + } + return env; +} + +void logErrorMessageAndDie(const char* message) { + VANILLAJNI_LOG_ERROR( + "VanillaJni", + "Aborting due to error detected in native code: %s", + message); + VANILLAJNI_DIE(); +} + +void assertNoPendingJniException(JNIEnv* env) { + // This method cannot call any other method of the library, since other + // methods of the library use it to check for exceptions too + if (env->ExceptionCheck()) { + env->ExceptionDescribe(); + logErrorMessageAndDie("Aborting due to pending Java exception in JNI"); + } +} + +} // namespace vanillajni +} // namespace yoga +} // namespace facebook diff --git a/java/jni/corefunctions.h b/java/jni/corefunctions.h new file mode 100644 index 00000000..264678a6 --- /dev/null +++ b/java/jni/corefunctions.h @@ -0,0 +1,51 @@ +/* + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the LICENSE + * file in the root directory of this source tree. + */ +#pragma once + +#include +#include + +namespace facebook { +namespace yoga { +namespace vanillajni { + +/** + * This method has to be called before using the vanillajni library. This method + * is typically called when doing initialization in the "on load" JNI hook of a + * particular library. + * + * This method is thread safe, and after the first time it's called it has no + * initialization effect. + * + * @param env use this output parameter to get a JNIEnv to use for things such + * as registering native methods and such. + * @param vm the VM instance passed by JNI. This is usually the VM instance + * that is passed to the "on load" JNI hook. + * @return an integer value to return from the "on load" hook. + */ +jint ensureInitialized(JNIEnv** env, JavaVM* vm); + +/** + * Returns a JNIEnv* suitable for the current thread. If the current thread is + * not attached to the Java VM, this method aborts execution. + */ +JNIEnv* getCurrentEnv(); + +/** + * Logs an error message and aborts the current process. + */ +void logErrorMessageAndDie(const char* message); + +/** + * Checks whether there is a pending JNI exception. If so, it logs an error + * message and aborts the current process. Otherwise it does nothing. + */ +void assertNoPendingJniException(JNIEnv* env); + +} // namespace vanillajni +} // namespace yoga +} // namespace facebook diff --git a/java/jni/macros.h b/java/jni/macros.h new file mode 100644 index 00000000..2d5549f6 --- /dev/null +++ b/java/jni/macros.h @@ -0,0 +1,22 @@ +/* + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the LICENSE + * file in the root directory of this source tree. + */ +#pragma once + +#include + +#ifdef __ANDROID__ +#include +#endif + +#ifdef __ANDROID__ +#define VANILLAJNI_LOG_ERROR(tag, format, ...) \ + __android_log_print(ANDROID_LOG_ERROR, tag, format, ##__VA_ARGS__) +#else +#define VANILLAJNI_LOG_ERROR(tag, format, ...) +#endif + +#define VANILLAJNI_DIE() std::abort()