Skip to content

Commit 0d6c43d

Browse files
committed
feat(swift-interop): async sequences as JS async iterators
Results typed AsyncStream<T>, AsyncThrowingStream<T, E> or `some AsyncSequence<T, E>` now return JS async iterators, so `for await` works over them (ABI 11). The signature code is ~<element>. The thunk wraps the sequence in a type-erased __NSAsyncIterator. Its step runs the iterator's next() in a Task and reports the element as a one-element array, or an empty array at the end, through the existing async collection path. The runtime hands the iterator to a step function. A small factory compiled once per isolate makes that function an async iterator whose next() calls run one at a time. The step function's weak handle releases the Swift iterator when collected. A pending async result now keeps its own result code and name instead of a member pointer, since an iterator step's member can be collected before the step settles. Opaque results bind without naming their type.
1 parent 225586e commit 0d6c43d

22 files changed

Lines changed: 1363 additions & 558 deletions

‎NativeScript/runtime/SwiftInterop.h‎

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@
4040
// calls it by reporting the arguments as an array: callbackBegin, each
4141
// argument through the complete* entries, then completeArrayEnd.
4242

43-
#define NS_SWIFT_ABI_VERSION 10
43+
#define NS_SWIFT_ABI_VERSION 11
4444

4545
#ifdef __cplusplus
4646
extern "C" {
@@ -173,6 +173,8 @@ enum {
173173
// or a JS object implementing it (NSSwiftTagJSObject)
174174
// @<Name> instance of Objective-C class <Name> (its runtime name)
175175
// (NSSwiftTagObjC) <-> NativeScript's Objective-C wrapper
176+
// ~<code> asynchronous sequence of <code> (results only) -> JS async
177+
// iterator; see returnAsyncSequence
176178
// <code>? optional: JS null/undefined <-> NSSwiftTagNull
177179
// &<code> inout parameter: JS passes a reference object `{ value }`;
178180
// the slot holds its value, and a successful call reports the
@@ -301,6 +303,15 @@ struct NSSwiftAPI {
301303

302304
/* 48 */ void (*registerTracker)(NSSwiftRegistrar* registrar,
303305
NSSwiftTracker tracker);
306+
307+
// An asynchronous sequence, as a retained (+1) iterator object the runtime
308+
// takes ownership of. Each JS `next()` calls `next` with the iterator as
309+
// args[0] once the previous step settled; it reports the next element as
310+
// a one-element array, or an empty array at the end, through beginAsync.
311+
/* 49 */ void (*returnAsyncSequence)(NSSwiftCall* call, void* iterator,
312+
NSSwiftThunk next);
313+
/* 50 */ void (*completeAsyncSequence)(void* token, void* iterator,
314+
NSSwiftThunk next);
304315
};
305316

306317
typedef void (*NSSwiftRegisterFunction)(NSSwiftRegistrar* registrar);

‎NativeScript/runtime/SwiftInterop.mm‎

Lines changed: 175 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
Tagged,
5959
ObjC,
6060
Protocol,
61+
AsyncSequence,
6162
};
6263
Kind kind = Kind::Void;
6364
int bits = 64;
@@ -106,6 +107,13 @@ bool ParseCode(std::string text, ValueCode& code) {
106107
if (text.empty()) {
107108
return false;
108109
}
110+
if (text.front() == '~') {
111+
code.kind = ValueCode::Kind::AsyncSequence;
112+
code.element.emplace_back();
113+
ValueCode& element = code.element.back();
114+
return ParseCode(text.substr(1), element) && element.kind != ValueCode::Kind::Void &&
115+
element.kind != ValueCode::Kind::Function && !element.optional;
116+
}
109117
if ((text.front() == '[' && text.back() == ']') || (text.front() == '{' && text.back() == '}')) {
110118
code.kind = text.front() == '[' ? ValueCode::Kind::Array : ValueCode::Kind::Map;
111119
code.element.emplace_back();
@@ -397,6 +405,9 @@ bool IsA(uint32_t typeIndex, int expected) const {
397405
case ValueCode::Kind::ObjC:
398406
name = code.className;
399407
break;
408+
case ValueCode::Kind::AsyncSequence:
409+
name = "AsyncSequence<" + TypeDisplayName(module, code.element.at(0)) + ">";
410+
break;
400411
}
401412
return (code.inout ? "inout " : "") + (code.optional ? name + "?" : name);
402413
}
@@ -546,6 +557,13 @@ void ApiCompleteError(void* token, const uint8_t* message, intptr_t messageLengt
546557
void ApiReturnJSObject(NSSwiftCall* call, void* object);
547558
void ApiCompleteJSObject(void* token, void* object);
548559
void ApiRegisterTracker(NSSwiftRegistrar* registrar, NSSwiftTracker tracker);
560+
void ApiReturnAsyncSequence(NSSwiftCall* call, void* iterator, NSSwiftThunk next);
561+
void ApiCompleteAsyncSequence(void* token, void* iterator, NSSwiftThunk next);
562+
struct IsolateState;
563+
// A JS async iterator over a Swift iterator (+1, taken over).
564+
MaybeLocal<Value> MakeAsyncIterator(Isolate* isolate, Local<Context> context,
565+
const ModuleInfo* module, void* iterator, NSSwiftThunk next,
566+
const ValueCode& code, const std::string& name);
549567
struct SwiftCallback;
550568
Local<Value> JSObjectOf(SwiftCallback* handle, Isolate* isolate);
551569
void ReleaseCallback(SwiftCallback* callback);
@@ -567,7 +585,7 @@ void ApiCompleteError(void* token, const uint8_t* message, intptr_t messageLengt
567585
ApiWriteBackEnd, ApiReturnObjC, ApiCompleteObjC,
568586
ApiRegisterUnitModule, ApiObjectCallBegin, ApiObjectCallEnd,
569587
ApiObjectResultRelease, ApiReturnJSObject, ApiCompleteJSObject,
570-
ApiRegisterTracker,
588+
ApiRegisterTracker, ApiReturnAsyncSequence, ApiCompleteAsyncSequence,
571589
};
572590

573591
// "Polygon.Style" -> "Style".
@@ -818,9 +836,12 @@ void BuildGroups(ModuleInfo& module) {
818836
};
819837

820838
// An async call's Promise, settled on the isolate's thread.
839+
// Its own copy of what the result is: the call's member may be gone by the
840+
// time it settles (an iterator step's).
821841
struct PendingAsync {
822842
Global<Promise::Resolver> resolver;
823-
const MemberInfo* member;
843+
ValueCode code;
844+
std::string name;
824845
};
825846

826847
struct IsolateState {
@@ -833,6 +854,8 @@ void BuildGroups(ModuleInfo& module) {
833854
robin_hood::unordered_map<void*, SwiftObjectWrapper*> identity;
834855
// Thunk calls in progress on the isolate's thread, innermost last.
835856
std::vector<CallContext*> calls;
857+
// Builds async iterators from step functions (MakeAsyncIterator).
858+
Global<v8::Function> iteratorFactory;
836859
};
837860

838861
// Owns one retain of a Swift object for as long as its JS wrapper lives.
@@ -1015,6 +1038,22 @@ bool IsObjCInstance(id object, const ValueCode& code) {
10151038
return object != nil && (code.objcClass == nil || [object isKindOfClass:code.objcClass]);
10161039
}
10171040

1041+
void ApiReturnAsyncSequence(NSSwiftCall* call, void* iterator, NSSwiftThunk next) {
1042+
CallContext* context = ContextOf(call);
1043+
const ValueCode* code = CurrentCode(context);
1044+
Local<Value> value;
1045+
if (code == nullptr || code->kind != ValueCode::Kind::AsyncSequence ||
1046+
!MakeAsyncIterator(context->isolate, context->context, context->module, iterator, next, *code,
1047+
context->member->swiftName)
1048+
.ToLocal(&value)) {
1049+
if (code == nullptr || code->kind != ValueCode::Kind::AsyncSequence) {
1050+
context->module->release(iterator);
1051+
}
1052+
return Fail(context);
1053+
}
1054+
Complete(context, value);
1055+
}
1056+
10181057
void ApiReturnJSObject(NSSwiftCall* call, void* object) {
10191058
CallContext* context = ContextOf(call);
10201059
Complete(context, JSObjectOf(static_cast<SwiftCallback*>(object), context->isolate));
@@ -1127,7 +1166,9 @@ void ApiReturnNull(NSSwiftCall* call) {
11271166
void BeginContainer(CallContext* context, bool isMap, intptr_t count) {
11281167
const ValueCode* code = CurrentCode(context);
11291168
ValueCode::Kind expected = isMap ? ValueCode::Kind::Map : ValueCode::Kind::Array;
1130-
if (code == nullptr || code->kind != expected || code->element.empty()) {
1169+
// An iterator step reports its element as an array.
1170+
bool step = !isMap && code != nullptr && code->kind == ValueCode::Kind::AsyncSequence;
1171+
if (code == nullptr || (code->kind != expected && !step) || code->element.empty()) {
11311172
Fail(context);
11321173
context->isolate->ThrowException(Exception::Error(tns::ToV8String(
11331174
context->isolate, context->member->swiftName + " reported an unexpected collection")));
@@ -1354,6 +1395,8 @@ void ApiThrowTypeError(NSSwiftCall* call, const uint8_t* message, intptr_t lengt
13541395
ObjC,
13551396
// `object` is a JS object handle the outcome holds a reference to.
13561397
JSObject,
1398+
// `object` is a Swift iterator, stepped by `next`.
1399+
Sequence,
13571400
};
13581401
Kind kind = Kind::Void;
13591402
bool boolean = false;
@@ -1365,6 +1408,7 @@ void ApiThrowTypeError(NSSwiftCall* call, const uint8_t* message, intptr_t lengt
13651408
void* object = nullptr;
13661409
uint32_t typeIndex = 0;
13671410
const ModuleInfo* module = nullptr;
1411+
NSSwiftThunk next = nullptr;
13681412
std::vector<std::shared_ptr<AsyncOutcome>> children;
13691413

13701414
// An undelivered object (the isolate went away) still owns its retain.
@@ -1464,6 +1508,16 @@ bool OutcomeToJS(Isolate* isolate, Local<Context> context, IsolateState* state,
14641508
case AsyncOutcome::Kind::JSObject:
14651509
out = JSObjectOf(static_cast<SwiftCallback*>(node.object), isolate);
14661510
return true;
1511+
case AsyncOutcome::Kind::Sequence: {
1512+
if (code.kind != ValueCode::Kind::AsyncSequence) {
1513+
return failed("returned an unexpected sequence");
1514+
}
1515+
void* iterator = node.object;
1516+
node.object = nullptr;
1517+
return MakeAsyncIterator(isolate, context, node.module, iterator, node.next, code, name)
1518+
.ToLocal(&out) ||
1519+
failed("returned a sequence JavaScript cannot iterate");
1520+
}
14671521
case AsyncOutcome::Kind::ObjC: {
14681522
id object = static_cast<id>(node.object);
14691523
node.object = nullptr;
@@ -1547,8 +1601,8 @@ void SettleOnIsolate(Isolate* isolate, uint64_t id, AsyncOutcome& outcome) {
15471601
Local<Promise::Resolver> resolver = pending->resolver.Get(isolate);
15481602
Local<Value> value;
15491603
Local<Value> rejection;
1550-
if (OutcomeToJS(isolate, context, state, pending->member->swiftName, outcome,
1551-
pending->member->signature.result, value, rejection)) {
1604+
if (OutcomeToJS(isolate, context, state, pending->name, outcome, pending->code, value,
1605+
rejection)) {
15521606
resolver->Resolve(context, value).Check();
15531607
} else {
15541608
resolver->Reject(context, rejection).Check();
@@ -1740,7 +1794,8 @@ void Emit(void* rawToken, std::shared_ptr<AsyncOutcome> node) {
17401794
if (Promise::Resolver::New(context->context).ToLocal(&resolver)) {
17411795
auto pending = std::make_unique<PendingAsync>();
17421796
pending->resolver.Reset(isolate, resolver);
1743-
pending->member = context->member;
1797+
pending->code = *context->resultCode;
1798+
pending->name = context->member->swiftName;
17441799
context->state->pending.emplace(id, std::move(pending));
17451800
Complete(context, resolver->GetPromise());
17461801
} else {
@@ -1828,6 +1883,13 @@ void ApiCompleteMapBegin(void* token, intptr_t /*count*/) {
18281883
Emit(token, Outcome(AsyncOutcome::Kind::Map));
18291884
}
18301885

1886+
void ApiCompleteAsyncSequence(void* token, void* iterator, NSSwiftThunk next) {
1887+
auto outcome = Outcome(AsyncOutcome::Kind::Sequence);
1888+
outcome->object = iterator;
1889+
outcome->next = next;
1890+
Emit(token, outcome);
1891+
}
1892+
18311893
void ApiCompleteJSObject(void* token, void* object) {
18321894
auto outcome = Outcome(AsyncOutcome::Kind::JSObject);
18331895
ApiCallbackRetain(object);
@@ -1932,6 +1994,7 @@ bool Accepts(Isolate* isolate, const ModuleInfo& module, const ValueCode& code,
19321994
? wrapper->Module() == &module
19331995
: value->IsObject() && !value->IsArray() && ObjCObjectOf(isolate, value) == nil;
19341996
}
1997+
case ValueCode::Kind::AsyncSequence:
19351998
case ValueCode::Kind::Void:
19361999
return false;
19372000
}
@@ -2321,6 +2384,7 @@ bool Convert(Isolate* isolate, Local<Context> context, const ModuleInfo& module,
23212384
return true;
23222385
}
23232386

2387+
case ValueCode::Kind::AsyncSequence:
23242388
case ValueCode::Kind::Void:
23252389
break;
23262390
}
@@ -2580,6 +2644,111 @@ bool Select(Isolate* isolate, Local<Context> context, const MemberGroup& group,
25802644
return false;
25812645
}
25822646

2647+
LoadedModule* LoadedFor(Isolate* isolate, const ModuleInfo* module);
2648+
Local<External> DataFor(Isolate* isolate, const void* pointer);
2649+
2650+
// A Swift iterator owned by the step function JS calls; released when that
2651+
// function is collected.
2652+
struct IteratorBox {
2653+
Global<v8::Function> step;
2654+
const ModuleInfo* module;
2655+
void* iterator;
2656+
NSSwiftThunk next;
2657+
// Names the steps in errors; its result is the sequence's code.
2658+
MemberInfo member;
2659+
};
2660+
2661+
void StepCallback(const FunctionCallbackInfo<Value>& info) {
2662+
Isolate* isolate = info.GetIsolate();
2663+
auto* box = static_cast<IteratorBox*>(
2664+
info.Data().As<External>()->Value(v8::kExternalPointerTypeTagDefault));
2665+
auto* state = Caches::StateFor<IsolateState>(isolate);
2666+
LoadedModule* loaded = LoadedFor(isolate, box->module);
2667+
if (state == nullptr || loaded == nullptr) {
2668+
return;
2669+
}
2670+
NSSwiftValue slot{};
2671+
slot.tag = NSSwiftTagObject;
2672+
slot.payload.object = box->iterator;
2673+
2674+
CallContext call;
2675+
call.api = &kAPI;
2676+
call.isolate = isolate;
2677+
call.context = isolate->GetCurrentContext();
2678+
call.module = box->module;
2679+
call.loaded = loaded;
2680+
call.state = state;
2681+
call.member = &box->member;
2682+
call.resultCode = &box->member.signature.result;
2683+
state->calls.push_back(&call);
2684+
box->next(&call, &slot, 1);
2685+
state->calls.pop_back();
2686+
if (!call.callbackException.IsEmpty()) {
2687+
isolate->ThrowException(call.callbackException.Get(isolate));
2688+
} else if (!call.threw && call.completed) {
2689+
info.GetReturnValue().Set(call.result);
2690+
}
2691+
}
2692+
2693+
// Turns a step function (a Promise of `[element]`, or `[]` at the end) into
2694+
// an async iterator, one step at a time.
2695+
constexpr const char* kIteratorFactory = R"js((function (step) {
2696+
let pending = Promise.resolve();
2697+
return {
2698+
next() {
2699+
const result = pending.then(() => step()).then((values) =>
2700+
values.length ? { value: values[0], done: false } : { value: undefined, done: true });
2701+
pending = result.then(() => undefined, () => undefined);
2702+
return result;
2703+
},
2704+
[Symbol.asyncIterator]() {
2705+
return this;
2706+
},
2707+
};
2708+
}))js";
2709+
2710+
MaybeLocal<Value> MakeAsyncIterator(Isolate* isolate, Local<Context> context,
2711+
const ModuleInfo* module, void* iterator, NSSwiftThunk next,
2712+
const ValueCode& code, const std::string& name) {
2713+
auto* state = Caches::StateFor<IsolateState>(isolate);
2714+
if (state == nullptr) {
2715+
module->release(iterator);
2716+
return MaybeLocal<Value>();
2717+
}
2718+
if (state->iteratorFactory.IsEmpty()) {
2719+
Local<Script> script;
2720+
Local<Value> factory;
2721+
if (!Script::Compile(context, tns::ToV8String(isolate, kIteratorFactory)).ToLocal(&script) ||
2722+
!script->Run(context).ToLocal(&factory) || !factory->IsFunction()) {
2723+
module->release(iterator);
2724+
return MaybeLocal<Value>();
2725+
}
2726+
state->iteratorFactory.Reset(isolate, factory.As<v8::Function>());
2727+
}
2728+
2729+
auto* box = new IteratorBox{{}, module, iterator, next, MemberInfo()};
2730+
box->member.swiftName = name + " (iterator)";
2731+
box->member.signature.result = code;
2732+
Local<v8::Function> step;
2733+
if (!v8::Function::New(context, StepCallback, DataFor(isolate, box)).ToLocal(&step)) {
2734+
module->release(iterator);
2735+
delete box;
2736+
return MaybeLocal<Value>();
2737+
}
2738+
box->step.Reset(isolate, step);
2739+
box->step.SetWeak(
2740+
box,
2741+
[](const WeakCallbackInfo<IteratorBox>& info) {
2742+
IteratorBox* collected = info.GetParameter();
2743+
collected->step.Reset();
2744+
collected->module->release(collected->iterator);
2745+
delete collected;
2746+
},
2747+
WeakCallbackType::kParameter);
2748+
Local<Value> argv[] = {step};
2749+
return state->iteratorFactory.Get(isolate)->Call(context, v8::Undefined(isolate), 1, argv);
2750+
}
2751+
25832752
LoadedModule* LoadedFor(Isolate* isolate, const ModuleInfo* module) {
25842753
auto* state = Caches::StateFor<IsolateState>(isolate);
25852754
if (state == nullptr) {

0 commit comments

Comments
 (0)