diff --git a/test-app/app/src/main/assets/app/tests/testsForRuntimeBindingGenerator.js b/test-app/app/src/main/assets/app/tests/testsForRuntimeBindingGenerator.js index 98011f4cf..3c6c82685 100644 --- a/test-app/app/src/main/assets/app/tests/testsForRuntimeBindingGenerator.js +++ b/test-app/app/src/main/assets/app/tests/testsForRuntimeBindingGenerator.js @@ -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(); @@ -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(); diff --git a/test-app/build-tools/static-binding-generator/src/main/java/org/nativescript/staticbindinggenerator/Generator.java b/test-app/build-tools/static-binding-generator/src/main/java/org/nativescript/staticbindinggenerator/Generator.java index be7649181..9047a45b5 100644 --- a/test-app/build-tools/static-binding-generator/src/main/java/org/nativescript/staticbindinggenerator/Generator.java +++ b/test-app/build-tools/static-binding-generator/src/main/java/org/nativescript/staticbindinggenerator/Generator.java @@ -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) { @@ -523,6 +525,7 @@ private void writeMethodsToWriter(Writer writer, GenericHierarchyView genericHie methodsWriter.writeInternalRuntimeHashCodeMethod(); methodsWriter.writeInternalRuntimeEqualsMethod(); + methodsWriter.writeInternalRuntimeIdAccessorMethods(); } /** diff --git a/test-app/build-tools/static-binding-generator/src/main/java/org/nativescript/staticbindinggenerator/generating/writing/FieldsWriter.java b/test-app/build-tools/static-binding-generator/src/main/java/org/nativescript/staticbindinggenerator/generating/writing/FieldsWriter.java index 8e9f3497b..01d427fce 100644 --- a/test-app/build-tools/static-binding-generator/src/main/java/org/nativescript/staticbindinggenerator/generating/writing/FieldsWriter.java +++ b/test-app/build-tools/static-binding-generator/src/main/java/org/nativescript/staticbindinggenerator/generating/writing/FieldsWriter.java @@ -2,4 +2,5 @@ public interface FieldsWriter extends JavaCodeWriter { void writeStaticThizField(String className); + void writeRuntimeIdField(); } diff --git a/test-app/build-tools/static-binding-generator/src/main/java/org/nativescript/staticbindinggenerator/generating/writing/MethodsWriter.java b/test-app/build-tools/static-binding-generator/src/main/java/org/nativescript/staticbindinggenerator/generating/writing/MethodsWriter.java index 888c8c28f..37d104a23 100644 --- a/test-app/build-tools/static-binding-generator/src/main/java/org/nativescript/staticbindinggenerator/generating/writing/MethodsWriter.java +++ b/test-app/build-tools/static-binding-generator/src/main/java/org/nativescript/staticbindinggenerator/generating/writing/MethodsWriter.java @@ -8,5 +8,6 @@ public interface MethodsWriter extends JavaCodeWriter { void writeGetInstanceMethod(String className); void writeInternalRuntimeEqualsMethod(); void writeInternalRuntimeHashCodeMethod(); + void writeInternalRuntimeIdAccessorMethods(); void writeInternalServiceOnCreateMethod(); } diff --git a/test-app/build-tools/static-binding-generator/src/main/java/org/nativescript/staticbindinggenerator/generating/writing/impl/ClassWriterImpl.java b/test-app/build-tools/static-binding-generator/src/main/java/org/nativescript/staticbindinggenerator/generating/writing/impl/ClassWriterImpl.java index d91c1f4ee..092ae0f09 100644 --- a/test-app/build-tools/static-binding-generator/src/main/java/org/nativescript/staticbindinggenerator/generating/writing/impl/ClassWriterImpl.java +++ b/test-app/build-tools/static-binding-generator/src/main/java/org/nativescript/staticbindinggenerator/generating/writing/impl/ClassWriterImpl.java @@ -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; @@ -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); diff --git a/test-app/build-tools/static-binding-generator/src/main/java/org/nativescript/staticbindinggenerator/generating/writing/impl/FieldsWriterImpl.java b/test-app/build-tools/static-binding-generator/src/main/java/org/nativescript/staticbindinggenerator/generating/writing/impl/FieldsWriterImpl.java index eecad2d4e..b977633fe 100644 --- a/test-app/build-tools/static-binding-generator/src/main/java/org/nativescript/staticbindinggenerator/generating/writing/impl/FieldsWriterImpl.java +++ b/test-app/build-tools/static-binding-generator/src/main/java/org/nativescript/staticbindinggenerator/generating/writing/impl/FieldsWriterImpl.java @@ -7,6 +7,7 @@ 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; @@ -14,6 +15,13 @@ 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); diff --git a/test-app/build-tools/static-binding-generator/src/main/java/org/nativescript/staticbindinggenerator/generating/writing/impl/MethodsWriterImpl.java b/test-app/build-tools/static-binding-generator/src/main/java/org/nativescript/staticbindinggenerator/generating/writing/impl/MethodsWriterImpl.java index 4bd43e011..1701381bc 100644 --- a/test-app/build-tools/static-binding-generator/src/main/java/org/nativescript/staticbindinggenerator/generating/writing/impl/MethodsWriterImpl.java +++ b/test-app/build-tools/static-binding-generator/src/main/java/org/nativescript/staticbindinggenerator/generating/writing/impl/MethodsWriterImpl.java @@ -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"; @@ -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)) { @@ -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); diff --git a/test-app/build-tools/static-binding-generator/src/test/java/android/util/Log.java b/test-app/build-tools/static-binding-generator/src/test/java/android/util/Log.java new file mode 100644 index 000000000..30c04ed4a --- /dev/null +++ b/test-app/build-tools/static-binding-generator/src/test/java/android/util/Log.java @@ -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; + } +} diff --git a/test-app/build-tools/static-binding-generator/src/test/java/com/tns/NativeScriptRuntimeBound.java b/test-app/build-tools/static-binding-generator/src/test/java/com/tns/NativeScriptRuntimeBound.java new file mode 100644 index 000000000..891409c17 --- /dev/null +++ b/test-app/build-tools/static-binding-generator/src/test/java/com/tns/NativeScriptRuntimeBound.java @@ -0,0 +1,9 @@ +package com.tns; + +public interface NativeScriptRuntimeBound { + int INVALID_RUNTIME_ID = -1; + + int getRuntimeId__ns(); + + void setRuntimeId__ns(int runtimeId); +} diff --git a/test-app/build-tools/static-binding-generator/src/test/java/com/tns/Runtime.java b/test-app/build-tools/static-binding-generator/src/test/java/com/tns/Runtime.java index d3f59ce4d..63dbe7acc 100644 --- a/test-app/build-tools/static-binding-generator/src/test/java/com/tns/Runtime.java +++ b/test-app/build-tools/static-binding-generator/src/test/java/com/tns/Runtime.java @@ -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) {} } diff --git a/test-app/build-tools/static-binding-generator/src/test/java/org/nativescript/staticbindinggenerator/test/GeneratorTest.java b/test-app/build-tools/static-binding-generator/src/test/java/org/nativescript/staticbindinggenerator/test/GeneratorTest.java index 689f32217..e1f492329 100644 --- a/test-app/build-tools/static-binding-generator/src/test/java/org/nativescript/staticbindinggenerator/test/GeneratorTest.java +++ b/test-app/build-tools/static-binding-generator/src/test/java/org/nativescript/staticbindinggenerator/test/GeneratorTest.java @@ -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 lines = Utils.getDataRowsFromResource("datarow-named-extend.txt"); + DataRow dataRow = new DataRow(lines.get(0)); + + File outputDir = null; + List 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 options = new ArrayList(Arrays.asList("-cp", dependenciesDir)); + Class helloClass = InMemoryJavaCompiler.compile(binding.getClassname(), sourceCode, options); + + Assert.assertNotNull(helloClass); + } + + @Test + public void testBindingCarriesItsOwningRuntimeId() throws Exception { + List lines = Utils.getDataRowsFromResource("datarow-named-extend.txt"); + DataRow dataRow = new DataRow(lines.get(0)); + + File outputDir = null; + List libs = new ArrayList<>(); + Generator generator = new Generator(outputDir, libs); + Binding binding = generator.generateBinding(dataRow); + Assert.assertNotNull(binding); + + Iterable options = new ArrayList(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 @@ -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 @@ -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 @@ -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 diff --git a/test-app/runtime-binding-generator/src/main/java/com/tns/NativeScriptRuntimeBound.java b/test-app/runtime-binding-generator/src/main/java/com/tns/NativeScriptRuntimeBound.java new file mode 100644 index 000000000..9d1ed231f --- /dev/null +++ b/test-app/runtime-binding-generator/src/main/java/com/tns/NativeScriptRuntimeBound.java @@ -0,0 +1,24 @@ +package com.tns; + +/** + * Implemented by generated binding classes so that a Java->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); +} diff --git a/test-app/runtime-binding-generator/src/main/java/com/tns/bindings/Dump.java b/test-app/runtime-binding-generator/src/main/java/com/tns/bindings/Dump.java index 4a11a3931..93695780c 100644 --- a/test-app/runtime-binding-generator/src/main/java/com/tns/bindings/Dump.java +++ b/test-app/runtime-binding-generator/src/main/java/com/tns/bindings/Dump.java @@ -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(); @@ -449,6 +450,8 @@ private void generateCtor(ClassVisitor cv, ClassDescriptor classTo, MethodDescri mv.visitMethodInsn(org.ow2.asmdex.Opcodes.INSN_INVOKE_DIRECT_RANGE, objectClass, "", ctorSignature, args); } + generateRuntimeIdInitialization(mv, thisRegister, tnsClassSignature); + if (!isApplicationClass(classTo)) { generateInitializedBlock(mv, thisRegister, classSignature, tnsClassSignature); } @@ -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(); @@ -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) { @@ -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 implementedInterfaces, AnnotationDescriptor[] annotations) { ClassVisitor cv; @@ -817,7 +860,7 @@ private ClassVisitor generateClass(ApplicationWriter aw, ClassDescriptor classTo ArrayList 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); diff --git a/test-app/runtime/src/main/java/com/tns/Runtime.java b/test-app/runtime/src/main/java/com/tns/Runtime.java index 4a02c22c4..c91e4d2d3 100644 --- a/test-app/runtime/src/main/java/com/tns/Runtime.java +++ b/test-app/runtime/src/main/java/com/tns/Runtime.java @@ -124,12 +124,51 @@ void passDiscardedExceptionToJs(Throwable ex, String prefix) { passExceptionToJsNative(getRuntimeId(), ex, ex.getMessage(), Runtime.getStackTraceErrorMessage(ex), Runtime.getJSStackTrace(ex), true); } + /** + * Retained for bindings generated before the instance-carrying overload; the + * calling thread's runtime is only a guess at where the exception belongs. + */ public static void passSuppressedExceptionToJs(Throwable ex, String methodName) { - com.tns.Runtime runtime = com.tns.Runtime.getCurrentRuntime(); + passSuppressedExceptionToJs(null, ex, methodName); + } + + public static void passSuppressedExceptionToJs(Object instance, Throwable ex, String methodName) { + com.tns.Runtime runtime = null; + + if (instance != null) { + int owningRuntimeId = getOwningRuntimeId(instance); + if (owningRuntimeId != NativeScriptRuntimeBound.INVALID_RUNTIME_ID) { + runtime = runtimeCache.get(owningRuntimeId); + } + } + + if (runtime == null) { + runtime = com.tns.Runtime.getCurrentRuntime(); + } + if (runtime != null) { - String errorMessage = "Error on \"" + Thread.currentThread().getName() + "\" thread for " + methodName + "\n"; - runtime.passDiscardedExceptionToJs(ex, ""); + runtime.postDiscardedExceptionToJs(ex); + } + } + + /* + * Reports on the owning runtime's thread without waiting: the caller has + * already swallowed the exception and has no result to collect. A runtime + * whose looper is gone simply drops the report - throwing out of an + * error-reporting path would replace a suppressed exception with a live one. + */ + private void postDiscardedExceptionToJs(final Throwable ex) { + if (config.appConfig.getEnableMultithreadedJavascript() || threadScheduler.getThread().equals(Thread.currentThread())) { + passDiscardedExceptionToJs(ex, ""); + return; } + + threadScheduler.post(new Runnable() { + @Override + public void run() { + passDiscardedExceptionToJs(ex, ""); + } + }); } private boolean initialized; @@ -373,6 +412,35 @@ public static String[] getRemoteModuleAllowlist() { return new String[0]; } + /* + * Records which runtime a binding instance belongs to. Called once, from + * initInstance, by the runtime that registers the instance; later + * registrations in other runtimes (getOrCreateJavaObjectID when the instance + * crosses into another isolate) must not overwrite it, because only the + * creating runtime holds the JS implementation behind the generated overrides. + */ + private static void recordOwningRuntime(Object instance, int runtimeId) { + if (instance instanceof NativeScriptRuntimeBound) { + NativeScriptRuntimeBound bound = (NativeScriptRuntimeBound) instance; + if (bound.getRuntimeId__ns() == NativeScriptRuntimeBound.INVALID_RUNTIME_ID) { + bound.setRuntimeId__ns(runtimeId); + } + } + } + + /* + * The id of the runtime that created the instance, or INVALID_RUNTIME_ID for + * anything that does not carry one - bindings built by a generator older than + * this interface, and plain objects handed to callJSMethod directly. Note that + * a returned id may name a runtime that has since been torn down; callers + * decide how to report that. + */ + private static int getOwningRuntimeId(Object javaObject) { + return (javaObject instanceof NativeScriptRuntimeBound) + ? ((NativeScriptRuntimeBound) javaObject).getRuntimeId__ns() + : NativeScriptRuntimeBound.INVALID_RUNTIME_ID; + } + private static Runtime getObjectRuntime(Object object) { Runtime runtime = null; @@ -790,6 +858,14 @@ public static void initInstance(Object instance) { try { Runtime runtime = Runtime.getCurrentRuntime(); + if (runtime == null) { + throw new NativeScriptException("Cannot initialize an instance of " + instance.getClass().getName() + + ": no NativeScript runtime is bound to thread \"" + Thread.currentThread().getName() + + "\". Construct it on the runtime's own thread, or from JS."); + } + + recordOwningRuntime(instance, runtime.getRuntimeId()); + int objectId = runtime.currentObjectId; if (objectId != -1) { @@ -1152,15 +1228,35 @@ public static Object callJSMethodWithDelay(Object javaObject, String methodName, } public static Object callJSMethod(Object javaObject, String methodName, Class retType, boolean isConstructor, long delay, Object... args) throws NativeScriptException { - Runtime runtime = Runtime.getCurrentRuntime(); - - // if we're not in a runtime or the runtime we're in does not have the object, try to find the right one (this might happen if a worker fires a JS method on an object created in the main thread or another worker) - if (runtime == null || runtime.getJavaObjectID(javaObject) == null) { - runtime = getObjectRuntime(javaObject); - } + int owningRuntimeId = getOwningRuntimeId(javaObject); + Runtime runtime; + + if (owningRuntimeId != NativeScriptRuntimeBound.INVALID_RUNTIME_ID) { + runtime = runtimeCache.get(owningRuntimeId); + + if (runtime == null) { + // Naming the class rather than the instance: toString() may be one + // of the JS overrides, and calling it here would re-enter the + // runtime we just failed to reach. + return discardCall(methodName, retType, "an instance of " + javaObject.getClass().getName() + + " outlived the runtime that created it (id=" + owningRuntimeId + ")"); + } + } else { + // Nothing recorded the owner: fall back to the calling thread's + // runtime, then to whichever runtime holds the object. A wrapper in + // some runtime's map is only evidence that the object crossed into + // it, so this can pick a runtime that never implemented the method. + // Both generators stamp the id now, so this is for bindings built by + // an older one. + runtime = Runtime.getCurrentRuntime(); + + if (runtime == null || runtime.getJavaObjectID(javaObject) == null) { + runtime = getObjectRuntime(javaObject); + } - if (runtime == null) { - throw new NativeScriptException("Cannot find runtime for instance=" + ((javaObject == null) ? "null" : javaObject)); + if (runtime == null) { + throw new NativeScriptException("Cannot find runtime for instance=" + ((javaObject == null) ? "null" : javaObject)); + } } return runtime.callJSMethodImpl(javaObject, methodName, retType, isConstructor, delay, args); @@ -1347,6 +1443,10 @@ public void run() { ret = e; } } + } else { + // arr[0] stays null and primitive returns are defaulted below + logDiscardedCall(methodName, "runtime id=" + getRuntimeId() + " (workerId=" + workerId + + ") is no longer accepting work"); } ret = arr[0]; @@ -1362,6 +1462,25 @@ public void run() { return ret; } + private static void logDiscardedCall(String methodName, String reason) { + android.util.Log.w("Warning", "NativeScript discarding call to \"" + methodName + "\": " + reason); + } + + /* + * A call with nowhere left to run is dropped rather than thrown. These arrive + * on Android callbacks - lifecycle, listeners, draw - where an exception would + * take down the process because a worker ended, so the loss is contained to + * the one call and reported through the log instead. Primitive returns still + * need a value the generated cast can unbox. + */ + private static Object discardCall(String methodName, Class retType, String reason) { + logDiscardedCall(methodName, reason); + + return (retType != null && retType.isPrimitive() && retType != void.class) + ? defaultPrimitiveValue(retType) + : null; + } + private static Object defaultPrimitiveValue(Class type) { if (type == boolean.class) { return Boolean.FALSE;