Skip to content

Commit d406ff3

Browse files
committed
feat: memory management model for custom props
1 parent 2f64404 commit d406ff3

File tree

4 files changed

+152
-45
lines changed

4 files changed

+152
-45
lines changed

examples/memory_management.js

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import "@nativescript/macos-node-api";
2+
3+
let array, weakRef;
4+
5+
const finalizationRegistry = new FinalizationRegistry((value) => {
6+
console.log("finalized", value);
7+
8+
const obj = array[0];
9+
10+
console.log("got object from array", obj);
11+
console.log("custom property after gc", obj.helloWorld);
12+
});
13+
14+
(() => {
15+
const obj = NSObject.new();
16+
17+
obj.helloWorld = "Hello, world!";
18+
19+
console.log("created object", obj);
20+
console.log("custom property before gc", obj.helloWorld);
21+
22+
finalizationRegistry.register(obj, "NativeObject");
23+
24+
// weakRef = new WeakRef(obj);
25+
26+
array = NSMutableArray.arrayWithCapacity(1);
27+
array.addObject(obj);
28+
29+
console.log("added object to array", array);
30+
})();
31+
32+
console.log("out of scope");
33+
34+
gc();
35+
36+
console.log("gc called");
37+
38+
// console.log("weakRef", weakRef.deref());

include/ObjCBridge.h

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ using namespace metagen;
2626

2727
namespace objc_bridge {
2828

29+
void finalize_objc_object(napi_env /*env*/, void *data, void *hint);
30+
2931
// Determines how retain/release should be called when an Objective-C
3032
// object is exposed to JavaScript land.
3133
typedef enum ObjectOwnership {
@@ -68,7 +70,7 @@ class ObjCBridgeState {
6870
MethodCif *getMethodCif(napi_env env, MDSectionOffset offset);
6971

7072
napi_value proxyNativeObject(napi_env env, napi_value object,
71-
bool isArray = false);
73+
id nativeObject);
7274

7375
napi_value getObject(napi_env env, id object, napi_value constructor,
7476
ObjectOwnership ownership = kUnownedObject);
@@ -112,6 +114,7 @@ class ObjCBridgeState {
112114
napi_ref referenceClass;
113115
napi_ref createNativeProxy;
114116
napi_ref createFastEnumeratorIterator;
117+
napi_ref transferOwnershipToNative;
115118

116119
std::unordered_map<MDSectionOffset, ObjCClass *> classes;
117120
std::unordered_map<MDSectionOffset, ObjCProtocol *> protocols;

src/ObjCBridge.mm

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -89,13 +89,30 @@ void finalize_bridge_data(napi_env env, void *data, void *hint) {
8989
}
9090

9191
napi_value ObjCBridgeState::proxyNativeObject(napi_env env, napi_value object,
92-
bool isArray) {
92+
id nativeObject) {
93+
NAPI_PREAMBLE
94+
9395
napi_value factory = get_ref_value(env, createNativeProxy);
96+
napi_value transferOwnershipFunc = get_ref_value(env, this->transferOwnershipToNative);
9497
napi_value result, global;
95-
napi_value args[2] = {object};
96-
napi_get_boolean(env, isArray, &args[1]);
98+
napi_value args[3] = {object, nullptr, transferOwnershipFunc};
99+
napi_get_boolean(env, [nativeObject isKindOfClass:NSArray.class], &args[1]);
97100
napi_get_global(env, &global);
98-
napi_call_function(env, global, factory, 2, args, &result);
101+
napi_call_function(env, global, factory, 3, args, &result);
102+
103+
// We need to wrap the proxied object separately except for Hermes,
104+
// We'll just ignore the error there.
105+
napi_wrap(env, result, nativeObject, nullptr, nullptr, nullptr);
106+
107+
napi_ref ref = nullptr;
108+
NAPI_GUARD(napi_add_finalizer(env, result, nativeObject, finalize_objc_object, this,
109+
&ref)) {
110+
NAPI_THROW_LAST_ERROR
111+
return nullptr;
112+
}
113+
114+
objectRefs[nativeObject] = ref;
115+
99116
return result;
100117
}
101118

src/Object.mm

Lines changed: 89 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,71 @@
11
#include "Object.h"
2+
#include "JSObject.h"
23
#include "ObjCBridge.h"
34
#include "js_native_api.h"
45
#include "node_api_util.h"
56

67
#import <Foundation/Foundation.h>
78
#include <objc/runtime.h>
89

10+
static SEL JSWrapperObjectAssociationKey = @selector(JSWrapperObjectAssociationKey);
11+
12+
@interface JSWrapperObjectAssociation : NSObject
13+
14+
@property (nonatomic) napi_env env;
15+
@property (nonatomic) napi_ref ref;
16+
17+
+ (void)transferOwnership:(napi_env)env of:(napi_value)value toNative:(id)object;
18+
19+
+ (instancetype)associationFor:(id)object;
20+
21+
- (instancetype)initWithEnv:(napi_env)env ref:(napi_ref)ref;
22+
23+
@end
24+
25+
@implementation JSWrapperObjectAssociation
26+
27+
- (instancetype)initWithEnv:(napi_env)env ref:(napi_ref)ref {
28+
self = [super init];
29+
if (self) {
30+
self.env = env;
31+
self.ref = ref;
32+
}
33+
return self;
34+
}
35+
36+
+ (void)transferOwnership:(napi_env)env of:(napi_value)value toNative:(id)object {
37+
napi_ref ref = objc_bridge::make_ref(env, value);
38+
JSWrapperObjectAssociation *association = [[JSWrapperObjectAssociation alloc] initWithEnv:env ref:ref];
39+
objc_setAssociatedObject(object, JSWrapperObjectAssociationKey, association, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
40+
}
41+
42+
+ (instancetype)associationFor:(id)object {
43+
return objc_getAssociatedObject(object, JSWrapperObjectAssociationKey);
44+
}
45+
46+
- (void)dealloc {
47+
napi_delete_reference(self.env, self.ref);
48+
}
49+
50+
@end
51+
52+
napi_value JS_transferOwnershipToNative(napi_env env, napi_callback_info cbinfo) {
53+
size_t argc = 1;
54+
napi_value arg;
55+
napi_get_cb_info(env, cbinfo, &argc, &arg, nullptr, nullptr);
56+
57+
id obj = nil;
58+
napi_unwrap(env, arg, (void **)&obj);
59+
60+
[JSWrapperObjectAssociation transferOwnership:env of:arg toNative:obj];
61+
}
62+
963
namespace objc_bridge {
1064

1165
const char *nativeObjectProxySource = R"(
12-
(function (object, isArray) {
66+
(function (object, isArray, transferOwnershipToNative) {
67+
let isTransfered = false;
68+
1369
return new Proxy(object, {
1470
get (target, name) {
1571
if (name in target) {
@@ -22,31 +78,26 @@
2278
return target.objectAtIndex(index);
2379
}
2480
}
25-
26-
// return target[name];
2781
},
2882
29-
// set (target, name, value) {
30-
// if (name in target) {
31-
// target[name] = value;
32-
// return true;
33-
// }
34-
35-
// // if (isArray) {
36-
// // const index = Number(name);
37-
// // if (!isNaN(index)) {
38-
// // target.setObjectAtIndexedSubscript(value, index);
39-
// // return true;
40-
// // }
41-
// // }
42-
43-
// if (!target.__customProps__) {
44-
// target.__customProps__ = {};
45-
// }
46-
47-
// target.__customProps__[name] = value;
48-
// return true;
49-
// },
83+
set (target, name, value) {
84+
if (isArray) {
85+
const index = Number(name);
86+
if (!isNaN(index)) {
87+
target.setObjectAtIndexedSubscript(value, index);
88+
return true;
89+
}
90+
}
91+
92+
if (!(name in target) && !isTransfered) {
93+
isTransfered = true;
94+
transferOwnershipToNative(target);
95+
}
96+
97+
target[name] = value;
98+
99+
return true;
100+
},
50101
});
51102
})
52103
)";
@@ -57,6 +108,10 @@ void initProxyFactory(napi_env env, ObjCBridgeState *state) {
57108
&script);
58109
napi_run_script(env, script, &result);
59110
state->createNativeProxy = make_ref(env, result);
111+
112+
napi_value transferOwnershipToNative;
113+
napi_create_function(env, "transferOwnershipToNative", NAPI_AUTO_LENGTH, JS_transferOwnershipToNative, nullptr, &transferOwnershipToNative);
114+
state->transferOwnershipToNative = make_ref(env, transferOwnershipToNative);
60115
}
61116

62117
void finalize_objc_object(napi_env /*env*/, void *data, void *hint) {
@@ -84,6 +139,13 @@ void finalize_objc_object(napi_env /*env*/, void *data, void *hint) {
84139
unregisterObject(obj);
85140
}
86141

142+
JSWrapperObjectAssociation *association = [JSWrapperObjectAssociation associationFor:obj];
143+
if (association != nil) {
144+
napi_value jsObject = get_ref_value(env, association.ref);
145+
[obj retain];
146+
return proxyNativeObject(env, jsObject, obj);
147+
}
148+
87149
napi_value result = nil;
88150

89151
Class cls = object_getClass(obj);
@@ -114,25 +176,12 @@ void finalize_objc_object(napi_env /*env*/, void *data, void *hint) {
114176

115177
napi_value orig = result;
116178

117-
result =
118-
proxyNativeObject(env, result, [obj isKindOfClass:[NSArray class]]);
119-
120-
// We need to wrap the proxied object separately except for Hermes,
121-
// We'll just ignore the error there.
122-
napi_wrap(env, result, obj, nullptr, nullptr, nullptr);
123-
124-
napi_ref ref = nullptr;
125-
NAPI_GUARD(napi_add_finalizer(env, result, obj, finalize_objc_object, this,
126-
&ref)) {
127-
NAPI_THROW_LAST_ERROR
128-
return nullptr;
129-
}
130-
131-
objectRefs[obj] = ref;
132-
133179
if (ownership == kUnownedObject) {
134180
[obj retain];
135181
}
182+
183+
result = proxyNativeObject(env, result, obj);
184+
136185
// #if DEBUG
137186
// napi_value global, Error, error, stack;
138187
// napi_get_global(env, &global);

0 commit comments

Comments
 (0)