JNI Keep Object in C++ alive over multiple JNI calls - java

I have a problem in my Java native audio library but first, here is my current approach:
With a native method I'm opening a 'global' stream, which receives data over a callback function.
The callback function runs until there is no data.
If there is no data, the stream only stops, but does not get closed.
Now I wanted to feed the stream with data again [trying to start stream again(this operation is allowed)], but the stream has already been deleted.
So now I tried to figure out how to prevent deletion of the stream from C++ or Java.
One solution was to create a thread in the stream, which prevents the deletion.
But I don't like this solution...
So I searched how to keep such objects alive and found out, that there are so called "global references" which can be made with the JNI. But I did not understand whether they are only for java objects or for both.
Also I tried out whether another pointer type of C++ could help.
I appreciate any help or ideas, it does not have to be JNI only. C++ standard library methods/functions/classes etc. are also good :) !
System information:
Compiler: MinGW64 over MSYS2
JDK8u91
Of course 64bit operation system (Does not have to be named xD)
With global stream is meant, that the stream is accessible to all JNI methods.
EDIT:
Okay, 'to let the cat out of the back' I'm using RtAudio.
Realtime C++ Audio Library
Example:
//THIS IS C++ CODE
RtAudio audio(RtAudio::WASAPI);
int callback(//Buffer stuff etc.){
//do something
if(data.isEmpty())return 1;//invokes audio.closeStream() but this does NOT closes the stream!
else return 0; //Go on with the stream rather wait for the next call
}
JNIEXPORT void JNICALL openStream(jintArray data){
//This is a outputstream
audio.openStream(&outputParams,....., &callback,....);
audio.startStream();
}
JNIEXPORT void JNICALL fillData(jintArray data){
//filldata again!
stream.start(); //Starts the stream but does nothing, because the stream is deleted because of Java
}
If I would change the openStream method to this, the stream won't be deleted but I look for a better solution...
JNIEXPORT void JNICALL openStream(jintArray data){
//This is a outputstream
audio.openStream(&outputParams,....., &callback,....);
audio.startStream();
**while(true); //ADD THIS AND THE STREAM WON'T BE DELETED!**
}
Another solution is to add into the RtAudio API a "keepInstanceAliveThread" which is called after the stopStream() method and deleted after calling startStream() or closeStream(). I would rather prefer another solution but at all, there isn't any yet.
Pre-outcomes:
Thanks to #marcinj:
global object are known to cause many problems, its hard to control their construction/destruction.
EDIT:
I found out in the internet (also on stackoverflow), that the destructor is called after the return of a JNI method.

Use a long in the Java object to hold a pointer to the C++ object.
A Java long is 64 bits, and every platform Java runs on has either 32- or 64-bit pointers. And every platform Java is supplied for will support this, despite it not being strictly-conforming C or C++ code.
Java:
// class member
private long audio
// native functions
private native long openStream( int[] data );
private native void deleteStream( long audio );
private native void nativeFillData( long audio, int[] data );
public MyClass()
{
audio = openStream( data );
}
public void fillData( int[] data )
{
nativeFillData( this.audio, data );
}
// delete the C++ object - you may want to
// control this directly and not rely on
// finalize() getting called
protected void finalize()
{
deleteStream( audio );
super.finalize();
}
C++:
JNIEXPORT jlong JNICALL openStream(jintArray data)
{
RtAudio *audio = new RtAudio(RtAudio::WASAPI);
audio->openStream(&outputParams,....., &callback,....);
audio->startStream();
// C-style cast - JNI interface is C, not C++
return( ( jlong ) audio );
}
JNIEXPORT void JNICALL deleteStream(jlong jaudio)
{
RtAudio *audio = static_cast <RtAudio *>( jaudio );
delete audio;
}
JNIEXPORT void JNICALL nativeFillData(jlong jaudio, jintArray data)
{
RtAudio *audio = static_cast <RtAudio *>( jaudio );
audio->start();
...
}

1) JAVA THREAD WAY
We can create a new thread to keep running in a JNI function locked with a monitor or conditional while loop.
Then a separate call would stop the execution of the thread in your function by releasing the monitor or changing the condition in the while loop.
2) JAVA OBJECT REFERENCE WAY
Another option is to create a Global Reference of your object
Android JNI and NewGlobalRef.
Here a separate call on USB disconnect would do DeleteGlobalRef.
We can also move the life cycle of your C++ object into java by passing it back to the java layer
keep some sort of c++ object alive over multiple jni calls.
Here a separate call on USB disconnect would remove any reference to C++ object in your java code.
Implementation
Native File (mynative.cpp)
extern "C" JNIEXPORT jobject JNICALL
Java_com_android_nativecpp_MainActivity_createJniNativeReference(JNIEnv* env, jobject obj, jint size) {
void* buf = malloc(size);
jobject sharedbytebuffer = env->NewDirectByteBuffer(buf, size);
return env->NewGlobalRef(sharedbytebuffer);
}
extern "C" JNIEXPORT void JNICALL
Java_com_android_nativecpp_MainActivity_deleteJniNativeReference(JNIEnv* env, jobject obj, jobject sharedbytebuffer) {
env->DeleteGlobalRef(sharedbytebuffer);
void* buf = env->GetDirectBufferAddress(sharedbytebuffer);
free(buf);
return;
}
Java file (MainActivity.java)
import java.nio.ByteBuffer;
private ByteBuffer mJniReference;
public native ByteBuffer createJniNativeReference(int size);
public native void deleteJniNativeReference(ByteBuffer mJniReference);
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mJniReference = createJniNativeReference(1000);
}
protected void onDestroy() {
deleteJniNativeReference(mJniReference);
super.onDestroy();
}
EXPLAINATION
The reason for either of 1) or 2) is otherwise, the creating frame is then exited from the stack and the JNI local references are deleted.
This will end all the threads in C++ when all std::thread references (which are not detached, ideally) are deleted.
In the non-detached case std::thread destructors are called on the main exit, or when a thread object goes out of scope and then terminate() is called.
In the detached case the detached threads exit on app close which kills the host process. They can also be garbage collected.

Related

JNIEnv::NewObject() throwing java.lang.InstantiantionException

I am attempting to call JNIEnv::NewObject() in some JNI code when a C function returns a non-zero error code.
The order of events looks like:
Call C function.
If return code is non-zero, call a helper function which throws a custom excpetion.
The class I am trying to construct so that I can throw it is:
public final class HseException extends Exception {
private static final long serialVersionUID = 8995408998818557762L;
private final int errno;
private final Context ctx;
/* Only called from C */
HseException(final String message, final int errno, final Context ctx) {
super(message);
this.errno = errno;
this.ctx = ctx;
}
public Context getContext() {
return this.ctx;
}
public int getErrno() {
return this.errno;
}
public static enum Context {
NONE
}
}
In my code I am caching the jclass and jmethodID for the class and the constructor in a global struct, but the code looks like:
globals.com.micron.hse.HseException.class =
(*env)->FindClass(env, "com/micron/hse/HseException");
globals.com.micron.hse.HseException.init = (*env)->GetMethodID(
env,
globals.com.micron.hse.HseException.class,
"<init>",
"(Ljava/lang/String;ILcom/micron/hse/HseException$Context;)V");
globals.com.micron.hse.HseException.Context.class =
(*env)->FindClass(env, "com/micron/hse/HseException$Context");
globals.com.micron.hse.HseException.Context.NONE = (*env)->GetStaticFieldID(
env,
globals.com.micron.hse.HseException.Context.class,
"NONE",
"Lcom/micron/hse/HseException$Context;");
Note that the above code is located in the JNI_OnLoad() function of my library. This function completes without error, so this tells me that at least my classes and methods are being loaded correctly.
Lastly here is my helper function where I throw my custom exception type:
/* hse_err_t is a scalar type.
* hse_strerror() creates a string out of that scalar.
* hse_err_to_ctx() gets the enum context value embedded within the scalar.
* hse_err_to_errno() gets the errno value embedded within the scalar.
*/
jint
throw_new_hse_exception(JNIEnv *env, hse_err_t err)
{
assert(env);
assert(err);
const size_t needed_sz = hse_strerror(err, NULL, 0);
char *buf = malloc(needed_sz + 1);
if (!buf)
return (*env)->ThrowNew(
env,
globals.java.lang.OutOfMemoryError.class,
"Failed to allocate memory for error buffer");
hse_strerror(err, buf, needed_sz + 1);
const jstring message = (*env)->NewStringUTF(env, buf);
free(buf);
if ((*env)->ExceptionCheck(env))
return JNI_ERR;
const int rc = hse_err_to_errno(err);
const enum hse_err_ctx ctx = hse_err_to_ctx(err);
jfieldID err_ctx_field = NULL;
switch (ctx) {
case HSE_ERR_CTX_NONE:
err_ctx_field = globals.com.micron.hse.HseException.Context.NONE;
break;
}
assert(err_ctx_field);
const jobject err_ctx_obj = (*env)->GetStaticObjectField(
env, globals.com.micron.hse.HseException.Context.class, err_ctx_field);
if ((*env)->ExceptionCheck(env))
return JNI_ERR;
const jobject hse_exception_obj = (*env)->NewObject(
env,
globals.com.micron.hse.HseException.class,
globals.com.micron.hse.HseException.init,
message,
rc,
err_ctx_obj);
if ((*env)->ExceptionCheck(env))
return JNI_ERR;
return (*env)->Throw(env, (jthrowable)hse_exception_obj);
}
I know for a fact that the (*env)->NewObject() call is what is raising the exception because an exception check before and after will tell me so. The (*env)->NewStringUTF() call is successful and contains the string it should contain. The context field is also retrieved successfully.
What I am not understanding is why I am getting an InstantiationException. The Throws section of the JNIEnv::NewObject() is marked as the following:
THROWS:
InstantiationException: if the class is an interface or an abstract class.
OutOfMemoryError: if the system runs out of memory.
Any exceptions thrown by the constructor.
My class is not an interface nor is it an abstract class, so where could this exception be generated from? The weird thing is that I swear this worked before, but since I am writing these Java bindings from scratch, I have just been overwriting commits and force pushing to my branch.
Any help is appreciated. Unfortunately getMessage() on the exception returns null which just isn't helpful at all. There is no message from the JVM telling me potentially what I have done wrong either.
One detail that could be helpful is that when I try to call JNIEnv::ThrowNew() (after putting a (Ljava/lang/String;)V constructor in the same HseException class, jni_ThrowNew() segfaults, and I cannot understand why. The class is valid when I stash the jclass, and I know for a fact that the memory it is stashed in isn't overwritten in any way, since I have checked the pointer.
The repo where all this code lives is: https://github.com/hse-project/hse-java. Unfinished product, but at least it is buildable and tests can be ran. In the event that someone decides to clone the repo and build it, I will repeat the directions here:
meson build
ninja -C build
meson test -C build -t 0 KvsTest # I am using this test to exercise the code path
My goal tomorrow will be to try to reproduce the issue in a smaller manner. I may also try to peer into the OpenJDK code assuming that is where the JNI interfaces live. Figure if I look hard enough, I might find the line of code which generates the exception.
Edit: I did a test where in my current code, I added a main function and a native function whose only purpose is to throw an exception from C. The code looks something like:
private static native void throwException();
public static void main(String[] args) {
System.load("/path/to/.so");
throwException();
}
The implementation of the native function is:
void Java_com_micron_hse_Hse_throwException
(JNIEnv *env, jclass hse_cls)
{
(void)hse_cls;
/* Generate error */
hse_err_t err = hse_kvdb_txn_begin(NULL, NULL);
throw_new_hse_exception(env, err);
}
This printed the following after executing java -jar path/to/jar:
Exception in thread "main" com.micron.hse.HseException: lib/binding/kvdb_interface.c:1046: Invalid argument (22)
at com.micron.hse.Hse.throwException(Native Method)
at com.micron.hse.Hse.main(Hse.java:28)
That is exactly what I expect to be printed, so now I would say I am even more lost than when I started. For some reason in the context of my tests, the InstantiationException is raised. Not sure if an application using the JAR would hit the same issue or if it is just a test context thing.
Edit 2:
Changed the main method from the previous edit to the following which is pretty much exactly what my test does:
public static void main(String[] args) throws HseException {
try {
loadLibrary(Paths.get("/home/tpartin/Projects/hse-java/build/src/main/c/libhsejni-2.so"));
init();
final Kvdb kvdb = Kvdb.open(Paths.get("/media/hse-tests"));
final Kvs kvs = kvdb.kvsOpen("kvs");
kvs.delete((byte[])null);
kvs.close();
kvdb.close();
} finally {
// fini();
}
}
And was able throw the exception from C appropriately. This must mean that something is wrong with my test environment somehow.
Edit 3: Another clue. On one test, this issue generates the InstantiationException. On another test, this issue segfaults in jni_NewObject.
My issue was that I was holding onto jclass et al. references for too long.
Prior question: Why I should not reuse a jclass and/or jmethodID in JNI?
Java docs: https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/design.html#global_and_local_references
All Java objects returned by JNI functions are local references.
Thanks to Andrew Henle for pointing this out in the comments of the question. I have highlighted his comment in this answer, and will mark it is as the answer.

Passing a Java object to C++ via JNI and then back to Java, via void*

I have an Android app that employs both React Native and JNI. C++ (via a fork of the JUCE library) is used to generate one of the views.
React Native requires that a new instance of a view be returned from the overridden method createViewInstance(context). This seems to get called each time a React Native component containing this view is updated.
Here is my (simplified) implementation:
protected JuceViewHolder createViewInstance(ThemedReactContext themedReactContext) {
JuceBridge juceBridge = JuceBridge.getInstance();
juceViewHolder = new JuceViewHolder(themedReactContext);
// JNI method: this will trigger JuceBridge.createNewView
MainActivity.createJuceWindow(juceViewHolder);
return juceViewHolder;
}
For reference, createJuceWindow is defined as:
JUCE_JNI_CALLBACK (JUCE_ANDROID_ACTIVITY_CLASSNAME, createJuceWindow, jobject, (JNIEnv* env, jclass, jobject view))
{
JuceView::getInstance().createJuceWindow(view);
}
which calls:
void createJuceWindow(jobject view)
{
viewToAttachTo = GlobalRef(view); // [GlobalRef][1] manages NewGlobalRef
myComponent = std::unique_ptr<MyComponent> (new MyComponent);
myComponent->setVisible (true);
myComponent->setOpaque(true);
if (viewToAttachTo.get() == nullptr)
DBG ("createJuceWindow: viewToAttachTo null!!!");
myComponent->setBounds(Desktop::getInstance().getDisplays().getMainDisplay().userArea);
myComponent->addToDesktop (0, viewToAttachTo.get()); // This passes in the `jobject` held by GlobalRef
}
I have separated the JuceViewHolder derived from ViewGroup, and I am passing it to a C++ function via JNI in order to attach the (Java)ComponentPeerView generated from the (C++) AndroidComponentPeer constructor
AndroidComponentPeer (Component& comp, const int windowStyleFlags, void* viewToAttachTo)
: ComponentPeer (comp, windowStyleFlags),
usingAndroidGraphics (false),
fullScreen (false),
sizeAllocated (0),
scale ((float) Desktop::getInstance().getDisplays().getMainDisplay().scale)
{
// NB: must not put this in the initialiser list, as it invokes a callback,
// which will fail if the peer is only half-constructed.
if (viewToAttachTo == nullptr) DBG ("viewToAttachTo null");
view = GlobalRef (android.bridge.callObjectMethod (JuceBridge.createNewView,
(jboolean) component.isOpaque(),
(jlong) this,
(jobject) viewToAttachTo));
if (view.get() == nullptr)
DBG ("view null!");
else
DBG ("got view.");
if (isFocused())
handleFocusGain();
}
via the createNewView method: (simplified here)
public final ComponentPeerView createNewView (boolean opaque, long host, ViewGroup viewToAttachTo)
{
ComponentPeerView v = new ComponentPeerView (viewToAttachTo.getContext(), opaque, host);
viewToAttachTo.addView(v);
return v;
}
host is a pointer to the C++ AndroidComponentPeer instance.
This works initially, however it seems that if I switch to another View or Activity and/or switch back to the C++ View, I get a crash similar to the following:
JNI ERROR (app bug): accessed deleted global reference 0x100566
art/runtime/java_vm_ext.cc:410] JNI DETECTED ERROR IN APPLICATION: use of deleted global reference 0x100566
art/runtime/java_vm_ext.cc:410] from void com.juce.JuceBridge$ComponentPeerView.focusChanged(long, boolean)
My current assumption is that the error is due to the void* pointer becoming obsolete at some point due to the garbage collector moving the underlying object, as described in this article. What I got from the article is that jobject should be used in place of a pointer.
However the method signature of the Component::addToDesktop method that I have employed uses void*:
virtual void addToDesktop (int windowStyleFlags,
void* nativeWindowToAttachTo = nullptr);
(This is the method used in iOS to pass a native UIView to attach a Component to)
Is my assumption valid? If so, is it possible to safely cast a void* pointer to a Java object, store it as a jobject (via NewGlobalRef) and then pass it back to Java?

Pass a C++ object to JAVA and back using JNI

I am trying to implement a function that calls a function on main thread in C++ called execute. This is what I have so far:
//
// #brief Allows intercommunication between main and secondary threads.
//
template <class... Args>
class Dispatcher final {
public:
#ifdef __APPLE__
// iOS implementation
static void execute(std::function<void(Args...)> handler, Args... args) {
__block auto _args = std::move(args...);
dispatch_async(dispatch_get_main_queue(), ^{
handler(std::move(_args));
});
}
#elif defined(__ANDROID__)
static std::map<size_t, std::pair<std::function<void(Args...)>, Args...>> _callableMapping;
static size_t _lastId;
// Android implementation
static void execute(std::function<void(Args...)> handler, Args... args) {
auto callable = std::make_pair(std::move(handler), std::move(args...));
// store the callable in a map
_callableMapping.insert(std::make_pair(++_lastId, callable));
...
// call a Java method that calls runOnUiThread() which in turn calls
// a JNIEXPORT function, which calls
// __handleExecutionOnMainThread with the same ID that is passed in
}
static void __handleExecutionOnMainThread(size_t callableId) {
auto& mapping = _callableMapping.find(callableId)->second;
// call the function with arguments
mapping->first(std::move(mapping->second...));
}
#endif
};
The problem is that when I call __handleExecutionOnMainThread from the JNIEXPORT function, I need to provide template parameter for Dispatcher<Args...>, which are not known at this point.
I then thought of a different approach. Is it possible to pass a C++ object (i.e. the pair of std::function and its args stored in auto callable in function execution) to JAVA where it wouldn't be used (I imagine it could be stored as object) and pass it back via runOnUiThread back to C++ to be executed in __handleExecutionOnMainThread?
I hope the problem is understandable. If not, I can explain in more detail.

JNI Error: accessed stale weak global reference

I cache a reference to a Java object in my native code, just like this:
// java global reference deleter
// _JAVA_ENV is an instance of JNIEnv that is cached globally and just
// valid in current thread scope
static void g_java_ref_deleter(jobject ptr) {
_JAVA_ENV->DeleteGlobalRef(ptr);
}
// native class caches a java object reference
class NativeA {
private:
shared_ptr<_jobject> M_java_object;
public:
setJavaObject(jobject obj) {
M_java_object = shared_ptr<_jobject>(_JAVA_ENV->NewGlobalRef(obj), g_java_ref_deleter);
}
shared_ptr<_jobject> getJavaObject() const {
return M_java_object;
}
}
and I access it in another native class:
class NativeB {
public:
void doSomething(NativeA& a) {
// here I got an error: accessed stale weak global reference
// make_record do something on java object (set float field actually)
make_record(a.getJavaObject().get());
}
}
This code run onto Android 4.3. Why do I get this error and how can I fix it?
OK, I've solved this problem! Actually I cached _JAVA_ENV and use it mistakenly. From this blog I found :
Although any given JNIEnv* is only valid for use on one thread, because Android never had any per-thread state in a JNIEnv*, it used to be possible to get away with using a JNIEnv* on the wrong thread. Now there’s a per-thread local reference table, it’s vital that you only use a JNIEnv* on the right thread.
So I thought there is no problem that I cache the JNIEnv and use it in one thread, but actually the JNIEnv was stale when the program get into java environment and returned to native environment. (eh... forgive my poor English)
And from the documentation, I found :
If a piece of code has no other way to get its JNIEnv, you should share the JavaVM, and use GetEnv to discover the thread's JNIEnv.
So, you should cache the JavaVM, and use it to get JNIEnv, the code would be like this :
JavaVM* g_jvm;
jint JNI_OnLoad(JavaVM* vm, void* reserved)
{
g_jvm = vm;
JNIEnv* env;
if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) {
return -1;
}
// Get jclass with env->FindClass.
// Register methods with env->RegisterNatives.
return JNI_VERSION_1_6;
}
JNIEnv* getJNIEnv() {
JNIEnv* env;
g_jvm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6 /*version*/);
return env;
}
Hope can help someone! (please forgive my poor English again..)

OpenCV Java JNIEXPORT memory management

I have a question abour memory management of java port of OpenCV.
JNIEXPORT jlong JNICALL Java_org_opencv_core_Mat_n_1Mat__III
(JNIEnv* env, jclass, jint rows, jint cols, jint type)
{
try {
LOGD("Mat::n_1Mat__III()");
Mat* _retval_ = new Mat( rows, cols, type );
return (jlong) _retval_;
} catch(cv::Exception e) {
LOGD("Mat::n_1Mat__III() catched cv::Exception: %s", e.what());
jclass je = env->FindClass("org/opencv/core/CvException");
if(!je) je = env->FindClass("java/lang/Exception");
env->ThrowNew(je, e.what());
return 0;
} catch (...) {
LOGD("Mat::n_1Mat__III() catched unknown exception (...)");
jclass je = env->FindClass("java/lang/Exception");
env->ThrowNew(je, "Unknown exception in JNI code {Mat::n_1Mat__III()}");
return 0;
}
}
This code block is taken from '..\OpenCV-2.4.5\modules\java\generator\src\cpp\Mat.cpp'. My question is about following part:
Mat* _retval_ = new Mat( rows, cols, type );
return (jlong) _retval_;
It returns mat objects address by casting it to jlong and does not delete the object. So, how does the memory management is done? Does java runs Garbage Collector? Or are there any other code in C++ side that clears the memory somehow?
There is no memory manage done here.
The function really returns a pointer to a heap-allocated object without caring about the memory management.
Actually this method correspond to the Java class org.opencv.core.Mat which has a long attribute named nativeObj. So this java class is managing a pointer and it is always passed to the underlying C++ implementation.
On the Java Mat object, you have to call the release method, which in turn call the JNI function Java_org_opencv_core_Mat_n_release.
The finalize method also call n_delete which free the memory.
You can see the Java code here.
Well, I couldn't find the answer but I did a little trick. I defined a member variable as;
cv::Mat* mat = nullptr;
And when I need to allocate memory for a new Mat object first I run the following code and then do the memory allocation.
if(mat != nullptr) // this is satisfied if memory is already allocated and not deleted
{
delete mat;
mat = nullptr;
}
mat = new cv::Mat(rows, cols, type);
return (jlong)mat;
But I'm still loking forward to learn, how does OpenCV overcomes this problem.

Categories

Resources