Skip to content
Draft
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 @@ -109,9 +109,9 @@ describe("Tests for runtime binding generator", function () {

var interfaces = clazz.getInterfaces();

var expectedInterfaces = ["java.util.jar.Pack200$Packer", "java.util.Formattable", "java.util.Observer", "java.util.jar.Pack200$Unpacker", "com.tns.NativeScriptHashCodeProvider"];
var expectedInterfaces = ["java.util.jar.Pack200$Packer", "java.util.Formattable", "java.util.Observer", "java.util.jar.Pack200$Unpacker", "com.tns.NativeScriptHashCodeProvider", "com.tns.NativeScriptRuntimeBound"];

expect(interfaces.length).toBe(5);
expect(interfaces.length).toBe(6);

for(var i = 0; i < interfaces.length; i++) {
var interfaceName = interfaces[i].getName().toString();
Expand Down Expand Up @@ -168,9 +168,9 @@ describe("Tests for runtime binding generator", function () {

var interfaces = clazz.getInterfaces();

var expectedInterfaces = ["java.util.jar.Pack200$Packer", "java.util.Formattable", "java.util.Observer", "java.util.jar.Pack200$Unpacker", "com.tns.NativeScriptHashCodeProvider"];
var expectedInterfaces = ["java.util.jar.Pack200$Packer", "java.util.Formattable", "java.util.Observer", "java.util.jar.Pack200$Unpacker", "com.tns.NativeScriptHashCodeProvider", "com.tns.NativeScriptRuntimeBound"];

expect(interfaces.length).toBe(5);
expect(interfaces.length).toBe(6);

for(var i = 0; i < interfaces.length; i++) {
var interfaceName = interfaces[i].getName().toString();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,8 @@ private void writeFieldsToWriter(Writer writer, JavaClass clazz) {
String normalizedClassName = BcelNamingUtil.resolveClassName(clazz.getClassName());
fieldsWriter.writeStaticThizField(normalizedClassName);
}

fieldsWriter.writeRuntimeIdField();
}

private void writeConstructorsToWriter(Writer writer, JavaClass clazz, DataRow dataRow, String generatedClassName, GenericHierarchyView genericHierarchyView) {
Expand Down Expand Up @@ -523,6 +525,7 @@ private void writeMethodsToWriter(Writer writer, GenericHierarchyView genericHie

methodsWriter.writeInternalRuntimeHashCodeMethod();
methodsWriter.writeInternalRuntimeEqualsMethod();
methodsWriter.writeInternalRuntimeIdAccessorMethods();
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@

public interface FieldsWriter extends JavaCodeWriter {
void writeStaticThizField(String className);
void writeRuntimeIdField();
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@ public interface MethodsWriter extends JavaCodeWriter {
void writeGetInstanceMethod(String className);
void writeInternalRuntimeEqualsMethod();
void writeInternalRuntimeHashCodeMethod();
void writeInternalRuntimeIdAccessorMethods();
void writeInternalServiceOnCreateMethod();
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ public class ClassWriterImpl implements ClassWriter {
private static final String PUBLIC_CLASS_KEYWORD = "public class";
private static final String JAVASCRIPT_IMPLEMENTATION_FILE_NAME_PATTERN = "@com.tns.JavaScriptImplementation(javaScriptFile = \"./%s\")";
private static final String NATIVESCRIPT_HASHCODE_PROVIDER_INTERFACE_NAME = "com.tns.NativeScriptHashCodeProvider";
private static final String NATIVESCRIPT_RUNTIME_BOUND_INTERFACE_NAME = "com.tns.NativeScriptRuntimeBound";
private static final String NOT_EXTENDING_ANY_CLASS_MESSAGE = "Not extending any class!";

private final Writer writer;
Expand Down Expand Up @@ -42,6 +43,9 @@ public void writeBeginningOfChildClass(String className, String extendedClassNam
writer.write(IMPLEMENTS_KEYWORD);
writer.write(SPACE_LITERAL);
writer.write(NATIVESCRIPT_HASHCODE_PROVIDER_INTERFACE_NAME);
writer.write(COMMA_LITERAL);
writer.write(SPACE_LITERAL);
writer.write(NATIVESCRIPT_RUNTIME_BOUND_INTERFACE_NAME);

if (!isEmpty(implementedInterfacesNames) && !isEmpty(implementedInterfacesNames.get(0))) {
writer.write(COMMA_LITERAL);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,21 @@ public class FieldsWriterImpl implements FieldsWriter {


private static final String THIZ_KEYWORD = "thiz";
private static final String RUNTIME_ID_FIELD_DECLARATION = "private int runtimeId__ns = com.tns.NativeScriptRuntimeBound.INVALID_RUNTIME_ID;";

private final Writer writer;

public FieldsWriterImpl(final Writer writer) {
this.writer = writer;
}

@Override
public void writeRuntimeIdField() {
writer.write(TABULATION_LITERAL);
writer.writeln(RUNTIME_ID_FIELD_DECLARATION);
writer.writeln();
}

@Override
public void writeStaticThizField(String className) {
writer.write(TABULATION_LITERAL);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ public class MethodsWriterImpl implements MethodsWriter {

private static final String INTERNAL_RUNTIME_EQUALS_METHOD_SIGNATURE = "public boolean equals__super(java.lang.Object other)";
private static final String INTERNAL_RUNTIME_HASHCODE_METHOD_SIGNATURE = "public int hashCode__super()";
private static final String INTERNAL_RUNTIME_GET_RUNTIME_ID_METHOD_SIGNATURE = "public int getRuntimeId__ns()";
private static final String INTERNAL_RUNTIME_GET_RUNTIME_ID_RETURN_STATEMENT = "return runtimeId__ns;";
private static final String INTERNAL_RUNTIME_SET_RUNTIME_ID_METHOD_SIGNATURE = "public void setRuntimeId__ns(int runtimeId)";
private static final String INTERNAL_RUNTIME_SET_RUNTIME_ID_ASSIGNMENT = "runtimeId__ns = runtimeId;";
private static final String INTERNAL_SERVICES_ONCREATE_METHOD_SIGNATURE = "public void " + ON_CREATE_METHOD_NAME + "()";

private static final String RETURN_KEYWORD = "return";
Expand Down Expand Up @@ -369,7 +373,7 @@ private void writeCallJsMethodExceptionsSuppressBlockClosingIfNecessary(Type ret
if (shouldSuppressCallJsMethodExceptions) {
writer.write(CLOSING_CURLY_BRACKET_LITERAL);
writer.write(GENERIC_CATCH_BLOCK_BEGINNING);
writer.writeln("\t\t\tcom.tns.Runtime.passSuppressedExceptionToJs(throwable, \"" + methodName + "\");");
writer.writeln("\t\t\tcom.tns.Runtime.passSuppressedExceptionToJs(this, throwable, \"" + methodName + "\");");
writer.writeln(ANDROID_LOG_METHOD_CALL_STATEMENT);

if (!returnType.equals(Type.VOID)) {
Expand Down Expand Up @@ -398,6 +402,12 @@ public void writeInternalRuntimeHashCodeMethod() {
writeInternalRuntimeMethod(INTERNAL_RUNTIME_HASHCODE_METHOD_SIGNATURE, INTERNAL_RUNTIME_HASHCODE_METHOD_RETURN_STATEMENT);
}

@Override
public void writeInternalRuntimeIdAccessorMethods() {
writeInternalRuntimeMethod(INTERNAL_RUNTIME_GET_RUNTIME_ID_METHOD_SIGNATURE, INTERNAL_RUNTIME_GET_RUNTIME_ID_RETURN_STATEMENT);
writeInternalRuntimeMethod(INTERNAL_RUNTIME_SET_RUNTIME_ID_METHOD_SIGNATURE, INTERNAL_RUNTIME_SET_RUNTIME_ID_ASSIGNMENT);
}

private void writeInternalRuntimeMethod(String signature, String returnStatement) {
writer.write(signature);
writer.write(OPENING_CURLY_BRACKET_LITERAL);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package android.util;

/**
* Stub for compiling generated bindings in tests: the exception-suppressing
* variant emits a call to android.util.Log, which is not on the test classpath.
*/
public class Log {
public static int w(String tag, String msg) {
return 0;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.tns;

public interface NativeScriptRuntimeBound {
int INVALID_RUNTIME_ID = -1;

int getRuntimeId__ns();

void setRuntimeId__ns(int runtimeId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ public static void initInstance(Object instance) {}
public static Object callJSMethod(Object javaObject, String methodName, Class<?> retType, Object... args) {
return null;
}
public static void passSuppressedExceptionToJs(Object instance, Throwable ex, String methodName) {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,50 @@ public void testCanCompileBinding() throws Exception {
Class<?> helloClass = InMemoryJavaCompiler.compile(binding.getClassname(), sourceCode.toString(), options);

Assert.assertNotNull(helloClass);
Assert.assertEquals(3, helloClass.getDeclaredMethods().length);
Assert.assertEquals(5, helloClass.getDeclaredMethods().length);
}

@Test
public void testCanCompileBindingWithSuppressedCallJsMethodExceptions() throws Exception {
List<String> lines = Utils.getDataRowsFromResource("datarow-named-extend.txt");
DataRow dataRow = new DataRow(lines.get(0));

File outputDir = null;
List<DataRow> libs = new ArrayList<>();
Generator generator = new Generator(outputDir, libs, true);
Binding binding = generator.generateBinding(dataRow);
Assert.assertNotNull(binding);

String sourceCode = binding.getContent();
Assert.assertTrue(sourceCode.contains("com.tns.Runtime.passSuppressedExceptionToJs(this,"));

Iterable<String> options = new ArrayList<String>(Arrays.asList("-cp", dependenciesDir));
Class<?> helloClass = InMemoryJavaCompiler.compile(binding.getClassname(), sourceCode, options);

Assert.assertNotNull(helloClass);
}

@Test
public void testBindingCarriesItsOwningRuntimeId() throws Exception {
List<String> lines = Utils.getDataRowsFromResource("datarow-named-extend.txt");
DataRow dataRow = new DataRow(lines.get(0));

File outputDir = null;
List<DataRow> libs = new ArrayList<>();
Generator generator = new Generator(outputDir, libs);
Binding binding = generator.generateBinding(dataRow);
Assert.assertNotNull(binding);

Iterable<String> options = new ArrayList<String>(Arrays.asList("-cp", dependenciesDir));
Class<?> boundClass = InMemoryJavaCompiler.compile(binding.getClassname(), binding.getContent(), options);

Assert.assertTrue(com.tns.NativeScriptRuntimeBound.class.isAssignableFrom(boundClass));

com.tns.NativeScriptRuntimeBound instance = (com.tns.NativeScriptRuntimeBound) boundClass.newInstance();
Assert.assertEquals(com.tns.NativeScriptRuntimeBound.INVALID_RUNTIME_ID, instance.getRuntimeId__ns());

instance.setRuntimeId__ns(7);
Assert.assertEquals(7, instance.getRuntimeId__ns());
}

@Test
Expand All @@ -79,7 +122,7 @@ public void testCanCompileBindingOfInterfaceWithStaticInitializer() throws Excep
Class<?> helloClass = InMemoryJavaCompiler.compile("com.tns.gen.com.example.MyInterface", sourceCode.toString(), options);

Assert.assertNotNull(helloClass);
Assert.assertEquals(3, helloClass.getDeclaredMethods().length); // 3 methods (includes 'hashCode__super' and 'equals__super')
Assert.assertEquals(5, helloClass.getDeclaredMethods().length); // 5 methods (includes 'hashCode__super', 'equals__super', 'getRuntimeId__ns' and 'setRuntimeId__ns')
}

@Test
Expand All @@ -100,7 +143,7 @@ public void testCanCompileBindingClassImplementingMultipleInterfaces() throws Ex
Class<?> ComplexClass = InMemoryJavaCompiler.compile(binding.getClassname(), sourceCode.toString(), options);

Assert.assertNotNull(ComplexClass);
Assert.assertEquals(3, ComplexClass.getInterfaces().length); // 2 + 1 (hashcodeprovider)
Assert.assertEquals(4, ComplexClass.getInterfaces().length); // 2 + hashcodeprovider + runtimebound
}

@Test
Expand All @@ -127,7 +170,7 @@ public void testCanCompileBindingClassExtendingAnExtendedClassWithMethodsWithThe
Class<?> ComplexClass = InMemoryJavaCompiler.compile(binding.getClassname(), sourceCode.toString(), options);

Assert.assertNotNull(ComplexClass);
Assert.assertEquals(4, ComplexClass.getDeclaredMethods().length); // 1 + constructor + (equals + hashcode)
Assert.assertEquals(6, ComplexClass.getDeclaredMethods().length); // 1 + constructor + (equals + hashcode) + (getRuntimeId__ns + setRuntimeId__ns)
}

@Test
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.tns;

/**
* Implemented by generated binding classes so that a Java-&gt;JS call can be routed
* to the runtime that created the instance rather than inferred from the calling
* thread.
*
* The runtime id is written once, by the runtime that registers the instance, and
* is never reassigned: an instance may later be wrapped by other runtimes (each
* keeps its own object-id map), but only the creating runtime holds the JS
* implementation the generated overrides dispatch to.
*/
public interface NativeScriptRuntimeBound {
/**
* Not 0: that is a real runtime id (the main runtime's), and JsV8InspectorClient
* hard-codes it as such. Implementations must therefore seed the backing field
* rather than rely on its natural zero default.
*/
int INVALID_RUNTIME_ID = -1;

int getRuntimeId__ns();

void setRuntimeId__ns(int runtimeId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ public class Dump {
static final String runtimeClass = LCOM_TNS_RUNTIME;
static final String callJSMethodName = "callJSMethod";
static final String initInstanceMethodName = "initInstance";
static final String RUNTIME_ID_FIELD_NAME = "runtimeId__ns";

static final StringBuffer methodDescriptorBuilder = new StringBuffer();

Expand Down Expand Up @@ -449,6 +450,8 @@ private void generateCtor(ClassVisitor cv, ClassDescriptor classTo, MethodDescri
mv.visitMethodInsn(org.ow2.asmdex.Opcodes.INSN_INVOKE_DIRECT_RANGE, objectClass, "<init>", ctorSignature, args);
}

generateRuntimeIdInitialization(mv, thisRegister, tnsClassSignature);

if (!isApplicationClass(classTo)) {
generateInitializedBlock(mv, thisRegister, classSignature, tnsClassSignature);
}
Expand All @@ -473,6 +476,20 @@ private void generateCtorOverridenBlock(MethodVisitor mv, int thisRegister, Meth
{ 3, 1, 2, 0 }); //invoke callJSMethod(this, "init", true, params)
}

/*
* Seeds runtimeId__ns with NativeScriptRuntimeBound.INVALID_RUNTIME_ID, which a
* dex field's zero default does not give us - 0 is the main runtime's id. Runs
* as early as a constructor can touch the instance, which still leaves the
* superclass constructor above it reading 0; a proxied method called from there
* resolves to the main runtime, finds no object id registered, and reports that
* instead. It cannot run earlier: the verifier rejects field access on an
* uninitialized reference.
*/
private void generateRuntimeIdInitialization(MethodVisitor mv, int thisRegister, String tnsClassSignature) {
mv.visitVarInsn(org.ow2.asmdex.Opcodes.INSN_CONST_4, thisRegister - 1, -1);
mv.visitFieldInsn(org.ow2.asmdex.Opcodes.INSN_IPUT, tnsClassSignature, RUNTIME_ID_FIELD_NAME, "I", thisRegister - 1, thisRegister);
}

private void generateInitializedBlock(MethodVisitor mv, int thisRegister, String classSignature, String tnsClassSignature) {
mv.visitFieldInsn(org.ow2.asmdex.Opcodes.INSN_IGET_BOOLEAN, tnsClassSignature, "__initialized", "Z", thisRegister - 2, thisRegister); //put __initialized in local var 1
Label label = new Label();
Expand All @@ -493,6 +510,28 @@ private void generateMethods(ClassVisitor cv, ClassDescriptor classTo, MethodDes

generateEqualsSuper(cv);
generateHashCodeSuper(cv);
generateGetRuntimeId(cv, tnsClassSignature);
generateSetRuntimeId(cv, tnsClassSignature);
}

// 2 registers, 1 parameter: 'this' takes the last one, v0 is left as scratch
private void generateGetRuntimeId(ClassVisitor cv, String tnsClassSignature) {
MethodVisitor mv = cv.visitMethod(org.ow2.asmdex.Opcodes.ACC_PUBLIC, "getRuntimeId__ns", "I", null, null);
mv.visitCode();
mv.visitMaxs(2, 0);
mv.visitFieldInsn(org.ow2.asmdex.Opcodes.INSN_IGET, tnsClassSignature, RUNTIME_ID_FIELD_NAME, "I", 0, 1);
mv.visitIntInsn(org.ow2.asmdex.Opcodes.INSN_RETURN, 0);
mv.visitEnd();
}

// 2 registers, 2 parameters: v0 is 'this' and v1 the id, leaving no scratch
private void generateSetRuntimeId(ClassVisitor cv, String tnsClassSignature) {
MethodVisitor mv = cv.visitMethod(org.ow2.asmdex.Opcodes.ACC_PUBLIC, "setRuntimeId__ns", "VI", null, null);
mv.visitCode();
mv.visitMaxs(2, 0);
mv.visitFieldInsn(org.ow2.asmdex.Opcodes.INSN_IPUT, tnsClassSignature, RUNTIME_ID_FIELD_NAME, "I", 1, 0);
mv.visitInsn(org.ow2.asmdex.Opcodes.INSN_RETURN_VOID);
mv.visitEnd();
}

private void generateEqualsSuper(ClassVisitor cv) {
Expand Down Expand Up @@ -804,10 +843,14 @@ private void generateReturnFromObject(MethodVisitor mv, ClassDescriptor targetRe
private void generateFields(ClassVisitor cv) {
FieldVisitor fv = cv.visitField(org.ow2.asmdex.Opcodes.ACC_PRIVATE, "__initialized", "Z", null, null);
fv.visitEnd();

// seeded per constructor, see generateRuntimeIdInitialization
fv = cv.visitField(org.ow2.asmdex.Opcodes.ACC_PRIVATE, RUNTIME_ID_FIELD_NAME, "I", null, null);
fv.visitEnd();
}

static final String[] classImplentedInterfaces = new String[] { "Lcom/tns/NativeScriptHashCodeProvider;" };
static final String[] interfaceImplementedInterfaces = new String[] { "Lcom/tns/NativeScriptHashCodeProvider;", "" };
static final String[] classImplentedInterfaces = new String[] { "Lcom/tns/NativeScriptHashCodeProvider;", "Lcom/tns/NativeScriptRuntimeBound;" };
static final String[] interfaceImplementedInterfaces = new String[] { "Lcom/tns/NativeScriptHashCodeProvider;", "Lcom/tns/NativeScriptRuntimeBound;", "" };

private ClassVisitor generateClass(ApplicationWriter aw, ClassDescriptor classTo, String classSignature, String tnsClassSignature, HashSet<ClassDescriptor> implementedInterfaces, AnnotationDescriptor[] annotations) {
ClassVisitor cv;
Expand All @@ -817,7 +860,7 @@ private ClassVisitor generateClass(ApplicationWriter aw, ClassDescriptor classTo
ArrayList<String> interfacesToImplement = new ArrayList(Arrays.asList(classImplentedInterfaces));

if (classTo.isInterface()) {
interfaceImplementedInterfaces[1] = classSignature; //new String[] { "Lcom/tns/NativeScriptHashCodeProvider;", classSignature };
interfaceImplementedInterfaces[interfaceImplementedInterfaces.length - 1] = classSignature;
for (String interfaceToImpl : interfaceImplementedInterfaces) {
if (!interfacesToImplement.contains(interfaceToImpl)) {
interfacesToImplement.add(interfaceToImpl);
Expand Down
Loading