Closed nilesritter closed 3 years ago
@nilesritter Thank you for reporting this issue. I will take a closer look to it. Any questions or updates I will let you know.
Hi @nilesritter. I have closely reviewed your code. The problem is that you cannot call
private native String evaluate(long isolateId, String inputString)
method and then expect that java Strings are automatically converted to CCharPointer when invoking your native library method
@CEntryPoint(name="Java_demo_AppDemo_evaluate")
public static CCharPointer evaluate(Pointer jniEnv, Pointer clazz, @CEntryPoint.IsolateThreadContext long isolateId, CCharPointer inputPtr)
Unlike primitives, passing java objects to the native library and then manipulating them inside it is a bit more complex. You should continue reading the JNI docs on the section calling-jvm-from-native-java because when you utilize java String and append it "Foo-" you are calling jvm from native java. In the next lines, I will guide you on how to make your sample application work. I already ran it myself.
You need to add the class JNIHeaderDirectives as defined in the docs.
final class JNIHeaderDirectives implements CContext.Directives {
@Override
public List<String> getOptions() {
File[] jnis = findJNIHeaders();
return Arrays.asList("-I" + jnis[0].getParent(), "-I" + jnis[1].getParent());
}
@Override
public List<String> getHeaderFiles() {
File[] jnis = findJNIHeaders();
return Arrays.asList("<" + jnis[0] + ">", "<" + jnis[1] + ">");
}
private static File[] findJNIHeaders() throws IllegalStateException {
final File jreHome = new File(System.getProperty("java.home"));
final File include = new File(jreHome.getParentFile(), "include");
final File[] jnis = {
new File(include, "jni.h"),
new File(new File(include, "linux"), "jni_md.h"),
};
return jnis;
}
}
You need to define some essential JVM structures (like JNIEnv) and a couple of the Java API wrappers that you will use for converting your java Strings to CCharPointer and vice versa. I wrote the code and it is as follows:
@CContext(JNIHeaderDirectives.class)
@CStruct(value = "JNIEnv_", addStructKeyword = true)
interface JNIEnv extends PointerBase {
@CField("functions")
JNINativeInterface getFunctions();
@CStruct(value = "JNINativeInterface_", addStructKeyword = true)
interface JNINativeInterface extends PointerBase {
@CField
NewStringUTF getNewStringUTF();
@CField
GetStringUTFChars getGetStringUTFChars();
}
interface JObject extends PointerBase {
}
interface JString extends JObject {
}
interface NewStringUTF extends CFunctionPointer {
@InvokeCFunctionPointer
JString call(JNIEnv env, CCharPointer cCharPointer);
}
interface GetStringUTFChars extends CFunctionPointer {
@InvokeCFunctionPointer
CCharPointer call(JNIEnv env, JString str, byte isCopy);
}
}
@CEntryPoint(name="Java_demo_AppDemo_evaluate")
public static JNIEnv.JString evaluate(
JNIEnv jniEnv, Pointer clazz,
@CEntryPoint.IsolateThreadContext long isolateId, JNIEnv.JString inputString)
{
JNIEnv.JNINativeInterface fn = jniEnv.getFunctions();
CCharPointer cCharPointer = fn.getGetStringUTFChars().call(jniEnv, inputString, (byte) 0);
final String resultString = evaluate_internal(CTypeConversion.toJavaString(cCharPointer));
try(final CTypeConversion.CCharPointerHolder holder = CTypeConversion.toCString(resultString)) {
return fn.getNewStringUTF().call(jniEnv, holder.get());
}
}
After that you can run your application and your test will succeed. Please give it a try.
Excellent response, Rodrigo, I will try out your suggestion and see how it goes. Thank you so much!
Follow-up: I have confirmed that your code changes makes the package work. Thanks again!
One final follow-up: there is a small but important "Tweak" that is needed. In the updated code, if run in a long loop, there is a memory leak which will exhaust all resources. The fix is to add in the JNI classes a reference to the ReleaseStringUTFChars()
function. Then in the library you use this to release the memory allocated in the GetStringUTFChars()
function, like this:
@CEntryPoint(name="Java_demo_AppDemo_evaluate")
public static JNIEnv.JString evaluate(
JNIEnv jniEnv, Pointer clazz,
@CEntryPoint.IsolateThreadContext long isolateId, JNIEnv.JString inputString)
{
JNIEnv.JNINativeInterface fn = jniEnv.getFunctions();
CCharPointer cCharPointer = fn.getGetStringUTFChars().call(jniEnv, inputString, (byte) 0);
final String resultString = evaluate_internal(CTypeConversion.toJavaString(cCharPointer));
# RELEASE THE MEMORY:
fn.getReleaseStringUTFChars().call(jniEnv, inputString, cCharPointer );
try(final CTypeConversion.CCharPointerHolder holder = CTypeConversion.toCString(resultString)) {
return fn.getNewStringUTF().call(jniEnv, holder.get());
}
}
Thanks for sharing @nilesritter-ym.
Describe the issue When a java function is used to define a CE Graal Native Image shared library (--shared), whose parameters and return value are java String type, and the calling program is likewise Java (via JNI), the invocation crashes with a core dump. The shared library code closely follows the example in "Implementing Native Methods in Jave with Native Image", but with an additional method that takes and returns Java String arguments.
Steps to reproduce the issue Please include both build steps as well as run steps
Describe GraalVM and your environment:
More details Contact: niles@nilesritter.com
Native Image Build: /usr/lib/jvm/graalvm-ce-java11-21.2.0/lib/svm/bin/native-image -cp /home/ndr/dev/libdemo/target/libdemo-1.0-SNAPSHOT.jar --no-fallback --shared -H:Class=demo.NativeImpl -H:Name=libnativeimpl
ndr@ubuntu:~/dev/ng2$ java --version openjdk 11.0.12 2021-07-20 OpenJDK Runtime Environment GraalVM CE 21.2.0 (build 11.0.12+6-jvmci-21.2-b08) OpenJDK 64-Bit Server VM GraalVM CE 21.2.0 (build 11.0.12+6-jvmci-21.2-b08, mixed mode, sharing)
Add any other information about the problem here. Especially important are stack traces or log output. Feel free to link to gists or to screenshots if necessary.