diff --git a/NativeScript/runtime/ClassBuilder.mm b/NativeScript/runtime/ClassBuilder.mm index 3baac770..2d9f7002 100644 --- a/NativeScript/runtime/ClassBuilder.mm +++ b/NativeScript/runtime/ClassBuilder.mm @@ -1002,16 +1002,35 @@ void ScopeClassNameToIsolate(std::string& name, int isolateId) { void ClassBuilder::SuperAccessorGetterCallback(const FunctionCallbackInfo& info) { Isolate* isolate = info.GetIsolate(); Local context = isolate->GetCurrentContext(); - Local thiz = info.This(); + // Copying the implementation object or the class prototype (Object.assign, + // spread) reads `super` with a receiver that is no native instance: like the + // other native accessors, it reads undefined. + Local target = tns::UnwrapProxy(info.This()); + if (target.IsEmpty() || !target->IsObject() || target.As()->InternalFieldCount() < 1) { + return; + } + // Field 0 holds the object that carries the wrapper, never another alias, + // so resolving an alias takes one step. + Local receiver = tns::SuperAliasReceiver(target.As()); + if (receiver.IsEmpty()) { + receiver = target.As(); + } + + // receiver -> extended class prototype -> implementation object -> base + // class prototype. + Local superPrototype = receiver; + for (int i = 0; i < 3; i++) { + if (!superPrototype->IsObject()) { + return; + } + superPrototype = superPrototype.As()->GetPrototype(); + } std::shared_ptr> poValue = ArgConverter::CreateEmptyObject(context); Local superValue = poValue->Get(isolate).As(); - superValue - ->SetPrototype(context, - thiz->GetPrototype().As()->GetPrototype().As()->GetPrototype()) - .ToChecked(); - superValue->SetInternalField(0, thiz->GetInternalField(0)); + superValue->SetPrototype(context, superPrototype).ToChecked(); + superValue->SetInternalField(0, receiver); superValue->SetInternalField(1, tns::ToV8String(isolate, "super")); info.GetReturnValue().Set(superValue); diff --git a/NativeScript/runtime/Helpers.h b/NativeScript/runtime/Helpers.h index d4fd42b8..75a9df55 100644 --- a/NativeScript/runtime/Helpers.h +++ b/NativeScript/runtime/Helpers.h @@ -290,9 +290,19 @@ v8::Local GetCreationContextOrCurrent(v8::Isolate* isolate, // Set/Get/DeleteValue all resolve through proxies: wrapper state is keyed by // the Proxy target, so a proxied wrapper yields its target's wrapper. +// GetValue alone also follows a `this.super` alias to its receiver +// (SuperAliasReceiver), so code that writes back to an object it read with +// GetValue must first resolve an alias, or it overwrites the alias's receiver +// field instead of the wrapper it read. void SetValue(v8::Isolate* isolate, const v8::Local& obj, BaseDataWrapper* value); BaseDataWrapper* GetValue(v8::Isolate* isolate, const v8::Local& val); +// The receiver a `this.super` alias (ClassBuilder::SuperAccessorGetterCallback) +// stands for, or empty when obj is not one. An alias keeps its receiver in +// internal field 0 and the string "super" in internal field 1; it owns no +// wrapper, so GetValue reads the receiver's at every use. +v8::Local SuperAliasReceiver(const v8::Local& obj); + // What happens when JS touches a wrapper whose native counterpart has already // been released (its internal field was neutered by DisposeValue) — a state a // resurrected object can expose by outliving a referenced object's native diff --git a/NativeScript/runtime/Helpers.mm b/NativeScript/runtime/Helpers.mm index 41d5361b..8d9e8505 100644 --- a/NativeScript/runtime/Helpers.mm +++ b/NativeScript/runtime/Helpers.mm @@ -305,6 +305,21 @@ throw NativeScriptException( } } +Local tns::SuperAliasReceiver(const Local& obj) { + if (obj->InternalFieldCount() < 2) { + return Local(); + } + Local marker = obj->GetInternalField(1).As(); + if (marker.IsEmpty() || !marker->IsString()) { + return Local(); + } + Local receiver = obj->GetInternalField(0).As(); + if (receiver.IsEmpty() || !receiver->IsObject() || receiver->IsExternal()) { + return Local(); + } + return receiver.As(); +} + tns::BaseDataWrapper* tns::GetValue(Isolate* isolate, const Local& val) { if (val.IsEmpty() || val->IsNullOrUndefined() || !val->IsObject()) { return nullptr; @@ -320,9 +335,13 @@ throw NativeScriptException( } if (obj->InternalFieldCount() > 0) { Local field = obj->GetInternalField(0).As(); - if (field.IsEmpty() || field->IsNullOrUndefined() || !field->IsExternal()) { + if (field.IsEmpty() || field->IsNullOrUndefined()) { return nullptr; } + if (!field->IsExternal()) { + Local receiver = SuperAliasReceiver(obj); + return receiver.IsEmpty() ? nullptr : GetValue(isolate, receiver); + } return static_cast( field.As()->Value(v8::kExternalPointerTypeTagDefault)); diff --git a/NativeScript/runtime/ObjectManager.mm b/NativeScript/runtime/ObjectManager.mm index 793612cd..a433c64c 100644 --- a/NativeScript/runtime/ObjectManager.mm +++ b/NativeScript/runtime/ObjectManager.mm @@ -271,10 +271,11 @@ void DisposeHandle(v8::Isolate* isolate, } Local obj = value.As(); - if (obj->InternalFieldCount() > 1 && !isFinalDisposal) { + if (obj->InternalFieldCount() > 1) { Local superValue = obj->GetInternalField(1).As(); if (!superValue.IsEmpty() && superValue->IsString()) { - // Do not dispose the ObjCWrapper contained in a "super" instance + // A "super" alias owns no wrapper; the one GetValue finds through it is + // its receiver's. return true; } } @@ -431,8 +432,15 @@ void DisposeHandle(v8::Isolate* isolate, } // The lookup, the Instances key and the final SetValue must all address the - // same object, so a Proxy is resolved once here. + // same object, so a Proxy is resolved once here, and a "super" alias to the + // receiver whose wrapper it reads. Local value = tns::UnwrapProxy(info[0]); + if (!value.IsEmpty() && value->IsObject()) { + Local receiver = tns::SuperAliasReceiver(value.As()); + if (!receiver.IsEmpty()) { + value = receiver; + } + } BaseDataWrapper* wrapper = tns::GetValue(isolate, value); if (wrapper == nullptr) { diff --git a/TestRunner/app/tests/SuperAliasTests.js b/TestRunner/app/tests/SuperAliasTests.js new file mode 100644 index 00000000..26b4b62b --- /dev/null +++ b/TestRunner/app/tests/SuperAliasTests.js @@ -0,0 +1,279 @@ +// `this.super` on an extend() class: an alias that dispatches to the +// superclass implementation and reads its receiver's native object, never a +// copy of it. +describe("this.super aliases", function () { + function scrub(n) { + return n > 0 ? scrub(n - 1) + n : 0; + } + + // Collect, then give autorelease pools and posted retain/release work a + // turn of the run loop. + function settle(rounds, callback) { + scrub(300); + __collect(); + __collect(); + setTimeout(function () { + NSObject.new(); + if (rounds > 1) { + settle(rounds - 1, callback); + } else { + callback(); + } + }, 10); + } + + function deallocs() { + return (TNSGetOutput().match(/TNSAllocLog dealloc/g) || []).length; + } + + function expectReleased(obj) { + var error = null; + try { + obj.hash; + } catch (e) { + error = e; + } + expect(error === null ? "" : String(error.message)).toContain("disposed native object"); + } + + function superGetter(cls) { + return Object.getOwnPropertyDescriptor(Object.getPrototypeOf(cls.prototype), "super").get; + } + + var superInits = 0; + var SuperAliasLog = TNSAllocLog.extend({ + init: function () { + var self = this.super.init(); + superInits++; + return self; + }, + get description() { + return "js:" + this.super.description; + } + }, { name: "SuperAliasLog" }); + + // Objects earlier specs dropped are collected first, so the dealloc log + // only counts this spec's objects. + beforeEach(function (done) { + settle(2, function () { + TNSClearOutput(); + superInits = 0; + done(); + }); + }); + + it("dispatches initializers, methods and properties to the superclass", function (done) { + (function () { + var constructed = new SuperAliasLog(); + var created = SuperAliasLog.new(); + expect(superInits).toBe(2); + [constructed, created].forEach(function (obj) { + expect(obj instanceof SuperAliasLog).toBe(true); + expect(obj.description).toMatch(/^js:$/); + expect(obj.super.description).toMatch(/^$/); + expect(typeof obj.hash).toBe("number"); + expect(obj.super.hash).toBe(obj.hash); + expect(obj.super.isEqual(obj)).toBe(true); + }); + })(); + settle(3, function () { + expect(deallocs()).toBe(2); + done(); + }); + }); + + it("reports an alias as released once its receiver was released with __releaseNativeCounterpart", function (done) { + var obj = SuperAliasLog.new(); + var alias = obj.super; + expect(typeof obj.hash).toBe("number"); + expect(alias.hash).toBe(obj.hash); + __releaseNativeCounterpart(obj); + settle(3, function () { + expect(deallocs()).toBe(1); + expectReleased(alias); + expectReleased(obj); + obj = null; + alias = null; + settle(3, function () { + expect(deallocs()).toBe(1); + done(); + }); + }); + }); + + it("keeps its receiver alive while the alias is reachable, then frees it once", function (done) { + var watch = NSHashTable.weakObjectsHashTable(); + var alias = null; + var hash = 0; + (function () { + var obj = SuperAliasLog.new(); + hash = obj.hash; + expect(typeof hash).toBe("number"); + watch.addObject(obj); + alias = obj.super; + })(); + settle(4, function () { + expect(deallocs()).toBe(0); + expect(watch.allObjects.count).toBe(1); + expect(alias.hash).toBe(hash); + settle(2, function () { + expect(deallocs()).toBe(0); + expect(alias.description).toMatch(/^$/); + alias = null; + settle(4, function () { + expect(deallocs()).toBe(1); + expect(watch.allObjects.count).toBe(0); + settle(2, function () { + expect(deallocs()).toBe(1); + done(); + }); + }); + }); + }); + }); + + it("reports an alias saved from a JS method run by -dealloc as released", function (done) { + var saved = null; + var Keeper = TNSApi.extend({ + methodCalledInDealloc: function () { + saved = this.super; + } + }, { name: "SuperAliasDeallocKeeper" }); + (function () { + var holder = NSMutableArray.alloc().init(); + holder.addObject(Keeper.alloc().init()); + })(); + settle(4, function () { + expect(saved).not.toBeNull(); + expectReleased(saved); + saved = null; + done(); + }); + }); + + it("releases the receiver when __releaseNativeCounterpart is given an alias", function (done) { + var obj = SuperAliasLog.new(); + var alias = obj.super; + __releaseNativeCounterpart(alias); + settle(3, function () { + expect(deallocs()).toBe(1); + expectReleased(obj); + expectReleased(alias); + expectReleased(obj.super); + obj = null; + alias = null; + settle(3, function () { + expect(deallocs()).toBe(1); + done(); + }); + }); + }); + + it("resolves the alias of a Proxy over the receiver to the receiver", function (done) { + (function () { + var obj = SuperAliasLog.new(); + var alias = new Proxy(obj, {}).super; + expect(typeof obj.hash).toBe("number"); + expect(alias.hash).toBe(obj.hash); + expect(alias.description).toMatch(/^$/); + })(); + settle(3, function () { + expect(deallocs()).toBe(1); + done(); + }); + }); + + it("resolves the alias of an alias to the original receiver", function (done) { + var getter = superGetter(SuperAliasLog); + var obj = SuperAliasLog.new(); + var alias = obj.super; + var hash = obj.hash; + expect(typeof hash).toBe("number"); + [getter.call(alias), getter.call(new Proxy(alias, {}))].forEach(function (aliasOfAlias) { + expect(Object.getPrototypeOf(aliasOfAlias)).toBe(Object.getPrototypeOf(alias)); + expect(aliasOfAlias.hash).toBe(hash); + expect(aliasOfAlias.description).toMatch(/^$/); + }); + var aliasOfAlias = getter.call(alias); + __releaseNativeCounterpart(aliasOfAlias); + expect(function () { + __releaseNativeCounterpart(aliasOfAlias); + }).toThrowError(/not a native wrapper/); + settle(3, function () { + expect(deallocs()).toBe(1); + expectReleased(obj); + expectReleased(alias); + expectReleased(aliasOfAlias); + obj = null; + alias = null; + aliasOfAlias = null; + settle(3, function () { + expect(deallocs()).toBe(1); + done(); + }); + }); + }); + + // `super` is an enumerable accessor of the implementation object. + it("reads undefined when the accessor's receiver is not a native instance", function () { + var SuperAliasPlain = NSObject.extend({ + init: function () { + return this.super.init(); + } + }, { name: "SuperAliasPlain" }); + var implementation = Object.getPrototypeOf(SuperAliasPlain.prototype); + expect(superGetter(SuperAliasPlain).call({})).toBeUndefined(); + expect(implementation.super).toBeUndefined(); + expect(SuperAliasPlain.prototype.super).toBeUndefined(); + var copy = null; + expect(function () { + copy = Object.assign({}, implementation); + }).not.toThrow(); + expect(Object.prototype.hasOwnProperty.call(copy, "super")).toBe(true); + expect(copy.super).toBeUndefined(); + }); +}); + +// Teardown disposes the worker's registered objects newest first, so an alias +// is disposed before its receiver while both still read the same wrapper. An +// alias that is not kept stays registered until a major collection finalizes +// it, so the one the initializer evaluates counts too. +describe("this.super aliases at worker teardown", function () { + var originalTimeout; + beforeEach(function () { + originalTimeout = jasmine.DEFAULT_TIMEOUT_INTERVAL; + jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000; + }); + afterEach(function () { + jasmine.DEFAULT_TIMEOUT_INTERVAL = originalTimeout; + }); + + function terminateWhenReady(source, done) { + var worker = new Worker("~/shared/Workers/EvalWorker.js"); + worker.onmessage = function (msg) { + expect(msg.data).toBe("ready"); + worker.terminate(); + setTimeout(done, 600); + }; + worker.onerror = function (e) { + expect(String(e && e.message ? e.message : e)).toBe(""); + done(); + }; + worker.postMessage({ + eval: "var Ext = NSObject.extend({" + + " init: function () { return this.super.init(); }" + + "}, { name: 'SuperAliasTeardownObject' });" + + "globalThis.superAliasObject = Ext.new();" + + source + + "postMessage('ready');" + }); + } + + it("disposes an object and a kept alias of it", function (done) { + terminateWhenReady("globalThis.superAlias = globalThis.superAliasObject.super;", done); + }); + + it("disposes an object whose only alias was its initializer's", function (done) { + terminateWhenReady("", done); + }); +}); diff --git a/TestRunner/app/tests/index.js b/TestRunner/app/tests/index.js index 91589b5a..bb93113c 100644 --- a/TestRunner/app/tests/index.js +++ b/TestRunner/app/tests/index.js @@ -128,6 +128,7 @@ require("./ObjCConstructors"); require("./MetadataTests"); // require("./ApiTests"); +require("./SuperAliasTests"); require("./ProtocolBindingTests"); require("./ProxyReceiverTests"); require("./NsRuntimeTests");