Skip to content

Commit c4538d3

Browse files
committed
feat: implement toString and Symbol.iterator for NSFastEnumeration objects
1 parent d20ca79 commit c4538d3

File tree

6 files changed

+200
-18
lines changed

6 files changed

+200
-18
lines changed

examples/foundation.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,13 @@ const arr = NSMutableArray.arrayWithCapacity(1);
2626
arr.insertObjectAtIndex(NSObject.new(), 0);
2727
console.log(arr[0]);
2828
console.log(arr);
29+
30+
const dict = NSMutableDictionary.dictionary();
31+
// console.log(dict["key"]);
32+
dict.setObjectForKey(NSObject.new(), "key");
33+
dict.setObjectForKey(NSObject.new(), "key2");
34+
console.log(`${dict}`);
35+
36+
for (const key of dict) {
37+
console.log(key);
38+
}

include/Class.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ using namespace metagen;
1313

1414
namespace objc_bridge {
1515

16+
void initFastEnumeratorIteratorFactory(napi_env env,
17+
ObjCBridgeState *bridgeState);
18+
1619
NAPI_FUNCTION(registerClass);
1720
NAPI_FUNCTION(import);
1821
NAPI_FUNCTION(classGetter);

include/ObjCBridge.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ class ObjCBridgeState {
115115
napi_ref pointerClass;
116116
napi_ref referenceClass;
117117
napi_ref createNativeProxy;
118+
napi_ref createFastEnumeratorIterator;
118119

119120
std::unordered_map<MDSectionOffset, ObjCClass *> classes;
120121
std::unordered_map<MDSectionOffset, ObjCProtocol *> protocols;

src/Class.mm

Lines changed: 184 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,151 @@
216216
return nullptr;
217217
}
218218

219+
static const char *FastEnumerationIteratorFactorySource = R"(
220+
(function () {
221+
return {
222+
stack: new Array(16),
223+
stacklen: -1,
224+
stackptr: -1,
225+
done: false,
226+
227+
next() {
228+
if (this.stackptr < 0 && !this.done) {
229+
this.stacklen = this._fillStack(this.stack);
230+
if (this.stacklen == 0) {
231+
this.done = true;
232+
this.stackptr = -1;
233+
} else {
234+
this.stackptr = 0;
235+
}
236+
}
237+
238+
if (this.done) {
239+
return { done: true };
240+
}
241+
242+
const result = { value: this.stack[this.stackptr++], done: false };
243+
if (this.stackptr >= this.stacklen) {
244+
this.stackptr = -1;
245+
}
246+
247+
return result;
248+
},
249+
};
250+
})
251+
)";
252+
253+
void initFastEnumeratorIteratorFactory(napi_env env,
254+
ObjCBridgeState *bridgeState) {
255+
napi_value result, script;
256+
napi_create_string_utf8(env, FastEnumerationIteratorFactorySource,
257+
NAPI_AUTO_LENGTH, &script);
258+
napi_run_script(env, script, &result);
259+
bridgeState->createFastEnumeratorIterator = make_ref(env, result);
260+
}
261+
262+
class FastEnumerationIterator {
263+
public:
264+
FastEnumerationIterator(id<NSFastEnumeration> collection)
265+
: collection(collection) {}
266+
267+
static void finalize(napi_env env, void *data, void *hint) {
268+
FastEnumerationIterator *iterator = (FastEnumerationIterator *)data;
269+
delete iterator;
270+
}
271+
272+
static napi_value fillStack(napi_env env, napi_callback_info cbinfo) {
273+
ObjCBridgeState *bridgeState = ObjCBridgeState::InstanceData(env);
274+
275+
napi_value jsThis;
276+
void *data;
277+
size_t argc = 1;
278+
napi_value stackArray;
279+
280+
napi_get_cb_info(env, cbinfo, &argc, &stackArray, &jsThis, &data);
281+
282+
FastEnumerationIterator *self = nil;
283+
napi_unwrap(env, jsThis, (void **)&self);
284+
285+
NSUInteger count =
286+
[self->collection countByEnumeratingWithState:&self->state
287+
objects:self->stackbuf
288+
count:16];
289+
290+
for (NSUInteger index = 0; index < count; index++) {
291+
id obj = self->state.itemsPtr[index];
292+
napi_value jsObj = bridgeState->getObject(env, obj);
293+
napi_set_element(env, stackArray, index, jsObj);
294+
}
295+
296+
napi_value result;
297+
napi_create_int32(env, count, &result);
298+
299+
return result;
300+
}
301+
302+
napi_value toJS(napi_env env) {
303+
ObjCBridgeState *bridgeState = ObjCBridgeState::InstanceData(env);
304+
305+
napi_value createIterator =
306+
get_ref_value(env, bridgeState->createFastEnumeratorIterator);
307+
308+
napi_value result;
309+
napi_call_function(env, createIterator, createIterator, 0, nullptr,
310+
&result);
311+
312+
napi_property_descriptor fillStack = {
313+
.utf8name = "_fillStack",
314+
.name = nil,
315+
.method = FastEnumerationIterator::fillStack,
316+
.getter = nil,
317+
.setter = nil,
318+
.value = nil,
319+
.attributes = napi_enumerable,
320+
.data = nil,
321+
};
322+
323+
napi_define_properties(env, result, 1, &fillStack);
324+
325+
napi_ref ref;
326+
napi_wrap(env, result, this, FastEnumerationIterator::finalize, nullptr,
327+
&ref);
328+
329+
return result;
330+
}
331+
332+
id<NSFastEnumeration> collection;
333+
NSFastEnumerationState state = {0};
334+
id stackbuf[16];
335+
BOOL firstLoop = YES;
336+
long mutationsPtrValue;
337+
};
338+
339+
NAPI_FUNCTION(fastEnumeration) {
340+
napi_value jsThis;
341+
void *data;
342+
size_t argc = 0;
343+
344+
napi_get_cb_info(env, cbinfo, &argc, nil, &jsThis, &data);
345+
346+
id self = nil;
347+
napi_unwrap(env, jsThis, (void **)&self);
348+
349+
if (self == nil) {
350+
napi_value result;
351+
napi_create_string_utf8(env, "(nil)", NAPI_AUTO_LENGTH, &result);
352+
return result;
353+
}
354+
355+
if (![self conformsToProtocol:@protocol(NSFastEnumeration)]) {
356+
napi_throw_error(env, nil, "Object does not conform to NSFastEnumeration");
357+
return nullptr;
358+
}
359+
360+
auto iterator = new FastEnumerationIterator((id<NSFastEnumeration>)self);
361+
return iterator->toJS(env);
362+
}
363+
219364
std::string NativeObjectName = "NativeObject";
220365

221366
// Bridge an Objective-C class to JavaScript on the fly. Runtime introspection
@@ -312,31 +457,53 @@
312457
this->prototype = make_ref(env, prototype);
313458

314459
if (isNativeObject) {
315-
napi_property_descriptor property = {
316-
.utf8name = nil,
317-
.name = jsSymbolFor(env, "nodejs.util.inspect.custom"),
318-
.method = JS_CustomInspect,
319-
.getter = nil,
320-
.setter = nil,
321-
.value = nil,
322-
.attributes = napi_enumerable,
323-
.data = nil,
324-
};
325-
326-
napi_define_properties(env, prototype, 1, &property);
327-
328-
napi_value global, Symbol, SymbolDispose;
460+
napi_value global, Symbol, SymbolDispose, SymbolIterator;
329461
napi_get_global(env, &global);
330462
napi_get_named_property(env, global, "Symbol", &Symbol);
463+
napi_get_named_property(env, Symbol, "iterator", &SymbolIterator);
331464
napi_get_named_property(env, Symbol, "dispose", &SymbolDispose);
332465
napi_valuetype type;
333466
napi_typeof(env, SymbolDispose, &type);
334467

468+
napi_property_descriptor properties[] = {
469+
{
470+
.utf8name = nil,
471+
.name = jsSymbolFor(env, "nodejs.util.inspect.custom"),
472+
.method = JS_CustomInspect,
473+
.getter = nil,
474+
.setter = nil,
475+
.value = nil,
476+
.attributes = napi_enumerable,
477+
.data = nil,
478+
},
479+
{
480+
.utf8name = "toString",
481+
.name = nil,
482+
.method = JS_CustomInspect,
483+
.getter = nil,
484+
.setter = nil,
485+
.value = nil,
486+
.attributes = napi_enumerable,
487+
.data = nil,
488+
},
489+
{
490+
.utf8name = nil,
491+
.name = SymbolIterator,
492+
.method = JS_fastEnumeration,
493+
.getter = nil,
494+
.setter = nil,
495+
.value = nil,
496+
.attributes = napi_enumerable,
497+
.data = nil,
498+
}};
499+
500+
napi_define_properties(env, prototype, 3, properties);
501+
335502
if (type == napi_symbol) {
336-
property.name = SymbolDispose;
337-
property.method = JS_releaseObject;
503+
properties[0].name = SymbolDispose;
504+
properties[0].method = JS_releaseObject;
338505

339-
napi_define_properties(env, prototype, 1, &property);
506+
napi_define_properties(env, prototype, 1, properties);
340507
}
341508

342509
return;

src/ObjCBridge.mm

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,7 @@ NAPI_EXPORT void objc_bridge_init(napi_env env, const char *metadata_path) {
176176
napi_define_properties(env, global, 3, globalProperties);
177177

178178
initProxyFactory(env, bridgeState);
179+
initFastEnumeratorIteratorFactory(env, bridgeState);
179180

180181
registerInterop(env, global);
181182
registerInlineFunctions(env);

src/Object.mm

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -277,7 +277,7 @@ napi_value findConstructorForObject(napi_env env, ObjCBridgeState *bridgeState,
277277
}
278278

279279
constructor = get_ref_value(env, bridgedCls->constructor);
280-
} else if (!protocolOffsets->empty()) {
280+
} else if (protocolOffsets != nullptr && !protocolOffsets->empty()) {
281281
auto proto = getProtocol(env, protocolOffsets->front());
282282

283283
if (proto == nullptr) {

0 commit comments

Comments
 (0)