Upgrade to Pro — share decks privately, control downloads, hide ads and more …

市販E-Readerを乗っ取れ 〜Embedded Swiftで電子ペーパーガジェットを制御する〜

Avatar for Trickart Trickart
September 11, 2026

市販E-Readerを乗っ取れ 〜Embedded Swiftで電子ペーパーガジェットを制御する〜

Avatar for Trickart

Trickart

September 11, 2026

More Decks by Trickart

Other Decks in Programming

Transcript

  1. 開発環境: テキストエディタ+ターミナル ̀swift̀ コマンドが実行できれば何でも良い • テキストエディタは好きなものを • VSCode, Vim, Emacs,

    etc… • もちろんXcodeを使っても良い(というか使っていた) • ビルドボタンとかは使えないけど補完とかはよく効く
  2. Package.swift: Embedded向けターゲット構成 .enableExperimentalFeature(“Embedded”) が重要 // swift-tools-version: 6.3 import PackageDescription let

    package = Package( name: "InkedFeather", products: [ .executable(name: "Application", targets: ["Application"]), ], targets: [ .executableTarget( name: "Application", swiftSettings: [ .enableExperimentalFeature("Embedded"), // ←これ!! ) ] ), ]
  3. ビルドパイプライン: swift buildから実機まで esptoolは使わず、イメージ変換も書き込みもSwiftスクリプト • swift build → ELF(リンカスクリプトで配置) •

    elf2image.swift → ESPイメージ(.bin)へ変換 • write- ash.swift → USBシリアル経由でFlashへ • ̀make ash̀ 一発で実機起動 macOS 側 ELF elf2image.swift write-flash.swift + toolset.json -Osize -wmo -nostdlib esp32c3.ld で配置 ESPイメージ .bin へ変換 シリアル経由で書き込み fl swift build fl Package.swift 実機で起動
  4. ベアメタルでは: 誰もやってくれない OSもlibcもない。あるのはCPUとメモリと仕様書だけ iOSアプリ このプロジェクト アプリの Swift コード アプリの Swift

    コード UIKit / Foundation 自作ドライバ(SPI / GPIO / e-ink / SD) Swiftランタイム / libswiftCore 自作ランタイム (malloc / memcpy / SoftFloat) dyld / libSystem(malloc など) ここから下は OSが用意 してくれる 自作ブートローダ(Swift) XNU カーネル ROMブートローダ(マスクROM・固定) ハードウェア ハードウェア(ESP32-C3) 層の数はほぼ同じ。違いは「誰が書いたか」だけ 自分で書く 必要あり
  5. 実はそもそもビルドが通らない! TOOLCHAINS=org.swift.630202603201a swift build --triple riscv32-none-none-eabi --product Application [1/1] Planning

    build Building for debugging... error: link command failed with exit code 1 (use -v to see invocation) clang: warning: argument unused during compilation: '-F/Applications/Xcode.app/ Contents/Developer/Platforms/MacOSX.platform/Developer/Library/Frameworks' [Wunused-command-line-argument] ld.lld: error: cannot open crt0.o: No such file or directory ld.lld: error: cannot open /Users/trick/Library/Developer/Toolchains/swift-6.3RELEASE.xctoolchain/usr/lib/clang/21/lib/riscv32-none-none-eabi/ libclang_rt.builtins.a: No such file or directory ld.lld: error: unable to find library -lc clang: error: linker command failed with exit code 1 (use -v to see invocation) [10/11] Linking Application make: *** [build] Error 1 • crt0.oとかlibclang̲rt.biltins.aがないらしい • ̀-nostdlib̀ をつけるとそれ無しでビルドをしてくれるらしい
  6. toolset.json: 細かいフラグをまとめて渡せる $ swift build --toolset toolset.json { "schemaVersion": "1.0",

    "swiftCompiler": { "extraCLIOptions": [ "-Xfrontend", "-disable-stack-protector", "-use-ld=lld", "-nostartfiles", "-Xlinker", "-nostdlib", "-Xclang-linker", "-nostdlib", "-Xlinker", "-static", "-wmo", "-Osize", "-Xlinker", "-e", "-Xlinker", "_start" ] }, "linker": { "extraCLIOptions": [ "-T", "linker/esp32c3.ld", "--gc-sections" ] } }
  7. 足りないなら作ればいいじゃない: Swiftで実装 案外シンプルな実装 @c(memset) @inline(never) public func memset(_ dest: UnsafeMutableRawPointer,

    _ value: Int32, _ count: Int) -> UnsafeMutableRawPointer { let byte = UInt8(truncatingIfNeeded: value) let ptr = dest.assumingMemoryBound(to: UInt8.self) for i in 0..<count { ptr[i] = byte } return dest } @c(memcpy) @inline(never) public func memcpy(_ dest: UnsafeMutableRawPointer, _ src: UnsafeRawPointer, _ count: Int) -> UnsafeMutableRawPointer { let d = dest.assumingMemoryBound(to: UInt8.self) let s = src.assumingMemoryBound(to: UInt8.self) for i in 0..<count { d[i] = s[i] } return dest }
  8. なぜSwiftがposix̲memalignを要求するのか posix̲memalign malloc(ヒープ確保) • シンプルにstructをつかうだけならスタックで完結する、が… • ヒープ必須の機能がある • class /

    closure / Array / String / indirect enum • ARCで数えるのもヒープ上の参照の数 • freeはアロケートしたメモリを解放する関数 final class Foo {} let foo = Foo() // この1行の裏で swift_allocObject → posix_memalign
  9. mallocは自作する: free-listアロケータ 教科書(Knuth著)に出てくるシンプルなメモリアロケータ • 方式はfree-list + rst- t (約200行) •

    free時は隣の空きブロックと即結合(boundary tag) 使用中ブロック 空きブロック ヘッダ 4B:size|使用中=1 ヘッダ 4B:size|使用中=0 nextFree 4B (フリーリストの次の空きへ) ペイロード (malloc が返す領域) (未使用領域) フッタ 4B:size のコピー フッタ 4B:size のコピー fi fi size を先頭(ヘッダ)と末尾(フッタ)の両方に持つ = boundary tag。 最小ブロックは ヘッダ4 + nextFree4 + パディング4 + フッタ4 = 16B 次の空きブロックへ
  10. 400KB SRAMの使い道 低アドレスにヒープ、高アドレスにスタック。教科書通りの配置 ICache 16KB(アプリからは使えない) 0x3FC8_0000 .data(初期値付きグローバル) .bss(ゼロ初期化グローバル) 400KB SRAM

    • .data / .bss: グローバル変数 • heap: 残り全部(約350KB) • stack: 16KB、DRAM上端から下に伸びる • 最上端14KBはマスクROMが使うので不可侵 • フレームバッファ48KBはheapから確保 ヒープ(_heap_start 〜 _heap_end) 約353KB フレームバッファ 48KB (800 × 480 × 1bit) ← malloc で確保 スタック 16KB(_start で sp を設定・低アドレスへ 伸びる) 0x3FCD_8700 0x3FCD_C700 ROM予約 14KB(ROMブートスタック+ROMデータ) 0x3FCE_0000 ※ .data / .bss の実サイズはビルドで変動(縮尺は概念図)。スタックは慣習どおり高ア ドレス側
  11. これでビルドが…通った! ̀-nostdlib̀ で外した物をSwiftで書いただけともいう TOOLCHAINS=org.swift.630202603201a swift build --triple riscv32-none-none-eabi --toolset toolset.json

    --product Application [1/1] Planning build Building for debugging... [9/9] Linking Application Build of product 'Application' complete! (18.41s)
  12. …と思ったら書いてなかったもの: ̲̲ashldi3 compiler-rt(libclang̲rt.builtins.a)の中身も自前で用意する • 64bitのシフト演算はRV32では1命令で出来ない • コンパイラは ̲̲ashldi3 という補助関数の呼び出しに変換する •

    本来はlibclang̲rt.builtins.aにいる(最初のエラーの「ない」やつ) • UInt64をシフトした瞬間にリンクエラー ld.lld: error: undefined symbol: __ashldi3 @c(__ashldi3) @inline(never) public func ashldi3(_ a: UInt64, _ b: Int32) -> UInt64 { let (lo, hi) = split64(a) if b >= 32 { return make64(lo: 0, hi: lo << (b &- 32)) } else if b == 0 { return a } else { return make64(lo: lo << b, hi: (hi << b) | (lo >> (32 &- b))) } }
  13. 浮動小数点も同じ: FPUがない Floatを使うと ̲̲addsf3 / ̲̲mulsf3 / ̲̲ oatsisf …

    がまとめて要求される • ESP32-C3(RV32IMC)には浮動小数点ユニットがない • 1.0 + 2.0 も全部ソフトウェアの関数呼び出しになる • IEEE 754の加減乗除・比較・変換をSwiftで実装(約1500行) • JPEGデコーダ用に用意したが固定小数点に書き直し、今は使っていない // SoftFloat/SoftFloat.swift @c(__addsf3) @inline(never) public func addsf3(_ a: Float, _ b: Float) -> Float { /* 符号・指数・仮数を分解して足す */ } @c(__mulsf3) @inline(never) public func mulsf3(_ a: Float, _ b: Float) -> Float { /* 仮数の積は UInt64 で 48bit */ } fl // SoftFloat/SoftDouble.swift: Double版も一式
  14. ESP32-C3のブートは3段構成 2nd Stage Bootloaderというやつがいる ① ROM Bootloader マスクROM内蔵 書き換え不可 ロードして実行

    ② 2nd Stage Bootloader flash 0x0 ロードして jump ③ Application アプリパーティション
  15. 2nd Stage Bootloader 最初はESP-IDFのバイナリを借りていた • Espressif IoT Development Framework(ESP-IDF)にすでにブートローダー のコードがある

    • C言語製 • アプリケーションはEmbedded Swiftで書けるようになった → 2nd Stage BootloaderもSwiftで書けるのでは? →やってみよう!!
  16. 自作ブートローダーの全体像 アプリを動かすために色々な準備をする 1. チップを起こす clearBSS() disableWatchdogs() 4つのWDTを止める configurePLL() CPU 160MHz

    へ 2. Flash を読めるようにする 3. ロードして跳ぶ configureFlashSPI() loadRAMSegments() DIO 読み出し設定 IRAM / DRAM へコピー findAppPartition() setupFlashMMU() 0x8000 のテーブルを読む IROM / DROM をマップ readImageHeader() entry() magic 0xE9 を確認 アプリへ jump
  17. BSS初期化 BSS: ゼロで初期化されてる変数を置く領域 • 起動時はゴミ値だったりするので全部ゼロで初期化する • 初期化しないと意味不明な挙動になったりする(1敗) @_extern(c, "_sbss") nonisolated(unsafe)

    var _sbss: UInt8 @_extern(c, "_ebss") nonisolated(unsafe) var _ebss: UInt8 func clearBSS() { let start = linkerSymbolAddress(&_sbss) let end = linkerSymbolAddress(&_ebss) guard let ptr = UnsafeMutablePointer<UInt8>(bitPattern: start) else { return } var i = 0 while start &+ UInt(i) < end { ptr[i] = 0 // ループで全部ゼロを代入 i &+= 1 } }
  18. BSSクリアがmemsetになって無限再帰する LLVMは「ゼロ埋めループ」を見つけると memset 呼び出しに置き換える • ホストOSなら嬉しい最適化。libcのmemsetは速いので • 自作memsetの中のforループもmemset呼び出しに変換されうる • memsetがmemsetを呼んでスタックオーバーフロー

    • 対策: MemoryPrimitivesターゲットだけ変換を禁止 // Package.swift (MemoryPrimitives ターゲット) swiftSettings: [ .enableExperimentalFeature("Embedded"), .enableExperimentalFeature("Extern"), .unsafeFlags(["-Xllvm", "-disable-loop-idiom-memcpy"]), ]
  19. マスクROMの関数をSwiftから呼ぶ • ROMには工場出荷時から関数が焼き込まれている(アドレス固定) • ESP-IDFの ̀esp32c3.rom.ld̀ にアドレス一覧がある • リンカスクリプトで名前とアドレスを紐づけ、 ̀@̲extern(c)̀

    で呼ぶ /* linker/esp32c3.ld */ PROVIDE ( rom_i2c_readReg = 0x4000195c ); @_extern(c, "rom_i2c_writeReg") @discardableResult func romI2CWrite(_ block: UInt32, _ hostid: UInt32, _ regAddr: UInt32, _ data: UInt32) -> UInt32 romI2CWrite(I2C_BBPLL, 0, 4, 0x6B) // PLL を 480MHz モードに
  20. ウォッチドッグタイマー(WDT)を黙らせる WDT: ハング時に自動リセットさせる保険のタイマー • リセット直後から勝手に動いている→何とかする必要あり • 今回は簡単のため4つとも無効化 • 本当はfeedするようにしたほうがよいが… •

    設定には保護解除の合言葉 ̀0x50D83AA1̀ が必要 • 暴走したプログラムに誤って無効化されないようロックがかかっている regStore(base + 0x64, 0x50D8_3AA1) // 合言葉を書いて保護解除 // ここで WDT を無効化 regStore(base + 0x64, 0) // 別の値を書いて再ロック
  21. パーティションテーブルを読む パーティションテーブル=目次 0x0 • 場所は ̀0x8000̀ 固定 • 1エントリ32バイトの配列 0x8000

    パーティションテーブル 0x9000 nvs 20KB 0xE000 otadata 8KB 0x10000 • magicバイト/種別/o set/サイズ/ラベル • ここからapp0を探す 2nd stage bootloader(Swift製・自作) app0 6.25MB ← 自作ファームウェア本体はここ 0x650000 app1 6.25MB(OTA予備・未使用) • app0の先頭24バイトがイメージヘッダ 0xC90000 spiffs 3.375MB • magicバイト/セグメント数/エントリー ポイント 0xFF0000 coredump 64KB ff 16MB SPI Flash(縮尺は非等倍) ① テーブルを読む ② app0 を見つけてロード
  22. セグメントをIRAM/DRAMへロードする I=インストラクション(命令), D=データ アプリイメージ(flash 0x10000〜) SRAM(実体をコピー) イメージヘッダ 24B magic 0xE9

    / セグメント数 / entry DRAM:.data の実体 コピー .data セグメント (宛先 0x3FC8_xxxx) IRAM:trap_handler コピー .trap_handler (宛先 0x4037_Cxxx) Flashに置いたまま(コピーしない) .text (宛先 0x4200_xxxx) MMUでマップするだけ .text → IROM にマップ 0x4200_0020 .rodata (宛先 0x3C40_xxxx) MMUでマップするだけ .rodata → DROM にマップ 0x3C40_0020 宛先アドレスを見て、SRAM宛なら実体コピー、Flash領域宛ならMMUでマップ。この分岐がブートローダの仕事
  23. Flash MMU: Flashを ̀0x42000000̀にマップする SRAMに置かないけどCPUから見えるようにする SPI Flash 16MB(物理) CPU仮想アドレス空間 bootloader(0x0)

    パーティションテーブル (0x8000) DROM(マップ先) 0x3C40_0020 .rodata が見える app0(自作FW / 0x10000) .text(コード) Flash MMU 128エントリ 1エントリ = 64KBページ テーブル自体もMMIO (0x600C_5000) IROM(マップ先) 0x4200_0020 .text が見える .rodata(定数・文字列) app1 / spiffs / … SRAM / ペリフェラル … CPUはマップされた領域を普通のアドレスとして読むだけ。実体はFlashにあり、ICache / DCache 経由で読まれる
  24. IRAMとDRAMは同じSRAMの別名 物理的なSRAMは400KBが1つだけ。入口が2つある • IRAM(0x4037̲C000〜) = 命令フェッチ用バス経由 • DRAM(0x3FC8̲0000〜) = データ読み書き用バス経由

    • 同じバイトが2つのアドレスから見える(ハードリンクのようなもの) • 容量は共有: IRAMに置いた分だけDRAMが減る IRAM 0x4037_C000 I-bus(命令フェッチ) トラップハンドラを配置 同じ場所を指す 物理SRAM 400KB 実体は1つ 同じ場所を指す DRAM 0x3FC8_0000 D-bus(データ読み書き) .data / .bss / stack / heap 同じバイトが2つのアドレスから見える。CPUがどちらのバスでアクセスするかの違いだけ。 リンカスクリプトでは iram / dram を別 MEMORY として書くが、置き場所は同じ 400KB を分け合っている
  25. リンカスクリプトで配置を決める • 「何をどのアドレスに置くか」の指示書 • ここまで出てきたIRAMとかが勢揃い MEMORY { iram (RWX) :

    ORIGIN = 0x4037C000, LENGTH = 0x60000 dram (RW) : ORIGIN = 0x3FC80000, LENGTH = 0x60000 irom (RX) : ORIGIN = 0x42000020, LENGTH = 0x400000 - 0x20 /* Flashをマップ */ drom (R) : ORIGIN = 0x3C400020, LENGTH = 0x400000 - 0x20 /* Flashをマップ */ } SECTIONS { .trap_handler : { ... } > iram } /* 物理SRAM(I-bus) */ .text : { ... } > irom .rodata .data / .bss : { ... } > drom : { ... } > dram /* 同じSRAM(D-bus) */ /* RAMで実行したいコード */ /* コードはFlashに置いたまま */
  26. jump先にいるのは ̲start: 3行のアセンブラ toolset.jsonの ̀-e ̲start̀ の正体。crt0.oの手書き代替 • 本来crt0.oが提供するエントリーポイントを自前で書く •

    スタックポインタを自分の領域に向けてmainを呼ぶだけ • スタックがない状態で動く=Swiftでは書けない .section .text._start, "ax" .globl _start _start: la sp, _stack_top /* スタックを自分の領域に向ける */ 1: call main /* Swift の main() へ */ j /* main は帰ってこない */ 1b
  27. MMIO(Memory Mapped I/O)とは 特定アドレスへの読み書きが周辺機器への操作になる CPUアドレス空間 0x3C40_0000 ペリフェラルの一部 DROM UART0 0x3FC8_0000

    DRAM 0x4200_0000 IROM 0x6000_0000〜 ペリフェラル(MMIO) 0x6000_0000 GPIO 0x6000_4000 IO_MUX 0x6000_9000 SPI2 0x6002_4000 GPIO の出力レジスタに 1 を書き込む(ただの store) ピンが HIGH になる Flash MMUテーブル 0x600C_5000 特定アドレスへの load / store が、そのまま周辺機器の操作になる。OSもドライバAPIもない
  28. 素朴な書き方: UnsafeMutablePointer 直叩き やってることの根本の根本はこれだけ • 一応これで動く • が、2つ問題がある // GPIO4

    を HIGH にする(GPIO_OUT_W1TS レジスタ = 0x6000_4008) UnsafeMutablePointer<UInt32>(bitPattern: 0x6000_4008)!.pointee = 1 << 4
  29. 直叩きの2つの問題 意味不明な上に処理が実行されないかもしれない(!?!?) • ̀0x6000̲4008̀ に ̀1 << 4̀ をストア…意味不明! •

    全部のMMIOでアドレスを覚えてミスなく書く必要がある • あとから読んだときも意味不明!! • 理論上可能だけど人間には無理!! • コンパイラには「ただのメモリ」に見えるので最適化で消えうる • 「おっ、ここ無駄に2回も書き込んでるな。1回にしよ」→動かなくなる
  30. Before: 直叩き / After: 自作ツール わかりやすくなった…よね?(少なくとも確実に実行される) // GPIO4 を HIGH

    にする // GPIO4 を HIGH にする UnsafeMutablePointer<UInt32>( bitPattern: 0x6000_4008 )!.pointee = 1 << 4 gpio.out_w1ts.write { $0.raw.storage = 1 << 4 } • アドレスがただの数字 • アドレスを手打ちしなくて済む • 最適化で読み書きが消えうる • 中身はVolatileMappedRegister • 最適化で消えない!
  31. SPI2をマスタとして初期化する やることはレジスタ設定の積み重ね: クロック供給→ピン配線→モード→速度 • 周辺機器にもクロックを供給しないと動かない • GPIO Matrixで「SPI2の信号をどのピンに出すか」を配線する • 通信速度はAPB

    80MHzを8分周して10MHz • ハードウェアCSは全部オフ→誰と話すかはGPIOで自分で決める system.perip_clk_en0.modify { $0.raw.storage = $0.raw.storage | (1 << 6) } sclkPin.setFunction(output: 63) // GPIO Matrix: FSPICLK_OUT → GPIO8 mosiPin.setFunction(output: 65) // GPIO Matrix: FSPID_OUT → GPIO10 spi2.clock.write { $0.raw.storage = ... } // 80MHz ÷ 8 = 10MHz spi2.misc.write { $0.raw.storage = 0x3F } // HW CS 全無効(CSはGPIOで手動) // SPI2 にクロック供給
  32. パネルを起こす: リセットと初期化コマンド SSD1677: Xteink X4のE-Ink Display Driver • 電源投入直後のSSD1677は状態が不明→まず既知の状態に戻す •

    RSTピンをLOW→ソフトリセットコマンド→BUSY待ち • データシート通りに設定コマンドを流し込む • 温度センサ設定/ブースター設定/ドライバ出力制御/RAMウィンドウと書き 込み位置 • 正直よくわからんがデータシートやサンプルコードを信じて書く
  33. SSD1677と話す: DCピンとBUSYピン SSD1677にはSPI以外のピンがある • SPIは「バイトを送る」ことしか出来ない→DCピンでコマンド/データを区別 • E-Inkは書き換えに時間が秒単位でかかる→完了はBUSYピンを監視 DC = LOW

    ならコマンド、HIGH ならデータ / 完了は BUSY が LOW に戻るまで待つ -1 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 CS DC MOSI 0x24 フレームバッファ 0x20 BUSY CPU コマンド データ送信 コマンド BUSY 待ち CS・DC は GPIO で手動制御 / BUSY は HIGH = 駆動中。5 秒タイムアウト付き 次へ 16
  34. フレームバッファを用意する フレームバッファ: 画面の表示内容を保持するメモリ領域 • パネルは本来横長800x480だ がXteink X4は縦で使っている パネルネイティブ 800 ×

    480 1行 = 800ピクセル = 100バイト 1行 • 座標を転置して書き込む • 48KB(SRAMの約12%) b7 • 舐めてたら普通にヒープ使い きった(1敗) b1 b0 1バイト = 横8ピクセル。MSB(b7)が左端 白、0 = 黒(0xFF で塗りつぶし = 白背景) / 100バイト × 480行 = 48,000バイト = 48KB(400KB SRAM の約12%) 階調なし1bit。だからこそ 400KB SRAM に収まる b6 b5 b4 b3 b2 1=
  35. 処理の流れ: 転送→Display Update Sequence→表示 48KBのビット列を64Byteずつ750回に分けて送る ESP32-C3 SSD1677 0x24 Write RAM(フレームバッファ

    48KB) 0x22 Display Update Control 2 0x20 Master Activation BUSY = HIGH(パネル駆動中) BUSY ピンを待つ 画面が書き換わる BUSY = LOW ESP32-C3 SSD1677
  36. デバッグ手段: print()をUSBに繋ぐ ESP32-C3はUSB Serial/JTAGを内蔵。USB-Cを挿すだけでシリアルポートが生える • Xcodeのコンソールもデバッガもない。まず「どこまで動いたか」が欲しい • 公式ドキュメント: print()を使うなら putchar

    を用意してね • USBコントローラの送信FIFOに1バイト書く putchar をSwiftで実装 @c(putchar) @discardableResult public func putchar(_ c: Int32) -> Int32 { if c == 0x0A { _ = usbFifoWrite(0x0D); _ = usbFifoWrite(0x0A); usbFlush() } else { _ = usbFifoWrite(UInt8(truncatingIfNeeded: c)) // USB送信FIFOへ1バイト } return c } print("boot OK”) // いつもの print がそのまま動く
  37. 原因判明: ピンがJTAG割り当てされてた JTAG: デバッグ用のインターフェース • GPIO4~7は起動直後、JTAG機能に割り当てられている • E-Inkの制御線3本がすべてこれに割り当てられていた • DC

    = GPIO4 / RST = GPIO5 / BUSY = GPIO 6 • IO̲MUXで ̀MCU̲SEL = 1̀ にするまで操作が全く無視される io_mux.gpio[number].modify { $0.raw.storage = ($0.raw.storage & ~(0x7 << 12)) | (1 << 12) } // MCU_SEL = 1
  38. まとめ: Embedded Swiftで書けたこと Xteink X4の自作ファームウェアの9割以上 • ランタイム依存 • bootloader •

    アプリケーションのほとんど • E-Inkドライバ • ボタン監視 • etc…
  39. Thank You!! URL等 • trickart/InkedFeather • Embedded Swift language restrictions

    • Running Swift without an OS • Managing external dependencies • kishikawakatsumi/tryswift2026 • Getting Started with Embedded Swift Programming from ¥0
  40. 宣伝: 11月にSaitama.swift #1を開催します 所沢市民文化センター ミューズで僕と握手! • 埼玉県に縁があったりなかったりするiOS アプリ開発者の勉強会 • 2026/11/21(土)14:00~18:00

    • 所沢市民文化センター ミューズ • 西武新宿線 航空公園駅 徒歩10分 • 参加者募集中! • Embedded SwiftでGBAのゲームを作る ワークショップやります
  41. free-listヒープの設計: boundary tag Knuthのアルゴリズム 使用中ブロック 空きブロック ヘッダ 4B:size|使用中=1 ヘッダ 4B:size|使用中=0

    nextFree 4B (フリーリストの次の空きへ) ペイロード (malloc が返す領域) (未使用領域) フッタ 4B:size のコピー フッタ 4B:size のコピー size を先頭(ヘッダ)と末尾(フッタ)の両方に持つ = boundary tag。 最小ブロックは ヘッダ4 + nextFree4 + パディング4 + フッタ4 = 16B 次の空きブロックへ
  42. 隣接ブロックの結合(コアレッシング) freeしたとき空きブロックを結合する(デフラグ対策) free(B) の瞬間 空きブロック A free() されたブロック B Bの直前4Bを読む

    = Aのフッタ → Aのsizeが分かる → Aの先頭に O(1) で戻れる 空きブロック C Bの直後4Bを読む = Cのヘッダ → Cが空きだと分かる → そのまま後ろにも結合 フリーリストを走査せず、隣の4Bを読むだけで前後と結合できる 結合後 空きブロック A + B + C(1つに結合)
  43. リンカシンボルをSwiftで読み取る ̲̀heap̲start̀ / ̲̀heap̲end̀ • ヒープの範囲を決めているのはリンカスクリプト(Swiftのコードではない) • どうにかしてSwift側で読み取る必要がある • ̀@̲extern(c)̀

    で宣言してアドレスだけ取る(型はUInt8だが何でもよい) @_extern(c, "_heap_start") nonisolated(unsafe) var _heap_start: UInt8 @_extern(c, "_heap_end") nonisolated(unsafe) var _heap_end: UInt8 func linkerSymbolAddress(_ symbol: inout UInt8) -> UInt { withUnsafePointer(to: &symbol) { UInt(bitPattern: $0) } } heapStart = linkerSymbolAddress(&_heap_start) heapEnd = linkerSymbolAddress(&_heap_end) // 0x3FC800F8 // 0x3FCD8700 ≒ 353KB