Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions NativeScript/runtime/ArgConverter.mm
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
#include "WrapperLifetime.h"
#include "Runtime.h"
#include "RuntimeConfig.h"
#include "WrapperLifetimeCanary.h"

using namespace v8;
using namespace std;
Expand Down Expand Up @@ -1183,6 +1184,7 @@ throw NativeScriptException(

instanceTemplate->SetHandler(IndexedPropertyHandlerConfiguration(IndexedPropertyGetterCallback,
IndexedPropertySetterCallback));
WrapperLifetimeCanary::ConfigureInstanceTemplate(instanceTemplate);

Local<v8::Function> emptyInstanceCtorFunc;
if (!emptyInstanceCtorFuncTemplate->GetFunction(context).ToLocal(&emptyInstanceCtorFunc)) {
Expand Down
3 changes: 3 additions & 0 deletions NativeScript/runtime/ClassBuilder.mm
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
#include "ObjectManager.h"
#include "Runtime.h"
#include "TNSDerivedClass.h"
#include "WrapperLifetimeCanary.h"

using namespace v8;

Expand Down Expand Up @@ -100,6 +101,8 @@ void ScopeClassNameToIsolate(std::string& name, int isolateId) {
Local<FunctionTemplate> extendedClassCtorFuncTemplate =
FunctionTemplate::New(isolate, ExtendedClassConstructorCallback, ext);
extendedClassCtorFuncTemplate->InstanceTemplate()->SetInternalFieldCount(1);
WrapperLifetimeCanary::ConfigureInstanceTemplate(
extendedClassCtorFuncTemplate->InstanceTemplate());

Local<v8::Function> extendClassCtorFunc;
if (!extendedClassCtorFuncTemplate->GetFunction(context).ToLocal(&extendClassCtorFunc)) {
Expand Down
2 changes: 2 additions & 0 deletions NativeScript/runtime/MetadataBuilder.mm
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
#include "SymbolLoader.h"
#include "Tasks.h"
#include "Worker.h"
#include "WrapperLifetimeCanary.h"

using namespace v8;

Expand Down Expand Up @@ -553,6 +554,7 @@ throw NativeScriptException(

ctorFuncTemplate = FunctionTemplate::New(isolate, ClassConstructorCallback, ext);
ctorFuncTemplate->InstanceTemplate()->SetInternalFieldCount(1);
WrapperLifetimeCanary::ConfigureInstanceTemplate(ctorFuncTemplate->InstanceTemplate());
ctorFuncTemplate->SetClassName(tns::ToV8String(isolate, meta->name()));

Local<v8::Function> baseCtorFunc;
Expand Down
2 changes: 2 additions & 0 deletions NativeScript/runtime/NsBuiltinModules.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,8 @@ void GetWrapperLifetimeStatsCallback(const FunctionCallbackInfo<Value>& info) {
{"promotions", stats->promotions},
{"demotions", stats->demotions},
{"queuedReleases", stats->queuedReleases},
{"canaryStates", stats->canaryStates},
{"canaryNativeDeaths", stats->canaryNativeDeaths},
};
for (const auto& [name, value] : fields) {
result
Expand Down
11 changes: 11 additions & 0 deletions NativeScript/runtime/ObjectManager.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

namespace tns {

struct CanaryRecord;

class ObjectManager;
class ObjCDataWrapper;

Expand Down Expand Up @@ -51,6 +53,11 @@ struct ObjectWeakCallbackState {
bool lifetimeTracked_ = false;
// The handle is currently strong.
bool lifetimeStrong_ = false;
// Canary mode: the wrapper forwards JS properties to a state object that
// outlives it (WrapperLifetimeCanary), found through this record once the
// object has state.
bool lifetimeCanary_ = false;
std::shared_ptr<CanaryRecord> canaryRecord_;
};

class ObjectManager {
Expand Down Expand Up @@ -79,6 +86,10 @@ class ObjectManager {
uint64_t promotions = 0;
uint64_t demotions = 0;
uint64_t queuedReleases = 0;
// Canary mode: state objects created for native objects, and state
// objects let go of because their native object was deallocated.
uint64_t canaryStates = 0;
uint64_t canaryNativeDeaths = 0;
};
static LifetimeStats* GetLifetimeStats(v8::Isolate* isolate);

Expand Down
21 changes: 11 additions & 10 deletions NativeScript/runtime/SymbolIterator.mm
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,18 @@
void SymbolIterator::Set(Local<Context> context, Local<Value> object) {
Isolate* isolate = v8::Isolate::GetCurrent();

// Iterates its receiver rather than capturing `object`: in the canary
// wrapper lifetime mode this property lives on the object's state, which a
// reference back to the object would keep alive until native death.
Local<v8::Function> iterator;
bool success = v8::Function::New(
context,
[](const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
Local<Context> context = isolate->GetCurrentContext();
Local<Value> object = args.Data();
Local<Object> result = CreateIteratorObject(context, object);
args.GetReturnValue().Set(result);
},
object)
bool success = v8::Function::New(context,
[](const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
Local<Context> context = isolate->GetCurrentContext();
Local<Object> result =
CreateIteratorObject(context, args.This());
args.GetReturnValue().Set(result);
})
.ToLocal(&iterator);
tns::Assert(success, isolate);

Expand Down
9 changes: 7 additions & 2 deletions NativeScript/runtime/WrapperLifetime.h
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,16 @@ enum class WrapperLifetimeMode {
// Phantom callbacks, no resurrection: a wrapper is a strong root while
// something other than the runtime retains its object, weak otherwise.
kRooted,
// Phantom callbacks, no resurrection: the wrapper is a canary that dies with
// its JS references. JS properties set on it live on a separate state object
// that stays alive until the native object is deallocated, and a canary
// built for the same object later shares it (WrapperLifetimeCanary).
kCanary,
};

enum class WrapperLifetimeScope {
// Only JS-extended instances (TNSDerivedClass) are rooted; other wrappers are
// weak and die with their JS references.
// Only JS-extended instances (TNSDerivedClass) are rooted, or canaries;
// other wrappers are weak and die with their JS references.
kExtended,
kAll,
};
Expand Down
32 changes: 27 additions & 5 deletions NativeScript/runtime/WrapperLifetime.mm
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
#include "Helpers.h"
#include "Runtime.h"
#import "TNSDerivedClass.h"
#include "WrapperLifetimeCanary.h"

using namespace v8;

Expand All @@ -33,6 +34,8 @@ void ReadConfig() {
if ([mode isKindOfClass:[NSString class]]) {
if ([mode isEqualToString:@"rooted"]) {
detail::wrapperLifetimeMode = WrapperLifetimeMode::kRooted;
} else if ([mode isEqualToString:@"canary"]) {
detail::wrapperLifetimeMode = WrapperLifetimeMode::kCanary;
} else if (![mode isEqualToString:@"resurrecting"]) {
Log("NativeScript: unknown wrapperLifetime mode \"%s\"; using \"resurrecting\".",
[mode UTF8String]);
Expand Down Expand Up @@ -116,7 +119,8 @@ bool IsBlock(id obj) {
return blockClass != nil && [obj isKindOfClass:blockClass];
}

bool IsRootable(id obj) {
// Rooted (rooted mode) or a canary (canary mode).
bool IsInScope(id obj) {
if (IsTaggedPointer(obj) || IsBlock(obj)) {
return false;
}
Expand Down Expand Up @@ -258,7 +262,15 @@ void ReleaseAll(Isolate* isolate, RootedState* rooted) {
} // namespace

const char* WrapperLifetimeModeName(WrapperLifetimeMode mode) {
return mode == WrapperLifetimeMode::kRooted ? "rooted" : "resurrecting";
switch (mode) {
case WrapperLifetimeMode::kRooted:
return "rooted";
case WrapperLifetimeMode::kCanary:
return "canary";
case WrapperLifetimeMode::kResurrecting:
break;
}
return "resurrecting";
}

const char* WrapperLifetimeScopeName(WrapperLifetimeScope scope) {
Expand All @@ -267,6 +279,7 @@ void ReleaseAll(Isolate* isolate, RootedState* rooted) {

void WrapperLifetime::Init(Isolate* isolate) {
std::call_once(configOnce_, ReadConfig);
WrapperLifetimeCanary::Init(isolate);
if (GetWrapperLifetimeMode() != WrapperLifetimeMode::kRooted) {
return;
}
Expand All @@ -277,7 +290,7 @@ void ReleaseAll(Isolate* isolate, RootedState* rooted) {

void WrapperLifetime::Adopt(Isolate* isolate, ObjCDataWrapper* wrapper,
const std::shared_ptr<Persistent<Value>>& handle, id target) {
if (GetWrapperLifetimeMode() != WrapperLifetimeMode::kRooted || target == nil) {
if (GetWrapperLifetimeMode() == WrapperLifetimeMode::kResurrecting || target == nil) {
return;
}
RootedState* rooted = Caches::StateFor<RootedState>(isolate);
Expand All @@ -291,9 +304,17 @@ void ReleaseAll(Isolate* isolate, RootedState* rooted) {
}
state->lifetimeWrapper_ = wrapper;
state->lifetimeTarget_ = target;
state->runtimeRetains_ = AcquireRuntimeRetain(target);
if (GetWrapperLifetimeMode() == WrapperLifetimeMode::kRooted) {
state->runtimeRetains_ = AcquireRuntimeRetain(target);
}
wrapper->SetLifetimeState(state);
if (IsRootable(target)) {
if (GetWrapperLifetimeMode() == WrapperLifetimeMode::kCanary) {
state->lifetimeCanary_ = IsInScope(target);
if (state->lifetimeCanary_) {
state->canaryRecord_ = WrapperLifetimeCanary::Find(isolate, target);
}
MakeWeak(state);
} else if (IsInScope(target)) {
// Born strong: a wrapper just handed to JS or native is in use; the next
// major collection decides whether native code keeps it.
state->lifetimeTracked_ = true;
Expand Down Expand Up @@ -333,6 +354,7 @@ void ReleaseAll(Isolate* isolate, RootedState* rooted) {
}

void WrapperLifetime::DrainForTeardown(Isolate* isolate) {
WrapperLifetimeCanary::CloseForTeardown(isolate);
RootedState* rooted = Caches::ExistingStateFor<RootedState>(isolate);
if (rooted == nullptr) {
return;
Expand Down
53 changes: 53 additions & 0 deletions NativeScript/runtime/WrapperLifetimeCanary.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
#ifndef WrapperLifetimeCanary_h
#define WrapperLifetimeCanary_h

#include <memory>
#include "Common.h"

namespace tns {

struct CanaryRecord;

// The canary wrapper lifetime mode (WrapperLifetimeMode::kCanary).
//
// An ObjC object's wrapper is a canary: a phantom-weak JS object that dies
// with its JS references, releasing the runtime's retain on the object. JS
// properties set on a canary are forwarded by kNonMasking named interceptors
// to a state object, so prototype members (native methods and properties,
// JS-extended class methods) never reach these handlers. A canary built later
// for the same object forwards to the same state.
//
// The state object is rooted from its first write until the native object is
// deallocated (an associated token's -dealloc reports it). It cannot be rooted
// only once the canary dies instead: a state reachable only through the canary
// is garbage in the same collection, before any phantom callback runs. The
// cost is that state referencing its own canary (a closure over `this`, a
// WeakMap value naming the object) keeps the canary, and through its retain the
// native object, alive. This is the portable contract, the one the Node-API
// runtimes and Android can implement.
//
// SWITCH POINT (iOS only, not implemented): to collect that self-referencing
// shape, a major-GC prologue could make a record's state handle weak while the
// object is not natively owned and no canary is alive, the way the rooted mode
// scans retain counts (WrapperLifetime.mm, PrologueCallback). Records already
// carry everything such a scan needs: the native object and the state handle.
class WrapperLifetimeCanary {
public:
static void Init(v8::Isolate* isolate);

// Installs the forwarding handlers on an ObjC instance template. No-op unless
// the mode is canary.
static void ConfigureInstanceTemplate(v8::Local<v8::ObjectTemplate> instanceTemplate);

// The record of an object that already has state in this isolate, for a new
// canary to share. Null otherwise.
static std::shared_ptr<CanaryRecord> Find(v8::Isolate* isolate, id object);

// Teardown: lets go of every state object while the isolate is alive. Tokens
// that outlive the isolate find the registry closed.
static void CloseForTeardown(v8::Isolate* isolate);
};

} // namespace tns

#endif /* WrapperLifetimeCanary_h */
Loading
Loading