BT

Facilitating the Spread of Knowledge and Innovation in Professional Software Development

Write for InfoQ

Topics

Choose your language

InfoQ Homepage News Swift 5.9 Brings a Macro System and C++ Interoperability

Swift 5.9 Brings a Macro System and C++ Interoperability

In addition to an expressive macro system and a limited form of C++ interoperability, Swift 5.9, now officially available, also introduces parameter packs, ownership-based memory management, and more.

InfoQ already covered Swift's macro system when it was first announced at last WWDC developer conference. In short, macros are Swift functions that use the SwiftSyntax library to generate code to replace the macro call.

Swift has two kinds of macros: freestanding macros, which appear on their own, without being attached to a declaration; and attached macros, which modify the declaration of the program entity that follows them. Syntactically, freestanding macros are prefixed by #, attached macros by @:

#aFreestandingMacro("with argument")

@AttachedMacro<T> struct AStruct {
...
}

The Swift community has already created several tools and frameworks based on macros, including swift-power-assert, swift-spyable, swift-macro-testing, and MetaCodable.

Parameter packs solve a limitation of Swift generic types and functions, which must explicitly name each templated type they work on. Using parameter packs you can instead define a generic type or function that accepts an arbitrary number of types.

This is the case, for example, with SwiftUI ViewBuilder, which is defined in terms of a bunch of different generic function declarations, one for each specific number of View types that can be passed into the builder. This is the reason why you cannot add more than ten Views inside of a superview's body and are required to bundle them into Groups. Using parameter packs, instead, the kind of behaviour defined in a ViewBuilder can be declared in a single function. For example, the following defines a function all to check if all of its arguments are not nil:

func all<each Wrapped>(_ optional: repeat (each Wrapped)?) -> (repeat each Wrapped)?


if let (int, double, string, bool) = all(optionalInt, optionalDouble, optionalString, optionalBool) {
  print(int, double, string, bool)
}
else {
  print("got a nil")
}

Ownership is another game-changing feature introduced by Swift 5.9 to fine-tune memory management in performance-critical code. It is based on the concept of consuming and borrowing variables and arguments and on noncopyable structs and enums. Specifically, the new consume operator allows you to control the lifetime of a variable by ending it at any point in time and instructing the compiler to flag any attempt to use it later:

useX(x) // do some stuff with local variable x

// Ends lifetime of x, y's lifetime begins.
let y = consume x // [1]

useY(y) // do some stuff with local variable y
useX(x) // error, x's lifetime was ended at [1]

// Ends lifetime of y, destroying the current value.
_ = consume y // [2]
useX(x) // error, x's lifetime was ended at [1]
useY(y) // error, y's lifetime was ended at [2]

In addition, you can use new borrowing and consuming parameter modifiers to explicitly choose the ownership convention that a function uses to receive immutable parameters according to the following semantics: if a callee borrows a parameter, the caller guarantees the argument object will stay alive for the duration of the call and the callee does not need to release it; if a callee consumes a parameter, it becomes responsible for either releasing the parameter or passing its ownership along.

A final piece in Swift ownership is non-copyable, aka move-only types, which always have unique ownership to reduce heap allocation overhead and eliminate the need for reference counting.

Swift 5.9 also introduces a limited form of interoperability with C++ code, which only extends to certain kinds of APIs. For example, given the following C++ function:

#pragma once
#include <vector>

std::vector<std::string> generatePromptResponse(std::string prompt);

you can call it from Swift like this:

let codeLines = generatePromptResponse("Write Swift code that prints hello world")
  .map(String.init)
  .filter { !$0.isEmpty }

for line in codeLines {
  print(line)
}

C++ interoperability is still evolving, though, and will undergo changes in future, based on feedback from real-world adoption in mixed codebases, says the Swift team.

Another minor but surely welcome new feature is Swift support for using if and switch as expressions for variable assignment and return values.

As a final note on Swift 5.9, it is worth mentioning an improved debug expression evaluator and enhanced crash handling, which aim to improve developer experience. Indeed, the Swift runtime is now able to display a backtrace on the output console when a crash happens. Additionally, the p and po commands are faster when evaluating simple expressions and you can refer to generic type parameters in conditional breakpoints, for example to have a breakpoint only trigger when a type parameter is instantiated with a given concrete type.

About the Author

Rate this Article

Adoption
Style

BT