From 53f9373f2e8951095a86d2490ceb47e00c63507b Mon Sep 17 00:00:00 2001 From: Eduardo Speroni Date: Sat, 15 Aug 2026 17:08:22 -0300 Subject: [PATCH] fix: route Java->JS calls to the runtime that created the binding instance Binding classes carried no runtime identity, so com.tns.Runtime had to re-derive the target of every Java->JS call from the calling thread: the currentRuntime thread-local, then the entered V8 isolate, then a linear scan of every live runtime's object maps (getObjectRuntime). That is wrong in two ways and racy in a third. Ownership was inferred from "some runtime has a wrapper for this object", but a wrapper only records that the instance crossed into that isolate. An instance created by one runtime and later passed into another exists in both maps, so a call could dispatch into an isolate that holds a wrapper but not the user's JS implementation. The scan also reads other runtimes' strongJavaObjectToID / weakJavaObjectToID from a foreign thread; with enableMultithreadedJavascript off (the default) those are plain HashMap/NativeScriptHashMap being mutated concurrently by their owning threads. Both generators now emit com.tns.NativeScriptRuntimeBound: the static binding generator in Java source, and the runtime binding generator - which dexes the proxies behind new java.lang.Runnable({...}) and is the more common of the two - in asmdex alongside the __initialized field and equals__super it already emits. initInstance stamps the id once; later registrations by 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. INVALID_RUNTIME_ID is -1, so both generators seed the field rather than rely on its zero default: 0 is the main runtime's id, and JsV8InspectorClient hard-codes it as such. The dex proxies seed it as early as a constructor may touch the instance - the verifier rejects field access on an uninitialized reference, so not before the superclass constructor. A proxied method invoked from within that superclass constructor therefore still reads 0 and resolves to the main runtime, which finds no object id registered for the instance and reports that; before this change the same call reported that no runtime held the instance. A call with nowhere left to run is dropped and logged rather than executed against an unrelated runtime. These arrive on Android callbacks - lifecycle, listeners, draw - so throwing would take down the process because a worker ended; the loss stays contained to the one call. Primitive returns reuse the existing default-value substitution so the generated cast can still unbox. - callJSMethod against a terminated owner drops instead of scanning and landing on whichever runtime happens to hold a wrapper. - dispatchCallJSMethodNative ignored the result of threadScheduler.post(); a post to a quitting worker looper already returned null silently, and now says so in the log. initInstance is the one case that still throws: it dereferenced a null runtime, so constructing a binding off a runtime thread produced a bare NPE. Dropping there would leave an unregistered instance whose every later call fails anyway, further from the cause, so it reports instead. passSuppressedExceptionToJs routed by calling thread, so an exception suppressed on behalf of another runtime was reported to the wrong one - and the message it built was never passed to passDiscardedExceptionToJs. It now takes the instance, resolves the same owner, and reports on that runtime's own thread without blocking. The two-argument overload stays for already-generated bindings. Bindings from an older generator carry no id and keep the previous thread-based resolution. Costs two methods and one int field per binding class; noted for dex budgets. --- .../tests/testsForRuntimeBindingGenerator.js | 8 +- .../staticbindinggenerator/Generator.java | 3 + .../generating/writing/FieldsWriter.java | 1 + .../generating/writing/MethodsWriter.java | 1 + .../writing/impl/ClassWriterImpl.java | 4 + .../writing/impl/FieldsWriterImpl.java | 8 + .../writing/impl/MethodsWriterImpl.java | 12 +- .../src/test/java/android/util/Log.java | 11 ++ .../com/tns/NativeScriptRuntimeBound.java | 9 ++ .../src/test/java/com/tns/Runtime.java | 1 + .../test/GeneratorTest.java | 51 ++++++- .../com/tns/NativeScriptRuntimeBound.java | 24 +++ .../src/main/java/com/tns/bindings/Dump.java | 49 +++++- .../src/main/java/com/tns/Runtime.java | 141 ++++++++++++++++-- 14 files changed, 300 insertions(+), 23 deletions(-) create mode 100644 test-app/build-tools/static-binding-generator/src/test/java/android/util/Log.java create mode 100644 test-app/build-tools/static-binding-generator/src/test/java/com/tns/NativeScriptRuntimeBound.java create mode 100644 test-app/runtime-binding-generator/src/main/java/com/tns/NativeScriptRuntimeBound.java 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;