ART: Improve double-JNI-load exception message
Try to print the involved classloaders. The code is suboptimal
and will not cache any intermediates, as this is an unexpected
failure - but aids in debugging application issues.
Add a test for the behavior of duplicate library loading in
separate classloaders. For ART, add a rough test for the pattern
of the error message.
Bug: 65574359
Test: m test-art-host
Test: art/test/testrunner/testrunner.py -b --host -t 004-JniTest
Change-Id: I6c6c7726a79172f39153d4a458eba4fb3e8e85b0
diff --git a/runtime/java_vm_ext.cc b/runtime/java_vm_ext.cc
index 1593577..c0d1861 100644
--- a/runtime/java_vm_ext.cc
+++ b/runtime/java_vm_ext.cc
@@ -35,6 +35,7 @@
#include "mirror/class_loader.h"
#include "nativebridge/native_bridge.h"
#include "nativehelper/ScopedLocalRef.h"
+#include "nativehelper/ScopedUtfChars.h"
#include "nativeloader/native_loader.h"
#include "object_callbacks.h"
#include "parsed_options.h"
@@ -833,9 +834,42 @@
// The library will be associated with class_loader. The JNI
// spec says we can't load the same library into more than one
// class loader.
+ //
+ // This isn't very common. So spend some time to get a readable message.
+ auto call_to_string = [&](jobject obj) -> std::string {
+ if (obj == nullptr) {
+ return "null";
+ }
+ // Handle jweaks. Ignore double local-ref.
+ ScopedLocalRef<jobject> local_ref(env, env->NewLocalRef(obj));
+ if (local_ref != nullptr) {
+ ScopedLocalRef<jclass> local_class(env, env->GetObjectClass(local_ref.get()));
+ jmethodID to_string = env->GetMethodID(local_class.get(),
+ "toString",
+ "()Ljava/lang/String;");
+ DCHECK(to_string != nullptr);
+ ScopedLocalRef<jobject> local_string(env,
+ env->CallObjectMethod(local_ref.get(), to_string));
+ if (local_string != nullptr) {
+ ScopedUtfChars utf(env, reinterpret_cast<jstring>(local_string.get()));
+ if (utf.c_str() != nullptr) {
+ return utf.c_str();
+ }
+ }
+ env->ExceptionClear();
+ return "(Error calling toString)";
+ }
+ return "null";
+ };
+ std::string old_class_loader = call_to_string(library->GetClassLoader());
+ std::string new_class_loader = call_to_string(class_loader);
StringAppendF(error_msg, "Shared library \"%s\" already opened by "
- "ClassLoader %p; can't open in ClassLoader %p",
- path.c_str(), library->GetClassLoader(), class_loader);
+ "ClassLoader %p(%s); can't open in ClassLoader %p(%s)",
+ path.c_str(),
+ library->GetClassLoader(),
+ old_class_loader.c_str(),
+ class_loader,
+ new_class_loader.c_str());
LOG(WARNING) << *error_msg;
return false;
}
diff --git a/test/004-JniTest/expected.txt b/test/004-JniTest/expected.txt
index 7e85ab1..1d05160 100644
--- a/test/004-JniTest/expected.txt
+++ b/test/004-JniTest/expected.txt
@@ -60,3 +60,4 @@
hi-default δλ
Clinit Lookup: ClassWithoutClinit: <NSME Exception>
Clinit Lookup: ClassWithClinit: Main$ClassWithClinit()(Class: class java.lang.reflect.Constructor)
+Got UnsatisfiedLinkError for duplicate loadLibrary
diff --git a/test/004-JniTest/src-ex/A.java b/test/004-JniTest/src-ex/A.java
new file mode 100644
index 0000000..8fe0e0a
--- /dev/null
+++ b/test/004-JniTest/src-ex/A.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+public class A {
+ public static void run(String lib) {
+ System.loadLibrary(lib);
+ }
+}
\ No newline at end of file
diff --git a/test/004-JniTest/src/Main.java b/test/004-JniTest/src/Main.java
index fe5f4e3..871107c 100644
--- a/test/004-JniTest/src/Main.java
+++ b/test/004-JniTest/src/Main.java
@@ -14,9 +14,12 @@
* limitations under the License.
*/
+import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
+import java.util.regex.Pattern;
import dalvik.annotation.optimization.CriticalNative;
import dalvik.annotation.optimization.FastNative;
@@ -58,6 +61,8 @@
testCriticalNativeMethods();
testClinitMethodLookup();
+
+ testDoubleLoad(args[0]);
}
private static native boolean registerNativesJniTest();
@@ -346,6 +351,57 @@
private static class ClassWithClinit {
static {}
}
+
+ private static void testDoubleLoad(String library) {
+ // Test that nothing observably happens on loading "library" again.
+ System.loadLibrary(library);
+
+ // Now load code in a separate classloader and try to let it load.
+ ClassLoader loader = createClassLoader();
+ try {
+ Class<?> aClass = loader.loadClass("A");
+ Method runMethod = aClass.getDeclaredMethod("run", String.class);
+ runMethod.invoke(null, library);
+ } catch (InvocationTargetException ite) {
+ if (ite.getCause() instanceof UnsatisfiedLinkError) {
+ if (!(loader instanceof java.net.URLClassLoader)) {
+ String msg = ite.getCause().getMessage();
+ String pattern = "^Shared library .*libarttest.* already opened by ClassLoader.*" +
+ "004-JniTest.jar.*; can't open in ClassLoader.*004-JniTest-ex.jar.*";
+ if (!Pattern.matches(pattern, msg)) {
+ throw new RuntimeException("Could not find pattern in message", ite.getCause());
+ }
+ }
+ System.out.println("Got UnsatisfiedLinkError for duplicate loadLibrary");
+ } else {
+ throw new RuntimeException(ite);
+ }
+ } catch (Throwable t) {
+ // Anything else just let die.
+ throw new RuntimeException(t);
+ }
+ }
+
+ private static ClassLoader createClassLoader() {
+ String location = System.getenv("DEX_LOCATION");
+ try {
+ Class<?> class_loader_class = Class.forName("dalvik.system.PathClassLoader");
+ Constructor<?> ctor = class_loader_class.getConstructor(String.class, ClassLoader.class);
+
+ return (ClassLoader)ctor.newInstance(location + "/004-JniTest-ex.jar",
+ Main.class.getClassLoader());
+ } catch (ClassNotFoundException e) {
+ // Running on RI. Use URLClassLoader.
+ try {
+ return new java.net.URLClassLoader(
+ new java.net.URL[] { new java.net.URL("file://" + location + "/classes-ex/") });
+ } catch (Throwable t) {
+ throw new RuntimeException(t);
+ }
+ } catch (Throwable t) {
+ throw new RuntimeException(t);
+ }
+ }
}
@FunctionalInterface