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