Skip to content

feat(swift-interop): modules, generics, protocols, Observation and SwiftUI (3/7) - #520

Draft
NathanWalker wants to merge 6 commits into
swift-interop/2-runtimefrom
swift-interop/3-modules-protocols
Draft

NathanWalker wants to merge 6 commits into
swift-interop/2-runtimefrom
swift-interop/3-modules-protocols

Conversation

@NathanWalker

@NathanWalker NathanWalker commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

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.

  1. feat(metadata-generator): Swift symbol graphs to thunks and TypeScript declarations (Swift interop 1/7) #518 generator: symbol graphs → IR → thunks + .d.ts
  2. feat(runtime): ns:swift calls Swift through generated thunks (Swift interop 2/7) #519 runtime: ns:swift core
  3. feat(swift-interop): modules, generics, protocols, Observation and SwiftUI (3/7) #520 modules, generics, protocols, Observation, SwiftUI ← this PR
  4. feat(swift-interop): generate and link thunks in app builds (4/7) #521 app builds
  5. feat(swift-interop): returning closures, generic functions, tuples, Result and C structs (5/7) #522 more values: returning closures, generic functions, tuples, Result, C structs
  6. feat(swift-interop): adapter lifetime, cancellation, subscripts, Combine and operators (6/7) #523 adapter lifetime, cancellation, subscripts, Combine, operators
  7. feat(swift-interop): Any, UUID and Decimal, performance, and docs (Swift interop 7/7) #517 Any/UUID/Decimal, performance, docs

Swift APIs that span modules or are defined by protocols and generics.

new Kit.Pair(1, "one").swapped();              // generic types through the instantiations the code spells
SwiftDemo.introduce({ name: "js", greet: (o) => "hi " + o, reset() {} }, "ada"); // JS implements a Swift protocol
swift.observe(() => thermostat.celsius, update); // @Observable tracking
for await (const n of SwiftDemo.countUp(3)) {}   // AsyncStream → async iterator
new SwiftDemo.Badge("Inbox", 3).hostingController(); // SwiftUI view → UIViewController
  • Generation units: one IR across several modules, so types and extensions work across them (SwiftDemoKit fixture).
  • Generics: one JS class per generic family, instantiation chosen by arguments.
  • Protocols: JS objects become Swift conformers through generated adapters.
  • types/ns-swift.d.ts declares the ns:swift module itself.

Evidence

  • Before: only single-module, non-generic declarations cross.
    After: 94 ns:swift TestRunner specs (24 new), covering generics, protocol adapters on background threads, Observation, async iteration and SwiftUI hosting.

Merge Danger

Door: two-way

Blast Radius: ns:swift

Changes stay inside the Swift generator, SwiftInterop.mm and the fixtures.

@coderabbitai

coderabbitai Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Several Swift modules can now be generated as one unit (ABI 8), via
--swift-unit with a --swift-symbolgraph-dir per module. The unit is merged
into one IR. Every symbol keeps its own module, so declarations can name
another module's types, and extensions of those types become their
members. A unit has one thunk file, with shared support code and one type
table, and one `.d.ts` per module that imports the others' types.

At runtime, each ns_swift_register_<Module> symbol registers the whole
unit, and every module maps to it. Each isolate builds the unit's templates
once and gives every module its own ns:swift/<Module> namespace, so a type
has one JS class wherever it appears. Types and members register with
their module. Module-level overloads group per module.

A module that is an implementation detail of another (SwiftUICore of
SwiftUI) can't be imported. With --swift-reexport, which is implied for
that pair, its declarations join the module that re-exports it. As a unit,
SwiftUI and SwiftUICore verify and type-check with 2547 callables, against
873 for SwiftUI alone.

The fixture gains a SwiftDemoKit module, which SwiftDemo uses and
extends. It is built as its own static library target for TestRunner.
A generic type is now bridged through every concrete instantiation that
the unit's declarations spell: signatures and public typealiases (for
example `Measurement<UnitTemperature>` in WeatherKit). Instantiations found
inside other instantiations' members are followed two levels deep.

Before classification, instantiateGenerics adds a synthetic non-generic
type for each instantiation. It copies the generic type's members, each
with a substitution map for the generic parameters. Members whose
same-type requirements contradict the arguments are left out, such as a
`where UnitType == UnitMass` extension or a synthesized `Self == Style<T>`
member. The classifier, thunks and probes then treat instantiations like
any other type, and the compiler verifies every instantiated member.
Typealiases now bridge as the type they name.

Instantiations of one generic type share one JS class. They share a
module and a JS name, and the runtime groups them into a family: one
template and one member group per name. The constructor resolves among
every instantiation's initializers, and instance members only consider
the receiver's own instantiation. TypeScript declares the generic class
once from template capabilities (`class Pair<First, Second>`), names
instantiations with their arguments, and declares typealiases.

Fix: the compiler log is removed before each verification run. LLVM's
redirect doesn't truncate it, so a shorter second run kept the previous
run's errors.

At SDK scale, Foundation goes from 3038 to 3486 callables (74
instantiations) and WeatherKit from 267 to 379 (23). The WeatherKit and
Foundation unit has 3968 callables. All of them type-check.
A parameter typed by a module protocol (`any P`, or a bare `P`) now
accepts a Swift value that conforms to it, or a JS object implementing it
(ABI 9). A protocol is adoptable when:
- its requirements are instance methods and properties that bridge;
- it has no associated types or `Self` requirements;
- it has no static, async or main-actor requirements;
- it refines at most one module protocol.

For each adoptable protocol, the thunks get an __NSAdapter class
conforming to it. Each requirement calls the JS object's member of the
same name through new object-call entries and reads the returned value.
A failure makes a throwing requirement throw. Otherwise the requirement
returns its type's zero value, or traps when the type has none.

Results come back as the JS object they started as, as their own
registered class or struct, or boxed as the protocol, whose JS class
carries the protocol's members.

The runtime checks that a JS implementation provides every requirement,
and builds each protocol's requirement table: the refined protocol's
requirements, then its own. A requirement called from another thread runs
on the isolate's thread while the Swift caller waits. A JS exception raised
during a JS call takes precedence over the error a thunk reports. Callback
handles generalize to JS object handles. TypeScript declares protocols as
interfaces.

The fixture covers JS and Swift conformers, throwing requirements,
refinement, implementations Swift holds, and calls from a background task.
@NathanWalker
NathanWalker force-pushed the swift-interop/2-runtime branch from 5efedd7 to 63b3a51 Compare October 21, 2026 14:47
@NathanWalker
NathanWalker force-pushed the swift-interop/3-modules-protocols branch from b951726 to 39dac9d Compare October 21, 2026 14:47
@NathanWalker
NathanWalker marked this pull request as draft October 21, 2026 16:32
`ns:swift` gains track(read, onChange) and observe(read, onChange) (ABI
10).
- track is Swift's withObservationTracking. It runs `read`, and the getter
  thunks `read` reaches run inside the tracking scope, so the observable
  properties it touches are recorded. `onChange` runs once, when one of
  them is about to change.
- observe re-arms after every change, calls `onChange` after the change is
  made, and returns a stop function.

Every generated unit carries the tracker and registers it. The runtime
uses any loaded unit's tracker and throws what `read` throws. A change
made on another thread reaches JS on its thread.
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.
A bridged struct that conforms to SwiftUI.View gains a synthesized
hostingController() member. It returns a UIHostingController showing a
copy of the view, as an ordinary NativeScript UIViewController that can go
into a NativeScript layout. The thunk runs on the main actor and refuses
other threads. The compiler verifies it like every other thunk, and the
unit imports SwiftUI and UIKit when it hosts views.
@NathanWalker
NathanWalker force-pushed the swift-interop/3-modules-protocols branch from 39dac9d to 83c8aff Compare October 21, 2026 16:44
@NathanWalker NathanWalker added this to the 10.0 milestone Oct 21, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant