Skip to content

feat(swift-interop): returning closures, generic functions, tuples, Result and C structs (5/7) - #522

Draft
NathanWalker wants to merge 9 commits into
swift-interop/4-app-buildsfrom
swift-interop/5-values
Draft

NathanWalker wants to merge 9 commits into
swift-interop/4-app-buildsfrom
swift-interop/5-values

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
  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 ← this PR
  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

More Swift value shapes cross.

SwiftDemo.transform([1, 2], (n) => "#" + n);   // closures that return values or throw
SwiftDemo.largest(["b", "c"]);                 // generic functions: an overload per concrete stand-in
service.weather(location, WeatherQuery.daily); // generic functions instantiated from the unit's generic types
SwiftDemo.divide(7, 2);                        // labeled tuple → { quotient: 3, remainder: 1 }; Set ↔ JS Set
SwiftDemo.parseNumber("x");                    // Result → { case: "failure", value: SwiftError }
SwiftDemo.midpoint({ x: 0, y: 0 }, CGPointMake(4, 2)); // C structs as plain objects
CounterView.new().configure(1, null);          // Swift-only members of an Objective-C-visible Swift class

Also: optional elements in arrays and dictionaries, and some P parameters. The thunk ABI moves to 13.

Evidence

  • Before: closures could not return values, and generic functions, tuples, sets, Result and C structs did not cross.
    After: 117 ns:swift TestRunner specs (23 new) and 69 generator tests.

Merge Danger

Door: two-way

Blast Radius: ns:swift

Swift-only members install on NativeScript's own class for Objective-C-visible Swift classes and never replace NativeScript's members.

@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.

A closure parameter can now return a value, throw, or both. The JS
function's result converts through the value table. Called from another
thread, the closure waits for the function to run on the isolate's
thread, since Swift needs the value before it continues.

When the function throws or returns a value that does not convert, the
exception surfaces from the Swift call in progress. A `throws` closure
throws into Swift. Any other closure returns its result type's empty
value (nil, false, 0, "", an empty collection).

Thunk ABI 12: signature codes gain `(params>result)`, and API slot 51
(callbackCallEnd) ends an invocation and returns the result. Generated
TypeScript types the function's result.
A generic function, method or initializer is now instantiated once for
each concrete stand-in of its type parameters. Each instantiation is an
overload of its JS name, and TypeScript declares each one.

Stand-ins by constraint:
- none: Double, String and Bool;
- standard library protocols: those of Double (Int for integer
  protocols), String and Bool that conform;
- one protocol of the unit: its existential, which Swift opens at the
  call;
- a superclass: the superclass.

At most one number type stands in, since a JS number converts to every
one. Compiler verification drops any instantiation that does not
type-check.

Functions are left generic when:
- a parameter is used as a metatype or through an associated type;
- a parameter is constrained by another module's protocol;
- the arguments could not tell instantiations apart;
- they would need more than six instantiations;
- they are nested in a generic context.

When several overloads accept a call, the runtime now tells array
arguments apart by their first elements. The TypeScript writer and the
classifier's ambiguity check match this. Coverage counts a generic
function once, as generated when any of its instantiations is.

Against the iOS 18 SDK this adds, for example, JSONEncoder.encode(_:),
URL.appending(path:directoryHint:) and NavigationPath.append(_:). Every
module's thunks still type-check.
…neric types

A type parameter that a generic function passes to one of the unit's
generic types (`WeatherQuery<T>`, `Box<T>`) now stands in with that
type's instantiated arguments. The unit's own instantiations bound these
combinations, so up to sixteen are allowed instead of six.

WeatherKit's `weather(for:including:)` is now callable, with one overload
per WeatherQuery instantiation:
`service.weather(location, WeatherQuery.daily)` resolves to a
`Forecast<DayWeather>`. Combinations that name no existing instantiation
are rejected as unbridged.
`[T?]` and `[String: T?]` now cross as JS arrays and objects holding
nulls, in both directions. In thunks, the element readers return `T??`:
nil still means an element did not convert, and `.some(nil)` is a JS
null. Async sequences keep rejecting optional elements, since a nil
there ends the sequence.
Tuples of two or more values cross as JS arrays, or as plain objects
when every element is labeled: `(quotient: Int, remainder: Int)` becomes
`{ quotient, remainder }`. TypeScript types them as `[A, B]` or
`{ x: A; y: B }`. Elements may be optional.

`Set<T>` crosses as a JS Set copy, and arrays are also accepted as
input. TypeScript types it as `Set<T>` (input: `ReadonlySet<T>` or an
array).

New signature codes: `S[<code>]` and `*(<code>,...)`, with labels as
`*(<label>:<code>,...)`. Both travel as array slots and array reports,
so the API table is unchanged. Thunks report tuples through a new
`returnTuple` reporter method built on the array entries.

Against the iOS 18 SDK this adds 46 Foundation and 5 SwiftData
callables. Every module's thunks still type-check.
`any Error` now bridges as a value. In JS it is an Error named
SwiftError, with the Swift type in `swiftType`. From JS, any value is
accepted, and the Swift error describes itself by the JS error's
message. This covers `(Error?) -> Void` completion handlers.

`Result<Success, Failure>` crosses as the same tagged object enums with
associated values use: `{ case: "success", value }` or
`{ case: "failure", value }`, with no value for `Result<Void, F>`.
Failure is `any Error` or one of the unit's types. Completion handlers
taking a Result now bridge too, such as
`(Result<String, ShapeError>) -> Void`.

Thunk ABI 13:
- the signature codes `!` and `R(<success>,<failure>)`;
- API slots 52 (returnErrorValue) and 53 (completeErrorValue), which
  report an error as a value instead of throwing it.

The runtime gives a Result code its two cases, so case reporting and
conversion share the tagged-enum paths.
CGPoint, CGSize, CGRect, CGVector, CGAffineTransform, UIEdgeInsets,
NSDirectionalEdgeInsets, UIOffset, NSRange, CLLocationCoordinate2D,
MKCoordinateSpan and MKCoordinateRegion now cross as plain JS object
copies of their fields, for example `{ x, y }`. Inputs also accept
NativeScript's own struct values, such as `CGPointMake(1, 2)`.

Symbol graphs don't describe C struct fields, so a built-in table lists
them. Each struct travels as its fields' labeled-tuple code, so the
runtime is unchanged. Thunks build the struct through its memberwise
initializer and import its framework.

Against the iOS 18 SDK this adds 26 SwiftUI and 5 Charts callables.
Every module's thunks still type-check.
…classes

A Swift class that subclasses NSObject (a UIView or UIViewController
subclass, for example) stays NativeScript's Objective-C class. When the
module loads, its Swift-only members, the ones Objective-C cannot see,
now install on that class:
- instance members on its prototype;
- static members and Swift-only initializers (as `init`) on its
  constructor.

They never replace a member NativeScript already defines. The receiver
is NativeScript's wrapper of the object, checked with isKindOfClass.
Initializers return the object as NativeScript's wrapper.

The generator registers these classes under a new type kind
(NSSwiftTypeObjCClass, 6) with their Objective-C class name. The
runtime installs members only when NativeScript metadata describes that
exact class, since installing on the nearest described superclass would
be wrong. An app gets that metadata from its -Swift.h header.
TypeScript merges the members into the class's global declaration.
Members of UIResponder subclasses are isolated to the main actor, as
UIKit declares them.

TestRunner's metadata step now includes the SwiftDemo fixture's
-Swift.h, the way nsld.sh includes an app's.
A parameter typed `some P` is now an implicit type parameter. It is
instantiated the way named generic parameters are, and the bound picks
the stand-ins. For example, `describe(_ value: some
CustomStringConvertible)` gets overloads for a number, a string and a
boolean, `some Greeting` takes `any Greeting`, and `some UIView` takes
UIView. Each copy retypes the parameter to its stand-in, so opaque
results such as `some View` stay untouched.

Against the iOS 18 SDK this adds one Charts callable. SDK `some`
parameters are mostly SwiftUI modifiers in generic contexts. The
fixture's four new functions all bridge.
@NathanWalker
NathanWalker force-pushed the swift-interop/4-app-builds branch from 91839dc to e76a9ec Compare October 21, 2026 16:44
@NathanWalker
NathanWalker force-pushed the swift-interop/5-values branch from 7a3876e to ae7d342 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