Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions test-app/app/src/main/assets/app/mainpage.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ require("./tests/java-array-test");
require("./tests/field-access-test");
require("./tests/byte-buffer-test");
require("./tests/dex-interface-implementation");
require("./tests/testClassForNameDiscovery");
require("./tests/testInterfaceImplementation");
require("./tests/testRuntimeImplementedAPIs");
require("./tests/testsInstanceOfOperator");
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
describe("Tests Class.forName discovery of runtime generated classes", function () {

// Android framework components (e.g. FragmentFactory) resolve classes with
// Class.forName(className, false, context.getClassLoader()). Runtime generated
// proxies must be discoverable through the app's class loader, otherwise
// framework lookups crash with ClassNotFoundException (see issue #1962 / PR #1951).
//
// The extend calls below are built dynamically so the static binding generator
// cannot pre-generate the proxies and DexFactory.resolveClass takes the runtime
// generation + parent class loader injection path.
var ext = "ex" + "tend";

// the app's PathClassLoader — the same loader the framework uses,
// e.g. in FragmentFactory.loadFragmentClass via context.getClassLoader()
var appClassLoader = com.tns.Runtime.class.getClassLoader();

it("When_extending_a_class_at_runtime_it_should_be_discoverable_through_the_app_class_loader", function () {
var MyObject = java.lang.Object[ext]("ClassForNameDiscoveryObject", {
toString: function () {
return "discoverable";
}
});

var instance = new MyObject();
var className = instance.getClass().getName();

var found = java.lang.Class.forName(className, false, appClassLoader);

expect(found.getName()).toBe(className);
expect(found.equals(instance.getClass())).toBe(true);
});

it("When_implementing_an_interface_at_runtime_it_should_be_discoverable_through_the_app_class_loader", function () {
var MyRunnable = java.lang.Runnable[ext]("ClassForNameDiscoveryRunnable", {
run: function () {
}
});

var instance = new MyRunnable();
var className = instance.getClass().getName();

var found = java.lang.Class.forName(className, false, appClassLoader);

expect(found.getName()).toBe(className);
expect(found.equals(instance.getClass())).toBe(true);
});

it("When_a_runtime_generated_class_is_instantiated_through_reflection_it_should_dispatch_to_the_JS_implementation", function () {
var MyObject = java.lang.Object[ext]("ClassForNameDiscoveryInstantiated", {
toString: function () {
return "created via reflection";
}
});

// make sure the implementation object is registered before Java constructs an instance
var instance = new MyObject();
var className = instance.getClass().getName();

// FragmentFactory resolves the class by name and instantiates it through
// reflection. Class.newInstance() invokes the no-arg constructor without
// the varargs marshalling getDeclaredConstructor() would need.
var found = java.lang.Class.forName(className, false, appClassLoader);
var created = found.newInstance();

expect(created.toString()).toBe("created via reflection");
});
});
110 changes: 77 additions & 33 deletions test-app/runtime/src/main/java/com/tns/DexFactory.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.tns;

import android.os.Build;
import android.util.Log;

import com.tns.bindings.AnnotationDescriptor;
Expand All @@ -20,8 +21,11 @@
import java.io.OutputStreamWriter;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

Expand Down Expand Up @@ -166,13 +170,19 @@ public Class<?> resolveClass(String baseClassName, String name, String className
}
jarFile.setReadOnly();

Class<?> result;
Class<?> result = null;
String classNameToLoad = isInterface ? fullClassName : desiredDexClassName;

if (injectIntoParentClassLoader && classLoader instanceof BaseDexClassLoader) {
injectDexIntoClassLoader((BaseDexClassLoader) classLoader, jarFilePath);
result = classLoader.loadClass(classNameToLoad);
} else {
if (injectIntoParentClassLoader && classLoader instanceof BaseDexClassLoader
&& injectDexIntoClassLoader((BaseDexClassLoader) classLoader, jarFilePath)) {
try {
result = classLoader.loadClass(classNameToLoad);
} catch (ClassNotFoundException e) {
// fall through to the isolated DexClassLoader below
}
}

if (result == null) {
DexClassLoader dexClassLoader = new DexClassLoader(jarFilePath, this.odexDir.getAbsolutePath(), null, classLoader);
result = dexClassLoader.loadClass(classNameToLoad);
}
Expand Down Expand Up @@ -389,40 +399,74 @@ private String getCachedProxyThumb(File proxyDir) {
* (e.g. FragmentFactory) use Class.forName() to instantiate classes by name, but
* NativeScript's dynamically-generated classes normally live in isolated DexClassLoaders
* that Class.forName() doesn't search.
*
* The jar must be added through the target class loader's own DexPathList so that
* the resulting DexFile has the PathClassLoader as its only owner. Opening the jar
* through a separate DexClassLoader first and splicing its dex element would leave
* the same DexFile claimed by two loaders, which ART rejects on non-debuggable
* builds with "Attempt to register dex file ... with multiple class loaders".
*
* @return true if the jar was injected and the class can be loaded through the
* target class loader, false if the caller should fall back to an
* isolated DexClassLoader.
*/
private void injectDexIntoClassLoader(BaseDexClassLoader targetClassLoader, String jarFilePath) {
private boolean injectDexIntoClassLoader(BaseDexClassLoader targetClassLoader, String jarFilePath) {
try {
// Create a temporary DexClassLoader to produce the optimized dex
DexClassLoader tempLoader = new DexClassLoader(jarFilePath, this.odexDir.getAbsolutePath(), null, targetClassLoader);

// Get pathList from both classloaders
Field pathListField = BaseDexClassLoader.class.getDeclaredField("pathList");
pathListField.setAccessible(true);

Object targetPathList = pathListField.get(targetClassLoader);
Object sourcePathList = pathListField.get(tempLoader);
if (Build.VERSION.SDK_INT >= 24) {
// BaseDexClassLoader.addDexPath exists since API 24
Method addDexPath = BaseDexClassLoader.class.getDeclaredMethod("addDexPath", String.class);
addDexPath.setAccessible(true);
addDexPath.invoke(targetClassLoader, jarFilePath);
} else {
appendDexElements(targetClassLoader, jarFilePath);
}
return true;
} catch (Exception e) {
Log.w("JS", "Failed to inject dex into parent classloader: " + e);
return false;
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// Get dexElements from both pathLists
Field dexElementsField = targetPathList.getClass().getDeclaredField("dexElements");
dexElementsField.setAccessible(true);
/**
* Pre API 24 equivalent of BaseDexClassLoader.addDexPath: builds the dex elements
* through DexPathList's static factory methods (so no temporary class loader is
* involved) and splices them into the target loader's dexElements array. This is
* the same technique MultiDex used on these OS versions.
*/
private void appendDexElements(BaseDexClassLoader targetClassLoader, String jarFilePath) throws Exception {
Field pathListField = BaseDexClassLoader.class.getDeclaredField("pathList");
pathListField.setAccessible(true);
Object pathList = pathListField.get(targetClassLoader);

ArrayList<File> files = new ArrayList<File>();
files.add(new File(jarFilePath));
ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();

Object newElements;
if (Build.VERSION.SDK_INT >= 23) {
Method makePathElements = pathList.getClass().getDeclaredMethod("makePathElements", List.class, File.class, List.class);
makePathElements.setAccessible(true);
newElements = makePathElements.invoke(null, files, this.odexDir, suppressedExceptions);
} else {
Method makeDexElements = pathList.getClass().getDeclaredMethod("makeDexElements", ArrayList.class, File.class, ArrayList.class);
makeDexElements.setAccessible(true);
newElements = makeDexElements.invoke(null, files, this.odexDir, suppressedExceptions);
}

Object targetElements = dexElementsField.get(targetPathList);
Object sourceElements = dexElementsField.get(sourcePathList);
if (!suppressedExceptions.isEmpty()) {
throw suppressedExceptions.get(0);
}

int targetLen = Array.getLength(targetElements);
int sourceLen = Array.getLength(sourceElements);
Field dexElementsField = pathList.getClass().getDeclaredField("dexElements");
dexElementsField.setAccessible(true);
Object oldElements = dexElementsField.get(pathList);

// Create merged array: target + source
Object merged = Array.newInstance(targetElements.getClass().getComponentType(), targetLen + sourceLen);
System.arraycopy(targetElements, 0, merged, 0, targetLen);
System.arraycopy(sourceElements, 0, merged, targetLen, sourceLen);
int oldLen = Array.getLength(oldElements);
int newLen = Array.getLength(newElements);
Object merged = Array.newInstance(oldElements.getClass().getComponentType(), oldLen + newLen);
System.arraycopy(oldElements, 0, merged, 0, oldLen);
System.arraycopy(newElements, 0, merged, oldLen, newLen);

dexElementsField.set(targetPathList, merged);
} catch (Exception e) {
if (logger.isEnabled()) {
logger.write("Failed to inject dex into parent classloader: " + e.getMessage());
}
// Non-fatal: class will still be loadable via the ClassStorageService fallback
}
dexElementsField.set(pathList, merged);
}
}