動機
Swiftが2026年になってLinuxでも普通に使える状態になりました。
普段はGoとVEXばかり書いてますが、Goが生産性が高すぎて面白くなかったなどと供述を始めてしまうくらいにはGoが楽すぎてコレはイカン?と思い、おもしろそうな言語ということで今年はSwift on Linuxを始めました。
最近はAI(Gemini🤪がメイン)のお陰で導入で詰まってもすぐ解決できるので、とりあえずやってみようの敷居が低くていいですね。
もともとTinyGoを使ってGBAのゲームでも作ってみっかーって言いつつ止まってるプロジェクトがあったので、供養がてらSwiftでやり直してみることにしました。
2024年ごろの記事を見てうぇ……ってなりましたが、こちらにはGemini🤪先生がついているのでなんとかなるだろうと思って1ヶ月覚悟で取り組みはじめましたが、なんか1日で動いちゃいました。
Swiftの進化がヤバイ!と言いたいところですが、だいたいDockerのおかげ_(:3」∠)_
環境構築
- WSL(Fedora43)
- mise
- podman
最低限必要な環境はこのくらいなので、だいたい簡単に揃えられると思います。
最終的にはDockerを使ってリンクするのでclang/llvmはSwift環境付属のもので十分だと思いますが、command not foundって言われるようならllvmのインストールも必要かも。
Swiftの導入
自分はmise/chezmoi/ansibleを使ってdotfilesで環境を管理しているので、swiftlyや公式インストーラを使わずmiseで入れちゃいました。
$ mise use swift
6.3.3が入りました。
podmanの導入
Fedora使いなのでpodman派です。常用環境ではpodman-composeまで入れてますがWSLではサービスを立ち上げないのでpodmanだけで十分かなって。
$ sudo dnf install podman
リンクに必要なライブラリやGBA-ROMへのコンバートにDevkitProというツールを使うのですが、dockerイメージ(devkitpro/devkitarm)が配られているのでお気楽に行くならコンテナ1択。
https://devkitpro.org/
ビルドを通す
参考
https://speakerdeck.com/k_koheyi/draft
プロジェクトを作る(VSCodeで楽をするため)
素の.swiftファイルを書いてtaskfile管理でも十分なのですが、VSCodeを使う場合はプロジェクト構成になってるほうが色々便利なので。
$ mkdir swift-gba
$ cd swift-gba
$ swift package init --type executable
ソースを修正する
プロジェクト生成で自動で作られた.swiftファイルをコンパイルすると@mainで怒られるのでコメントアウトしておく。
print文の行を削除して、while trueの無限ループを入れておく。
//@main
struct swift_gba {
static func main() {
// print("Hello, world!")
while true {
}
}
}
コンパイルする
後は参考のリンクのスライドを見ながらコンパイルオプションの設定をしつつビルドを成功させるよう頑張ります。
-enable-experimental-feature Embeddedをつけると-wmoをつけろと言われ、リンカーがlldでないと/usr/sbin/ld: unrecognised emulation mode: armelfとエラーがでたので-use-ld=lldを付けました。
$ swiftc Sources/swift-gba/swift_gba.swift -target armv4t-none-none-eabi -enable-experimental-feature Embedded -wmo -use-ld=lld
これでコンパイルは成功しましたが、リンクでcrt0.o等がないとエラーがでて成功しません。
Gemini🤪先生に聞いたら🤪な回答しか得られなかったので小一時間Webを巡って、Swift Embeddedも結局はllvmなのでclang/llvm向けの十分な実績のあるツールを使えばいい、ということでリンク以降はDevkitProを導入することにしました。
というわけで-use-ld=lldが不要になります。clang/llvm環境いらなかったんや……
-cオプションを付けてコンパイルで止めるように変更。
$ swiftc Sources/swift-gba/swift_gba.swift -target armv4t-none-none-eabi -enable-experimental-feature Embedded -wmo -c -o swift_gba.o
リンクする
DevkitProを使ってリンクします。
オプション?よくわからない。俺達は雰囲気でコンパイルをやっている。
$ podman run --rm -v "$(pwd):/work" -w /work devkitpro/devkitarm sh -c "/opt/devkitpro/devkitARM/bin/arm-none-eabi-gcc -mthumb -mthumb-interwork -specs=gba.specs swift_gba.o -o output.elf"
ここまでで、リンカエラーがずらっと出現します。
-
posix_memalign
mallco/freeは自前実装でもしない限りGBAで使えないのでalloc自体を禁止
-Xfrontend -no-allocations -
warning: swift_gba.o uses 32-bit enums yet the output is to use variable-size enums
可変長enum?(゚Д゚)ハァ?
-fshort-enums -
atomicうんちゃら
Gemini💪先生にリンカエラーを投げたらスタブを作ってくれました。参考スライドでは最適化で消し飛ばしていましたので実行時に使われていないっぽく、空関数でよかったのかもしれません。
@_cdecl("__atomic_load_4")
public func __atomic_load_4(_ ptr: UnsafeRawPointer, _ memorder: Int32) -> UInt32 {
return ptr.load(as: UInt32.self)
}
@_cdecl("__atomic_store_4")
public func __atomic_store_4(_ ptr: UnsafeMutableRawPointer, _ val: UInt32, _ memorder: Int32) {
ptr.storeBytes(of: val, as: UInt32.self)
}
@_cdecl("__atomic_fetch_add_4")
public func __atomic_fetch_add_4(_ ptr: UnsafeMutableRawPointer, _ val: UInt32, _ memorder: Int32)
-> UInt32
{
let old = ptr.load(as: UInt32.self)
ptr.storeBytes(of: old &+ val, as: UInt32.self)
return old
}
@_cdecl("__atomic_fetch_sub_4")
public func __atomic_fetch_sub_4(_ ptr: UnsafeMutableRawPointer, _ val: UInt32, _ memorder: Int32)
-> UInt32
{
let old = ptr.load(as: UInt32.self)
ptr.storeBytes(of: old &- val, as: UInt32.self)
return old
}
@_cdecl("__atomic_compare_exchange_4")
public func __atomic_compare_exchange_4(
_ ptr: UnsafeMutableRawPointer,
_ expected: UnsafeMutableRawPointer,
_ desired: UInt32,
_ weak: Bool,
_ success_memorder: Int32,
_ failure_memorder: Int32
) -> Bool {
let current = ptr.load(as: UInt32.self)
let exp = expected.load(as: UInt32.self)
if current == exp {
ptr.storeBytes(of: desired, as: UInt32.self)
return true
} else {
expected.storeBytes(of: current, as: UInt32.self)
return false
}
}
@_cdecl("__atomic_store_2")
public func __atomic_store_2(_ ptr: UnsafeMutableRawPointer, _ val: UInt16, _ memorder: Int32) {
ptr.storeBytes(of: val, as: UInt16.self)
}
@_cdecl("__atomic_load_2")
public func __atomic_load_2(_ ptr: UnsafeRawPointer, _ memorder: Int32) -> UInt16 {
return ptr.load(as: UInt16.self)
}
- section .got LMA ... overlaps section .init_array LMA ...
-Oをつけると消えるのですがどうもメモリ配置が不安定っぽいのでオプションを付けておきました。
-Xcc -fno-pic -Xcc -fno-pie
- ついでに最適化も追加
-O
フラグを修正してビルドしなおす
リンクエラーの対処を含めてコンパイルからやり直します。
$ swiftc Sources/swift-gba/swift_gba.swift Sources/swift-gba/atomic.swift -target armv4t-none-none-eabi -enable-experimental-feature Embedded -wmo -c -o swift_gba.o -O -Xfrontend -no-allocations -Xcc -fshort-enums -Xcc -fno-pic -Xcc -fno-pie
フラグを変えただけなので基本的にコンパイルは通るはずです。問題はリンク
$ podman run --rm -v "$(pwd):/work" -w /work devkitpro/devkitarm sh -c "/opt/devkitpro/devkitARM/bin/arm-none-eabi-gcc -mthumb -mthumb-interwork -specs=gba.specs swift_gba.o -o output.elf"
ここまでで(.crt0+0x20c): undefined reference to 'main'というエラーが残ります。
main関数を見つけきらないみたいなので、@_cdecl("main")をつけます。struct中のメソッドには付けられないのでstructを外してmain関数を外に出します。
@_cdecl("main")
public func main() {
while true {
}
}
少し警告が残りますがGBAでは問題ない警告らしいので、これでリンク成功!
elfをgbaに変換
ラスト、GBA用のROMイメージに変換します。
$ podman run --rm -v "$(pwd):/work" -w /work devkitpro/devkitarm sh -c "/opt/devkitpro/devkitARM/bin/arm-none-eabi-objcopy -O binary output.elf output.gba && /opt/devkitpro/tools/bin/gbafix output.gba"
これでできたROMイメージをmGBA等で実行すると「なにもしない」が実行されます。
追記:
急にsysgetentropy.c:(.text.getentropy+0xe): undefined reference to _getentropy_r'
`ってエラーが出るように(´・ω:;.:...
お気楽Swiftプログラムなので-Wl,--defsym=_getentropy_r=0で使わネーyo、と対応。
podman run --rm -v "$(pwd):/work" -w /work devkitpro/devkitarm /opt/devkitpro/devkitARM/bin/arm-none-eabi-gcc -mthumb -mthumb-interwork -specs=gba.specs -Wl,-z,noexecstack,-Map=build/tmp/output.map,--defsym=_getentropy_r=0 -L /opt/devkitpro/libgba/lib build/obj/swift-ui-gba.o -o build/tmp/output.elf -lmm -lgba
絵を表示してみる
とりあえず元のプロジェクトで動かそうと思って描いた絵をDMAを使って表示してみました。
え?動いてない? 力尽きた_(:3」∠)_
メモリ読み書き関数を作る
作り始めは-Oでわりと処理が丸っと消されやすいので@_optimize(none)は必須です。
適当にプロジェクト中のどこかに用意しておきます。mainと一緒でもいいよ!
@_optimize(none)
@inline(__always)
public func mmio_write16(_ addr: UInt32, _ val: UInt16) {
let ptr = UnsafeMutablePointer<UInt16>(bitPattern: UInt(addr))!
ptr.pointee = val
}
@_optimize(none)
@inline(__always)
public func mmio_write32(_ addr: UInt32, _ val: UInt32) {
let ptr = UnsafeMutablePointer<UInt32>(bitPattern: UInt(addr))!
ptr.pointee = val
}
ROMの64KBポイントからVRAMにDMA転送する
ROMの開始地点が0x08000000ということで、そこからさらに64KB進めた位置にイメージがある前提です。
let bgDataAddr: UInt32 = 0x0801_0000
@_cdecl("main")
public func main() {
// DISPCNT: MODE 3 + BG2
mmio_write16(0x0400_0000, 0x0403)
// ポインタからアドレスを取得
let srcAddr: UInt32 = bgDataAddr
let dstAddr: UInt32 = 0x0600_0000
// 転送サイズ:240 × 160 = 38,400 ワード (16bit単位)
let transferCount: UInt32 = 240 * 160
// DMA3の設定と実行
// 送信元アドレス (DMA3SAD)
mmio_write32(0x0400_00D4, srcAddr)
// 送信先アドレス (DMA3DAD)
mmio_write32(0x0400_00D8, dstAddr)
// 制御レジスタ (DMA3CNT)
let DMA_SRC_CTRL: UInt32 = 0 << 23
let DMA_DST_CTRL: UInt32 = 0 << 21
let DMA_ENABLE: UInt32 = 1 << 31
let DMA_VBLANK: UInt32 = 1 << 28
let dmaCnt: UInt32 = transferCount | DMA_SRC_CTRL | DMA_DST_CTRL | DMA_VBLANK | DMA_ENABLE
mmio_write32(0x0400_00DC, dmaCnt)
// メイン無限ループ
while true {
}
}
イメージデータを用意する
変換が面倒なのでGemini💪先生に作ってもらいました。
8:8:8を5:5:5のrawデータに変換。
from PIL import Image
# 画像を読み込み
img = Image.open("assets/libgirl240.png").convert("RGB")
assert img.size == (240, 160), f"Unexpected size: {img.size}"
# GBA用 15-bitカラー (RGB555) に変換して書き出し
with open("assets/output.raw", "wb") as f:
for y in range(160):
for x in range(240):
r, g, b = img.getpixel((x, y))
# 8bit(0-255) を 5bit(0-31) に変換
r5 = r >> 3
g5 = g >> 3
b5 = b >> 3
# 16bit整数にまとめる (15bit + アルファ1bit=0)
val = r5 | (g5 << 5) | (b5 << 10)
# リトルエンディアンで2バイト書き出し
f.write(val.to_bytes(2, byteorder="little"))
ROMを64KBまで0埋めし画像データをくっつける
これもGemini💪先生作。
ROMの64KB地点から画像データが始まるよう、0埋めしてからくっつけます。
import sys
gba_path = ".build/output.gba"
raw_path = "assets/output.raw"
out_path = ".build/swift_gba.gba"
target_offset = 65536 # 64KBの場所に必ず画像を置く場合
# 1. gbaファイルを読み込む
with open(gba_path, "rb") as f:
gba_data = bytearray(f.read())
# 2. 指定オフセットまで足りない分を0埋め(パディング)する
if len(gba_data) < target_offset:
gba_data.extend(b"\x00" * (target_offset - len(gba_data)))
else:
print(f"Warning: GBA binary size ({len(gba_data)}) exceeded offset ({target_offset})!")
# 3. 画像バイナリを読み込んで結合する
with open(raw_path, "rb") as f:
raw_data = f.read()
# オフセット位置以降に画像データを書き込む(または上書きする)
gba_data[target_offset:target_offset + len(raw_data)] = raw_data
# 4. 最終的なGBAファイルを書き出す
with open(out_path, "wb") as f:
f.write(gba_data)
print(f"Successfully merged {raw_path} at offset {target_offset} in {out_path}")
[おまけ]Taskfile
version: '3'
vars:
VERSION: v0.0.1
OBJ_FILE: .build/swift_gba.o
GBA_FILE: .build/output.gba
ELF_FILE: .build/output.elf
OUT_FILE: .build/swift_gba.gba
tasks:
compile:
desc: "compile swift files"
vars:
SRC: Sources/swift-gba/*.swift
cmds:
- swiftc {{.SRC}} -target armv4t-none-none-eabi -enable-experimental-feature Embedded -wmo -c -o {{.OBJ_FILE}} -O -Xfrontend -no-allocations -Xcc -fshort-enums -Xcc -fno-pic -Xcc -fno-pie
sources:
- "{{.SRC}}"
generates:
- "{{.OBJ_FILE}}"
link:
desc: "link swift object files"
deps: [compile] # 実行前に必ず compile を終わらせる
cmds:
- podman run --rm -v "$(pwd):/work" -w /work devkitpro/devkitarm /opt/devkitpro/devkitARM/bin/arm-none-eabi-gcc -mthumb -mthumb-interwork -specs=gba.specs {{.OBJ_FILE}} -o {{.ELF_FILE}}
sources:
- "{{.OBJ_FILE}}"
generates:
- "{{.ELF_FILE}}"
convert:
desc: "run convert"
deps: [link] # 実行前に必ず link を終わらせる
cmds:
- podman run --rm -v "$(pwd):/work" -w /work devkitpro/devkitarm sh -c "/opt/devkitpro/devkitARM/bin/arm-none-eabi-objcopy -O binary {{.ELF_FILE}} {{.GBA_FILE}} && /opt/devkitpro/tools/bin/gbafix {{.GBA_FILE}}"
sources:
- "{{.ELF_FILE}}"
generates:
- "{{.GBA_FILE}}"
add_data:
desc: "add data to output.gba"
deps: [convert] # 実行前に convert までの一連の流れを終わらせる
cmds:
- python build-scripts/padding.py
sources:
- "{{.GBA_FILE}}"
- assets/output.raw
generates:
- "{{.OUT_FILE}}"
build:
desc: "build swift gba"
deps: [add_data] # 実行前に add_data までの一連の流れを終わらせる
cmds:
- cp {{.OUT_FILE}} ~/Desktop/swift_gba.gba
create_raw_image:
desc: "create raw image"
cmds:
- python build-scripts/raw_image.py
clean:
desc: "Clean"
cmds:
- rm -f {{.ELF_FILE}} {{.OBJ_FILE}} {{.GBA_FILE}} {{.OUT_FILE}}
- rm -f ~/Desktop/swift_gba.gba ~/Desktop/swift_gba.sav
default:
cmds:
- task --list
Swiftはいいぞ!
Swiftはいいぞおじさん「Swiftはいいぞ!」
というわけでSwiftを始めてみたのですが、ビルドがそこそこ早くてEmbedded向けの環境が最初から入っていて、後はエコシステムが揃ってくればかなりいい言語なのではないかと思いました。
昔から参照カウンタ式のメモリ管理が好きだったので、ARCやCopyOnWriteとかの素敵な仕組みになっているSwiftはもっと使っていきたいと思いました。けど、エコシステムがなぁ…
ちょっとTUIツールを作ろうと思っただけで一苦労でござる(´・ω:;.:...

