Embedded Swift Improvements Coming in Swift 6.4

Embedded Swift is a subset of Swift that’s designed for low resource usage, making it capable of running on constrained environments like microcontrollers. Using a special compilation mode, Embedded Swift produces significantly smaller binaries than regular Swift. While a subset of the full language, the vast majority of the Swift language works exactly the same in Embedded Swift. Additional information is described in the Embedded Swift vision document.

Embedded Swift is evolving rapidly. Following our updates on Embedded Swift improvements in Swift 6.3 late last year, this post describes a number of additional improvements made in the upcoming Swift 6.4 release. You can try them out today with a Swift development snapshot.

Language improvements

Embedded Swift continues to expand its subset of the language to include more aspects of “full” Swift, making it easier than ever to bring compatibility with Embedded Swift to existing Swift code bases. Many of these features have some dynamic aspect to them, meaning that they have an impact on runtime performance (for example, due to indirect calls) and code size (due to requiring additional metadata). However, this impact only occurs where these dynamic language features are actually used: code that is highly sensitive to code size and performance can choose to avoid them, for example by enabling warnings in the PerformanceHints diagnostic group.

Generalized support for existential (any) types

Embedded Swift previously only supported existential (any) types that had an AnyObject constraint, meaning they could only be used with class instances. Now, all any types are available in Embedded Swift, including Any itself. For example:

protocol P {
  func method()
}
​
extension Int: P {
  func method() { print("\(self) is here") }
}
​
let a: any P = 17
a.method() // prints "17 is here"

The Embedded Swift generics compilation model, which requires that all generic functions and types eventually be specialized, implies some limitations on the use of any types. Specifically, a generic function cannot be called on an any type:

extension P {
  func genericMethod<T: P>(_ other: T) { ... }
}
​
let a: any P = 17
a.genericMethod(a) // error: cannot use generic instance method 'genericMethod' on a value of type 'any P' in Embedded Swift

Untyped throws

Embedded Swift previously only allowed throwing specific error types, like this:

func parseRecord() throws(ParsingError) -> Record { ... }

“Untyped” throws, which can throw any Error-conforming instance, was previously disallowed in Embedded Swift:

func loadImage() throws -> Image { ... } // previously disallowed in Embedded Swift

Untyped throws is equivalent to throwing a value of type any Error. With the generalization of any types, Embedded Swift now fully supports untyped throws. Throwing a value of any Error typically requires a heap allocation, so typed throws should still be preferred for code bases that want to avoid heap allocations.

Metatypes

Embedded Swift has traditionally allowed metatypes (e.g., Int.self) only in very narrow places, for example when using them to specify argument types for generic functions:

rawPointer.bindMemory(to: Value.self, capacity: 1)

Swift 6.4 introduces complete support for metatypes in Embedded Swift: one can create and use instances of metatypes, including existential types like any (DefaultInitializable.Type). For example, this is now permitted and works in the same way as full Swift:

protocol DefaultInitializable {
  init()
}
​
extension Int: DefaultInitializable { }
​
let factory: any (DefaultInitializable.Type) = Int.self
let aValue: any DefaultInitializable = factory.init()

Library improvements

Additional features in the Swift standard library and associated libraries from full Swift are now available in Embedded Swift.

Floating point parsing

Swift floating point values can be parsed from a string, like this:

let inputText: String = getInputText()
if let value = Double(inputText) {
  // value is a Double
}

As part of a reimplementation of this functionality in Swift, these floating-point parsing APIs are now available in Embedded Swift as well.

Concurrency error handling

The Embedded Swift concurrency library now supports throwing operations, such as throwing tasks and task groups. For example:

let task = Task {
  if badThing {
    throw MyError.badThingHappened
  }
​
  return "ok"
}
​
print(try await task.value)

Try it out!

Embedded Swift support is available in the Swift development snapshots. The best way to get started is through the examples in the Swift Embedded Examples repository, which contains a number of sample projects to get Embedded Swift code building and running on various hardware.

37 Likes

This is incredible progress! Thank you to everyone who contributed!

2 Likes

Great work, this is very beneficial to a number of projects that I am working on. Thanks for keeping this a priority.

It has been great seeing Embedded Swift progressing, thanks!

I do have a question about "untyped throws" in Embedded:

As much as it unlocks on the surface, you quickly end up in the dead end that you cannot really do anything with an any Error. Simply trying to "log" it will fail (either at runtime with a (cannot print value in embedded Swift), or nowadays with a compiler diagnostic when trying to get a string out of it).

Even when everything uses typed throws, you'd need to manually map each error type to a common type, or you end up with an any Error.

I played around with a construct like this:

        do throws(Error & CustomStringConvertible) {
            try myThrowingFunc()
            try anotherThrowingFunc()
        } catch {
            print("\(error)")
        }

... which crashes the compiler ; ) ... but even if it didn't, we'd probably end up with an 'any CustomStringConvertible & Error' does not conform to the 'Error' protocol situation.

Unless I am missing something obvious, the only thing I can think of doing is to manually try to cast the any Error to each concrete type used in your application and handle it that way - or avoid any Error by cleanly mapping each throwing function into a common MyLoggableError type - both options being pretty laborious and "unswifty".

Is there any guidance or intended direction on how to actually use any Error in an Embedded Swift context?

This is not going to work:

Note that the constraint that the thrown error type must conform to Error means that one cannot use an existential type such as any Error & Codable as the thrown error type:

// error: any Error & Codable does not conform to Error
func remoteCall(function: String) async throws(any Error & Codable) -> String { ... }

The any Error existential has special semantics that allow it to conform to the Error protocol, introduced along with Result. A separate language change would be required to allow other existential types to conform to the Error protocol.

source: swift-evolution/proposals/0413-typed-throws.md at main · swiftlang/swift-evolution · GitHub

I am aware, but I included that snipped to half-ask the question: should it maybe? if we somehow extended the magic special casing of any Error: Error to any (Error & WhateverProtocol): Error - would that help? I am not sure.

Maybe the direction is to better support union types and have "types all the way"? (which clashes with the "any Error is a great default" narrative - and does not directly solve the "i just want to log this" either.)

Maybe the direction is to normalize "mapping" errors to common types and use that to have a clean, manual chain of type conversions all the way up? This is probably the simplest and most likely path - but currently all syntax for "mapping errors" is kind of ugly, and everybody has to make their own MyNotCompletelyUselessError type...