Repository navigation
feat(swift-interop): adapter lifetime, cancellation, subscripts, Combine and operators (6/7) - #523
Draft
NathanWalker wants to merge 6 commits into
Draft
NathanWalker wants to merge 6 commits into
NathanWalker wants to merge 6 commits into
Conversation
|
Important Draft PR not reviewedDraft PRs are not automatically reviewed by default.
To automatically review draft PRs, update your CodeRabbit configuration: reviews:
auto_review:
drafts: true
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
This was referenced Oct 21, 2026
Draft
NathanWalker
marked this pull request as ready for review
October 21, 2026 13:50
NathanWalker
force-pushed
the
swift-interop/6-lifetime-async
branch
from
October 21, 2026 14:47
4eb9b59 to
d43b891
Compare
NathanWalker
force-pushed
the
swift-interop/5-values
branch
from
October 21, 2026 14:47
8c4d970 to
7a3876e
Compare
NathanWalker
marked this pull request as draft
October 21, 2026 16:34
…fetime rule (ABI 14)
A JS object passed as a protocol value now gets one Swift adapter per
protocol instead of a new one per call. Swift sees one identity, so
`removeAll { $0 === listener }` matches.
The runtime owns each adapter while its JS object lives. Before each
garbage collection it holds the JS object strongly if anything else
retains the adapter, and weakly otherwise. This follows NativeScript's
rule for the JS side of Objective-C objects:
- strong Swift references (`var delegate`) keep the JS object alive;
- `weak var delegate` holds it for as long as JS keeps it, then becomes
nil.
Once neither side holds them, both are collected. Previously a weakly
held adapter was freed as soon as the call returned.
Calls through an adapter whose JS object was collected fail like a JS
implementation that throws.
Thunk ABI 14: API slots 54 (adapterOf) and 55 (adoptAdapter).
An async member now takes an optional AbortSignal after its arguments: `await fetchItems(query, controller.signal)`. Aborting rejects the Promise at once with the signal's reason, as fetch does, and cancels the Swift task. Results the task reports afterwards are dropped. A signal that is already aborted rejects without calling Swift. The signal is taken off before overload resolution, so it never competes with real arguments. Async thunks keep their Task and register a cancel hook for the call's token. TypeScript declares `signal?: AbortSignal` on async members, and types/ns-swift.d.ts declares a minimal AbortSignal for programs without the DOM library. Thunk ABI 15: API slot 56 (setCancel).
…hods
A subscript with indices is now a JS method: `shelf.subscript("apples")`
reads it. A settable one also gets `shelf.setSubscript("apples", 3)`,
which assigns it, writing through a struct's box in place. Subscripts
are overloaded like methods, by their indices. Named methods keep
property access untouched and can't collide with a type's own `get` or
`set`.
The surface adds the setter as a synthetic member, and the emitter
spells `receiver[indices]` and `receiver[indices] = value`. Against the
iOS 18 SDK this adds 2 WeatherKit, 4 Foundation, 5 SwiftUI, 1 SwiftData
and 1 Charts callables. Every module's thunks still type-check.
…oop exit A bridged class or struct conforming to AsyncSequence now keeps its JS class and other members, and is also async-iterable through a synthetic `[Symbol.asyncIterator]()`. The element comes from its `Element` typealias, or else from its iterator's own `next()`. AnyPublisher, PassthroughSubject and CurrentValueSubject are async iterators over their `values` on iOS 15+, and null on earlier systems. Thunks import Combine when one is bridged. Async iterators now implement `return()`, so leaving a `for await` loop early releases the Swift iterator. That ends a stream Swift still produces: AsyncStream's onTermination sees `.cancelled`, and a publisher's subscription is cancelled.
Operators are now JS methods named for what they do: - `+` `-` `*` `/` `%` are `plus`, `minus`, `times`, `dividedBy` and `remainder`; - `==`, `<`, `<=`, `>` and `>=` are `equals`, `lessThan`, `lessThanOrEqualTo`, `greaterThan` and `greaterThanOrEqualTo`; - prefix `-` and `!` are `negated()` and `not()`. An operator whose left operand is its own type is an instance method of that operand (`a.plus(b)`); any other is static (`Vector2.times(2, v)`). Compound assignment, bitwise and custom operators stay unexposed. Equatable classes and structs (also through Hashable or Comparable) now get `equals(other)`, and Comparable ones get `compare(other)`, which returns -1, 0 or 1 from Swift's `<`. Symbol graphs omit the `==` the compiler synthesizes, so these come from the conformances. Against the iOS 18 SDK this adds 238 Foundation, 106 SwiftUI, 42 WeatherKit, 20 SwiftData, 6 Charts and 3 Observation callables. Every module's thunks still type-check.
A type parameter constrained to Sequence, Collection or another protocol arrays conform to now stands in as an array. With `where C.Element == X` the stand-in is `[X]`; otherwise it is an array of each of the element's own stand-ins. `C.Element` in the signature is substituted to match. `some Sequence<Int>` stands in as `[Int]`. A parameter typed `any Sequence<E>` (or another such protocol) takes an array, and a result of that type is copied out as one. Against the iOS 18 SDK this adds 5 Foundation and 2 SwiftUI callables. Every module's thunks still type-check.
NathanWalker
force-pushed
the
swift-interop/5-values
branch
from
October 21, 2026 16:44
7a3876e to
ae7d342
Compare
NathanWalker
force-pushed
the
swift-interop/6-lifetime-async
branch
from
October 21, 2026 16:44
d43b891 to
1c568f9
Compare
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Swift interop stack — review bottom-up; each PR shows only its own commits. Merge in order; after a squash merge, rebase the next branch onto
main. Design discussion: RFC NativeScript/rfcs#57..d.tsns:swiftcoreLifetime and call-shape work.
Protocol adapters follow NativeScript's lifetime rule for the JS side of Objective-C objects (ABI 14). Async calls take a trailing
AbortSignal(ABI 15).Evidence
removeAll { $0 === listener }never matched), and a weakly held adapter was freed as soon as the call returned. Async calls could not be cancelled.After: 134
ns:swiftTestRunner specs (17 new), covering adapter identity, weak delegates across GC, cancellation, subscripts, Combine and collection protocols.Merge Danger
Door: two-way
Blast Radius: ns:swift
Adapters hold JS objects strongly while Swift shares them and weakly otherwise, so leak or early-release bugs would show here first.