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
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,17 @@ public void setProxyThumb(String proxyThumb) {
}

public String generateProxy(String proxyName, ClassDescriptor classToProxy, HashSet<String> methodOverrides, HashSet<ClassDescriptor> implementedInterfaces, boolean isInterface, AnnotationDescriptor[] annotations) throws IOException {
return generateProxy(proxyName, null, classToProxy, methodOverrides, implementedInterfaces, isInterface, annotations);
}

/**
* cacheDigest, when present, becomes part of the proxy's file name. The
* thumb only changes on reinstall, so name + thumb alone cannot see an
* edit to the proxy's contents (method overrides, interfaces) - the
* digest is what makes such an edit miss the cache instead of silently
* loading the previous dex.
*/
public String generateProxy(String proxyName, String cacheDigest, ClassDescriptor classToProxy, HashSet<String> methodOverrides, HashSet<ClassDescriptor> implementedInterfaces, boolean isInterface, AnnotationDescriptor[] annotations) throws IOException {
ApplicationWriter aw = new ApplicationWriter();
aw.visit();

Expand All @@ -37,7 +48,14 @@ public String generateProxy(String proxyName, ClassDescriptor classToProxy, Hash
String proxyFileName;

if (proxyName.contains(".")) {
// Thumb-suffix dotted names like the anonymous ones: DexFactory's
// cache probe (getDexFile) and purge (purgeDexesByThumb) both key
// on the thumb, so an unsuffixed file regenerates every launch and
// its stale .jar survives — and gets reused — across app versions.
proxyFileName = proxyName;
if (proxyThumb != null) {
proxyFileName += "-" + proxyThumb;
}
} else {
proxyFileName = classToProxy.getName().replace('$', '_');
if (!isInterface) {
Expand All @@ -47,6 +65,10 @@ public String generateProxy(String proxyName, ClassDescriptor classToProxy, Hash
proxyFileName += "-" + proxyThumb;
}
}
// After the thumb, so purgeDexesByThumb keeps matching old generations.
if (cacheDigest != null) {
proxyFileName += "-" + cacheDigest;
}

if (IsLogEnabled) {
System.out.println("Generator: Saving proxy with file name: " + proxyFileName);
Expand Down
26 changes: 25 additions & 1 deletion test-app/runtime/src/main/java/com/tns/ClassResolver.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package com.tns;

import android.util.Log;

import com.tns.system.classes.loading.ClassStorageService;
import com.tns.system.classes.loading.LookedUpClassNotFound;

import java.io.IOException;

Expand All @@ -26,7 +29,28 @@ Class<?> resolveClass(String baseClassName, String fullClassName, DexFactory dex
}

if (clazz == null) {
clazz = classStorageService.retrieveClass(className);
try {
clazz = classStorageService.retrieveClass(className);
} catch (LookedUpClassNotFound notFound) {
// A named proxy (`Base.extend('a.b.C', {...})` / @JavaProxy)
// whose class the static binding generator never compiled — it
// only scans assets/app, and dev servers keep most source off
// disk. The proxy generator accepts dotted names for classes
// and interfaces alike, so supply the class the same way
// anonymous extends are supplied.
Log.w("JS", "Class " + className + " not precompiled; generating at runtime. Framework references resolve only if dex injection into the app class loader succeeds.");
try {
clazz = dexFactory.resolveClass(canonicalBaseClassName, name, className, methodOverrides, implementedInterfaces, isInterface);
} catch (Throwable generationFailure) {
// The precise not-found is the actionable error; a failed
// generation attempt is its detail, not its replacement.
notFound.addSuppressed(generationFailure);
throw notFound;
}
if (clazz == null) {
throw notFound;
}
}
}

return clazz;
Expand Down
64 changes: 57 additions & 7 deletions test-app/runtime/src/main/java/com/tns/DexFactory.java
Original file line number Diff line number Diff line change
Expand Up @@ -120,13 +120,24 @@ public Class<?> resolveClass(String baseClassName, String name, String className
// strip the `com.tns.gen` off the base extended class name
String desiredDexClassName = this.getClassToProxyName(fullClassName);

// A named proxy (`Base.extend('a.b.C', {...})` / @JavaProxy) asks for
// exactly that Java class name; the substitutions below are for the
// anonymous form only, where the name is derived from the base.
boolean isNamedProxy = !fullClassName.startsWith(COM_TNS_GEN_PREFIX) && fullClassName.contains(".");

// when interfaces are extended as classes, we still want to preserve
// just the interface name without the extra file, line, column information
if (!baseClassName.isEmpty() && isInterface) {
if (!baseClassName.isEmpty() && isInterface && !isNamedProxy) {
fullClassName = COM_TNS_GEN_PREFIX + classToProxy;
}

File dexFile = this.getDexFile(desiredDexClassName);
// The thumb only changes on reinstall, so a cache key of name + thumb
// cannot see an edit to the proxy's contents: under HMR a named
// proxy's new method overrides would silently load the previous dex.
// The digest carries the contents into the file name.
String contentDigest = computeContentDigest(classToProxy, methodOverrides, implementedInterfaces, isInterface);

File dexFile = this.getDexFile(desiredDexClassName, contentDigest);

// generate dex file
if (dexFile == null) {
Expand All @@ -136,10 +147,10 @@ public Class<?> resolveClass(String baseClassName, String name, String className
}

String dexFilePath;
if (isInterface) {
dexFilePath = this.generateDex(name, classToProxy, methodOverrides, implementedInterfaces, isInterface);
if (isInterface && !isNamedProxy) {
dexFilePath = this.generateDex(name, contentDigest, classToProxy, methodOverrides, implementedInterfaces, isInterface);
} else {
dexFilePath = this.generateDex(desiredDexClassName, classToProxy, methodOverrides, implementedInterfaces, isInterface);
dexFilePath = this.generateDex(desiredDexClassName, contentDigest, classToProxy, methodOverrides, implementedInterfaces, isInterface);
}
dexFile = new File(dexFilePath);
long stopGenTime = System.nanoTime();
Expand Down Expand Up @@ -251,12 +262,51 @@ private String getClassToProxyName(String className) throws InvalidClassExceptio
return classToProxy;
}

private File getDexFile(String className) throws InvalidClassException {
/**
* Digest of everything that shapes the generated proxy besides its name,
* so the dex cache key changes when the proxy's contents do. Sorted, so
* JS property-enumeration order cannot produce a spurious miss.
*/
private static String computeContentDigest(String classToProxy, String[] methodOverrides, String[] implementedInterfaces, boolean isInterface) {
StringBuilder canonical = new StringBuilder(classToProxy).append('\n').append(isInterface);
if (methodOverrides != null) {
String[] sortedOverrides = methodOverrides.clone();
java.util.Arrays.sort(sortedOverrides);
for (String override : sortedOverrides) {
canonical.append('\n').append(override);
}
}
if (implementedInterfaces != null) {
String[] sortedInterfaces = implementedInterfaces.clone();
java.util.Arrays.sort(sortedInterfaces);
for (String iface : sortedInterfaces) {
canonical.append('').append(iface);
}
}
try {
byte[] hash = java.security.MessageDigest.getInstance("SHA-256")
.digest(canonical.toString().getBytes(java.nio.charset.StandardCharsets.UTF_8));
StringBuilder hex = new StringBuilder(10);
for (int i = 0; i < 5; i++) {
hex.append(String.format("%02x", hash[i]));
}
return hex.toString();
} catch (java.security.NoSuchAlgorithmException e) {
// SHA-256 is mandatory on Android; a digestless key only loses
// cache freshness, never correctness of a fresh generation.
return null;
}
}

private File getDexFile(String className, String contentDigest) throws InvalidClassException {
String classToProxyFile = className.replace("$", "_");

if (this.dexThumb != null) {
classToProxyFile += "-" + this.dexThumb;
}
if (contentDigest != null) {
classToProxyFile += "-" + contentDigest;
}

String dexFilePath = dexDir + "/" + classToProxyFile + ".dex";
File dexFile = new File(dexFilePath);
Expand All @@ -274,7 +324,7 @@ private File getDexFile(String className) throws InvalidClassException {
return null;
}

private String generateDex(String proxyName, String className, String[] methodOverrides, String[] implementedInterfaces, boolean isInterface) throws ClassNotFoundException, IOException {
private String generateDex(String proxyName, String contentDigest, String className, String[] methodOverrides, String[] implementedInterfaces, boolean isInterface) throws ClassNotFoundException, IOException {
Class<?> classToProxy = Class.forName(className);

HashSet<String> methodOverridesSet = null;
Expand Down
18 changes: 18 additions & 0 deletions test-app/runtime/src/main/java/com/tns/Module.java
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,24 @@ static String getApplicationFilesPath() {
return ApplicationFilesPath;
}

/**
* Resolves an entry-module path for runModule. A non-absolute, scheme-less
* name is an app-root-relative module name (the @JavaScriptImplementation
* convention) and goes through the same resolution require uses - extension
* and directory probing included - so the native side only ever receives a
* loadable identity: an absolute path or a URL. Missing entries throw here,
* with require's wording, instead of failing against the process cwd.
*/
static String resolveEntryPath(String path) {
if (path.isEmpty() || path.startsWith("/") || path.contains("://")) {
return path;
}
String relative = (path.startsWith("./") || path.startsWith("../") || path.startsWith("~/"))
? path
: "./" + path;
return resolvePath(relative, ApplicationFilesPath + ModulesFilesPath);
}

@RuntimeCallable
private static String resolvePath(String path, String baseDir) {
// The baseDir is the directory path of the calling module.
Expand Down
3 changes: 1 addition & 2 deletions test-app/runtime/src/main/java/com/tns/Runtime.java
Original file line number Diff line number Diff line change
Expand Up @@ -696,8 +696,7 @@ public void run() throws NativeScriptException {
}

public void runModule(File jsFile) throws NativeScriptException {
String filePath = jsFile.getPath();
runModule(getRuntimeId(), filePath);
runModule(getRuntimeId(), Module.resolveEntryPath(jsFile.getPath()));
}

public Object runScript(File jsFile) throws NativeScriptException {
Expand Down
1 change: 1 addition & 0 deletions test-app/tools/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.