Android NDK crash in CallObjectMethod calling static method with ART - java

In an Android app I have some JNI code that calls a java static method.
jbyteArray response = (jbyteArray)pEnv->CallObjectMethod(handlerClass, mid, jstrServiceUrl, jstrRequest);
Executing it in Android 5 in an ART environment, I get a check jni error:
JNI DETECTED ERROR IN APPLICATION: calling static method byte[] x.y.z(java.lang.String, java.lang.String) with CallObjectMethodV in call to CallObjectMethodV...
I don't get this error in Android 4 with a Dalvik environment.
The java method is this one:
public static byte[] z(String serviceURL, String request)
and is previously binded like this:
jclass handlerClass = pEnv->FindClass("x/y/z");
if (handlerClass == NULL) {
return -1;
}
mid = pEnv->GetStaticMethodID(handlerClass, "z", "(Ljava/lang/String;Ljava/lang/String;)[B");
if (mid == NULL) {
return -2;
}
// Construct Strings
jstring jstrServiceUrl = pEnv->NewStringUTF(szServiceURL);
jstring jstrRequest = pEnv->NewStringUTF(szRequest);

I don't know why your code worked with Dalvik, but the method id given to Call<type>Method must be obtained with GetMethodID. If you have a method id obtained with GetStaticMethodID you should use CallStatic<type>Method.
See the descriptions of Call<type>Method and CallStatic<type>Method in the JNI functions documentation.

Related

Java to C# using JNI results in StackOverflowException

1. Summarize the problem:
I would like to invoke a C# method by invoking a Java method to check license file. This license check is performed by using a C# dll. I'm using JNI and a C++ wrapper. I will provide necessary source code below.
The C# dll has a method public static string GetLicenseStatus() implemented which I wrote a wrapper for and now I'm trying to invoke this method from Java application.
I'm using jdk-17.0.2.8-hotspot from Eclipse Adoptium (64-bit) and IntelliJ IDEA as Java IDE and Visual Studio 2022 for C# project.
After Java method invocation I expect that it returns a String (number from 0-4, not valid, valid, expired, ...) but it results in a StackOverflowException when C# code is being executed/accessed.
2. Describe what you've tried
I also tried to return just a value in the C++ method without calling any C# code; this worked fine. So JNI <--> C++ wrapper is working fine.
Also I tried to run C# source code within a C# main class, that was also working fine. So there's no faulty C# code.
Good to know is maybe also that I tried to create an own C# dll to confirm that the issue is not related to the license dll (that's why I writing before about a "C# project in Visual Studio"). This dll is very basic and is just checking for dummy username & password. Even When I tried to just return true in the function, when invoking it from Java it resulted again in a StackOverflowException in Java IDE. Its running into this error when attempting to instantiate an object with gcnew. My own created C# class and also the C# license dll were added as reference in the C++ project.
Maybe also worth to mention:
The C# dll is relying on another dll to process license checking I assume.
I observed that Visual Studio for some reason doesn't recognise imported header files. I have to add them manually in visual Studio and copy paste code into the manual created file.
3. Show some code
"Authenticator.java":
package org.example;
public class Authenticator {
static {
System.loadLibrary("CppAuthenticator");
}
public native boolean authenticate(String username, String password);
public native String getLicenseStatus();
public static void main(String[] args) {
System.out.println("Program start");
Authenticator authenticator = new Authenticator();
System.out.println("Authenticator created");
/**boolean valid = authenticator.authenticate(args[0], args[1]);
System.out.println("Is valid?: "+valid);
if(!valid) {
System.err.println("Not valid!");
System.exit(1);
}
else {
System.out.println("Valid");
}**/
System.out.println("License Check...");
System.out.println("Status: "+authenticator.getLicenseStatus());
}
}
"CppAuthenticator.cpp"
#include "pch.h"
#include <msclr\marshal.h>
#include "CppAuthenticator.h"
#include "org_example_Authenticator.h"
// this is the main DLL file.
#include <string>
using System::Text::Encoding;
String^ toString(const char* chars) {
int len = (int)strlen(chars);
array<unsigned char>^ a = gcnew array<unsigned char> (len);
int i = 0;
while (i < len) {
a[i] = chars[i];
}
return Encoding::UTF8->GetString(a);
}
bool authenticate(const char* username, const char* password) {
SharpAuthenticator::Authenticator^ a = gcnew SharpAuthenticator::Authenticator(); // Fails here
return a->Authenticate(toString(username), toString(password));
}
JNIEXPORT jboolean JNICALL Java_org_example_Authenticator_authenticate
(JNIEnv* env, jobject c, jstring username, jstring password) {
jboolean isCopyUsername;
const char *c_username = env->GetStringUTFChars(username, &isCopyUsername);
jboolean isCopyPassword;
const char* c_password = env->GetStringUTFChars(password, &isCopyPassword);
jboolean result = authenticate(c_username, c_password);
env->ReleaseStringUTFChars(username, c_username);
env->ReleaseStringUTFChars(password, c_password);
return result;
}
String^ getLicenseStatus() {
return LicenseCheck::ValidateLicense::GetLicenseStatus(); // Fails here
}
JNIEXPORT jstring JNICALL Java_org_example_Authenticator_getLicenseStatus
(JNIEnv* env, jobject c) {
String^ cliString = getLicenseStatus();
msclr::interop::marshal_context context;
const char* utf8String = context.marshal_as<const char*>(cliString);
jstring result = env->NewStringUTF(utf8String);
return result;
}
"SharpAuthenticator.cs":
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SharpAuthenticator
{
public class Authenticator
{
public bool Authenticate(String username, String password)
{
return username == "user" && password == "pass";
}
public bool Authenticate1()
{
return false;
}
}
}
Here is the project structure I have in Visual Studio ("org_example_Authenticator.h" code was created with "javac -h ..."-command located in bin folder of JDK mentioned above.)
Here are the C++ project properties in Visual Studio:
Here are C# project properties for my own created dummy dll mentioned above:
It was a stupid mistake... It just cost me 1.5 days figuring out that I forgot to increment i in the while loop in toString method of "CppAuthenticator.cpp". Why these things always happen to me...? :D
Here the correct working method:
String^ toString(const char* chars) {
int len = (int)strlen(chars);
array<unsigned char>^ a = gcnew array<unsigned char> (len);
int i = 0;
while (i < len) {
a[i] = chars[i];
i++;
}
return Encoding::UTF8->GetString(a);
}

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.

Java Invocation API: Call the C function back from the java code

I have a C (navive) program and a jar file with the main() method. From my native program I am initializing the JVM, and calling the main() method. I have no problems with this, everything is completely fine. But then I wanted to call back a C function from my java code.
The C function is defined in the native code in the same module as the one, that have created the JVM. The header is auto-generated, and the body is as simple as this:
JNIEXPORT void JNICALL Java_eu_raman_chakhouski_NativeUpdaterBus_connect0(JNIEnv* env, jclass clazz)
{
return;
}
So, from the java code I'm calling NativeUpdaterBus.connect0(), continuosly getting an UnsatisfiedLinkError. I have no System.loadLibrary() calls in my java code, because I thought, that there will be no problems calling the native code back from the java code if the target module is (possibly?) already loaded.
Well, maybe my approach is completely incorrect, but I can't see any obvious defects, maybe you could help?
What possibly could help (but I didn't tried any of these approaches, because I'm still not quite sure)
Use a kind of a "trampoline" dynamic library with these JNI methods, load it from the java code, then marshal native calls through it.
Define a java.lang.Runnable's anonymous inheritor, created with jni_env->DefineClass() but this involves some bytecode trickery.
Use an another, less invasive approach, like sockets, named pipes, etc. But in my case I'm using only one native process, so this might be an overkill.
I'm using OpenJDK 11.0.3 and Windows 10. My C program is compiled with the Microsoft cl.exe 19.16.27031.1 for x64 (Visual Studio 2017).
One possibility, as others have already mentioned, is to create a shared library (.dll) and call it from the native code and from Java to exchange data.
However, if you want to callback to a C function defined in the native code in the same module as the one the JVM originally created, you can use RegisterNatives.
Simple Example
C program creates JVM
it calls a Main of a class
the Java Main calls back a C function named connect0 in the calling C code
to have a test case the native C function constructs a Java string and returns it
the Java side prints the result
Java
package com.software7.test;
public class Main {
private native String connect0() ;
public static void main(String[] args) {
Main m = new Main();
m.makeTest(args);
}
private void makeTest(String[] args) {
System.out.println("Java: main called");
for (String arg : args) {
System.out.println(" -> Java: argument: '" + arg + "'");
}
String res = connect0(); //callback into native code
System.out.println("Java: result of connect0() is '" + res + "'"); //process returned String
}
}
C Program
One can create the Java VM in C as shown here
(works not only with cygwin but still with VS 2019) and then register with RegisterNatives native C callbacks. So using the function invoke_class from the link above it could look like this:
#include <stdio.h>
#include <windows.h>
#include <jni.h>
#include <stdlib.h>
#include <stdbool.h>
...
void invoke_class(JNIEnv* env) {
jclass helloWorldClass;
jmethodID mainMethod;
jobjectArray applicationArgs;
jstring applicationArg0;
helloWorldClass = (*env)->FindClass(env, "com/software7/test/Main");
mainMethod = (*env)->GetStaticMethodID(env, helloWorldClass, "main", "([Ljava/lang/String;)V");
applicationArgs = (*env)->NewObjectArray(env, 1, (*env)->FindClass(env, "java/lang/String"), NULL);
applicationArg0 = (*env)->NewStringUTF(env, "one argument");
(*env)->SetObjectArrayElement(env, applicationArgs, 0, applicationArg0);
(*env)->CallStaticVoidMethod(env, helloWorldClass, mainMethod, applicationArgs);
}
jstring connect0(JNIEnv* env, jobject thiz);
static JNINativeMethod native_methods[] = {
{ "connect0", "()Ljava/lang/String;", (void*)connect0 },
};
jstring connect0(JNIEnv* env, jobject thiz) {
printf("C: connect0 called\n");
return (*env)->NewStringUTF(env, "Some Result!!");
}
static bool register_native_methods(JNIEnv* env) {
jclass clazz = (*env)->FindClass(env, "com/software7/test/Main");
if (clazz == NULL) {
return false;
}
int num_methods = sizeof(native_methods) / sizeof(native_methods[0]);
if ((*env)->RegisterNatives(env, clazz, native_methods, num_methods) < 0) {
return false;
}
return true;
}
int main() {
printf("C: Program starts, creating VM...\n");
JNIEnv* env = create_vm();
if (env == NULL) {
printf("C: creating JVM failed\n");
return 1;
}
if (!register_native_methods(env)) {
printf("C: registering native methods failed\n");
return 1;
}
invoke_class(env);
destroy_vm();
getchar();
return 0;
}
Result
Links
Creating a JVM from a C Program: http://www.inonit.com/cygwin/jni/invocationApi/c.html
Registering Native Methods: https://docs.oracle.com/en/java/javase/11/docs/specs/jni/functions.html#registering-native-methods
System.loadLibrary() is essential for the jni lookup to work. You also have a more flexible System.load() alternative.
Make sure that the native method implementation is declared with extern "C" and is not hidden by linker.

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?

jmethodID in android ART

I'm working on an app.
Inside JNI_OnLoad():
jclass cls = (*env)->FindClass(env, "dummy/dummy/dummy/Dummy");
if(cls == NULL) {
LOG_ERROR("unable to class");
return;
}
cached_dummy.cls = (*env)->NewGlobalRef(env, cls);
if(cached_dummy.cls == NULL) {
LOG_ERROR("unable to create a global reference of class");
return;
}
cached_dummy.method = (*env)->GetStaticMethodID(env, cls, "testMethod", "()V");
and when i used this cached result in java callback using (*env)->CallStaticVoidMethod(env, cached_dummy.cls, cached_dummy.method)
Result: "JNI DETECTED ERROR IN APPLICATION: jmethodID was NULL"
Android 5.0.2
Query:
Incompatibility?
Solution? (Do i need to make a global reference of jmethodID?)
Note: https://stackoverflow.com/a/2093300/1500988
This has to do with recursive loading of shared library.
when class is loaded, its static block is executed.
Inside the static block, shared library loader is placed on top and then rest of init code.
class(Foo) load => its static block executed => jni library loader => JNI_OnLoad => cache static method Foo.Bar() => class(Foo) load => WARN(recusive-load-warning) and execute the rest static block remaining code that call jni code and it uses Foo.Bar() and BOOM!

Categories

Resources