この記事の要点
- Embedded Swift は、マイクロコントローラのような制約の厳しい環境でも動くように設計された Swift のサブセットです。専用のコンパイルモードによって、通常の Swift よりはるかに小さなバイナリを生成しつつ、言語のほとんどはそのまま使えます。本記事は、ここ数か月で入った多くの改善をまとめたもので、これらは Swift 6.3 に含まれます。
- 改善は ライブラリと診断、C 相互運用、デバッグ、リンク の 4 つの領域にまたがります。浮動小数点型の
descriptionが使えるようになった、Embedded Swift で使えない言語機能を知らせる診断が追加された、@c属性で C 互換の関数やenumを定義できるようになった、LLDB のデバッグ体験が大きく改善された、などです。 - とくにリンクまわりでは、Embedded Swift 独自のリンクモデルを正式化する作業が進みました。インポートしたモジュールのシンボルを weak 定義として出力することで、依存関係が菱形になったときの重複シンボルエラーが解消され、
-mergeable-symbolsのような回避用フラグが不要になりました。 - 多くの機能はメモリマップ I/O 用パッケージ Swift MMIO の 0.1.x や、新しい Proposal(SE-0495・SE-0492・SE-0497)と連動しています。
ライブラリと診断
浮動小数点数の文字列化
これまで Embedded Swift の標準ライブラリには、Float や Double などの浮動小数点型の description / debugDescription プロパティが含まれていませんでした。新たに全体を Swift で書き直した実装により、Embedded Swift でもこれらが使えるようになりました。
Embedded Swift の制約に関する診断
Embedded Swift で使えない言語構文(untyped throws や、existential な値に対するジェネリック関数の呼び出しなど)を知らせる、新しい診断グループ EmbeddedRestrictions が追加されました。
この診断は Embedded Swift のビルド時には既定で有効になります。加えて、通常の(Embedded でない)Swift でも、コンパイラフラグ -Wwarning EmbeddedRestrictions か、パッケージマニフェストの次の指定で有効にできます。
swiftSettings: [
.treatWarning("EmbeddedRestrictions", as: .warning),
]
Embedded Swift への移植を見据えて、通常のビルドであらかじめ制約を洗い出すのに役立ちます。
Swift MMIO 0.1.x
メモリマップ I/O 用パッケージ Swift MMIO の 0.1.x がリリースされ、多数のバグ修正と使い勝手の改善に加え、Swift Package Index 上に網羅的なドキュメントが整備されました。
最大の追加はコード生成のサポートです。CMSIS の System View Description(SVD)ファイルから Swift MMIO のインターフェイスを直接生成する svd2swift ツールと、対応する SwiftPM プラグインが用意されました。コマンドラインから手動で実行することも、ビルド時に自動実行するようプラグインを設定することもできます。
デバッグ向けには LLDB プラグインの SVD2LLDB が追加されました。生のメモリアドレスを扱う代わりに、デバイスのレジスタを実際の名前で操作でき、レジスタ値を視覚的にデコードして読み解く機能も備えています。
(lldb) svd decode TIMER0.CR 0x0123_4567 --visual
TIMER0.CR: 0x0123_4567
╭╴CNTSRC ╭╴RST
╭╴S ╭╴RELOAD╭╴CAPEDGE ╭╴MODE
┴ ┴─ ┴─ ┴─── ┴── ┴
0b00000001001000110100010101100111
┬─ ┬─ ┬─── ┬ ┬─ ┬
╰╴IDR ╰╴TRGEXT ╰╴PSC ╰╴EN
╰╴CAPSRC ╰╴CNT
[31:31] S 0x0 (STOP)
[27:26] IDR 0x0 (KEEP)
[25:24] RELOAD 0x1 (RELOAD1)
[21:20] TRGEXT 0x2 (DMA2)
[17:16] CAPEDGE 0x3
[15:12] CAPSRC 0x4 (GPIOA_3)
[11:8] CNTSRC 0x5 (CAP_SRC_div32)
[7:7] PSC 0x0 (Disabled)
[6:4] MODE 0x6
[3:2] CNT 0x1 (Count_DOWN)
[1:1] RST 0x1 (Reset_Timer)
[0:0] EN 0x1 (Enable)
C 相互運用
@c による関数と enum
SE-0495 は、@c 属性を使って C 互換の関数や enum を定義する機能を導入しました。たとえば次の宣言は、C から MyLib_initialize という名前で呼び出せる関数を定義します。
@c(MyLib_initialize)
public func initialize() { ... }
このとき Swift が生成する compatibility header には、対応する C 関数の宣言が含まれ、任意の C / C++ コードから include できます。
void MyLib_initialize(void);
この機能は、SE-0436 で導入された @implementation 属性とも組み合わせられます。既存の C ヘッダで定義された C インターフェイスを、Swift の関数で実装できます。
@c @implementation
public func MyLib_initialize() { ... }
Swift コンパイラが、Swift 関数のシグネチャと C ヘッダのシグネチャが一致することを保証します。これにより、C クライアントに影響を与えずに、既存の C ライブラリを Swift の実装へ置き換えられます。
@c 属性は、従来の @_cdecl 属性を正式化し、その過程でさまざまな細かい不具合を修正したものです。
C シグネチャの不一致への寛容さの向上
これまで Swift コンパイラは、ヘッダからインポートされた C 関数や、@c(以前は @_cdecl)・@_extern(c) で Swift 側に宣言された C 関数について、Swift の型が C 側と厳密に一致することを要求していました。たとえば、ヘッダで nullability アノテーションなしに C 関数を宣言し、
void takePointer(const double *);
Swift 側では次のように定義したとします。
@c func takePointer(_ values: UnsafePointer<Double>) { ... }
このとき、不一致を検出したコンパイラがコンパイルに失敗し、理解しにくい「deserialization」エラーになることがありました。コンパイラは、同一の C 宣言に対する複数の見え方を別々に保持するようになり、問題を診断するのは基となる C 宣言どうしが矛盾している場合に限られるようになりました。これにより、nullability や sendability アノテーションのような細かな違いが C のシグネチャにあっても、コンパイルが失敗しなくなりました。
デバッグ
デバッガによる値の表示
Embedded Swift における、Swift 型の値を LLDB で表示する機能が改善されました。加えて、memory read コマンドが Swift の型名を任意で受け取れるようになり、任意のアドレスを、指定した Swift 型の値としてレンダリングできます。次の例は、アドレスを MyMessage 型として解釈したものです。
(lldb) memory read -t MyMessage 0x000000016fdfe970
(MessageLib.MyMessage) 0x16fdfe970 = {
id = 15
timestamp = 793219406
payload = "hello"
...
}
コアダンプで検査できるデータ型の拡充
LLDB が変数を表示するには型のレイアウト情報が必要です。Embedded Swift はリフレクション用のメタデータを含まないため、Swift コンパイラはすべての型レイアウト情報を DWARF のデバッグ情報として出力します。このデバッグ情報の出力が改善され(エクステンション内にネストした型宣言のサポートなど)、同時に LLDB 側も Embedded Swift でのネストしたジェネリックな型エイリアスに対応しました。
この 2 つの改善により、Dictionary や Array のような標準ライブラリの一般的なデータ型を、Embedded Swift のコアダンプから検査できるようになりました。従来、これらのデータ型は、稼働中のプロセスを必要とする expression evaluator 経由でしかアクセスできませんでした。
LLDB は新しい InlineArray 型もネイティブにサポートします。
armv7m の例外フレームのアンワインド
armv7m で例外発生後にバックトレースを生成する LLDB の機能が大きく改善されました。これまでは、例外を受けた後のバックトレースで、例外フレーム(下の例の UsageFault_Handler)より前のフレームが 1 つ以上欠落しがちでした。
(lldb) bt
* thread 1
* frame #0: 0x0020392c NeoPixelRainbow`NeoPixelRainbow.swift_UsageFault_Handler() -> () + 28
frame #1: 0x00203910 NeoPixelRainbow`UsageFault_Handler + 8
frame #2: 0xfffffff8
frame #3: 0x00202a86 NeoPixelRainbow`NeoPixelRainbow_main + 8
frame #4: 0x00200256 NeoPixelRainbow`cinit_ctor_loop_end at startup.S:98
frame #5: 0x00200210 NeoPixelRainbow`Reset_Handler at startup.S:33
LLDB は例外フレームをさかのぼって通常のプログラムフレームへ復帰できるようになり、欠落していたフレームを含む完全なバックトレースを生成します。
(lldb) bt
* thread #1
* frame #0: 0x0020392c NeoPixelRainbow`swift_UsageFault_Handler() + 28
frame #1: 0x00203910 NeoPixelRainbow`swift_UsageFault_Handler()
frame #2: 0x00203366 NeoPixelRainbow`static Application.main() + 2270 // Real exception location
frame #3: 0x00202a86 NeoPixelRainbow`NeoPixelRainbow_main + 8
frame #4: 0x00200256 NeoPixelRainbow`cinit_ctor_loop_end + 18
frame #5: 0x00200210 NeoPixelRainbow`Reset_Handler + 16
リンク
@section と @used 属性
SE-0492 は、Embedded Swift で有用な 2 つの属性を導入しました。@section 属性は、特定のグローバル変数を、名前付きのセクションへ出力するよう指定します。
@section("__DATA,mysection")
@used
let myLinkerSetEntry: Int = 42
あわせて、#if の中で使える objectFormat チェックも導入され、オブジェクトファイルの形式(ELF・COFF・MachO・Wasm)に応じてコードを条件付きコンパイルできます。これを使えば形式ごとに異なるセクション名を与えられます。
#if objectFormat(ELF)
@section("mysection")
#elseif objectFormat(MachO)
@section("__DATA,mysection")
#endif
var global = ...
@used 属性は、付与された要素が一見未使用に見えても常に出力されるべきだと、コンパイラに伝えます。
Embedded Swift のリンクモデルの前進
Embedded Swift は通常の Swift とは異なるコンパイルモデルを使い、コード生成をコンパイル工程の後半まで遅延させます。このモデルはこれまで完全には定義されておらず、いくつかの実用上の問題を抱えていました。その 1 つが重複シンボルです。次のように 4 つの Embedded Swift ライブラリの依存関係が菱形になっているとします。
A
/ \
B C
\ /
D
このとき、A のシンボルが B と C の両方で使われていると、両方を D にリンクする際に重複定義エラーになります。Swift コンパイラは、インポートしたモジュールのシンボルを weak 定義として出力するようになり、リンカがそれらを重複排除できるようになりました。これにより、部分的な回避策にすぎなかった -mergeable-symbols や -emit-empty-object-file といったフラグが不要になりました。
Embedded Swift のリンクモデルを正式化するもう 1 つの前進が SE-0497 です。これは、関数がクライアントからどう見えるかを制御する新しい @export 属性を定義します。@export(implementation) は、クライアントが実装を出力・特殊化・インライン化できるようにし、長らく非公式だった @_alwaysEmitIntoClient 属性を置き換えます。@export(interface) は、呼び出し可能なシンボルだけを出力することで、関数のインターフェイスだけをクライアントに公開し、定義そのものは公開しません。これにより、Embedded Swift でもライブラリ開発者が関数の実装を完全に隠せるようになります。
試してみる
Embedded Swift のサポートは Swift development snapshot で利用できます。始めるには、さまざまなハードウェア上で Embedded Swift をビルド・実行できるサンプルプロジェクトを集めた Swift Embedded Examples リポジトリから入るのがおすすめです。
関連リンク
- Swift Embedded Examples — さまざまなハードウェア向けの Embedded Swift サンプル集
- A Vision for Embedded Swift — Embedded Swift の構想
- SE-0495(C互換関数とenum) / SE-0436(
@implementation) —@c関連の C 相互運用機能 - SE-0492(
@section/@used) / SE-0497(@export) — リンク制御の新しい属性 - Byte-sized Swift: Playdate 向けの小さなゲームを作る — Embedded Swift で実際にゲームを開発する実例