diff --git a/NativeScript/runtime/SwiftInterop.h b/NativeScript/runtime/SwiftInterop.h index a53d994c..f321527c 100644 --- a/NativeScript/runtime/SwiftInterop.h +++ b/NativeScript/runtime/SwiftInterop.h @@ -18,9 +18,12 @@ // Any change to a layout, an API table slot, a tag, a kind or a signature // code requires bumping NS_SWIFT_ABI_VERSION in both places. // -// A Swift module `M` exposes one C symbol, `ns_swift_register_M`, taking an -// NSSwiftRegistrar*. It registers the module, its types and its members with -// the API table's register* entries. Each member carries a thunk: +// Thunks are generated per unit: one or more Swift modules whose declarations +// may name and extend each other's types. Every module `M` of a unit exposes +// a C symbol, `ns_swift_register_M`, taking an NSSwiftRegistrar*; each +// registers the whole unit (its modules, types and members) with the API +// table's register* entries. Types and members name the module whose +// `ns:swift/` namespace exposes them. Each member carries a thunk: // // void thunk(NSSwiftCall* call, const NSSwiftValue* args, intptr_t count) // @@ -37,7 +40,7 @@ // calls it by reporting the arguments as an array: callbackBegin, each // argument through the complete* entries, then completeArrayEnd. -#define NS_SWIFT_ABI_VERSION 7 +#define NS_SWIFT_ABI_VERSION 11 #ifdef __cplusplus extern "C" { @@ -80,6 +83,9 @@ enum { NSSwiftTagCase = 10, // An Objective-C object borrowed (+0) for the duration of the call. NSSwiftTagObjC = 11, + // A JS object implementing a protocol: payload.object is a handle, + // borrowed for the call like a callback handle. + NSSwiftTagJSObject = 12, }; typedef struct NSSwiftAPI NSSwiftAPI; @@ -95,6 +101,12 @@ typedef struct NSSwiftRegistrar { typedef void (*NSSwiftThunk)(NSSwiftCall* call, const NSSwiftValue* args, intptr_t count); +// Runs callback handle `read` (no arguments) tracking the observable +// properties it reads, then calls handle `changed` once, when one of them is +// about to change (Observation's withObservationTracking). Returns 0 when +// Observation is unavailable on this OS. +typedef uint8_t (*NSSwiftTracker)(const NSSwiftAPI* api, void* read, + void* changed); typedef void (*NSSwiftRelease)(void* object); enum { @@ -104,6 +116,10 @@ enum { NSSwiftTypeEnum = 3, // An enum with associated values. NSSwiftTypeTaggedEnum = 4, + // A protocol JS objects can implement; `superIndex` names the protocol it + // refines. Its members are its requirements (NSSwiftMemberRequirement) + // and extension members, called on boxed values of the protocol. + NSSwiftTypeProtocol = 5, }; enum { @@ -127,6 +143,10 @@ enum { NSSwiftMemberAsync = 1 << 3, // Isolated to the main actor; the thunk rejects calls from other threads. NSSwiftMemberMainActor = 1 << 4, + // A protocol requirement. A protocol's requirement table is its refined + // protocol's, then its own requirements in registration order; adapters + // call entry k as 2k (2k + 1 for a property setter). + NSSwiftMemberRequirement = 1 << 5, }; // Member signatures: comma-separated parameter codes, '>', result code. @@ -149,8 +169,12 @@ enum { // (,...) // closure returning Void, of the listed parameters // (NSSwiftTagFunction) <- JS function; parameters only +// p value of protocol type index n: a Swift object (tag Object) +// or a JS object implementing it (NSSwiftTagJSObject) // @ instance of Objective-C class (its runtime name) // (NSSwiftTagObjC) <-> NativeScript's Objective-C wrapper +// ~ asynchronous sequence of (results only) -> JS async +// iterator; see returnAsyncSequence // ? optional: JS null/undefined <-> NSSwiftTagNull // & inout parameter: JS passes a reference object `{ value }`; // the slot holds its value, and a successful call reports the @@ -185,20 +209,23 @@ struct NSSwiftAPI { intptr_t typeLength); /* 9 */ void (*throwTypeError)(NSSwiftCall* call, const uint8_t* message, intptr_t length); + // `name` names the unit. /* 10 */ void (*registerModule)(NSSwiftRegistrar* registrar, uint32_t abiVersion, const char* name, const char* fingerprint, NSSwiftRelease release); // `superIndex` is the type index of a registered superclass, or -1. /* 11 */ void (*registerType)(NSSwiftRegistrar* registrar, uint32_t index, - uint32_t kind, const char* jsName, - const char* swiftName, int32_t superIndex); - // `owner` is a type index, or -1 for module-level functions and variables. + uint32_t kind, const char* module, + const char* jsName, const char* swiftName, + int32_t superIndex); + // `owner` is a type index, or -1 for module-level functions and + // variables; a member of a type belongs to the type's module. /* 12 */ void (*registerMember)(NSSwiftRegistrar* registrar, int32_t owner, uint32_t kind, uint32_t flags, - const char* jsName, const char* swiftName, - const char* signature, NSSwiftThunk thunk, - NSSwiftThunk setter); + const char* module, const char* jsName, + const char* swiftName, const char* signature, + NSSwiftThunk thunk, NSSwiftThunk setter); // Async calls. beginAsync makes the call's JS result a Promise and returns // a token; exactly one complete* entry settles it. Those may be called from @@ -256,6 +283,35 @@ struct NSSwiftAPI { // An Objective-C object, retained (+1); the runtime takes ownership. /* 40 */ void (*returnObjC)(NSSwiftCall* call, void* object); /* 41 */ void (*completeObjC)(void* token, void* object); + + // A Swift module of the unit, registered before its types and members. + /* 42 */ void (*registerUnitModule)(NSSwiftRegistrar* registrar, + const char* name); + + // Calls requirement `index` of a JS object handle: Begin, the arguments + // through the complete* entries, then End, which runs the JS member on the + // isolate's thread (waiting for it from any other thread) and returns its + // result as one slot, or null when it threw or returned a value that does + // not convert. A non-null result is released with objectResultRelease. + // Handles are retained and released with callbackRetain/Release. + /* 43 */ void* (*objectCallBegin)(void* object, intptr_t index); + /* 44 */ const NSSwiftValue* (*objectCallEnd)(void* token); + /* 45 */ void (*objectResultRelease)(const NSSwiftValue* result); + // A JS object handle, as the JS object itself. + /* 46 */ void (*returnJSObject)(NSSwiftCall* call, void* object); + /* 47 */ void (*completeJSObject)(void* token, void* object); + + /* 48 */ void (*registerTracker)(NSSwiftRegistrar* registrar, + NSSwiftTracker tracker); + + // An asynchronous sequence, as a retained (+1) iterator object the runtime + // takes ownership of. Each JS `next()` calls `next` with the iterator as + // args[0] once the previous step settled; it reports the next element as + // a one-element array, or an empty array at the end, through beginAsync. + /* 49 */ void (*returnAsyncSequence)(NSSwiftCall* call, void* iterator, + NSSwiftThunk next); + /* 50 */ void (*completeAsyncSequence)(void* token, void* iterator, + NSSwiftThunk next); }; typedef void (*NSSwiftRegisterFunction)(NSSwiftRegistrar* registrar); diff --git a/NativeScript/runtime/SwiftInterop.mm b/NativeScript/runtime/SwiftInterop.mm index c71dba38..1117c89c 100644 --- a/NativeScript/runtime/SwiftInterop.mm +++ b/NativeScript/runtime/SwiftInterop.mm @@ -5,8 +5,10 @@ #include #include #include +#include #include #include +#include #include #include #include @@ -55,6 +57,8 @@ Function, Tagged, ObjC, + Protocol, + AsyncSequence, }; Kind kind = Kind::Void; int bits = 64; @@ -103,6 +107,13 @@ bool ParseCode(std::string text, ValueCode& code) { if (text.empty()) { return false; } + if (text.front() == '~') { + code.kind = ValueCode::Kind::AsyncSequence; + code.element.emplace_back(); + ValueCode& element = code.element.back(); + return ParseCode(text.substr(1), element) && element.kind != ValueCode::Kind::Void && + element.kind != ValueCode::Kind::Function && !element.optional; + } if ((text.front() == '[' && text.back() == ']') || (text.front() == '{' && text.back() == '}')) { code.kind = text.front() == '[' ? ValueCode::Kind::Array : ValueCode::Kind::Map; code.element.emplace_back(); @@ -175,6 +186,9 @@ bool ParseCode(std::string text, ValueCode& code) { case 't': code.kind = ValueCode::Kind::Tagged; return number(text.substr(1), code.typeIndex); + case 'p': + code.kind = ValueCode::Kind::Protocol; + return number(text.substr(1), code.typeIndex); case '@': code.kind = ValueCode::Kind::ObjC; code.className = text.substr(1); @@ -253,6 +267,7 @@ bool ParseSignature(const std::string& text, Signature& signature) { struct MemberInfo { int owner = -1; + std::string swiftModule; uint32_t kind = 0; uint32_t flags = 0; std::string jsName; @@ -268,6 +283,8 @@ bool ParseSignature(const std::string& text, Signature& signature) { struct MemberGroup { const ModuleInfo* module = nullptr; int owner = -1; + // Module-level groups: the Swift module whose namespace holds them. + std::string swiftModule; uint32_t kind = 0; std::string jsName; std::vector overloads; @@ -283,6 +300,7 @@ bool ParseSignature(const std::string& text, Signature& signature) { struct TypeInfo { uint32_t index = 0; uint32_t kind = 0; + std::string swiftModule; std::string jsName; std::string swiftName; int superIndex = -1; @@ -291,12 +309,22 @@ bool ParseSignature(const std::string& text, Signature& signature) { const MemberGroup* initializers = nullptr; // NSSwiftTypeTaggedEnum: the cases, by case index. std::vector cases; + // Instantiations of one generic type share a module and JS name; they + // form a family with one JS class, named by its first type's index. + int family = -1; + // NSSwiftTypeProtocol: the requirement table adapters call by index. + std::vector requirements; }; bool IsEnumKind(uint32_t kind) { return kind == NSSwiftTypeEnum || kind == NSSwiftTypeTaggedEnum; } +// A registered unit: one or more Swift modules sharing one type table. struct ModuleInfo { + // The unit's name. std::string name; + // Its Swift modules, each an `ns:swift/` namespace. + std::vector modules; + NSSwiftTracker tracker = nullptr; std::string fingerprint; uint32_t abiVersion = 0; NSSwiftRelease release = nullptr; @@ -359,6 +387,7 @@ bool IsA(uint32_t typeIndex, int expected) const { case ValueCode::Kind::Object: case ValueCode::Kind::Enum: case ValueCode::Kind::Tagged: + case ValueCode::Kind::Protocol: name = code.typeIndex >= 0 && code.typeIndex < static_cast(module.types.size()) ? module.types[code.typeIndex].jsName : "?"; @@ -376,6 +405,9 @@ bool IsA(uint32_t typeIndex, int expected) const { case ValueCode::Kind::ObjC: name = code.className; break; + case ValueCode::Kind::AsyncSequence: + name = "AsyncSequence<" + TypeDisplayName(module, code.element.at(0)) + ">"; + break; } return (code.inout ? "inout " : "") + (code.optional ? name + "?" : name); } @@ -399,15 +431,26 @@ void ApiRegisterModule(NSSwiftRegistrar* registrar, uint32_t abiVersion, const c const char* fingerprint, NSSwiftRelease release) { ModuleInfo* module = static_cast(registrar)->module; module->abiVersion = abiVersion; - if (name != nullptr && module->name != name) { - module->error = "registers as '" + std::string(name) + "'"; - } + module->name = name ? name : ""; module->fingerprint = fingerprint ? fingerprint : ""; module->release = release; } -void ApiRegisterType(NSSwiftRegistrar* registrar, uint32_t index, uint32_t kind, const char* jsName, - const char* swiftName, int32_t superIndex) { +void ApiRegisterTracker(NSSwiftRegistrar* registrar, NSSwiftTracker tracker) { + static_cast(registrar)->module->tracker = tracker; +} + +void ApiRegisterUnitModule(NSSwiftRegistrar* registrar, const char* name) { + ModuleInfo* module = static_cast(registrar)->module; + if (name != nullptr && + std::find(module->modules.begin(), module->modules.end(), name) == module->modules.end()) { + module->modules.push_back(name); + } +} + +void ApiRegisterType(NSSwiftRegistrar* registrar, uint32_t index, uint32_t kind, + const char* swiftModule, const char* jsName, const char* swiftName, + int32_t superIndex) { ModuleInfo* module = static_cast(registrar)->module; if (index != module->types.size()) { module->error = "type indices are not dense"; @@ -416,6 +459,7 @@ void ApiRegisterType(NSSwiftRegistrar* registrar, uint32_t index, uint32_t kind, TypeInfo type; type.index = index; type.kind = kind; + type.swiftModule = swiftModule ? swiftModule : ""; type.jsName = jsName ? jsName : ""; type.swiftName = swiftName ? swiftName : ""; type.superIndex = superIndex; @@ -423,11 +467,12 @@ void ApiRegisterType(NSSwiftRegistrar* registrar, uint32_t index, uint32_t kind, } void ApiRegisterMember(NSSwiftRegistrar* registrar, int32_t owner, uint32_t kind, uint32_t flags, - const char* jsName, const char* swiftName, const char* signature, - NSSwiftThunk thunk, NSSwiftThunk setter) { + const char* swiftModule, const char* jsName, const char* swiftName, + const char* signature, NSSwiftThunk thunk, NSSwiftThunk setter) { ModuleInfo* module = static_cast(registrar)->module; MemberInfo member; member.owner = owner; + member.swiftModule = swiftModule ? swiftModule : ""; member.kind = kind; member.flags = flags; member.jsName = jsName ? jsName : ""; @@ -505,6 +550,23 @@ void ApiCompleteError(void* token, const uint8_t* message, intptr_t messageLengt void ApiWriteBackEnd(NSSwiftCall* call); void ApiReturnObjC(NSSwiftCall* call, void* object); void ApiCompleteObjC(void* token, void* object); +void ApiRegisterUnitModule(NSSwiftRegistrar* registrar, const char* name); +void* ApiObjectCallBegin(void* object, intptr_t index); +const NSSwiftValue* ApiObjectCallEnd(void* token); +void ApiObjectResultRelease(const NSSwiftValue* result); +void ApiReturnJSObject(NSSwiftCall* call, void* object); +void ApiCompleteJSObject(void* token, void* object); +void ApiRegisterTracker(NSSwiftRegistrar* registrar, NSSwiftTracker tracker); +void ApiReturnAsyncSequence(NSSwiftCall* call, void* iterator, NSSwiftThunk next); +void ApiCompleteAsyncSequence(void* token, void* iterator, NSSwiftThunk next); +struct IsolateState; +// A JS async iterator over a Swift iterator (+1, taken over). +MaybeLocal MakeAsyncIterator(Isolate* isolate, Local context, + const ModuleInfo* module, void* iterator, NSSwiftThunk next, + const ValueCode& code, const std::string& name); +struct SwiftCallback; +Local JSObjectOf(SwiftCallback* handle, Isolate* isolate); +void ReleaseCallback(SwiftCallback* callback); const NSSwiftAPI kAPI = { ApiReturnVoid, ApiReturnBool, ApiReturnInt64, @@ -521,6 +583,9 @@ void ApiCompleteError(void* token, const uint8_t* message, intptr_t messageLengt ApiCallbackRelease, ApiReturnCaseBegin, ApiCompleteCaseBegin, ApiReturnArrayEnd, ApiCompleteContainerEnd, ApiWriteBackBegin, ApiWriteBackEnd, ApiReturnObjC, ApiCompleteObjC, + ApiRegisterUnitModule, ApiObjectCallBegin, ApiObjectCallEnd, + ApiObjectResultRelease, ApiReturnJSObject, ApiCompleteJSObject, + ApiRegisterTracker, ApiReturnAsyncSequence, ApiCompleteAsyncSequence, }; // "Polygon.Style" -> "Style". @@ -543,18 +608,53 @@ bool IsValidModuleName(const std::string& name) { return true; } +void BuildRequirements(ModuleInfo& module) { + std::vector built(module.types.size(), false); + std::function build = [&](TypeInfo& type) { + if (built[type.index]) { + return; + } + built[type.index] = true; + if (type.superIndex >= 0 && module.types[type.superIndex].kind == NSSwiftTypeProtocol) { + build(module.types[type.superIndex]); + type.requirements = module.types[type.superIndex].requirements; + } + for (const MemberInfo& member : module.members) { + if (member.owner == static_cast(type.index) && + (member.flags & NSSwiftMemberRequirement)) { + type.requirements.push_back(&member); + } + } + }; + for (TypeInfo& type : module.types) { + if (type.kind == NSSwiftTypeProtocol) { + build(type); + } + } +} + +void BuildFamilies(ModuleInfo& module) { + std::map first; + for (TypeInfo& type : module.types) { + auto inserted = first.emplace(type.swiftModule + ":" + type.jsName, type.index); + type.family = inserted.first->second; + } +} + void BuildGroups(ModuleInfo& module) { robin_hood::unordered_map byKey; for (const MemberInfo& member : module.members) { - std::string key = - std::to_string(member.owner) + ":" + std::to_string(member.kind) + ":" + member.jsName; + int owner = member.owner < 0 ? -1 : module.types[member.owner].family; + std::string key = std::to_string(owner) + ":" + std::to_string(member.kind) + ":" + + member.jsName + (member.owner < 0 ? ":" + member.swiftModule : ""); auto it = byKey.find(key); MemberGroup* group; if (it == byKey.end()) { module.groups.push_back(std::make_unique()); group = module.groups.back().get(); group->module = &module; - group->owner = member.owner; + group->owner = owner; + group->swiftModule = member.owner < 0 ? member.swiftModule : ""; group->kind = member.kind; group->jsName = member.jsName; byKey.emplace(key, group); @@ -564,6 +664,7 @@ void BuildGroups(ModuleInfo& module) { group->overloads.push_back(&member); if (member.kind == NSSwiftMemberInitializer && member.owner >= 0) { module.types[member.owner].initializers = group; + module.types[owner].initializers = group; } } @@ -607,6 +708,19 @@ void BuildGroups(ModuleInfo& module) { if (module.release == nullptr) { return "registered no release function"; } + auto inUnit = [&](const std::string& name) { + return std::find(module.modules.begin(), module.modules.end(), name) != module.modules.end(); + }; + for (const TypeInfo& type : module.types) { + if (!inUnit(type.swiftModule)) { + return "type " + type.swiftName + " names an unregistered module"; + } + } + for (const MemberInfo& member : module.members) { + if (member.owner < 0 && !inUnit(member.swiftModule)) { + return member.swiftName + " names an unregistered module"; + } + } auto validType = [&](int index) { return index >= 0 && index < static_cast(module.types.size()); }; @@ -636,6 +750,11 @@ void BuildGroups(ModuleInfo& module) { module.types[code->typeIndex].kind != NSSwiftTypeTaggedEnum)) { return member.swiftName + " references an unknown enum"; } + if (code->kind == ValueCode::Kind::Protocol && + (!validType(code->typeIndex) || + module.types[code->typeIndex].kind != NSSwiftTypeProtocol)) { + return member.swiftName + " references an unknown protocol"; + } for (const auto& element : code->element) { codes.push_back(&element); } @@ -662,7 +781,6 @@ void BuildGroups(ModuleInfo& module) { } auto module = std::make_shared(); - module->name = name; std::string symbol = "ns_swift_register_" + name; auto registerFunction = reinterpret_cast(dlsym(RTLD_DEFAULT, symbol.c_str())); @@ -675,10 +793,25 @@ void BuildGroups(ModuleInfo& module) { registrar.module = module.get(); registerFunction(®istrar); std::string problem = Validate(*module); + if (problem.empty() && + std::find(module->modules.begin(), module->modules.end(), name) == module->modules.end()) { + problem = "registers a unit without it"; + } + for (const auto& other : module->modules) { + auto registered = gModules.find(other); + if (problem.empty() && other != name && registered != gModules.end()) { + problem = "shares module '" + other + "' with another unit"; + } + } if (!problem.empty()) { module->error = "Swift module '" + name + "' " + problem; } else { + BuildFamilies(*module); + BuildRequirements(*module); BuildGroups(*module); + for (const auto& other : module->modules) { + gModules.emplace(other, module); + } } } gModules.emplace(name, module); @@ -693,30 +826,36 @@ void BuildGroups(ModuleInfo& module) { class SwiftObjectWrapper; +// A unit as one isolate sees it. struct LoadedModule { std::shared_ptr info; - Global exports; + // Each Swift module's namespace. + robin_hood::unordered_map> exports; // Constructor templates by type index; empty for enums. std::vector> templates; }; // An async call's Promise, settled on the isolate's thread. +// Its own copy of what the result is: the call's member may be gone by the +// time it settles (an iterator step's). struct PendingAsync { Global resolver; - const MemberInfo* member; + ValueCode code; + std::string name; }; struct IsolateState { - robin_hood::unordered_map> modules; // Resolvers stay here, on the isolate's thread: a v8::Global must not be // destroyed by the thread that completes the call. robin_hood::unordered_map> pending; uint64_t nextAsyncId = 1; - robin_hood::unordered_map byInfo; + robin_hood::unordered_map> byInfo; // Swift object -> its JS wrapper, so a Swift object has one JS identity. robin_hood::unordered_map identity; // Thunk calls in progress on the isolate's thread, innermost last. std::vector calls; + // Builds async iterators from step functions (MakeAsyncIterator). + Global iteratorFactory; }; // Owns one retain of a Swift object for as long as its JS wrapper lives. @@ -899,6 +1038,27 @@ bool IsObjCInstance(id object, const ValueCode& code) { return object != nil && (code.objcClass == nil || [object isKindOfClass:code.objcClass]); } +void ApiReturnAsyncSequence(NSSwiftCall* call, void* iterator, NSSwiftThunk next) { + CallContext* context = ContextOf(call); + const ValueCode* code = CurrentCode(context); + Local value; + if (code == nullptr || code->kind != ValueCode::Kind::AsyncSequence || + !MakeAsyncIterator(context->isolate, context->context, context->module, iterator, next, *code, + context->member->swiftName) + .ToLocal(&value)) { + if (code == nullptr || code->kind != ValueCode::Kind::AsyncSequence) { + context->module->release(iterator); + } + return Fail(context); + } + Complete(context, value); +} + +void ApiReturnJSObject(NSSwiftCall* call, void* object) { + CallContext* context = ContextOf(call); + Complete(context, JSObjectOf(static_cast(object), context->isolate)); +} + void ApiReturnObjC(NSSwiftCall* call, void* object) { CallContext* context = ContextOf(call); Local value; @@ -1006,7 +1166,9 @@ void ApiReturnNull(NSSwiftCall* call) { void BeginContainer(CallContext* context, bool isMap, intptr_t count) { const ValueCode* code = CurrentCode(context); ValueCode::Kind expected = isMap ? ValueCode::Kind::Map : ValueCode::Kind::Array; - if (code == nullptr || code->kind != expected || code->element.empty()) { + // An iterator step reports its element as an array. + bool step = !isMap && code != nullptr && code->kind == ValueCode::Kind::AsyncSequence; + if (code == nullptr || (code->kind != expected && !step) || code->element.empty()) { Fail(context); context->isolate->ThrowException(Exception::Error(tns::ToV8String( context->isolate, context->member->swiftName + " reported an unexpected collection"))); @@ -1213,6 +1375,9 @@ void ApiThrowTypeError(NSSwiftCall* call, const uint8_t* message, intptr_t lengt // What a Swift task produced, in a form that can cross threads. Arrays and // maps hold their elements (a map's alternate key and value) as children, a // tagged enum case its values (with its case index in `int64`). +struct SwiftCallback; +void ReleaseCallback(SwiftCallback* callback); + struct AsyncOutcome { enum class Kind { Void, @@ -1228,6 +1393,10 @@ void ApiThrowTypeError(NSSwiftCall* call, const uint8_t* message, intptr_t lengt Map, Case, ObjC, + // `object` is a JS object handle the outcome holds a reference to. + JSObject, + // `object` is a Swift iterator, stepped by `next`. + Sequence, }; Kind kind = Kind::Void; bool boolean = false; @@ -1239,11 +1408,14 @@ void ApiThrowTypeError(NSSwiftCall* call, const uint8_t* message, intptr_t lengt void* object = nullptr; uint32_t typeIndex = 0; const ModuleInfo* module = nullptr; + NSSwiftThunk next = nullptr; std::vector> children; // An undelivered object (the isolate went away) still owns its retain. ~AsyncOutcome() { - if (object != nullptr) { + if (object != nullptr && kind == Kind::JSObject) { + ReleaseCallback(static_cast(object)); + } else if (object != nullptr) { module->release(object); } } @@ -1259,6 +1431,8 @@ void ApiThrowTypeError(NSSwiftCall* call, const uint8_t* message, intptr_t lengt std::vector open; // Set for a callback invocation; the token holds a reference. struct SwiftCallback* callback = nullptr; + // An object call (objectCallBegin): the encoded requirement. + intptr_t requirement = -1; }; // Converts `node` (of `code`) to JS; on failure sets `rejection` and returns @@ -1321,15 +1495,29 @@ bool OutcomeToJS(Isolate* isolate, Local context, IsolateState* state, void* object = node.object; // WrapSwiftObject takes over the retain. node.object = nullptr; - Local wrapper = WrapSwiftObject( - isolate, context, node.module, loaded == state->byInfo.end() ? nullptr : loaded->second, - state, object, node.typeIndex, nullptr); + Local wrapper = + WrapSwiftObject(isolate, context, node.module, + loaded == state->byInfo.end() ? nullptr : loaded->second.get(), state, + object, node.typeIndex, nullptr); if (wrapper.IsEmpty()) { return failed("returned an object JavaScript cannot wrap"); } out = wrapper; return true; } + case AsyncOutcome::Kind::JSObject: + out = JSObjectOf(static_cast(node.object), isolate); + return true; + case AsyncOutcome::Kind::Sequence: { + if (code.kind != ValueCode::Kind::AsyncSequence) { + return failed("returned an unexpected sequence"); + } + void* iterator = node.object; + node.object = nullptr; + return MakeAsyncIterator(isolate, context, node.module, iterator, node.next, code, name) + .ToLocal(&out) || + failed("returned a sequence JavaScript cannot iterate"); + } case AsyncOutcome::Kind::ObjC: { id object = static_cast(node.object); node.object = nullptr; @@ -1413,8 +1601,8 @@ void SettleOnIsolate(Isolate* isolate, uint64_t id, AsyncOutcome& outcome) { Local resolver = pending->resolver.Get(isolate); Local value; Local rejection; - if (OutcomeToJS(isolate, context, state, pending->member->swiftName, outcome, - pending->member->signature.result, value, rejection)) { + if (OutcomeToJS(isolate, context, state, pending->name, outcome, pending->code, value, + rejection)) { resolver->Resolve(context, value).Check(); } else { resolver->Reject(context, rejection).Check(); @@ -1423,28 +1611,33 @@ void SettleOnIsolate(Isolate* isolate, uint64_t id, AsyncOutcome& outcome) { // --- Callbacks --------------------------------------------------------------------- -// A JS function passed for a closure parameter. The call's arena holds one -// reference, each Swift closure capturing it another, and each invocation in -// flight one more. `function` is only touched on the isolate's thread. +// A JS function passed for a closure parameter, or a JS object passed for a +// protocol value (`code` says which). The call's arena holds one reference, +// each Swift closure or adapter holding it another, and each invocation in +// flight one more. `target` is only touched on the isolate's thread. struct SwiftCallback { SwiftCallback(Isolate* isolate, const ModuleInfo* module, const ValueCode& code, - const std::string& name, Local function) + const std::string& name, Local target) : isolate(isolate), wrapper(isolate), module(module), code(code), name(name), - function(isolate, function) {} + target(isolate, target) {} Isolate* isolate; IsolateWrapper wrapper; const ModuleInfo* module; ValueCode code; std::string name; - Global function; + Global target; std::atomic references{1}; }; +Local JSObjectOf(SwiftCallback* handle, Isolate* isolate) { + return handle->target.Get(isolate); +} + // Runs `body` on the isolate's thread with the isolate locked and valid: // now when called there, otherwise queued. Returns false when it will not run. bool OnIsolateThread(Isolate* isolate, IsolateWrapper wrapper, std::function body) { @@ -1514,7 +1707,8 @@ void RunCallback(SwiftCallback* callback, AsyncOutcome& arguments) { } if (exception.IsEmpty()) { Local ignored; - if (callback->function.Get(isolate) + if (callback->target.Get(isolate) + .As() ->Call(context, v8::Undefined(isolate), static_cast(argv.size()), argv.data()) .ToLocal(&ignored) || !tryCatch.HasCaught() || !tryCatch.CanContinue()) { @@ -1600,7 +1794,8 @@ void Emit(void* rawToken, std::shared_ptr node) { if (Promise::Resolver::New(context->context).ToLocal(&resolver)) { auto pending = std::make_unique(); pending->resolver.Reset(isolate, resolver); - pending->member = context->member; + pending->code = *context->resultCode; + pending->name = context->member->swiftName; context->state->pending.emplace(id, std::move(pending)); Complete(context, resolver->GetPromise()); } else { @@ -1688,6 +1883,26 @@ void ApiCompleteMapBegin(void* token, intptr_t /*count*/) { Emit(token, Outcome(AsyncOutcome::Kind::Map)); } +void ApiCompleteAsyncSequence(void* token, void* iterator, NSSwiftThunk next) { + auto outcome = Outcome(AsyncOutcome::Kind::Sequence); + outcome->object = iterator; + outcome->next = next; + Emit(token, outcome); +} + +void ApiCompleteJSObject(void* token, void* object) { + auto outcome = Outcome(AsyncOutcome::Kind::JSObject); + ApiCallbackRetain(object); + outcome->object = object; + Emit(token, outcome); +} + +void* ApiObjectCallBegin(void* object, intptr_t index) { + auto* token = static_cast(ApiCallbackBegin(object)); + token->requirement = index; + return token; +} + void ApiCompleteObjC(void* token, void* object) { auto outcome = Outcome(AsyncOutcome::Kind::ObjC); outcome->object = object; @@ -1772,16 +1987,29 @@ bool Accepts(Isolate* isolate, const ModuleInfo& module, const ValueCode& code, return value->IsFunction(); case ValueCode::Kind::ObjC: return IsObjCInstance(ObjCObjectOf(isolate, value), code); + case ValueCode::Kind::Protocol: { + // A Swift value is checked by the thunk. + SwiftObjectWrapper* wrapper = SwiftWrapperOf(isolate, value); + return wrapper + ? wrapper->Module() == &module + : value->IsObject() && !value->IsArray() && ObjCObjectOf(isolate, value) == nil; + } + case ValueCode::Kind::AsyncSequence: case ValueCode::Kind::Void: return false; } return false; } +// The index Convert is given for a JS implementation's return value. +constexpr size_t kReturnedValue = static_cast(-1); + bool ThrowArgument(Isolate* isolate, const MemberInfo& member, size_t index, const std::string& problem, bool range = false) { - std::string message = - member.swiftName + ": argument " + std::to_string(index + 1) + " " + problem; + std::string message = member.swiftName + ": " + + (index == kReturnedValue ? std::string("the value returned") + : "argument " + std::to_string(index + 1)) + + " " + problem; Local text = tns::ToV8String(isolate, message); isolate->ThrowException(range ? Exception::RangeError(text) : Exception::TypeError(text)); return false; @@ -2095,6 +2323,45 @@ bool Convert(Isolate* isolate, Local context, const ModuleInfo& module, return true; } + case ValueCode::Kind::Protocol: { + const TypeInfo& protocol = module.types[code.typeIndex]; + if (SwiftObjectWrapper* wrapper = SwiftWrapperOf(isolate, value)) { + if (wrapper->Module() != &module) { + return ThrowArgument(isolate, member, index, "must be a " + protocol.jsName); + } + slot.tag = NSSwiftTagObject; + slot.payload.object = wrapper->Object(); + return true; + } + if (!value->IsObject() || value->IsArray() || ObjCObjectOf(isolate, value) != nil) { + return ThrowArgument(isolate, member, index, + "must be a " + protocol.jsName + " or an object implementing it"); + } + // A JS implementation must provide every requirement. + Local object = value.As(); + std::string missing; + for (const MemberInfo* requirement : protocol.requirements) { + Local key = tns::ToV8String(isolate, requirement->jsName); + Local found; + bool present = requirement->kind == NSSwiftMemberInstanceProperty + ? object->Has(context, key).FromMaybe(false) + : object->Get(context, key).ToLocal(&found) && found->IsFunction(); + if (!present) { + missing += (missing.empty() ? "" : ", ") + requirement->jsName; + } + } + if (!missing.empty()) { + return ThrowArgument( + isolate, member, index, + "does not implement " + protocol.jsName + " (missing " + missing + ")"); + } + auto* handle = new SwiftCallback(isolate, &module, code, member.swiftName, object); + arena.callbacks.push_back(handle); + slot.tag = NSSwiftTagJSObject; + slot.payload.object = handle; + return true; + } + case ValueCode::Kind::ObjC: { id object = ObjCObjectOf(isolate, value); if (!IsObjCInstance(object, code)) { @@ -2110,19 +2377,188 @@ bool Convert(Isolate* isolate, Local context, const ModuleInfo& module, return ThrowArgument(isolate, member, index, "must be a function"); } auto* callback = - new SwiftCallback(isolate, &module, code, member.swiftName, value.As()); + new SwiftCallback(isolate, &module, code, member.swiftName, value.As()); arena.callbacks.push_back(callback); slot.tag = NSSwiftTagFunction; slot.payload.object = callback; return true; } + case ValueCode::Kind::AsyncSequence: case ValueCode::Kind::Void: break; } return ThrowArgument(isolate, member, index, "has an unsupported type"); } +// --- Calls into JS implementations of protocols ------------------------------------- + +// A JS implementation's return value as one slot, with the storage it +// points into. +struct ObjectResult { + NSSwiftValue value{}; + SlotArena arena; +}; + +// Runs requirement `encoded` of `handle` with `arguments`, on the isolate's +// thread; null when it threw or its value does not convert. +ObjectResult* RunObjectCall(SwiftCallback* handle, AsyncOutcome& arguments, intptr_t encoded) { + Isolate* isolate = handle->isolate; + Isolate::Scope isolateScope(isolate); + HandleScope handleScope(isolate); + auto* state = Caches::StateFor(isolate); + if (state == nullptr) { + return nullptr; + } + Local context = Caches::Get(isolate)->GetContext(); + Context::Scope contextScope(context); + + const ModuleInfo& module = *handle->module; + const TypeInfo& protocol = module.types[handle->code.typeIndex]; + size_t position = static_cast(encoded / 2); + bool setter = encoded % 2 == 1; + if (encoded < 0 || position >= protocol.requirements.size()) { + return nullptr; + } + const MemberInfo& member = *protocol.requirements[position]; + bool property = member.kind == NSSwiftMemberInstanceProperty; + + // The innermost call in progress on this thread made this one. + CallContext* call = state->calls.empty() ? nullptr : state->calls.back(); + if (call != nullptr && !call->callbackException.IsEmpty()) { + return nullptr; + } + + TryCatch tryCatch(isolate); + Local exception; + std::vector codes; + if (!property) { + codes = member.signature.parameters; + } else if (setter) { + codes.push_back(member.signature.result); + } + std::vector> argv; + for (size_t i = 0; i < codes.size() && i < arguments.children.size(); i++) { + Local value; + if (!OutcomeToJS(isolate, context, state, member.swiftName, *arguments.children[i], codes[i], + value, exception)) { + break; + } + argv.push_back(value); + } + + auto result = std::make_unique(); + Local target = handle->target.Get(isolate); + Local key = tns::ToV8String(isolate, member.jsName); + bool done = false; + if (exception.IsEmpty()) { + Local returned; + if (property && setter) { + done = + target->Set(context, key, argv.empty() ? Local(v8::Undefined(isolate)) : argv[0]) + .FromMaybe(false); + result->value.tag = NSSwiftTagNull; + } else if (property) { + done = target->Get(context, key).ToLocal(&returned); + } else { + Local function; + if (target->Get(context, key).ToLocal(&function)) { + if (function->IsFunction()) { + done = function.As() + ->Call(context, target, static_cast(argv.size()), argv.data()) + .ToLocal(&returned); + } else { + exception = Exception::TypeError(tns::ToV8String( + isolate, protocol.jsName + "." + member.jsName + " is not a function")); + } + } + } + if (done && !(property && setter)) { + if (member.signature.result.kind == ValueCode::Kind::Void) { + result->value.tag = NSSwiftTagNull; + } else { + done = Convert(isolate, context, module, member, kReturnedValue, member.signature.result, + returned, result->value, result->arena); + } + } + } + if (done) { + return result.release(); + } + if (exception.IsEmpty()) { + if (!tryCatch.HasCaught() || !tryCatch.CanContinue()) { + return nullptr; + } + exception = tryCatch.Exception(); + } + if (call != nullptr) { + call->callbackException.Reset(isolate, exception); + } else { + NativeScriptException::ReportToJsHandlersAndLog(isolate, exception, tryCatch.Message()); + } + return nullptr; +} + +const NSSwiftValue* ApiObjectCallEnd(void* rawToken) { + std::unique_ptr token(static_cast(rawToken)); + SwiftCallback* handle = token->callback; + std::shared_ptr arguments = token->root; + intptr_t encoded = token->requirement; + ObjectResult* result = nullptr; + + if (Isolate::TryGetCurrent() == handle->isolate) { + v8::Locker locker(handle->isolate); + if (handle->wrapper.IsValid()) { + result = RunObjectCall(handle, *arguments, encoded); + } + } else { + // Run on the isolate's thread and wait. The guard ends the wait when the + // loop drops the task without running it. + struct Wait { + std::mutex mutex; + std::condition_variable condition; + bool done = false; + ObjectResult* result = nullptr; + void Finish(ObjectResult* value) { + std::lock_guard lock(mutex); + if (!done) { + done = true; + result = value; + } else { + delete value; + } + condition.notify_all(); + } + }; + struct Guard { + std::shared_ptr wait; + ~Guard() { wait->Finish(nullptr); } + }; + auto wait = std::make_shared(); + std::shared_ptr guard(new Guard{wait}); + NativeScriptPlatform* platform = NativeScriptPlatform::Instance(); + std::shared_ptr loop = + platform ? platform->LookupEventLoop(handle->isolate) : std::shared_ptr(); + IsolateWrapper isolateWrapper = handle->wrapper; + bool posted = loop && loop->PostInternal([guard, handle, arguments, encoded, isolateWrapper]() { + guard->wait->Finish(isolateWrapper.IsValid() ? RunObjectCall(handle, *arguments, encoded) + : nullptr); + }); + guard.reset(); + if (posted) { + std::unique_lock lock(wait->mutex); + wait->condition.wait(lock, [&] { return wait->done; }); + result = wait->result; + } + } + ReleaseCallback(handle); + return result ? &result->value : nullptr; +} + +void ApiObjectResultRelease(const NSSwiftValue* value) { + delete reinterpret_cast(const_cast(value)); +} + // --- Dispatch ---------------------------------------------------------------------- bool IsInstanceKind(uint32_t kind) { @@ -2208,13 +2644,118 @@ bool Select(Isolate* isolate, Local context, const MemberGroup& group, return false; } +LoadedModule* LoadedFor(Isolate* isolate, const ModuleInfo* module); +Local DataFor(Isolate* isolate, const void* pointer); + +// A Swift iterator owned by the step function JS calls; released when that +// function is collected. +struct IteratorBox { + Global step; + const ModuleInfo* module; + void* iterator; + NSSwiftThunk next; + // Names the steps in errors; its result is the sequence's code. + MemberInfo member; +}; + +void StepCallback(const FunctionCallbackInfo& info) { + Isolate* isolate = info.GetIsolate(); + auto* box = static_cast( + info.Data().As()->Value(v8::kExternalPointerTypeTagDefault)); + auto* state = Caches::StateFor(isolate); + LoadedModule* loaded = LoadedFor(isolate, box->module); + if (state == nullptr || loaded == nullptr) { + return; + } + NSSwiftValue slot{}; + slot.tag = NSSwiftTagObject; + slot.payload.object = box->iterator; + + CallContext call; + call.api = &kAPI; + call.isolate = isolate; + call.context = isolate->GetCurrentContext(); + call.module = box->module; + call.loaded = loaded; + call.state = state; + call.member = &box->member; + call.resultCode = &box->member.signature.result; + state->calls.push_back(&call); + box->next(&call, &slot, 1); + state->calls.pop_back(); + if (!call.callbackException.IsEmpty()) { + isolate->ThrowException(call.callbackException.Get(isolate)); + } else if (!call.threw && call.completed) { + info.GetReturnValue().Set(call.result); + } +} + +// Turns a step function (a Promise of `[element]`, or `[]` at the end) into +// an async iterator, one step at a time. +constexpr const char* kIteratorFactory = R"js((function (step) { + let pending = Promise.resolve(); + return { + next() { + const result = pending.then(() => step()).then((values) => + values.length ? { value: values[0], done: false } : { value: undefined, done: true }); + pending = result.then(() => undefined, () => undefined); + return result; + }, + [Symbol.asyncIterator]() { + return this; + }, + }; +}))js"; + +MaybeLocal MakeAsyncIterator(Isolate* isolate, Local context, + const ModuleInfo* module, void* iterator, NSSwiftThunk next, + const ValueCode& code, const std::string& name) { + auto* state = Caches::StateFor(isolate); + if (state == nullptr) { + module->release(iterator); + return MaybeLocal(); + } + if (state->iteratorFactory.IsEmpty()) { + Local