diff --git a/test-app/app/src/main/assets/app/mainpage.js b/test-app/app/src/main/assets/app/mainpage.js index 208901d6e..b599550cf 100644 --- a/test-app/app/src/main/assets/app/mainpage.js +++ b/test-app/app/src/main/assets/app/mainpage.js @@ -46,6 +46,7 @@ require("./tests/testGC"); require("./tests/testsMemoryManagement"); require("./tests/testFieldGetSet"); require("./tests/extendedClassesTests"); +require("./tests/testNativeESClasses"); //require("./tests/extendClassNameTests"); // as tests now run with SBG, this test fails the whole build process require("./tests/testJniReferenceLeak"); require("./tests/testNativeModules"); diff --git a/test-app/app/src/main/assets/app/tests/testNativeESClasses.js b/test-app/app/src/main/assets/app/tests/testNativeESClasses.js new file mode 100644 index 000000000..8ad5fb06c --- /dev/null +++ b/test-app/app/src/main/assets/app/tests/testNativeESClasses.js @@ -0,0 +1,483 @@ +describe("Tests native ES class extensions (class X extends NativeType)", function () { + + var appClassLoader = com.tns.Runtime.class.getClassLoader(); + + // DummyClass.method2(Object) returns "obj=" + obj.toString(), forcing a Java-side virtual + // toString dispatch through the generated proxy. (java.lang.String.valueOf is unusable for + // this: accessing `java.lang.String.null` in other suites permanently replaces `valueOf` on + // the ctor function with the runtime's null-returning valueOf.) + function javaToString(obj) { + return new com.tns.tests.DummyClass().method2(obj); + } + + it("When_extending_a_class_with_es_class_syntax_instances_should_construct_and_dispatch_overrides", function () { + class EsButton extends com.tns.tests.Button1 { + getIMAGE_ID_PROP() { + return "es class override"; + } + } + + var button = new EsButton(); + + expect(button instanceof EsButton).toBe(true); + expect(button instanceof com.tns.tests.Button1).toBe(true); + expect(button.getIMAGE_ID_PROP()).toBe("es class override"); + // non-overridden base methods still work + expect(button.echo("hello")).toBe("hello"); + }); + + it("When_java_calls_a_virtual_method_it_should_dispatch_to_the_es_class_override", function () { + class EchoButton extends com.tns.tests.Button1 { + echo(s) { + return "es:" + s; + } + } + + var button = new EchoButton(); + + // triggerEcho calls this.echo(s) on the Java side - it must route + // through the generated proxy back into the ES class method + expect(button.triggerEcho("x")).toBe("es:x"); + }); + + it("When_calling_super_method_from_an_es_class_override_it_should_invoke_the_java_implementation", function () { + class SuperEchoButton extends com.tns.tests.Button1 { + echo(s) { + return "es:" + super.echo(s); + } + } + + var button = new SuperEchoButton(); + + expect(button.echo("y")).toBe("es:y"); + // round trip: Java triggerEcho -> proxy echo -> JS override -> super.echo -> Java echo + expect(button.triggerEcho("z")).toBe("es:z"); + }); + + it("When_the_es_class_constructor_passes_arguments_to_super_the_matching_java_constructor_should_be_used", function () { + class CtorButton extends com.tns.tests.Button1 { + constructor(value) { + super(value); // Button1(int) overload + this.value = value; + } + } + + var button = new CtorButton(5); + + expect(button.value).toBe(5); + expect(button instanceof com.tns.tests.Button1).toBe(true); + }); + + it("When_accessing_the_class_property_before_any_instance_exists_the_class_should_be_registered_lazily", function () { + class LazyTouch extends java.lang.Object { + toString() { + return "lazy touch"; + } + } + + // no `new` has happened - static touch must register the proxy class + var clazz = LazyTouch.class; + + expect(clazz).not.toBe(null); + expect(clazz.getName()).toContain("LazyTouch"); + + // the class is fully functional before any JS-side construction: + // Java instantiates it through reflection and dispatches to the JS override + var created = clazz.newInstance(); + expect(javaToString(created)).toBe("obj=lazy touch"); + }); + + it("When_passing_the_es_class_to_a_java_method_expecting_a_class_it_should_marshal_to_java_lang_Class", function () { + class MarshalledClass extends com.tns.tests.DummyClass { + } + + // Class.isAssignableFrom(Class) - MarshalledClass is registered lazily during marshalling + expect(com.tns.tests.DummyClass.class.isAssignableFrom(MarshalledClass)).toBe(true); + expect(java.lang.Object.class.isAssignableFrom(MarshalledClass)).toBe(true); + }); + + it("When_passing_the_es_class_where_java_lang_Object_is_expected_it_should_marshal_to_its_java_lang_Class", function () { + class ObjectMarshalledClass extends java.lang.Object { + } + + var list = new java.util.ArrayList(); + list.add(ObjectMarshalledClass); + + var stored = list.get(0); + expect(stored.getName()).toBe(ObjectMarshalledClass.class.getName()); + }); + + it("When_extending_an_es_class_that_extends_a_native_class_each_level_should_get_its_own_proxy", function () { + class LevelOne extends com.tns.tests.Button1 { + getIMAGE_ID_PROP() { + return "level one"; + } + echo(s) { + return "L1:" + s; + } + } + + class LevelTwo extends LevelOne { + getIMAGE_ID_PROP() { + return "level two + " + super.getIMAGE_ID_PROP(); + } + } + + var two = new LevelTwo(); + expect(two instanceof LevelTwo).toBe(true); + expect(two instanceof LevelOne).toBe(true); + expect(two instanceof com.tns.tests.Button1).toBe(true); + expect(two.getIMAGE_ID_PROP()).toBe("level two + level one"); + // method defined only on the intermediate level must be part of the proxy overrides + expect(two.triggerEcho("q")).toBe("L1:q"); + + var one = new LevelOne(); + expect(one.getIMAGE_ID_PROP()).toBe("level one"); + expect(one instanceof LevelTwo).toBe(false); + + expect(one.getClass().getName()).not.toBe(two.getClass().getName()); + }); + + it("When_constructing_the_es_class_multiple_times_all_instances_should_share_one_proxy_class", function () { + class SharedProxyButton extends com.tns.tests.Button1 { + } + + var first = new SharedProxyButton(); + var second = new SharedProxyButton(); + + expect(first.getClass().equals(second.getClass())).toBe(true); + expect(first.getClass().equals(SharedProxyButton.class)).toBe(true); + }); + + it("When_reading_base_class_statics_through_the_es_class_they_should_resolve_to_the_java_members", function () { + class StaticsButton extends com.tns.tests.Button1 { + static jsStatic = 42; + static jsStaticMethod() { + return "js static"; + } + } + + // Java statics are reachable through the constructor prototype chain + expect(StaticsButton.STATIC_IMAGE_ID).toBe("static image id"); + expect(StaticsButton.SGetStaticImageId()).toBe("static image id"); + + // plain JS statics live on the JS constructor and are untouched + expect(StaticsButton.jsStatic).toBe(42); + expect(StaticsButton.jsStaticMethod()).toBe("js static"); + }); + + it("When_the_es_class_is_registered_its_proxy_should_be_discoverable_through_the_app_class_loader", function () { + class DiscoverableEsClass extends java.lang.Object { + toString() { + return "discoverable es class"; + } + } + + var instance = new DiscoverableEsClass(); + var className = instance.getClass().getName(); + + var found = java.lang.Class.forName(className, false, appClassLoader); + + expect(found.getName()).toBe(className); + expect(found.equals(instance.getClass())).toBe(true); + expect(found.equals(DiscoverableEsClass.class)).toBe(true); + }); + + it("When_implementing_an_interface_with_es_class_syntax_java_should_dispatch_to_the_js_methods", function () { + var runCount = 0; + + class EsRunnable extends java.lang.Runnable { + run() { + runCount++; + } + } + + var runnable = new EsRunnable(); + expect(runnable instanceof java.lang.Runnable).toBe(true); + + // Thread.run() (not started) invokes target.run() synchronously on the current thread + var thread = new java.lang.Thread(runnable); + thread.run(); + + expect(runCount).toBe(1); + }); + + it("When_declaring_static_interfaces_the_proxy_should_implement_them", function () { + var ran = { value: false }; + + class WithInterfaces extends java.lang.Object { + static interfaces = [java.lang.Runnable]; + + run() { + ran.value = true; + } + } + + var instance = new WithInterfaces(); + + expect(instance instanceof java.lang.Runnable).toBe(true); + + var thread = new java.lang.Thread(instance); + thread.run(); + + expect(ran.value).toBe(true); + }); + + it("When_getting_the_class_name_it_should_be_stable_and_descriptive", function () { + class StableNameClass extends java.lang.Object { + } + + var name = StableNameClass.class.getName(); + + // runtime generated proxies are named _es_, mirroring the + // legacy ____ scheme (the com.tns.gen prefix is only + // present on build-time pre-generated bindings) + expect(name).toContain("java.lang.Object_es"); + expect(name).toContain("StableNameClass"); + // repeated access resolves to the very same class + expect(StableNameClass.class.getName()).toBe(name); + }); + + it("When_passing_an_es_class_instance_to_java_and_reading_it_back_it_should_be_the_same_object", function () { + class RoundTripObject extends java.lang.Object { + toString() { + return "round trip"; + } + } + + var instance = new RoundTripObject(); + var list = new java.util.ArrayList(); + list.add(instance); + + var stored = list.get(0); + expect(stored.equals(instance)).toBe(true); + expect(javaToString(stored)).toBe("obj=round trip"); + }); + + it("When_calling_extend_on_an_es_class_it_should_throw_a_descriptive_error", function () { + class NotExtendable extends com.tns.tests.Button1 { + } + + expect(function () { + NotExtendable.extend({ + toString: function () { + return "should not work"; + } + }); + }).toThrow(); + }); + + it("When_extending_a_class_created_with_legacy_extend_the_old_behavior_should_be_preserved", function () { + var LegacyButton = com.tns.tests.Button1.extend({ + getIMAGE_ID_PROP: function () { + return "legacy override"; + } + }); + + class EsOnLegacy extends LegacyButton { + } + + // the legacy proxy is used - ES levels above `.extend()`-created classes get no proxy of their own + var instance = new EsOnLegacy(); + expect(instance instanceof com.tns.tests.Button1).toBe(true); + expect(instance.getIMAGE_ID_PROP()).toBe("legacy override"); + }); + + it("When_a_plain_js_class_hierarchy_is_used_the_runtime_should_not_interfere", function () { + class PlainBase { + value() { + return 1; + } + } + + class PlainDerived extends PlainBase { + value() { + return super.value() + 1; + } + } + + var plain = new PlainDerived(); + expect(plain.value()).toBe(2); + expect(function () { + return plain instanceof java.lang.Object; + }).not.toThrow(); + }); + + it("When_java_instantiates_an_es_class_the_js_constructor_and_fields_should_run", function () { + var constructorRuns = 0; + + class ESAllocCtorObject extends java.lang.Object { + field = 42; + + constructor() { + super(); + constructorRuns++; + this.initializedFromJs = true; + } + } + + // Objects Java allocates — Class.newInstance here, but equally view + // inflation or framework construction — are adopted into a real ES + // construct so class fields and the constructor body run on both paths. + var allocated = ESAllocCtorObject.class.newInstance(); + expect(constructorRuns).toBe(1); + expect(allocated.field).toBe(42); + expect(allocated.initializedFromJs).toBe(true); + expect(allocated instanceof ESAllocCtorObject).toBe(true); + + var constructed = new ESAllocCtorObject(); + expect(constructorRuns).toBe(2); + expect(constructed.field).toBe(42); + expect(constructed.initializedFromJs).toBe(true); + }); + + it("When_java_instantiates_an_es_class_private_fields_should_be_readable", function () { + class ESPrivateAllocObject extends java.lang.Object { + #a = 1; + + constructor() { + super(); + } + + someMethod() { + return this.#a; + } + } + + expect(new ESPrivateAllocObject().someMethod()).toBe(1); + expect(ESPrivateAllocObject.class.newInstance().someMethod()).toBe(1); + }); + + it("When_java_instantiates_an_es_class_super_args_should_not_construct_again", function () { + class ESAdoptOnceObject extends com.tns.tests.DummyClass { + constructor() { + super("from-super"); + } + } + + // Java already called the no-arg DummyClass ctor (nameField = "dummy"). + // Adopt must not run DummyClass(String). + var allocated = ESAdoptOnceObject.class.newInstance(); + expect(allocated.nameField).toBe("dummy"); + expect(allocated instanceof ESAdoptOnceObject).toBe(true); + + var constructed = new ESAdoptOnceObject(); + expect(constructed.nameField).toBe("from-super"); + expect(constructed instanceof ESAdoptOnceObject).toBe(true); + }); + + it("When_an_es_class_constructor_throws_both_paths_should_surface_the_error", function () { + class ESThrowingCtorObject extends java.lang.Object { + constructor() { + super(); + throw new Error("adopt construct failed"); + } + } + + var threw = false; + try { + ESThrowingCtorObject.class.newInstance(); + } catch (e) { + threw = true; + } + expect(threw).toBe(true); + + threw = false; + try { + new ESThrowingCtorObject(); + } catch (e) { + threw = true; + } + expect(threw).toBe(true); + }); + + it("When_the_NativeClass_decorator_is_applied_it_should_apply_android_options", function () { + expect(typeof global.NativeClass).toBe("function"); + + const ESDecoratedPlain = global.NativeClass(class ESDecoratedPlainObject extends com.tns.tests.Button1 { + getIMAGE_ID_PROP() { + return "decorated"; + } + }); + + var button = new ESDecoratedPlain(); + expect(button instanceof ESDecoratedPlain).toBe(true); + expect(button.getIMAGE_ID_PROP()).toBe("decorated"); + + var ran = { value: false }; + const ESDecoratedInterfaces = global.NativeClass({ + android: { + interfaces: [java.lang.Runnable] + } + })( + class ESDecoratedInterfacesObject extends java.lang.Object { + run() { + ran.value = true; + } + } + ); + + var instance = new ESDecoratedInterfaces(); + expect(instance instanceof java.lang.Runnable).toBe(true); + + var thread = new java.lang.Thread(instance); + thread.run(); + expect(ran.value).toBe(true); + }); + + it("When_NativeClass_sets_an_android_name_the_proxy_should_register_immediately", function () { + const ESEagerNamed = global.NativeClass({ + android: { + name: "com.tns.gen.ESEagerNamedObject" + } + })(class UnusedJsNameForEager extends java.lang.Object { + }); + + expect(ESEagerNamed.class.getName()).toBe("com.tns.gen.ESEagerNamedObject"); + expect(java.lang.Class.forName("com.tns.gen.ESEagerNamedObject", false, appClassLoader).equals(ESEagerNamed.class)).toBe(true); + expect(new ESEagerNamed() instanceof ESEagerNamed).toBe(true); + }); + + it("When_NativeClass_runs_on_a_worker_it_should_be_a_noop", function (done) { + var worker = new Worker("../shared/Workers/EvalWorker.js"); + worker.onmessage = function (msg) { + expect(msg.data.isFunction).toBe(true); + expect(msg.data.isWorker).toBe(true); + expect(msg.data.hasName).toBe(false); + expect(msg.data.found).toBe(false); + worker.terminate(); + done(); + }; + worker.onerror = function (error) { + fail("worker failed: " + error.message); + worker.terminate(); + done(); + }; + worker.postMessage({ + eval: "var C = NativeClass({ android: { name: 'com.tns.gen.TNSWorkerNativeClassName' } })(class TNSWorkerNativeClass extends java.lang.Object {}); " + + "var found = false; " + + "try { java.lang.Class.forName('com.tns.gen.TNSWorkerNativeClassName'); found = true; } catch (e) {} " + + "postMessage({ isFunction: typeof NativeClass === 'function', isWorker: !!__ns__worker, hasName: C.nativeClassName === 'com.tns.gen.TNSWorkerNativeClassName', found: found });" + }); + }); + + it("When_anonymous_es_classes_extend_native_types_each_should_get_a_distinct_proxy", function () { + var First = class extends java.lang.Object { + toString() { + return "first anonymous"; + } + }; + var Second = class extends java.lang.Object { + toString() { + return "second anonymous"; + } + }; + + var firstInstance = new First(); + var secondInstance = new Second(); + + expect(javaToString(firstInstance)).toBe("obj=first anonymous"); + expect(javaToString(secondInstance)).toBe("obj=second anonymous"); + expect(firstInstance.getClass().equals(secondInstance.getClass())).toBe(false); + }); +}); diff --git a/test-app/app/src/main/assets/internal/ts_helpers.js b/test-app/app/src/main/assets/internal/ts_helpers.js index d1860dc47..64d6e11ec 100644 --- a/test-app/app/src/main/assets/internal/ts_helpers.js +++ b/test-app/app/src/main/assets/internal/ts_helpers.js @@ -166,6 +166,37 @@ } } + function applyNativeClassOptions(target, options) { + // Workers must not mint or rename process-global native classes. + if (global.__ns__worker) { + return target; + } + // This runtime implements `android`; `ios` is accepted and ignored. + var android = options && options.android; + var interfaces = (android && android.interfaces) || (options && options.interfaces); + var name = android && android.name; + + if (interfaces && interfaces.length > 0) { + target.interfaces = (target.interfaces && target.interfaces instanceof Array ? target.interfaces.concat(interfaces) : interfaces.slice()); + } + if (name) { + target.nativeClassName = name; + // Accessing `.class` lazily registers the proxy under the explicit name. + void target.class; + } + return target; + } + + function NativeClass(arg) { + if (typeof arg === "function") { + return applyNativeClassOptions(arg, {}); + } + var options = arg || {}; + return function (target) { + return applyNativeClassOptions(target, options); + }; + } + Object.defineProperty(global, "__native", { value: __native }); Object.defineProperty(global, "__extends", { value: __extends }); Object.defineProperty(global, "__decorate", { value: __decorate }); @@ -174,4 +205,5 @@ global.JavaProxy = JavaProxy; } global.Interfaces = Interfaces; + global.NativeClass = NativeClass; })() \ No newline at end of file diff --git a/test-app/runtime/src/main/cpp/CallbackHandlers.cpp b/test-app/runtime/src/main/cpp/CallbackHandlers.cpp index 800f9a6fe..5110ed3d5 100644 --- a/test-app/runtime/src/main/cpp/CallbackHandlers.cpp +++ b/test-app/runtime/src/main/cpp/CallbackHandlers.cpp @@ -94,6 +94,18 @@ bool CallbackHandlers::RegisterInstance(Isolate *isolate, const Local &j auto runtime = Runtime::GetRuntime(isolate); auto objectManager = runtime->GetObjectManager(); + int adoptObjectId = -1; + if (MetadataNode::TryConsumePendingESAdopt(isolate, adoptObjectId)) { + // Adopt path: Java already created this object. Bind it to the ES + // construct and do not NewObject again (that would be a second + // instance, or recurse through initInstance). + objectManager->Link(jsObject, adoptObjectId, nullptr); + JEnv env; + jclass instanceClass = env.FindClass(fullClassName); + objectManager->SetJavaClass(jsObject, instanceClass); + return true; + } + // The Java constructor may synchronously call back into JS (extended // class init) - that whole window is a JS-initiated chain. JavaCallScope javaCallScope(runtime); @@ -185,6 +197,50 @@ jclass CallbackHandlers::ResolveClass(Isolate *isolate, const string &baseClassN return globalRefToGeneratedClass; } +jclass CallbackHandlers::ResolveClass(Isolate *isolate, const string &baseClassName, + const string &fullClassName, + const vector &methodOverrides, + const vector &implementedInterfaces, + bool isInterface) { + JEnv env; + jclass globalRefToGeneratedClass = env.CheckForClassInCache(fullClassName); + + if (globalRefToGeneratedClass == nullptr) { + JniLocalRef javaBaseClassName(env.NewStringUTF(baseClassName.c_str())); + JniLocalRef javaFullClassName(env.NewStringUTF(fullClassName.c_str())); + + jobjectArray methodOverridesArr = GetJavaStringArray(env, methodOverrides.size()); + for (size_t i = 0; i < methodOverrides.size(); i++) { + JniLocalRef name(env.NewStringUTF(methodOverrides[i].c_str())); + env.SetObjectArrayElement(methodOverridesArr, i, name); + } + + jobjectArray implementedInterfacesArr = GetJavaStringArray(env, implementedInterfaces.size()); + for (size_t i = 0; i < implementedInterfaces.size(); i++) { + JniLocalRef name(env.NewStringUTF(implementedInterfaces[i].c_str())); + env.SetObjectArrayElement(implementedInterfacesArr, i, name); + } + + auto runtime = Runtime::GetRuntime(isolate); + + // create or load generated binding (java class) + jclass generatedClass = (jclass) env.CallObjectMethod(runtime->GetJavaRuntime(), + RESOLVE_CLASS_METHOD_ID, + (jstring) javaBaseClassName, + (jstring) javaFullClassName, + methodOverridesArr, + implementedInterfacesArr, + isInterface); + + globalRefToGeneratedClass = env.InsertClassIntoCache(fullClassName, generatedClass); + + env.DeleteGlobalRef(methodOverridesArr); + env.DeleteGlobalRef(implementedInterfacesArr); + } + + return globalRefToGeneratedClass; +} + // Called by ExtendMethodCallback when extending a class string CallbackHandlers::ResolveClassName(Isolate *isolate, jclass &clazz) { auto runtime = Runtime::GetRuntime(isolate); diff --git a/test-app/runtime/src/main/cpp/CallbackHandlers.h b/test-app/runtime/src/main/cpp/CallbackHandlers.h index 86dba64a5..5bd7fef0a 100644 --- a/test-app/runtime/src/main/cpp/CallbackHandlers.h +++ b/test-app/runtime/src/main/cpp/CallbackHandlers.h @@ -42,6 +42,18 @@ namespace tns { const v8::Local &implementationObject, bool isInterface); + /* + * ResolveClass variant with explicitly collected method override names and implemented + * interface names. Used for plain ES class extensions where the overrides span multiple + * prototype levels and are non-enumerable (so the implementationObject-based scan above + * cannot see them). + */ + static jclass ResolveClass(v8::Isolate *isolate, const std::string &baseClassName, + const std::string &fullClassName, + const std::vector &methodOverrides, + const std::vector &implementedInterfaces, + bool isInterface); + static std::string ResolveClassName(v8::Isolate *isolate, jclass &clazz); static v8::Local diff --git a/test-app/runtime/src/main/cpp/JsArgConverter.cpp b/test-app/runtime/src/main/cpp/JsArgConverter.cpp index e5d82f0bd..995f54761 100644 --- a/test-app/runtime/src/main/cpp/JsArgConverter.cpp +++ b/test-app/runtime/src/main/cpp/JsArgConverter.cpp @@ -1,5 +1,6 @@ #include "JsArgConverter.h" #include "ObjectManager.h" +#include "MetadataNode.h" #include "JniSignatureParser.h" #include "JsArgToArrayConverter.h" #include "ArgConverter.h" @@ -151,6 +152,22 @@ bool JsArgConverter::ConvertArg(const Local &arg, int index) { if (!success) { sprintf(buff, "Cannot convert string to %s at index %d", typeSignature.c_str(), index); } + } else if (arg->IsFunction() && + (typeSignature == "Ljava/lang/Class;" || typeSignature == "Ljava/lang/Object;") && + !MetadataNode::TryResolveClassCtorTypeName(m_isolate, arg.As()).empty()) { + // a native type ctor (or a plain ES class extending one - registered lazily here) + // passed where Java expects a java.lang.Class. Typed nulls (`SomeClass.null`) and other + // functions resolve to an empty name and keep flowing through the object branch below. + auto typeName = MetadataNode::TryResolveClassCtorTypeName(m_isolate, arg.As()); + JEnv env; + jclass clazz = env.FindClass(typeName); + success = clazz != nullptr; + if (success) { + // JEnv caches classes as global refs - mark as global so the dtor doesn't delete it + SetConvertedObject(index, clazz, true /* isGlobal */); + } else { + sprintf(buff, "Cannot convert function to %s at index %d", typeSignature.c_str(), index); + } } else if (arg->IsObject()) { auto context = m_isolate->GetCurrentContext(); auto jsObject = arg->ToObject(context).ToLocalChecked(); diff --git a/test-app/runtime/src/main/cpp/JsArgToArrayConverter.cpp b/test-app/runtime/src/main/cpp/JsArgToArrayConverter.cpp index 63216a06f..adeb97923 100644 --- a/test-app/runtime/src/main/cpp/JsArgToArrayConverter.cpp +++ b/test-app/runtime/src/main/cpp/JsArgToArrayConverter.cpp @@ -134,6 +134,20 @@ bool JsArgToArrayConverter::ConvertArg(Local context, const LocalIsFunction() && !MetadataNode::TryResolveClassCtorTypeName(isolate, arg.As()).empty()) { + // a native type ctor (or a plain ES class extending one - registered lazily here) + // marshals to its java.lang.Class. Typed nulls (`SomeClass.null`) and other functions + // resolve to an empty name and keep flowing through the object branch below. + auto typeName = MetadataNode::TryResolveClassCtorTypeName(isolate, arg.As()); + jclass clazz = env.FindClass(typeName); + if (clazz != nullptr) { + // JEnv caches classes as global refs - mark as global so the dtor doesn't delete it + SetConvertedObject(env, index, clazz, true /* isGlobal */); + success = true; + } else { + s << "Cannot marshal JavaScript function at index " << index + << " to Java type. Only native type constructors can be marshalled (to java.lang.Class)."; + } } else if (arg->IsObject()) { auto jsObj = arg->ToObject(context).ToLocalChecked(); diff --git a/test-app/runtime/src/main/cpp/MetadataNode.cpp b/test-app/runtime/src/main/cpp/MetadataNode.cpp index 449fc7722..ab828c373 100644 --- a/test-app/runtime/src/main/cpp/MetadataNode.cpp +++ b/test-app/runtime/src/main/cpp/MetadataNode.cpp @@ -11,6 +11,7 @@ #include "Runtime.h" #include #include +#include #include #include #include @@ -197,7 +198,20 @@ bool MetadataNode::IsNodeTypeInterface() { } string MetadataNode::GetTypeMetadataName(Isolate* isolate, Local& value) { - auto data = GetTypeMetadata(isolate, value.As()); + if (value.IsEmpty() || !value->IsFunction()) { + throw NativeScriptException(string("Cannot resolve native type - the value is not a constructor function.")); + } + + auto func = value.As(); + auto data = TryGetTypeMetadata(isolate, func); + if (data == nullptr) { + // may be a not-yet-registered plain ES class extension + data = EnsureExtendedESClass(isolate, func); + } + + if (data == nullptr) { + throw NativeScriptException(string("Cannot resolve native type - the function does not stand for a native type or an extension of one.")); + } return data->name; } @@ -324,7 +338,22 @@ void MetadataNode::ClassAccessorGetterCallback(const FunctionCallbackInfo try { auto thiz = info.This(); auto isolate = info.GetIsolate(); - auto data = GetTypeMetadata(isolate, thiz.As()); + + if (thiz.IsEmpty() || !thiz->IsFunction()) { + throw NativeScriptException(string("The 'class' property may only be accessed on a native type or an extended class constructor function.")); + } + + auto func = thiz.As(); + auto data = TryGetTypeMetadata(isolate, func); + if (data == nullptr) { + // Plain ES class extension accessed statically before any instance was constructed + // (e.g. `MyView.class`) - lazily register it now + data = EnsureExtendedESClass(isolate, func); + } + + if (data == nullptr) { + throw NativeScriptException(string("Cannot resolve java.lang.Class - the function does not stand for a native type or an extension of one.")); + } auto value = CallbackHandlers::FindClass(isolate, data->name); info.GetReturnValue().Set(value); @@ -1138,6 +1167,364 @@ void MetadataNode::SetTypeMetadata(Isolate* isolate, Local value, Type V8SetPrivateValue(isolate, value, String::NewFromUtf8(isolate, "typemetadata").ToLocalChecked(), External::New(isolate, data, v8::kExternalPointerTypeTagDefault)); } +MetadataNode::TypeMetadata* MetadataNode::TryGetTypeMetadata(Isolate* isolate, const Local& value) { + Local hiddenVal; + V8GetPrivateValue(isolate, value, String::NewFromUtf8(isolate, "typemetadata").ToLocalChecked(), hiddenVal); + + if (hiddenVal.IsEmpty() || !hiddenVal->IsExternal()) { + return nullptr; + } + + return reinterpret_cast(hiddenVal.As()->Value()); +} + +std::string MetadataNode::TryResolveClassCtorTypeName(Isolate* isolate, const Local& func) { + // A ctor function that had its `.null` accessed doubles as the typed null value for its + // type (see NullObjectAccessorGetterCallback - the marker private is set on the ctor and + // the ctor itself is returned). Such functions must marshal as typed nulls, not as + // java.lang.Class references. + Local nullNodeMarker; + V8GetPrivateValue(isolate, func, V8StringConstants::GetNullNodeName(isolate), nullNodeMarker); + if (!nullNodeMarker.IsEmpty()) { + return std::string(); + } + + auto typeMetadata = TryGetTypeMetadata(isolate, func); + + if (typeMetadata == nullptr) { + typeMetadata = EnsureExtendedESClass(isolate, func); + } + + return typeMetadata != nullptr ? typeMetadata->name : std::string(); +} + +namespace { +// short deterministic identifier so the same ES class gets the same proxy class name across +// application launches (keeps the DexFactory on-disk dex cache warm) without depending on +// line/column numbers that shift with unrelated code edits +std::string HashESClassId(const std::string& input) { + uint32_t hash = 2166136261u; + for (char c : input) { + hash ^= static_cast(c); + hash *= 16777619u; + } + + char buff[9]; + snprintf(buff, sizeof(buff), "%08x", hash); + return std::string(buff); +} + +std::string SanitizeESClassNamePart(const std::string& name) { + std::string result; + result.reserve(name.size()); + for (char c : name) { + bool isValid = isalpha(c) || isdigit(c) || c == '_'; + result += isValid ? c : '_'; + } + return result; +} + +// True only for genuine `class` syntax constructors. Function source text is the reliable +// discriminator: per spec, Function.prototype.toString for a class constructor reproduces the +// `class` declaration/expression source (possibly behind leading comments/whitespace, which V8 +// does not emit for the class case - the text starts with "class"). +bool IsESClassConstructor(v8::Isolate* isolate, const v8::Local& func) { + auto context = isolate->GetCurrentContext(); + v8::Local sourceText; + if (!func->FunctionProtoToString(context).ToLocal(&sourceText)) { + return false; + } + + auto source = tns::ArgConverter::ConvertToString(sourceText); + return source.compare(0, 5, "class") == 0; +} +} // namespace + +MetadataNode::TypeMetadata* MetadataNode::EnsureExtendedESClass(Isolate* isolate, Local ctorFunc) { + // Already registered - a native class ctor, a legacy `.extend()`-created ctor or an + // ES class ctor registered by a previous call + auto existingMetadata = TryGetTypeMetadata(isolate, ctorFunc); + if (existingMetadata != nullptr) { + return existingMetadata; + } + + // Only the main isolate mints ES-derived Java proxies. Workers keep + // NativeClass / lazy registration as a no-op so they cannot claim + // process-global names. Legacy `.extend()` is unchanged. + if (Runtime::GetRuntime(isolate)->IsWorker()) { + return nullptr; + } + + auto context = isolate->GetCurrentContext(); + + // Only genuine `class` syntax constructors participate in lazy ES registration. Downleveled + // ES5 "classes" (TypeScript ES5 output, the JavaProxy decorator target, ts_helpers __extends + // children) have the same shape - an untagged function whose prototype chain reaches a native + // ctor - but must keep flowing through the legacy `.extend()` pipeline. + if (!IsESClassConstructor(isolate, ctorFunc)) { + return nullptr; + } + + // Walk the constructor prototype chain (mirrors the `class X extends Y` chain) and collect + // every plain (unregistered) ES constructor level until we reach a constructor holding type + // metadata. ES-registered ancestors are flattened into this registration; legacy + // `.extend()`-created ancestors are not supported and make this function bail out so callers + // preserve their old behavior. + std::vector> chainCtors; + Local current = ctorFunc; + TypeMetadata* baseTypeMetadata = nullptr; + while (true) { + chainCtors.push_back(current); + + Local parentValue = current->GetPrototype(); + if (parentValue.IsEmpty() || !parentValue->IsObject() || !parentValue->IsFunction()) { + // no native type in the chain - a plain JS class, leave it alone + return nullptr; + } + + auto parent = parentValue.As(); + auto parentMetadata = TryGetTypeMetadata(isolate, parent); + if (parentMetadata == nullptr) { + current = parent; + continue; + } + + if (parentMetadata->isESDerived) { + // Flatten: the parent's registered proxy class sits directly under the pure native + // base, so keep walking (collecting the parent's prototype for scanning) until we + // reach it + current = parent; + continue; + } + + auto cachedData = GetCachedExtendedClassData(isolate, parentMetadata->name); + if (cachedData.extendedCtorFunction != nullptr) { + // legacy `.extend()`-created ancestor - not supported for ES class chaining + return nullptr; + } + + baseTypeMetadata = parentMetadata; + break; + } + + string baseClassName = baseTypeMetadata->name; + auto node = GetOrCreate(baseClassName); + if (node == nullptr) { + return nullptr; + } + + uint8_t nodeType = s_metadataReader.GetNodeType(node->m_treeNode); + bool isInterface = s_metadataReader.IsNodeTypeInterface(nodeType); + + // Collect overridden method names level by level, most-derived first, so JS shadowing + // semantics carry over. ES class methods are non-enumerable, so unlike the legacy + // implementation-object scan we must use ALL_PROPERTIES. + std::vector methodOverrides; + std::vector implementedInterfaces; + robin_hood::unordered_set visitedNames; + std::string nativeClassName; + + auto prototypeKey = V8StringConstants::GetPrototype(isolate); + auto interfacesKey = ArgConverter::ConvertToV8String(isolate, "interfaces"); + auto nativeClassNameKey = ArgConverter::ConvertToV8String(isolate, "nativeClassName"); + auto propertyFilter = static_cast(PropertyFilter::ALL_PROPERTIES | PropertyFilter::SKIP_SYMBOLS); + + for (auto& levelCtor : chainCtors) { + Local protoValue; + if (!levelCtor->Get(context, prototypeKey).ToLocal(&protoValue) || protoValue.IsEmpty() || !protoValue->IsObject()) { + continue; + } + auto levelPrototype = protoValue.As(); + + Local propNames; + if (levelPrototype->GetOwnPropertyNames(context, propertyFilter).ToLocal(&propNames)) { + for (uint32_t i = 0; i < propNames->Length(); i++) { + Local nameValue; + if (!propNames->Get(context, i).ToLocal(&nameValue) || !nameValue->IsString()) { + continue; + } + + string name = ArgConverter::ConvertToString(nameValue.As()); + if (name == "constructor" || name == "super") { + continue; + } + + // skip names already handled by a more derived level (JS shadowing semantics) + if (!visitedNames.insert(name).second) { + continue; + } + + // inspect the descriptor instead of reading the property, so user-defined + // accessors are not invoked during registration + Local descriptor; + if (!levelPrototype->GetOwnPropertyDescriptor(context, nameValue.As()).ToLocal(&descriptor) + || descriptor.IsEmpty() || !descriptor->IsObject()) { + continue; + } + + Local methodValue; + if (descriptor.As()->Get(context, V8StringConstants::GetValue(isolate)).ToLocal(&methodValue) + && !methodValue.IsEmpty() && methodValue->IsFunction()) { + methodOverrides.push_back(name); + } + } + } + + // `static interfaces = [...]` - additional interfaces the proxy should implement + bool hasOwnInterfaces; + if (levelCtor->HasOwnProperty(context, interfacesKey).To(&hasOwnInterfaces) && hasOwnInterfaces) { + Local interfacesValue; + if (levelCtor->Get(context, interfacesKey).ToLocal(&interfacesValue) && interfacesValue->IsArray()) { + auto interfacesArr = interfacesValue.As(); + for (uint32_t i = 0; i < interfacesArr->Length(); i++) { + Local element; + if (!interfacesArr->Get(context, i).ToLocal(&element) || !element->IsFunction()) { + continue; + } + + auto interfaceName = GetTypeMetadataName(isolate, element); + interfaceName = Util::ReplaceAll(interfaceName, std::string("/"), std::string(".")); + if (std::find(implementedInterfaces.begin(), implementedInterfaces.end(), interfaceName) == implementedInterfaces.end()) { + implementedInterfaces.push_back(interfaceName); + } + } + } + } + + // `static nativeClassName = 'com.my.Thing'` - explicit proxy class name (most-derived wins) + if (nativeClassName.empty()) { + bool hasOwnNativeClassName; + if (levelCtor->HasOwnProperty(context, nativeClassNameKey).To(&hasOwnNativeClassName) && hasOwnNativeClassName) { + Local nativeClassNameValue; + if (levelCtor->Get(context, nativeClassNameKey).ToLocal(&nativeClassNameValue) && nativeClassNameValue->IsString()) { + nativeClassName = ArgConverter::ConvertToString(nativeClassNameValue.As()); + } + } + } + } + + // Compute the proxy class name + string fullClassName; + if (!nativeClassName.empty() && nativeClassName.find('.') != string::npos) { + fullClassName = nativeClassName; + } else if (isInterface) { + // interface extensions reuse the shared interface proxy, exactly like + // `new SomeInterface({...})` - all methods dispatch back to JS through the instance + fullClassName = node->m_implType; + } else { + string className; + auto jsClassName = ctorFunc->GetName(); + if (!jsClassName.IsEmpty() && jsClassName->IsString()) { + className = SanitizeESClassNamePart(ArgConverter::ConvertToString(jsClassName.As())); + } + if (className.empty()) { + className = "ESClass"; + } + + string scriptName; + auto scriptOrigin = ctorFunc->GetScriptOrigin(); + auto resourceName = scriptOrigin.ResourceName(); + if (!resourceName.IsEmpty() && resourceName->IsString()) { + scriptName = ArgConverter::ConvertToString(resourceName.As()); + } + + string extendNameAndLocation = "es" + HashESClassId(scriptName + "|" + baseClassName + "|" + className) + "_" + className; + string candidate = TNS_PREFIX + CreateFullClassName(baseClassName, extendNameAndLocation); + + // collision handling for distinct classes that produce the same deterministic name + // (e.g. a class factory evaluated multiple times in the same script) + fullClassName = candidate; + int suffix = 2; + while (GetCachedExtendedClassData(isolate, fullClassName).extendedCtorFunction != nullptr) { + fullClassName = candidate + "_" + std::to_string(suffix++); + } + } + + // Resolve (generate or load) the Java proxy class through the regular DexFactory pipeline + auto clazz = CallbackHandlers::ResolveClass(isolate, baseClassName, fullClassName, methodOverrides, implementedInterfaces, isInterface); + auto fullExtendedName = CallbackHandlers::ResolveClassName(isolate, clazz); + + // Tag the ES ctor the same way ExtendMethodCallback tags `.extend()`-created functions, so + // the rest of the runtime (construction, Java-initiated instantiation, `.class`, marshalling) + // treats it like any other extended class ctor + auto typeMetadata = new TypeMetadata(fullExtendedName, true /* isESDerived */); + SetTypeMetadata(isolate, ctorFunc, typeMetadata); + + // The ES class prototype acts as the implementation object. Mark it the way + // ExtendMethodCallback marks implementation objects, so GetImplementationObject (used + // during Java-initiated instantiation) can find it on the instance prototype chain. + Local ctorPrototypeValue; + if (ctorFunc->Get(context, prototypeKey).ToLocal(&ctorPrototypeValue) && ctorPrototypeValue->IsObject()) { + auto ctorPrototype = ctorPrototypeValue.As(); + auto implementationObjectPropertyName = V8StringConstants::GetClassImplementationObject(isolate); + Local hiddenVal; + V8GetPrivateValue(isolate, ctorPrototype, implementationObjectPropertyName, hiddenVal); + if (hiddenVal.IsEmpty()) { + V8SetPrivateValue(isolate, ctorPrototype, implementationObjectPropertyName, String::NewFromUtf8(isolate, fullExtendedName.c_str()).ToLocalChecked()); + } + } + + s_name2NodeCache.emplace(fullExtendedName, node); + + auto cache = GetMetadataNodeCache(isolate); + auto itCached = cache->ExtendedCtorFuncCache.find(fullExtendedName); + if (itCached == cache->ExtendedCtorFuncCache.end()) { + ExtendedClassCacheData cacheData(ctorFunc, fullExtendedName, node); + cache->ExtendedCtorFuncCache.emplace(fullExtendedName, cacheData); + } + + DEBUG_WRITE("EnsureExtendedESClass: registered %s (base %s)", fullExtendedName.c_str(), baseClassName.c_str()); + + return typeMetadata; +} + +bool MetadataNode::TryConstructESDerivedInstance(Isolate* isolate, const string& proxyClassName, int javaObjectID, Local& out) { + auto cache = GetMetadataNodeCache(isolate); + if (cache->PendingESAdoptObjectId != -1) { + return false; + } + + auto cacheData = GetCachedExtendedClassData(isolate, proxyClassName); + if (cacheData.extendedCtorFunction == nullptr) { + return false; + } + + Local ctor = Local::New(isolate, *cacheData.extendedCtorFunction); + auto typeMetadata = TryGetTypeMetadata(isolate, ctor); + if (typeMetadata == nullptr || !typeMetadata->isESDerived) { + return false; + } + + cache->PendingESAdoptObjectId = javaObjectID; + TryCatch tc(isolate); + auto context = isolate->GetCurrentContext(); + bool ok = !ctor->CallAsConstructor(context, 0, nullptr).IsEmpty(); + cache->PendingESAdoptObjectId = -1; + if (!ok) { + throw NativeScriptException(tc, "Failed to construct ES class for native instance"); + } + + auto objectManager = Runtime::GetRuntime(isolate)->GetObjectManager(); + auto cached = objectManager->GetJsObjectByJavaObject(javaObjectID); + if (cached.IsEmpty()) { + return false; + } + + out = cached; + return true; +} + +bool MetadataNode::TryConsumePendingESAdopt(Isolate* isolate, int& javaObjectID) { + auto cache = GetMetadataNodeCache(isolate); + if (cache->PendingESAdoptObjectId == -1) { + return false; + } + + javaObjectID = cache->PendingESAdoptObjectId; + cache->PendingESAdoptObjectId = -1; + return true; +} + MetadataNode* MetadataNode::GetInstanceMetadata(Isolate* isolate, const Local& value) { MetadataNode* node = nullptr; auto cache = GetMetadataNodeCache(isolate); @@ -1216,6 +1603,30 @@ void MetadataNode::InterfaceConstructorCallback(const v8::FunctionCallbackInfo v8ExtendName; auto context = isolate->GetCurrentContext(); + // Plain ES class implementing an interface: `class Handler extends java.lang.Runnable {}` + // invokes this callback through `super()` with new.target set to the ES constructor and + // no implementation object argument. The methods live on the ES class prototype. + auto newTargetValue = info.NewTarget(); + if (!newTargetValue.IsEmpty() && newTargetValue->IsFunction()) { + auto newTargetFunc = newTargetValue.As(); + auto typeMetadata = TryGetTypeMetadata(isolate, newTargetFunc); + if (typeMetadata == nullptr) { + typeMetadata = EnsureExtendedESClass(isolate, newTargetFunc); + } + + if (typeMetadata != nullptr && typeMetadata->isESDerived) { + auto esImplementationObject = newTargetFunc->Get(context, V8StringConstants::GetPrototype(isolate)).ToLocalChecked().As(); + + SetInstanceMetadata(isolate, thiz, node); + thiz->SetInternalField(static_cast(ObjectManager::MetadataNodeKeys::CallSuper), True(isolate)); + V8SetPrivateValue(isolate, thiz, V8StringConstants::GetImplementationObject(isolate), esImplementationObject); + + ArgsWrapper esArgWrapper(info, ArgType::Interface); + CallbackHandlers::RegisterInstance(isolate, thiz, typeMetadata->name, esArgWrapper, esImplementationObject, true); + return; + } + } + if (info.Length() == 1) { if (!info[0]->IsObject()) { throw NativeScriptException(string("First argument must be implementation object")); @@ -1278,6 +1689,32 @@ void MetadataNode::ClassConstructorCallback(const v8::FunctionCallbackInfom_name; + // Plain ES class extension: `class MyView extends android.view.View {}` invokes this + // callback through `super(...)` with new.target set to the most derived ES constructor. + // Lazily register the ES class as an extended class and construct its proxy instead of + // the base class. + auto newTargetValue = info.NewTarget(); + if (!newTargetValue.IsEmpty() && newTargetValue->IsFunction()) { + auto newTargetFunc = newTargetValue.As(); + auto typeMetadata = TryGetTypeMetadata(isolate, newTargetFunc); + if (typeMetadata == nullptr) { + typeMetadata = EnsureExtendedESClass(isolate, newTargetFunc); + } + + if (typeMetadata != nullptr && typeMetadata->isESDerived) { + auto context = isolate->GetCurrentContext(); + auto implementationObject = newTargetFunc->Get(context, V8StringConstants::GetPrototype(isolate)).ToLocalChecked().As(); + + SetInstanceMetadata(isolate, thiz, node); + thiz->SetInternalField(static_cast(ObjectManager::MetadataNodeKeys::CallSuper), True(isolate)); + V8SetPrivateValue(isolate, thiz, V8StringConstants::GetImplementationObject(isolate), implementationObject); + + ArgsWrapper esArgWrapper(info, ArgType::Class); + CallbackHandlers::RegisterInstance(isolate, thiz, typeMetadata->name, esArgWrapper, implementationObject, false, className); + return; + } + } + SetInstanceMetadata(isolate, thiz, node); ArgsWrapper argWrapper(info, ArgType::Class); @@ -1665,6 +2102,24 @@ void MetadataNode::ExtendMethodCallback(const v8::FunctionCallbackInfoIsFunction()) { + auto thisFunc = thisValue.As(); + auto thisMetadata = TryGetTypeMetadata(info.GetIsolate(), thisFunc); + bool isESClassReceiver = (thisMetadata != nullptr && thisMetadata->isESDerived) || + (thisMetadata == nullptr && IsESClassConstructor(info.GetIsolate(), thisFunc)); + if (isESClassReceiver) { + throw NativeScriptException(string("Cannot call 'extend' on a class extension created with ES class syntax. Extend it with `class X extends Y {}` instead.")); + } + } + } + Local implementationObject; Local extendName; string extendLocation; diff --git a/test-app/runtime/src/main/cpp/MetadataNode.h b/test-app/runtime/src/main/cpp/MetadataNode.h index 2b36fa92e..2e0152347 100644 --- a/test-app/runtime/src/main/cpp/MetadataNode.h +++ b/test-app/runtime/src/main/cpp/MetadataNode.h @@ -51,6 +51,16 @@ class MetadataNode { static v8::Local CreateExtendedJSWrapper(v8::Isolate* isolate, ObjectManager* objectManager, const std::string& proxyClassName); + /* + * Java-born instances of an ES-derived proxy (clazz.newInstance(), + * framework inflation, etc.) are adopted into a real construct of the + * ES class so fields and the constructor body run. super() binds the + * existing Java object and does not allocate again. + */ + static bool TryConstructESDerivedInstance(v8::Isolate* isolate, const std::string& proxyClassName, int javaObjectID, v8::Local& out); + + static bool TryConsumePendingESAdopt(v8::Isolate* isolate, int& javaObjectID); + static v8::Local GetImplementationObject(v8::Isolate* isolate, const v8::Local& object); static void CreateTopLevelNamespaces(v8::Isolate* isolate, const v8::Local& global); @@ -70,6 +80,15 @@ class MetadataNode { static bool TryGetPackageName(v8::Isolate* isolate, const v8::Local& value, std::string& out); + /* + * Resolves the Java class name a constructor function stands for, lazily registering a + * Java proxy class when the function is a plain ES `class X extends NativeType {}` that + * has not been registered yet. Returns an empty string when the function is not part of + * a native inheritance chain. Used when marshalling a constructor function to a Java + * `java.lang.Class` (or `java.lang.Object`) argument. + */ + static std::string TryResolveClassCtorTypeName(v8::Isolate* isolate, const v8::Local& func); + static void onDisposeIsolate(v8::Isolate* isolate); static MetadataReader* getMetadataReader(); @@ -140,8 +159,24 @@ class MetadataNode { static TypeMetadata* GetTypeMetadata(v8::Isolate* isolate, const v8::Local& value); + // Safe variant of GetTypeMetadata - returns nullptr when the function carries no type + // metadata (e.g. a plain ES class constructor) instead of crashing + static TypeMetadata* TryGetTypeMetadata(v8::Isolate* isolate, const v8::Local& value); + static void SetTypeMetadata(v8::Isolate* isolate, v8::Local value, TypeMetadata* data); + /* + * Lazily registers a Java proxy class for a plain ES `class X extends NativeType {}` + * constructor function (no `.extend()` call, no downleveling). Walks the constructor + * prototype chain to the native base, collects overridden method names from every ES + * level's prototype and implemented interfaces from `static interfaces = [...]`, resolves + * the proxy class through the regular DexFactory pipeline and tags the constructor the + * same way `.extend()` tags its result (typemetadata + ExtendedCtorFuncCache entry). + * Returns nullptr when ctorFunc is not part of a native inheritance chain or the chain + * goes through a legacy `.extend()`-created class. Idempotent. + */ + static TypeMetadata* EnsureExtendedESClass(v8::Isolate* isolate, v8::Local ctorFunc); + static std::string CreateFullClassName(const std::string& className, const std::string& extendNameAndLocation); static void MethodCallback(const v8::FunctionCallbackInfo& info); static void InterfaceConstructorCallback(const v8::FunctionCallbackInfo& info); @@ -242,12 +277,16 @@ class MetadataNode { }; struct TypeMetadata { - TypeMetadata(const std::string& _name) + TypeMetadata(const std::string& _name, bool _isESDerived = false) : - name(_name) { + name(_name), isESDerived(_isESDerived) { } std::string name; + + // true when the class was registered lazily from a plain ES + // `class X extends NativeType {}` constructor (see EnsureExtendedESClass) + bool isESDerived; }; struct CtorCacheData { @@ -283,6 +322,12 @@ class MetadataNode { robin_hood::unordered_map CtorFuncCache; robin_hood::unordered_map ExtendedCtorFuncCache; + + // Java object id being adopted by an in-flight ES construct + // (CreateJSInstanceNative → CallAsConstructor → super()). + // RegisterInstance consumes it so super() binds that id and does + // not NewObject again. + int PendingESAdoptObjectId = -1; }; }; } diff --git a/test-app/runtime/src/main/cpp/Runtime.cpp b/test-app/runtime/src/main/cpp/Runtime.cpp index c218dcdf0..6acddadba 100644 --- a/test-app/runtime/src/main/cpp/Runtime.cpp +++ b/test-app/runtime/src/main/cpp/Runtime.cpp @@ -454,6 +454,12 @@ void Runtime::CreateJSInstanceNative(JNIEnv* _env, jobject obj, auto proxyClassName = m_objectManager->GetClassName(javaObject); DEBUG_WRITE("createJSInstanceNative class %s", proxyClassName.c_str()); + + if (MetadataNode::TryConstructESDerivedInstance(isolate, proxyClassName, + javaObjectID, jsInstance)) { + return; + } + jsInstance = MetadataNode::CreateExtendedJSWrapper(isolate, m_objectManager, proxyClassName); diff --git a/test-app/runtime/src/main/cpp/Runtime.h b/test-app/runtime/src/main/cpp/Runtime.h index 8cecb1c26..3c3209b1d 100644 --- a/test-app/runtime/src/main/cpp/Runtime.h +++ b/test-app/runtime/src/main/cpp/Runtime.h @@ -122,6 +122,10 @@ class Runtime { int GetId(); + bool IsWorker() const { + return !m_isMainThread; + } + v8::Local GetContext(); static v8::Platform* platform;