Repository navigation
feat(swift-interop): Any, UUID and Decimal, performance, and docs (Swift interop 7/7) - #517
Draft
NathanWalker wants to merge 8 commits into
Draft
NathanWalker wants to merge 8 commits into
NathanWalker wants to merge 8 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 |
NathanWalker
force-pushed
the
feat/swift-metadata-gen
branch
2 times, most recently
from
October 21, 2026 13:40
03bee89 to
257d21f
Compare
This was referenced Oct 21, 2026
NathanWalker
changed the base branch from
main
to
swift-interop/6-lifetime-async
October 21, 2026 13:41
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
feat/swift-metadata-gen
branch
from
October 21, 2026 14:47
257d21f to
dd47003
Compare
NathanWalker
marked this pull request as draft
October 21, 2026 16:31
…tics named name/length SwiftInteropBenchmarks.js now calls one API implemented twice and consumes every result in JS: TestFixtures' TNSBenchmark in Objective-C (the metadata path) and SwiftDemo's Benchmark in Swift (ns:swift). The cases cover: - calls of several shapes, short and 10 KB strings; - objects (construction, methods, properties); - 1000-element arrays and 10-key dictionaries in both directions; - CGPoint, native calls into JS functions and delegates; - errors, a 1000-record build/filter/sort batch, and async calls. In a Release TestRunner on the iPhone 17 Pro simulator (iOS 26), the Swift path takes 0.6 to 0.9 of the time for scalar calls, 0.42 for construction, about a third for collections, 0.27 for the batch and 0.15 for async calls. Native code calling a JS function is about 2x slower (0.2 vs 0.1 us per call). docs/swift-interop.md has the full table and method. Callbacks and delegate calls made on the JS thread now run directly, without locking or queueing. They also use the current context. A Swift static member named `name` or `length` crashed module loading: V8 aborts when a function template redefines its own built-ins. Those members are now defined on the class after it exists.
Loading WeatherKit's thunks aborted V8. A generic type whose instantiations register the same overload (WeatherKit's Forecast `==`, or a static whose signature does not involve the type's arguments) produced the same overload key twice on one JS class template, and V8 aborts when a template defines a property twice. The fix: - overload keys are now unique per group; - the generator drops a member that duplicates another's owner, kind, Swift name and signature, since graphs can declare one member twice; - a call that matches only identical overloads takes the first instead of reporting it as ambiguous. The SwiftDemoKit fixture's Box.make has one signature on every instantiation and covers this case.
…I 16) `Any` now crosses as whatever JS value fits: - null as NSNull, booleans, integral numbers as Int and others as Double, strings, arrays as [Any], and plain objects as [String: Any]; - Swift values of the unit's types and Objective-C objects as themselves. Swift reports Any back by the value's type. Foundation's bridged NSNumbers keep booleans apart from numbers. The unit's own classes, structs and enums return as themselves, through a generated `__nsReportAnyObject` with subclasses checked first. JSON-like payloads (`[String: Any]`) now work both ways, and TypeScript types Any as `unknown`. UUID and Character cross as strings, and an invalid value throws a TypeError. Decimal (NSDecimal on Apple platforms) crosses as a number. None of these needs a new runtime value kind. Thunk ABI 16: the signature code `a`. Against the iOS 18 SDK this adds 64 Foundation, 3 SwiftUI and 2 SwiftData callables. Every module's thunks still type-check.
The guide now opens with the three steps an app takes: write Swift, list the module in build.xcconfig, and import it. It ends with recipes for the patterns apps reach for: - a SwiftUI view in a NativeScript layout; - a JS delegate held weakly; - a cancellable request; - streams; - JSON-like payloads; - Swift-only API on a UIKit subclass.
The matched suite adds a 1 MB Data argument (native checksums it) and a 1 MB Data result (JS materializes the bytes). In a Release run on the iPhone 17 Pro simulator, the Swift path takes 0.10 of the Objective-C time for the argument and 0.67 for the result. The docs table is refreshed from the same run.
In a Release app whose unit is its own module, WeatherKit and a SwiftPM package, the thunks compile to 0.8 MB and the first ns:swift import takes 2.7 ms on the simulator.
… JS thread A closure or protocol call that Swift makes on the isolate's thread inside a context now converts its arguments through the return* entries into a pooled DirectCall instead of recording an AsyncOutcome tree, so an invocation no longer allocates per argument. Invocations from other threads keep the recorded path. The three callback runners share one invoke, closures cache their MemberInfo, and object handles cache their requirement keys. Release, iPhone 17 Pro simulator: a closure calling JS 100 times goes from 1.97x to 0.82x of an Objective-C block; a protocol call on a JS object from 2.59x to 1.08x of a JS subclass.
…sh figures TNSBenchmark.echoLater now runs on a global queue and completes on the main queue, as a nonisolated Swift async function runs on the cooperative pool and settles on the JS thread. The docs table is refreshed from one Release run.
NathanWalker
force-pushed
the
feat/swift-metadata-gen
branch
from
October 21, 2026 16:44
dd47003 to
97b5a0d
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:swiftcoreThe top of the stack: the last value types, performance work and documentation.
TNSBenchmark(Objective-C, the metadata path) andBenchmark(Swift) implement the same API.docs/swift-interop.mdadds a quick start in an app, recipes, the performance table, and startup and size figures.The full feature overview is below.
Whole-stack overview
Summary
This PR lets JavaScript call Swift-only APIs. The iOS runtime can only reach Objective-C today, so these APIs are invisible to it.
The metadata generator now reads Swift symbol graphs for one or more modules and writes two things:
A new
ns:swiftbuiltin loads the thunks at runtime.From JavaScript, this looks like:
What crosses today:
String,Character, optionals,Date,URL,UUIDandDecimal(strings),Data,Anyand[String: Any](JSON-like values), arrays and[String: T](optional elements as nulls),Set(a JS Set), tuples (arrays, or objects when labeled),any Error(aSwiftError),Result({ case: "success" | "failure", value }), and CoreGraphics/UIKit/CoreLocation structs such asCGRectandCLLocationCoordinate2D(plain objects).any Pfor a unit protocol, the superclass, or the arguments of the unit's instantiations of a generic type they take (WeatherQuery<T>).plus,minus,times,equals,lessThan,negated, ...), andequals()/compare()onEquatable/Comparabletypes.some Sequence<Int>,C: Collection where C.Element == Xandany Sequence<E>take JS arrays.AbortSignal), actor and@MainActorisolation, closures (including ones that return values or throw, waited for from any thread), inout parameters, subscripts (subscript()/setSubscript()), Objective-C objects (as NativeScript's own wrappers), and JS implementations of Swift protocols (one adapter per JS object, so Swift sees one identity; held by NativeScript's rule for the JS side of Objective-C objects, soweak var delegateworks while JS keeps the delegate).NSObject/UIViewsubclasses install on NativeScript's own class (instance members on its prototype, Swift-only initializers asinit), never replacing NativeScript's members.AsyncSequence, and Combine publishers throughvalues; leaving afor awaitloop early ends the Swift stream),@Observabletracking, and SwiftUI view hosting.throwbecomes a JSSwiftError, and declarations newer than the running OS throw instead of crashing.App builds: an app lists the modules JS uses in
App_Resources/iOS/build.xcconfig, asNS_SWIFT_MODULES = WeatherKit MyPackage. The template'snsld.shthen generates, verifies, compiles and links the thunks, so the CLI needs no change.docs/swift-interop.mdhas the full semantics, andNativeScript/runtime/SwiftInterop.his the binary contract.Evidence
require("ns:swift")doesn't exist. Swift-only declarations aren't reachable at all.After:
ns:swiftagainst theSwiftDemoandSwiftDemoKitfixtures linked into TestRunner. They cover worker isolates, background-thread callbacks and protocol calls, GC release, generics and generic functions, Result, tuples, sets, C structs, Objective-C-visible classes, weak delegates, cancellation, subscripts, AsyncSequence types, Combine publishers, Observation and async iteration..d.ts, plus atscpass/fail usage file.--swift-verifyagainst the iOS 27.1 simulator SDK (deployment target 18.0), and every generated thunk file type-checks with 0 errors.A generic function counts once, as callable when any of its instantiations is. WeatherKit's
weather(for:including:)has 8 typed overloads, one perWeatherQueryinstantiation.Benchmarks (Release, iPhone 17 Pro simulator):
SwiftInteropBenchmarks.jscalls one API implemented twice, in Objective-C (TNSBenchmark, the metadata path) and in Swift (Benchmark, ns:swift), and consumes every result in JS. Swift / Objective-C time:Datain / outCGPointin and outCallbacks made on the JS thread convert their arguments straight to JS values (a pooled frame, no per-argument allocation); callbacks from other threads are recorded and run on the JS thread.
docs/swift-interop.mdhas the full table and method.App build: a real app (its own Swift module, WeatherKit, a local SwiftPM package and Combine) built through
ns build ioswith the packed runtime. Debug and Release simulator builds pass a startup check of every feature (22 checks at ABI 16), and Release keeps the register symbols visible todlsym. The checks cover value-returning closures, generic functions, WeatherKit's genericweather(for:including:), a SwiftUIViewsubclass reached through the app's-Swift.hmetadata, subscripts,AbortSignalcancellation,Anypayloads and Combine publishers. In Release the thunk object for the app's module, WeatherKit (424 callables) and the package is 0.8 MB, and the firstns:swiftimport takes 2.7 ms. An unchanged module reuses its cached thunk object on relink.Still to come (draft)
NSData's), which NativeScript's metadata already covers in Objective-C form.Evidence
After: 0.82x and 1.08x. The remaining workloads in the overview's table run at 0.12x–1.16x.
ns:swift, including benchmarks).Merge Danger
Door: two-way until release, then one-way for the ABI
The runtime refuses thunks generated for another ABI version, so mismatches fail loudly. Once a runtime ships with ABI 16, apps' generated thunks depend on it.
Blast Radius: runtime-wide (opt-in)
Nothing runs until JS imports
ns:swift. Apps withoutNS_SWIFT_MODULESbuild as before.