Streaming is available in most browsers,
and in the Developer app.
-
What’s new in Swift
Join us for an update on Swift. We'll show you how APIs are becoming more extensible and expressive with features like parameter packs and macros. We'll also take you through improvements to interoperability and share how we're expanding Swift's performance and safety benefits everywhere from Foundation to large-scale distributed programs on the server.
Chapters
- 0:39 - Swift project update
- 2:44 - Using if/else and switch statements as expressions
- 3:52 - Result builders
- 4:53 - type parameter packs
- 9:34 - Swift macros
- 19:47 - Swift foundation
- 23:25 - Ownership
- 27:59 - C++ interoperability
- 32:41 - What's new in Swift Concurrency
- 38:20 - FoundationDB: A case study
Resources
Related Videos
WWDC23
- Beyond the basics of structured concurrency
- Expand on Swift macros
- Generalize APIs with parameter packs
- Meet SwiftData
- Mix Swift and C++
- What’s new in SwiftUI
- Write Swift macros
WWDC21
-
Download
♪ ♪ Ben: Hi, and welcome to "What's New in Swift 5.9." I'm Ben, and together with my colleague Doug, we'll take you through some of the improvements to the Swift language this year. We'll talk about some ways it's easier to express what you mean using Swift's clean syntax, some powerful new features that help framework authors make their new APIs more natural to use, and we'll look at some new ways to get more control over performance and safety in low-level code.
But let's start by talking about the Swift open source project. This is a great update for Swift, and it couldn't have happened without the Swift community, the contributors and users of the language who gather at swift.org, working together to evolve the language and support new initiatives. Swift follows an open process for language evolution. New features or significant behavior changes are proposed and reviewed in the open on the Swift forums. If you want to follow along, you can find a dashboard of all the language proposals on the Swift website.
A year ago, we saw a significant restructuring of the Swift Project governance. The core team announced the formation of the Language Steering Group, which took on primary responsibility for oversight of the Swift language and standard library evolution. Since then, the language group has overseen 40 new language proposals, and we're going to talk about several of them today.
But sometimes, individual language proposals come together as part of a wider theme, like the addition of Swift concurrency, which was introduced through ten separate proposals.
For cases like this, the language steering group has introduced a new way of tying together these proposals, through vision documents. These documents lay out a proposal for larger changes to the language. The first one to be accepted by the language steering group was a vision of Swift macros, a new feature in Swift 5.9 that we'll be covering later in this talk.
Of course, evolution of the language is only part of the work of the Swift community. A successful language needs much more than this. It needs great tooling, robust support for multiple platforms, and rich documentation. To oversee progress in this area, the core team is creating an ecosystem steering group parallel to the language steering group. This new structure was recently laid out in a blog post on Swift.org, a look out for further announcements about the formation of this new group soon. Now let's talk about changes to the Swift language this year, starting with better ways to express yourself in your code. Swift 5.9 includes what is probably our most commonly requested language enhancement, allowing if/else and switch statements to be used as expressions, providing a nice way to clean up your code.
For example, if you wanted to initialize a let variable based on some complex condition, you had to resort to tricks, like this hard-to-read compound ternary expression.
If expressions let you instead use a much more familiar and readable chain of if statements.
Another place where this helps is if you're initializing a global variable or a stored property. Single expressions work fine here, but if you wanted a condition, you had to use the trick of wrapping it in a closure that you then immediately executed.
Now that an if statement can be an expression, you can just drop that clutter, leaving you with neater code.
Result builders, the declarative syntax that drives features like SwiftUI, have seen significant improvements this year, including optimized type checking performance, code completion, and improved error messages.
This improvement was particularly focused on invalid code. Previously, result builder code with errors would take a long time to fail, as the type checker explored the many possible invalid paths.
As of Swift 5.8, invalid code type checks much faster, and error messages on invalid code are now more precise. For example, previously, some invalid code could lead to misleading errors in a completely different part of the result builder. In Swift 5.7, you'd receive an error like this, when the mistake actually lies up here.
In the latest release, you now receive a more accurate compiler diagnostic identifying the real issue.
Next, let's talk about how an addition to the generics system will allow for some great improvements to frameworks you use every day.
Almost all the Swift you write is using generics in some way. Type inference enables using these types without needing to understand the advanced capabilities they're built with. For example, the standard library Array type uses generics to provide an array that works with any type of data that you might want to store. When you use an array, all you need to do is provide the elements. There's no need to specify an explicit argument for the element type because it can be inferred from the element values.
Swift's generics system enables natural APIs that preserve type information so that your code operates seamlessly on the concrete types that you provide.
Here's an example inspired by the Swift compiler's own codebase: An API that takes a request type and evaluates it to produce a strongly typed value. So you can make a request for a Boolean value and get back a Boolean result.
Now, some APIs want to abstract not only over concrete types, but also the number of arguments that you pass in. So a function might take one request and return one result or two requests and return two results, or three and return three results.
To support this, the generics system has to be used together with a mechanism to handle multiple argument lengths so that all of the types that you pass in are linked to the types that you get out.
Before Swift 5.9, the only way to accomplish this pattern was by adding an overload for each specific argument length the API supported. But this approach has limitations. It forces an artificial upper bound on the number of arguments you can pass, resulting in compiler errors if you pass too many.
In this case, there isn't an overload that can handle more than six arguments, but we've passed seven.
This overloading pattern and its limitations are pervasive across APIs that conceptually handle arbitrary argument length.
In Swift 5.9, the generics system is gaining first-class support for this API pattern by enabling generic abstraction over argument length. This is done with a new language concept that can represent multiple individual type parameters that are "packed" together. This new concept is called a type parameter pack.
Using parameter packs, APIs that currently have individual overloads for each fixed argument length can be collapsed down into a single function.
Instead of accepting a single type parameter, Result, representing the result type of a single request, the evaluate function now accepts a separate request over each Result type.
The function returns each result instance in parenthesis, which will either be a single value or a tuple containing each value.
The evaluate function now handles all argument lengths with no artificial limit.
Type inference makes APIs using parameter packs natural to use without needing to know that the API is using them.
Calling our new evaluate function that can now handle any number of arguments, looks just like calling the fixed-length overloads. Swift infers the type of each argument along with the total number based on how you're calling the function. To learn about how to write generic library APIs like these, check out Generalize APIs using parameter packs.
Calling generic APIs in a natural way demonstrates one of Swift's fundamental design goals, clear expression through concise code.
Swift's advanced language features enable beautiful APIs that make it easier to say what you mean.
You benefit from these advanced language features from the very first lines of Swift that you ever write, whether it's using generics through arrays or dictionaries or designing a UI in SwiftUI. Swift's embrace of progressive disclosure means that you can learn about the more advanced features when you're ready to.
Swift 5.9 takes this design approach to the next level by providing library authors with a new toolbox for expressive API design using a new macro system. Here's Doug to tell you more. Doug: With macros, you can extend the capabilities of the language itself, eliminating boilerplate and unlocking more of Swift's expressive power. Let's consider the ever-present assert function, which checks whether a condition is true. Assert will stop the program if the condition is false, but when that happens, you get very little information about what went wrong, just the file and line number. You'll need to add some logging or trap the program in the debugger to learn more. There have been attempts to improve on this. XCTest provides an assert-equal operation that takes the two values separately, so when things fail, you can at least see the two values that aren't equal. But we still don't know which value is wrong here. Was it a, b, or the result of max? And this approach really doesn't scale for all of the kinds of checks we perform in asserts. If we go back to the original assertion, there is so much information here in the source code that we'd like to see in the log when our assertion fails. What was the code? What are the values of a, and b, and c? What did max produce? We couldn't improve this in Swift before without some custom feature, but macros make it possible.
In this example, the "hash-assert" syntax is expanding the macro called "assert." The hash syntax might look familiar because Swift already has a few things with this same spelling, like hash-file, hash-selector, and hash-warning. The assert macro looks and feels like the function version, but because it's a macro, it can provide a richer experience when the assertion fails.
Now the program is showing the code for the failing assertion, along with each of the values that contributed to the result.
In Swift, macros are APIs, just like types or functions, so you access them by importing the module that defines them. Like many other APIs, macros are distributed as packages. The assert macro here comes from the power asserts library, an open-source Swift package available on GitHub.
If you were to look into the macro package, you would find a macro declaration for assert. It is introduced with the "macro" keyword, but otherwise, it looks a lot like a function. There's a single unlabeled Bool parameter for the condition to be checked. If this macro produced a value, that result type would be written with the usual arrow syntax. Uses of the macro will be type checked against the parameters. That means, if you were to make a mistake in using the macro, such as forgetting to compare the maximum value against something, you'll get a useful error message immediately, before the macro is ever expanded. This allows Swift to provide a great development experience when using macros because macros operate on well-typed inputs and produce code that augments your program in predictable ways. Most macros are defined as "external macros," specifying the module and type for a macro implementation via strings. The external macro types are defined in separate programs that act as compiler plugins. The Swift compiler passes the source code for the use of the macro to the plugin. The plugin produces new source code, which is then integrated back into the Swift program. Here, the macro is expanding the assertion into code that captures the individual values and where they should be displayed in the source code. You wouldn't want to write the boilerplate yourself, but the macro does it for you. Macro declarations have one additional piece of information, their role. The assert macro here is a freestanding expression macro. It is called freestanding because it uses the "hash" syntax and operates directly on that syntax to produce new code. It is an expression macro because it can be used anywhere that one can produce a value. The new Foundation Predicate APIs provide a great example of an expression macro. The predicate macro allows one to write predicates in a type-safe manner using closures. The resulting predicate values can then be used with a number of other APIs, including the Swift collection operations SwiftUI and SwiftData.
The macro itself is generic over the set of input types. It accepts a closure argument that's a function operating on values of those input types and produces a Boolean result, does the set of inputs match or not? And the macro returns an instance of the new Predicate type, which can be used elsewhere in the program.
But there is more to macros because a lot of the boilerplate we end up writing is because we need to augment code we have written with something else derived from it. Let's take an example. I find that I use enums a lot in my own code, like this Path enum that captures either relative or absolute paths. But I'll often find myself needing to check for a specific case, say, by filtering all absolute paths from a collection. I can write this isAbsolute check as a computed property, of course. But sooner or later, I'm going to have to write another one. This is getting a bit tedious.
Macros can help here by generating the boilerplate for us.
Case detection is an attached macro, written using the same custom-attribute syntax as property wrappers. Attached macros take as input the syntax of the declaration they apply to-- here it's the enum declaration itself-- and will generate new code.
This macro-expanded code is normal Swift code, which the compiler integrates into your program. You can inspect the macro-generated code in your editor, debug into it, copy it out if you want to customize it further, and so on.
Attached macros are classified into five different roles based on how they augment the declaration they are attached to. The case detection macro we just discussed is a "member" attached macro, meaning that it creates new members in a type or extension.
Peer macros add new declarations alongside the declaration they're attached to, for example, to create a completion-handler version of an async method or vice-versa.
Accessor macros can turn a stored property into a computed property, which can be used to perform specific actions on property access or abstract the actual storage in a manner similar to, but more flexible than property wrappers. And attached macros can introduce attributes onto specific members of a type, as well as add new protocol conformances.
Several attached macro roles can be composed together to achieve useful effects. One important example of this is with observation.
Observation has always been a part of SwiftUI. To be able to observe changes to the properties of a class, one need only make the type conform to ObservableObject, and mark every property at-Published, and use the ObservedObject property wrapper in your view.
That's a bunch of steps, and missing a step can mean that the UI doesn't update as expected. We can do better with macro-based observation.
Attaching the Observable macro to a class provides observation for all of its stored properties. There is no need to annotate each stored property or worry about what happens if you don't because the Observable macro handles it all.
The Observable macro works through composition of three macro roles. Let's dive into how these roles work together.
Each macro role corresponds to a specific way in which the Person class is augmented by the Observable macro. The member role introduces new properties and methods.
The member attribute role will add the @ObservationTracked macro to the stored properties of the observed class, which in turn expands to getters and setters to trigger observation events. Finally, the conformance role introduces the conformance to the Observable protocol.
This may look like a lot of code, but it's all just normal Swift code, and it's neatly folded away behind the Observable macro.
Whenever you need to see how any macro expands to better understand its effect on your program, it's right there at your fingertips in Xcode.
Use the "Expand Macro" action to see the macro-expanded source code in your editor. Any error messages within macro-generated code will automatically show the expanded code, and you can step into and out of it with your debugger.
Swift macros provide a new tool for enabling more expressive APIs and eliminating boilerplate from your Swift code, helping unlock Swift's expressive power. Macros type-check their inputs, produce normal Swift code, and integrate at defined points in your program, so their effects are easy to reason about. And any time you need to understand what a macro did, its expanded source code is right there in your editor. We've just scratched the surface of macros. "Expand on Swift macros" will go deep into the design of Swift macros to answer all of those questions you must have. And you can get hands-on implementing your own macros with "Write Swift macros." I can't wait to see what new macros the Swift community will build.
Ben: From the start, Swift was designed to be a scalable language.
Swift's design emphasizes expressivity with clear and concise code that is low on ceremony and easy to read and write. By leveraging Swift's powerful features, like generics and native concurrency support, frameworks like SwiftUI or SwiftData let you quickly achieve the results you want, leaving you more time to focus on what matters.
Despite these high-level capabilities though, Swift is also efficient. It compiles natively, and its use of value types and of reference counting instead of garbage collection means it's able to achieve a low memory footprint.
This scalability means we're able to push Swift to more places than was previously possible with Objective-C, to low-level systems, where previously you might expect to have to use C or C++. This means bringing Swift's clearer code and critical safety guarantees to more places. We recently open sourced the start of a rewrite of the Foundation framework in Swift. This initiative will lead to a single shared implementation of Foundation on both Apple and non-Apple platforms. But it also meant rewriting large amounts of Objective-C and C code in Swift.
As of MacOS Sonoma and iOS 17, there are new Swift-backed implementations of essential types like Date and Calendar, of formatting and internationalization essentials like Locale and AttributedString, and a new Swift implementation of JSON encoding and decoding. And the performance wins have been significant.
Calendar's ability to calculate important dates can take better advantage of Swift's value semantics to avoid intermediate allocations, resulting in over a 20% improvement in some benchmarks.
Date formatting using FormatStyle also gained some major performance upgrades, showing a massive 150% improvement in a benchmark of formatting with a standard date and time template.
Even more exciting are the improvements to JSON decoding in the new package. Foundation has a brand-new Swift implementation for JSONDecoder and JSONEncoder, eliminating costly roundtrips to and from the Objective-C collection types. The tight integration of parsing JSON in Swift for initializing Codable types improves performance too. In benchmarks parsing test data, the new implementation is between two and five times faster. These improvements came from both reducing the bridging cost from the old Objective-C implementation to Swift, but also by the new Swift-based implementations being faster.
Let's look at one benchmark as an example. In Ventura, calling enumerateDates from Objective-C was slightly faster than calling it from Swift because of bridging costs. In MacOS Sonoma, calling that same functionality from Swift is 20% faster. Some of that speed up comes from eliminating bridging costs, but the new function implementation itself is also faster, as seen when calling it from Objective-C. This particular date calculation is not overly complex, so this is a great way of seeing the reduction in overhead between the two languages. Now, sometimes, when you're operating at lower levels of the system, you need more fine-grained control to achieve a necessary level of performance. Swift 5.9 introduces some new opt-in capabilities that help you achieve this level of control. These capabilities focus on the concept of ownership, that is, what part of the code "owns" a value as it passes around your application.
To see when you might want to use these features, let's first look at some example code.
Here we have a very simple wrapper for a file descriptor that would allow us to give low-level system calls a nicer Swift interface. But there’s still some easy ways to make mistakes with this API. For example, you might try to write to the file after you’ve called close. And you have to be careful to always close it manually by calling the close method before the type goes out of scope. Otherwise, you would get a resource leak.
One solution would be to make it a class with a deinit that closes it automatically when the type goes out of scope.
But that has different downsides, like making an additional memory allocation, which is usually not a big problem, except in some very constrained systems contexts.
Classes also have reference semantics. You might unintentionally end up sharing a file descriptor type across threads, leading to race conditions, or store it unintentionally.
But let's go back and look at the struct version.
Really, this struct also behaves like a reference type. It holds an integer that references the true value, which is an open file. Making a copy of this type could also lead to unintentional sharing of mutable state across your app in ways that could lead to bugs. What you want is to suppress the ability to make a copy of this struct.
Swift types, whether structs or classes, are copyable by default. This is the right choice most of the time. While excessive unnecessary copies can sometimes be a bottleneck in your code, it's better to spend the time finding those bottlenecks in instruments occasionally than to be constantly bothered by the compiler requiring you to be explicit about those copies.
But sometimes that implicit copy isn't what you want--in particular, when making copies of a value might lead to correctness issues, like with our file descriptor wrapper. In Swift 5.9, you can do that with this new syntax that can be applied to struct and enum declarations and that suppresses the implicit ability to copy a type. Once a type is non-copyable, you can give it a deinit, like you can a class, that will run when a value of the type goes out of scope.
Non-copyable types can also be used to solve the problem of calling close, and then using other methods.
The close operation can be marked as consuming. Calling a consuming method or argument gives up ownership of a value to the method you called. Since our type is not copyable, giving up ownership means you can no longer use the value.
By default, methods in Swift borrow their arguments, including self. So you can call the write method, which borrows the file descriptor, uses it to write out to the buffer, and after that, ownership of the value returns to the caller, and you can call another method, like close.
But since close has been marked as consuming, not the default of borrowing, it must be its final use.
This means, if you close the file first and then attempt to call another method, like write, you'll get an error message at compile time, rather than a runtime failure. The compiler will also indicate where the consuming use occurred.
Non-copyable types are a powerful new feature for systems level programming in Swift. They're still at an early point in their evolution. Later versions of Swift will expand on non-copyable types in generic code.
If you're interested in following along with this work, it's being actively discussed on the Swift forums. Doug: A key to Swift's success has been its interoperability with Objective-C. From the start, developers have been able to take incremental steps towards Swift adoption in their existing code bases, mixing in Swift a single file or module at a time. But we know a lot of you don't just have code written in Objective-C. Many apps also have core business logic implemented in C++, and interfacing to that has not been so easy. Often it meant adding an extra manual bridging layer, going from Swift, through Objective-C, and then into C++, and all the way back. Swift 5.9 introduces the ability to interact with C++ types and functions directly from Swift. C++ interoperability works just like Objective-C interoperability always has, mapping C++ APIs into their Swift equivalents that you can use directly from Swift code.
C++ is a large language with its own notions of ideas like classes, methods, containers, and so on. The Swift compiler understands common C++ idioms, so many types can be used directly. For example, this Person type defines the five special member functions expected of a C++ value type: Copy and move constructors, assignment operators, and a destructor. The Swift compiler treats this as a value type and will automatically call the right special member function at the right time. Additionally, C++ containers like vectors and maps are accessible as Swift collections.
The result of all of this is that we can write straightforward Swift code that makes direct use of C++ functions and types. We can filter over the vector of Person instances, calling C++ member functions and accessing data members directly.
In the other direction, using Swift code from C++ is based on the same mechanism as with Objective-C. The Swift compiler will produce a "generated header" that contains a C++ view on the Swift APIs. However, unlike with Objective-C, you don't need to restrict yourself to only using Swift classes annotated with the objc attribute. C++ can directly use most Swift types and their full APIs, including properties, methods, and initializers, without any bridging overhead.
Here we can see how C++ can make use of our Point struct. After including the generated header, C++ can call Swift initializers to create Point instances, invoke mutating methods, and access both stored and computed properties, all without any change to the Swift code itself.
Swift's C++ interoperability makes it easier than ever to integrate Swift with existing C++ code bases. Many C++ idioms can be directly expressed in Swift, often automatically, but occasionally requiring some annotations to indicate the desired semantics. And Swift APIs can be directly accessed from C++, no annotation or code changes required, making it possible to incrementally adopt Swift throughout a code base using any mix of C, C++, and Objective-C.
C++ interoperability is an evolving story, guided by the C++ interoperability workgroup. For more information, please see the "Mix Swift and C++" talk, or join us in the discussion on the Swift forums.
Interoperability at the language level is really important, but you also have to be able to build your code. And having to replace your existing build system with Xcode or the Swift Package Manager to even get started with Swift can be just as big a barrier as rewriting a large amount of code.
That's why we worked with the CMake community to improve Swift support in CMake. You can integrate Swift code into your CMake build by declaring Swift as one of the languages for the project and putting Swift files into a target.
More importantly, you can mix C++ and Swift within a single target, and CMake will be sure to compile each separately and link all of the appropriate supporting libraries and runtimes for both languages. This means you can start adopting Swift in your cross-platform C++ projects today, file by file or target by target. We're also providing a sample repository with CMake projects containing Swift and mixed C++/Swift targets, including using the bridging and generated headers, to help you get started.
A few years ago, we introduced a new concurrency model into Swift based on the building blocks of async/await, structured concurrency, and actors. Swift's concurrency model is an abstract model, which can be adapted to different environments and libraries. The abstract model has two main pieces: Tasks and actors. Tasks represent a sequential unit of work that can conceptually run anywhere. Tasks can be suspended whenever there's an "await" in the program, and then resume once the task can continue.
Actors are a synchronization mechanism that provide mutually-exclusive access to isolated state. Entering an actor from the outside requires an "await" because it may suspend the task.
Tasks and actors are integrated into the abstract language model, but within that model, they can be implemented in different ways to fit different environments. Tasks are executed on the global concurrent pool. How that global concurrent pool decides to schedule work is up to the environment. For Apple's platforms, the Dispatch library provides optimized scheduling for the whole operating system, and has been extensively tuned for each platform. In more restrictive environments, the overhead of a multithreaded scheduler may not be acceptable. There Swift's concurrency model is implemented with a single-threaded cooperative queue. The same Swift code works in both environments because the abstract model is flexible enough to map to diverse runtime environments.
Additionally, interoperability with callback-based libraries was built into Swift's async/await support from the beginning. The withCheckedContinuation operations allow one to suspend a task, and then resume it later in response to a callback. This enables integration with existing libraries that manage tasks themselves.
The standard implementation of actors in the Swift concurrency runtime is a lock-free queue of tasks to execute on the actor, but it's not the only possible implementation. In a more restricted environment, one might not have atomics, and instead could use another concurrency primitive such as spinlocks. If that environment were single-threaded, no synchronization is needed, but the actor model maintains the abstract concurrency model for the program regardless. You could still take that same code to another environment that is multi-threaded. With Swift 5.9, custom actor executors allow a particular actor to implement its own synchronization mechanism. This makes actors more flexible and adaptable to existing environments. Let's take an example.
Here we consider an actor that manages a database connection. Swift ensures mutually-exclusive access to the storage of this actor, so there won't be any concurrent access to the database. However, what if you need more control over the specific way in which synchronization is done? For example, what if you want to use a specific dispatch queue for your database connection, perhaps because that queue is shared with other code that hasn't adopted actors? With custom actor executors, you can.
Here we've added a serial dispatch queue to our actor and an implementation of the unowned executor property that produces the executor corresponding to that dispatch queue. With this change, all of the synchronization for our actor instances will happen through that queue.
When you "await" on the call to pruneOldEntries from outside the actor, this will now perform a dispatch-async on the corresponding queue. This gives you more control over how individual actors provide synchronization, and even lets you synchronize an actor with other code that isn't using actors yet, perhaps because it's written in Objective-C or C++.
The synchronization of actors via dispatch queues is made possible because dispatch queue conforms to the new SerialExecutor protocol. You can provide your own synchronization mechanism to use with actors by defining a new type that conforms to this protocol which has only few core operations: Checking whether the code is already executing in the context of the executor. For example, are we running on the main thread? Extracting an unowned reference to the executor to allow access to it without excess reference-counting traffic. And the most core operation, enqueue, which takes ownership of an executor "job." A job is part of an asynchronous task that needs to run synchronously on the executor. At the point where enqueue is called, it's the responsibility of the executor to run that job at some point when there's no other code running on the serial executor. For example, enqueue for a dispatch queue would call dispatch async on that queue.
Swift Concurrency has been in use for a few years now, and its abstract model consisting of tasks and actors covers a large range of concurrent programming tasks. The abstract model itself is quite flexible, making it adaptable to different execution environments, from iPhones to Apple Watches, to servers and beyond. It also allowed customization at key points to enable it to interoperate with code that hasn't fully adopted Swift Concurrency yet. For more information, please see our "Behind the Scenes" talk, as well as "Beyond the basics of Structured Concurrency." I want to wrap up with a bit of a case study of Swift operating in an environment very different from the iOS or MacOS apps where we are used to seeing it. FoundationDB is a distributed database, providing a scalable solution for very large key-value stores running on commodity hardware and supporting a variety of platforms, including MacOS, Linux, and Windows.
FoundationDB is an open-source project with a large code base written in C++. The code is heavily asynchronous, with its own form of distributed actors and runtime that provides a critically important deterministic simulation environment for testing purposes. FoundationDB was looking to modernize their code base and found Swift to be a good match for its performance, safety, and code clarity. A complete rewrite would be a big, risky endeavor. Instead, we leveraged Swift's interoperability to integrate into the existing code base. For example, here's a part of the C++ implementation of FoundationDB's "master data" actor.
There's a lot going on here, and you don't need to understand all of this C++. However, I'd like to point out a few key aspects of the code. First, C++ doesn't have async/await, so FoundationDB has their own preprocessor-like approach to emulate it.
Like many C++ code bases, they've implemented their own C++ Future type to manage asynchronous tasks.
These pair with explicit messaging to send responses to the requests. Note the careful pairing of sending a reply with returning from the function. Finally, FoundationDB has its own reference-counted smart pointers to help manage memory automatically. We can implement this whole thing much more cleanly in Swift.
That's better. This function can be directly implemented as an async function in Swift. We have a normal return type and normal return statements to provide the response to this request, so you can't ever be out of sync. We have an "await" to indicate the suspension point in the same manner as all other Swift async code. And this Swift code ties in with the C++ Future type adapted using continuations.
We're using a number of C++ types here. The MasterData type in C++ was using a reference-counted smart pointer. By annotating the type in C++, the Swift compiler can use this type like any other class, automatically managing reference counts for us.
Other types, such as the request and reply types, are C++ value types being used directly in Swift. And the interoperability goes both ways. This asynchronous function, and indeed, all the work introduced by the Swift concurrency model, run on FoundationDB's existing deterministic runtime. So we can get the benefits of Swift where we want it, interfacing with the existing C++ to enable gradual adoption throughout.
In this session, we've covered a lot of ground. We described features like parameter packs and macros that enable more expressive APIs and can help you write better code faster. We talked about the use of Swift in performance-sensitive code and the introduction of non-copyable types to provide resource management without reference-counting overhead.
Then we dove into C++ interoperability, which provides support for using C++ APIs in Swift and vice-versa, making it easier to bring the benefits of Swift to more of your code.
Finally, we talked about how Swift's flexible concurrency model can adapt to myriad environments across devices and languages to make concurrency easier and safer. Parameter packs, macros, non-copyable types, and all of the other language enhancements in Swift 5.9 were designed and developed openly through the Swift Evolution process, and community feedback was crucial in shaping these features. Swift 5.9 is the culmination of countless contributions from members across the Swift community, including active design discussions, bug reports, pull requests, educational content, and more. Thank you for making Swift 5.9 the great release it is. ♪ ♪
-
-
3:06 - Hard-to-read compound ternary expression
let bullet = isRoot && (count == 0 || !willExpand) ? "" : count == 0 ? "- " : maxDepth <= 0 ? "▹ " : "▿ "
-
3:19 - Familiar and readable chain of if statements
let bullet = if isRoot && (count == 0 || !willExpand) { "" } else if count == 0 { "- " } else if maxDepth <= 0 { "▹ " } else { "▿ " }
-
3:30 - Initializing a global variable or stored property
let attributedName = AttributedString(markdown: displayName)
-
3:46 - In 5.9, if statements can be an expression
let attributedName = if let displayName, !displayName.isEmpty { AttributedString(markdown: displayName) } else { "Untitled" }
-
4:31 - In Swift 5.7, errors may appear in a different place
struct ContentView: View { enum Destination { case one, two } var body: some View { List { NavigationLink(value: .one) { // The issue actually occurs here Text("one") } NavigationLink(value: .two) { Text("two") } }.navigationDestination(for: Destination.self) { $0.view // Error occurs here in 5.7 } } }
-
4:47 - In Swift 5.9, you now receive a more accurate compiler diagnostic
struct ContentView: View { enum Destination { case one, two } var body: some View { List { NavigationLink(value: .one) { //In 5.9, Errors provide a more accurate diagnostic Text("one") } NavigationLink(value: .two) { Text("two") } }.navigationDestination(for: Destination.self) { $0.view // Error occurs here in 5.7 } } }
-
5:47 - An API that takes a request type and evaluates it to produce a strongly typed value
struct Request<Result> { ... } struct RequestEvaluator { func evaluate<Result>(_ request: Request<Result>) -> Result } func evaluate(_ request: Request<Bool>) -> Bool { return RequestEvaluator().evaluate(request) }
-
6:03 - APIs that abstract over concrete types and varying number of arguments
let value = RequestEvaluator().evaluate(request) let (x, y) = RequestEvaluator().evaluate(r1, r2) let (x, y, z) = RequestEvaluator().evaluate(r1, r2, r3)
-
6:35 - Writing multiple overloads for the evaluate function
func evaluate<Result>(_:) -> (Result) func evaluate<R1, R2>(_:_:) -> (R1, R2) func evaluate<R1, R2, R3>(_:_:_:) -> (R1, R2, R3) func evaluate<R1, R2, R3, R4>(_:_:_:_:)-> (R1, R2, R3, R4) func evaluate<R1, R2, R3, R4, R5>(_:_:_:_:_:) -> (R1, R2, R3, R4, R5) func evaluate<R1, R2, R3, R4, R5, R6>(_:_:_:_:_:_:) -> (R1, R2, R3, R4, R5, R6)
-
6:47 - Overloads create an arbitrary upper bound for the number of arguments
//This will cause a compiler error "Extra argument in call" let results = evaluator.evaluate(r1, r2, r3, r4, r5, r6, r7)
-
7:12 - Individual type parameter
<each Result>
-
7:36 - Collapsing the same set of overloads into one single evaluate function
func evaluate<each Result>(_: repeat Request<each Result>) -> (repeat each Result)
-
8:21 - Calling updated evaluate function looks identical to calling an overload
struct Request<Result> { ... } struct RequestEvaluator { func evaluate<each Result>(_: repeat Request<each Result>) -> (repeat each Result) } let results = RequestEvaluator.evaluate(r1, r2, r3)
-
10:01 - It isn't clear why an assert function fails
assert(max(a, b) == c)
-
10:20 - XCTest provides an assert-equal operation
XCAssertEqual(max(a, b), c) //XCTAssertEqual failed: ("10") is not equal to ("17")
-
11:02 - Assert as a macro
#assert(max(a, b) == c)
-
11:42 - Macros are distributed as packages
import PowerAssert #assert(max(a, b) == c)
-
12:07 - Macro declaration for assert
public macro assert(_ condition: Bool)
-
12:26 - Uses are type checked against the parameters
import PowerAssert #assert(max(a, b)) //Type 'Int' cannot be a used as a boolean; test for '!= 0' instead
-
12:52 - A macro definition
public macro assert(_ condition: Bool) = #externalMacro( module: “PowerAssertPlugin”, type: “PowerAssertMacro" )
-
13:11 - Swift compiler passes the source code for the use of the macro
#assert(a == b)
-
13:14 - Compiler plugin produces new source code, which is integrated back into the Swift program
PowerAssert.Assertion( "#assert(a == b)" ) { $0.capture(a, column: 8) == $0.capture(b, column: 13) }
-
13:33 - Macro declarations include roles
// Freestanding macro roles @freestanding(expression) public macro assert(_ condition: Bool) = #externalMacro( module: “PowerAssertPlugin”, type: “PowerAssertMacro" )
-
13:53 - New Foundation Predicate APIs uses a `@freestanding(expression)` macro role
let pred = #Predicate<Person> { $0.favoriteColor == .blue } let blueLovers = people.filter(pred)
-
14:14 - Predicate expression macro
// Predicate expression macro @freestanding(expression) public macro Predicate<each Input>( _ body: (repeat each Input) -> Bool ) -> Predicate<repeat each Input>
-
14:48 - Example of a commonly used enum
enum Path { case relative(String) case absolute(String) }
-
15:01 - Checking a specific case, like when filtering all absolute paths
let absPaths = paths.filter { $0.isAbsolute }
-
15:09 - Write an `isAbsolute` check as a computer property...
extension Path { var isAbsolute: Bool { if case .absolute = self { true } else { false } } }
-
15:12 - ...And another for `isRelative`
extension Path { var isRelative: Bool { if case .relative = self { true } else { false } } }
-
15:17 - Augmenting the enum with an attached macro
@CaseDetection enum Path { case relative(String) case absolute(String) } let absPaths = paths.filter { $0.isAbsolute }
-
15:36 - Macro-expanded code is normal Swift code
enum Path { case relative(String) case absolute(String) //Expanded @CaseDetection macro integrated into the program. var isAbsolute: Bool { if case .absolute = self { true } else { false } } var isRelative: Bool { if case .relative = self { true } else { false } } }
-
16:57 - Observation in SwiftUI prior to 5.9
// Observation in SwiftUI final class Person: ObservableObject { @Published var name: String @Published var age: Int @Published var isFavorite: Bool } struct ContentView: View { @ObservedObject var person: Person var body: some View { Text("Hello, \(person.name)") } }
-
17:25 - Observation now
// Observation in SwiftUI @Observable final class Person { var name: String var age: Int var isFavorite: Bool } struct ContentView: View { var person: Person var body: some View { Text("Hello, \(person.name)") } }
-
17:42 - Observable macro works with 3 macro roles
@attached(member, names: ...) @attached(memberAttribute) @attached(conformance) public macro Observable() = #externalMacro(...).
-
17:52 - Unexpanded macro
@Observable final class Person { var name: String var age: Int var isFavorite: Bool }
-
18:05 - Expanded member attribute role
@Observable final class Person { var name: String var age: Int var isFavorite: Bool internal let _$observationRegistrar = ObservationRegistrar<Person>() internal func access<Member>( keyPath: KeyPath<Person, Member> ) { _$observationRegistrar.access(self, keyPath: keyPath) } internal func withMutation<Member, T>( keyPath: KeyPath<Person, Member>, _ mutation: () throws -> T ) rethrows -> T { try _$observationRegistrar.withMutation(of: self, keyPath: keyPath, mutation) } }
-
18:12 - Member attribute role adds `@ObservationTracked` to stored properties
@Observable final class Person { @ObservationTracked var name: String @ObservationTracked var age: Int @ObservationTracked var isFavorite: Bool internal let _$observationRegistrar = ObservationRegistrar<Person>() internal func access<Member>( keyPath: KeyPath<Person, Member> ) { _$observationRegistrar.access(self, keyPath: keyPath) } internal func withMutation<Member, T>( keyPath: KeyPath<Person, Member>, _ mutation: () throws -> T ) rethrows -> T { try _$observationRegistrar.withMutation(of: self, keyPath: keyPath, mutation) } }
-
18:16 - The @ObservationTracked macro adds getters and setters to stored properties
@Observable final class Person { @ObservationTracked var name: String { get { … } set { … } } @ObservationTracked var age: Int { get { … } set { … } } @ObservationTracked var isFavorite: Bool { get { … } set { … } } internal let _$observationRegistrar = ObservationRegistrar<Person>() internal func access<Member>( keyPath: KeyPath<Person, Member> ) { _$observationRegistrar.access(self, keyPath: keyPath) } internal func withMutation<Member, T>( keyPath: KeyPath<Person, Member>, _ mutation: () throws -> T ) rethrows -> T { try _$observationRegistrar.withMutation(of: self, keyPath: keyPath, mutation) } }
-
18:33 - All that Swift code is folded away in the @Observable macro
@Observable final class Person { var name: String var age: Int var isFavorite: Bool }
-
23:59 - A wrapper for a file descriptor
struct FileDescriptor { private var fd: CInt init(descriptor: CInt) { self.fd = descriptor } func write(buffer: [UInt8]) throws { let written = buffer.withUnsafeBufferPointer { Darwin.write(fd, $0.baseAddress, $0.count) } // ... } func close() { Darwin.close(fd) } }
-
24:30 - The same FileDescriptor wrapper as a class
class FileDescriptor { private var fd: CInt init(descriptor: CInt) { self.fd = descriptor } func write(buffer: [UInt8]) throws { let written = buffer.withUnsafeBufferPointer { Darwin.write(fd, $0.baseAddress, $0.count) } // ... } func close() { Darwin.close(fd) } deinit { self.close(fd) } }
-
25:05 - Going back to the struct
struct FileDescriptor { private var fd: CInt init(descriptor: CInt) { self.fd = descriptor } func write(buffer: [UInt8]) throws { let written = buffer.withUnsafeBufferPointer { Darwin.write(fd, $0.baseAddress, $0.count) } // ... } func close() { Darwin.close(fd) } }
-
26:06 - Using Copyable in the FileDescriptor struct
struct FileDescriptor: ~Copyable { private var fd: CInt init(descriptor: CInt) { self.fd = descriptor } func write(buffer: [UInt8]) throws { let written = buffer.withUnsafeBufferPointer { Darwin.write(fd, $0.baseAddress, $0.count) } // ... } func close() { Darwin.close(fd) } deinit { Darwin.close(fd) } }
-
26:35 - `close()` can also be marked as consuming
struct FileDescriptor { private var fd: CInt init(descriptor: CInt) { self.fd = descriptor } func write(buffer: [UInt8]) throws { let written = buffer.withUnsafeBufferPointer { Darwin.write(fd, $0.baseAddress, $0.count) } // ... } consuming func close() { Darwin.close(fd) } deinit { Darwin.close(fd) } }
-
26:53 - When `close()` is called, it must be the final use
let file = FileDescriptor(fd: descriptor) file.write(buffer: data) file.close()
-
27:20 - Compiler errors instead of runtime failures
let file = FileDescriptor(fd: descriptor) file.close() // Compiler will indicate where the consuming use is file.write(buffer: data) // Compiler error: 'file' used after consuming
-
28:52 - Using C++ from Swift
// Person.h struct Person { Person(const Person &); Person(Person &&); Person &operator=(const Person &); Person &operator=(Person &&); ~Person(); std::string name; unsigned getAge() const; }; std::vector<Person> everyone(); // Client.swift func greetAdults() { for person in everyone().filter { $0.getAge() >= 18 } { print("Hello, \(person.name)!") } }
-
29:51 - Using Swift from C++
// Geometry.swift struct LabeledPoint { var x = 0.0, y = 0.0 var label: String = “origin” mutating func moveBy(x deltaX: Double, y deltaY: Double) { … } var magnitude: Double { … } } // C++ client #include <Geometry-Swift.h> void test() { Point origin = Point() Point unit = Point::init(1.0, 1.0, “unit”) unit.moveBy(2, -2) std::cout << unit.label << “ moved to “ << unit.magnitude() << std::endl; }
-
35:30 - An actor that manages a database connection
// Custom actor executors actor MyConnection { private var database: UnsafeMutablePointer<sqlite3> init(filename: String) throws { … } func pruneOldEntries() { … } func fetchEntry<Entry>(named: String, type: Entry.Type) -> Entry? { … } } await connection.pruneOldEntries()
-
35:58 - MyConnection with a serial dispatch queue and a custom executor
actor MyConnection { private var database: UnsafeMutablePointer<sqlite3> private let queue: DispatchSerialQueue nonisolated var unownedExecutor: UnownedSerialExecutor { queue.asUnownedSerialExecutor() } init(filename: String, queue: DispatchSerialQueue) throws { … } func pruneOldEntries() { … } func fetchEntry<Entry>(named: String, type: Entry.Type) -> Entry? { … } } await connection.pruneOldEntries()
-
36:44 - Dispatch queues conform to SerialExecutor protocol
// Executor protocols protocol Executor: AnyObject, Sendable { func enqueue(_ job: consuming ExecutorJob) } protocol SerialExecutor: Executor { func asUnownedSerialExecutor() -> UnownedSerialExecutor func isSameExclusiveExecutionContext(other executor: Self) -> Bool } extension DispatchSerialQueue: SerialExecutor { … }
-
39:22 - C++ implementation of FoundationDB's "master data" actor
// C++ implementation of FoundationDB’s “master data” actor ACTOR Future<Void> getVersion(Reference<MasterData> self, GetCommitVersionRequest req) { state std::map<UID, CommitProxyVersionReplies>::iterator proxyItr = self->lastCommitProxyVersionReplies.find(req.requestingProxy); ++self->getCommitVersionRequests; if (proxyItr == self->lastCommitProxyVersionReplies.end()) { req.reply.send(Never()); return Void(); } wait(proxyItr->second.latestRequestNum.whenAtLeast(req.requestNum - 1)); auto itr = proxyItr->second.replies.find(req.requestNum); if (itr != proxyItr->second.replies.end()) { req.reply.send(itr->second); return Void(); } // ... }
-
40:18 - Swift implementation of FoundationDB's "master data" actor
// Swift implementation of FoundationDB’s “master data” actor func getVersion( myself: MasterData, req: GetCommitVersionRequest ) async -> GetCommitVersionReply? { myself.getCommitVersionRequests += 1 guard let lastVersionReplies = lastCommitProxyVersionReplies[req.requestingProxy] else { return nil } // ... var latestRequestNum = try await lastVersionReplies.latestRequestNum .atLeast(VersionMetricHandle.ValueType(req.requestNum - UInt64(1))) if let lastReply = lastVersionReplies.replies[req.requestNum] { return lastReply } }
-
-
Looking for something specific? Enter a topic above and jump straight to the good stuff.
An error occurred when submitting your query. Please check your Internet connection and try again.