Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
わいわいswiftc#35夢が広がる!コード生成でどこでもSwift
Search
Iceman
April 25, 2022
Programming
480
0
Share
Embed
Copy iframe code
Copy JS code
Copy link
Start on current slide
わいわいswiftc#35夢が広がる!コード生成でどこでもSwift
Iceman
April 25, 2022
More Decks by Iceman
See All by Iceman
わいわいswift#39 Swiftの型をTypeScriptで表す
sidepelican
0
340
元ゲーム開発者が贈る描画パフォーマンス改善 / Rendering performance improvement from a game developer
sidepelican
4
1.8k
わいわいswiftc#19Genericsの特殊化
sidepelican
0
480
わいわいswiftc#17Genericsの特殊化
sidepelican
0
100
SwiftUI: 更新検知と値の生存期間
sidepelican
2
1.2k
クックパッドiOSアプリのパフォーマンス改善
sidepelican
0
800
DispatchQueue.syncが動作するスレッド
sidepelican
0
390
Other Decks in Programming
See All in Programming
Oxcを導入して開発体験が向上した話
yug1224
4
300
AIチームを指揮するOSS「TAKT」活用術 / How to Use “TAKT,” an OSS Tool for Orchestrating AI Teams
nrslib
6
850
A2UI という光を覗いてみる
satohjohn
1
120
dRuby over BLE
makicamel
2
330
不変条件と整合性境界—ビジネスが決める設計判断と実現パターン / Invariants and Consistency Boundaries
nrslib
13
3.6k
Observability in Practice:Grafana 與 Edge Device SRE 的那些事
blueswen
0
150
代数的データ型って何が嬉しいの? #frontend_phpcon_do
kajitack
8
3.3k
Copilot CLI の継戦能力を高める コンテキスト管理
nozomutu
1
1.2k
[2026年度第1回ORセミナー] 計画最適化ベンチャーと競技プログラミング人材
terryu16
0
250
CLIであることを活かしたGitHub Copilot CLI活用術 / GitHub Copilot CLI Pro Tips & Tricks
nao_mk2
1
1.2k
Javaの型とAI時代に型が大事な理由 / java types and type in AI era
kishida
2
120
AI時代の仕事技芸論 — ソフトウェア開発で「遊ぶように働く」職人的熟達のすすめ
kuranuki
1
640
Featured
See All Featured
Bridging the Design Gap: How Collaborative Modelling removes blockers to flow between stakeholders and teams @FastFlow conf
baasie
0
580
The Cult of Friendly URLs
andyhume
79
6.9k
Build your cross-platform service in a week with App Engine
jlugia
234
18k
The #1 spot is gone: here's how to win anyway
tamaranovitovic
2
1.1k
Raft: Consensus for Rubyists
vanstee
141
7.5k
Designing for Timeless Needs
cassininazir
1
250
Dominate Local Search Results - an insider guide to GBP, reviews, and Local SEO
greggifford
PRO
0
190
Templates, Plugins, & Blocks: Oh My! Creating the theme that thinks of everything
marktimemedia
31
2.8k
How Fast Is Fast Enough? [PerfNow 2025]
tammyeverts
3
600
Deep Space Network (abreviated)
tonyrice
0
170
Efficient Content Optimization with Google Search Console & Apps Script
katarinadahlin
PRO
1
600
The Straight Up "How To Draw Better" Workshop
denniskardys
239
140k
Transcript
わいわいswiftc #35 夢が広がる!コード生成でどこでもSwift Twitter @iceman5499 2022年4月25日 1
既存コード生成技術の紹介 ステンシルを書いてテンプレート出力する系 SwiftGen/SwiftGen krzysztofzablocki/Sourcery 単一の高度な機能を提供する系 uber/mockolo uber/needle SwiftGen以外は全てSwiftSyntaxを用いている 2
SwiftSyntaxの課題 多くのコード生成ライブラリはSwiftSyntaxを利用しているが、Xcodeとバージョンを揃えて 使う必要があって地味に大変 → BetaなXcodeを使用しているなどで利用できない → Xcodeから実行する際に環境変数の指定が必要 3
使いやすさの問題 stencilファイルの難しさ(SwiftGen, Sourcery) 独自文法を勉強するのが大変 できない表現があったりして、代替案を頑張って模索する regexが使えないなど: https://github.com/SwiftGen/StencilSwiftKit/pull/123 魔術的なコードになりやすい 4
使いやすさの問題 自由度の課題 ライブラリが提供する表現力の範囲でしかコード生成できない オプションで切り替えられる範囲にも限度がある → ライブラリが想定する使い方の範囲で強く効果を発揮する → 自分のプロジェクトのほうをライブラリの思想に合わせて設計する必要がある 5
BinarySwiftSyntax & SwiftTypeReader BinarySwiftSyntax ローカルのXcode依存を回避 SwiftTypeReader コード生成器を自作しやすくする 6
作例紹介 7
CodableToTypeScript https://github.com/omochi/CodableToTypeScript Swiftの型定義をTypeScriptの型定義に変換する 8
CodableToTypeScript 例1: シンプルなCodable public struct Foo: Codable { public var
bar: Int? public var baz: [String] } → export type Foo = { bar?: number; baz: string[]; }; 9
CodableToTypeScript 例2: 文字列enum public enum Language: String, Codable { case
ms case en case ja } → export type Language = "ms" | "en" | "ja"; 10
CodableToTypeScript 例3: 値付きenum public enum FilterItem: Codable, Equatable { case
name(String) case email(String) } ~~~Decode 関数も自動で生成 される kind を追加することで switchにおける網羅チェック とsmart castを有効にしている → export type FilterItemJSON = { name: { _0: string; }; } | { email: { _0: string; }; }; export type FilterItem = { kind: "name"; name: { _0: string; }; } | { kind: "email"; email: { _0: string; }; }; export function FilterItemDecode(json: FilterItemJSON): FilterItem { if ("name" in json) { return { "kind": "name", name: json.name }; } else if ("email" in json) { return { "kind": "email", email: json.email }; } else { throw new Error("unknown kind"); } } 11
CodableToTypeScript 使用例: switch (filter.kind) { case "name": const name =
filter.name._0; // .name をエラー無しに参照できる ... case "email": const email = filter.email._0; // .email をエラー無しに参照できる ... } smart castによってcaseごとの値を型安全に取り出せる 12
CodableToTypeScript SwiftサーバとTypeScriptクライアントな環境において、Swift側の型定義を変更するだけ でTS側もコンパイルエラーになってくれる enumのcaseをunionにしたり値付きenumが使えたりと、Swiftの表現力をそのまま 利用できて便利 .proto や .graphql などの専用の定義ファイルは不要 [T]
を T[] に変換したり、 T? を T|undefined として変換できる (ある程度は)Genericsにも対応 13
使い方 CodableToTypeScript単体はライブラリなので、自前でコード生成用ターゲットを作って そこから使う // Package.swift .package(url: "https://github.com/omochi/CodableToTypeScript", branch: "main"), ...
.executableTarget( name: "CodeGenStage2", dependencies: [ "CodableToTypeScript", ] ), 14
使い方 // main.swift import SwiftTypeReader import CodableToTypeScript let module =
try SwiftTypeReader.Reader().read(file: ...).module let generate = CodableToTypeScript.CodeGenerator(typeMap: .default) for swiftType in module.types { let tsCode = try generate(type: swiftType) _ = tsCode.description // TypeScript コードそのままの文字列になっている } SwiftTypeReaderで読み取った型をCodableToTypeScriptに渡す 15
CallableKit https://github.com/sidepelican/CallableKit サーバ上のSwift関数をクライアントにasync関数として出荷する descriptionなにもなくてごめんなさい 16
CallableKit 定義protocolから、サーバ用コードとクライアント用コードが生成される 例: 定義protocol public protocol EchoServiceProtocol { func hello(request:
EchoHelloRequest) async throws -> EchoHelloResponse } public struct EchoHelloRequest: Codable, Sendable { public var name: String } public struct EchoHelloResponse: Codable, Sendable { public var message: String } 17
CallableKit 例: サーバ用ルーティング実装(生成コード) import APIDefinition // 定義ファイルはそのままモジュールとしても利用する import Vapor struct
EchoServiceProvider<RequestHandler: RawRequestHandler, Service: EchoServiceProtocol>: RouteCollection { var requestHandler: RequestHandler var serviceBuilder: (Request) -> Service init(handler: RequestHandler, builder: @escaping (Request) -> Service) { self.requestHandler = handler self.serviceBuilder = builder } func boot(routes: RoutesBuilder) throws { routes.group("Echo") { group in group.post("hello", use: requestHandler.makeHandler(serviceBuilder) { s in try await s.hello() }) } } } RouteCollection なので、Vaporの RoutesBuilder にそのままregisterできる 18
CallableKit 例: クライアント用スタブ実装(生成コード) import APIDefinition public struct EchoServiceStub: EchoServiceProtocol, Sendable
{ private let client: StubClientProtocol public init(client: StubClientProtocol) { self.client = client } public func hello(request: EchoHelloRequest) async throws -> EchoHelloResponse { return try await client.send(path: "Echo/hello") } } 19
生成コードの役割は型をつけるだけなので、送信部分の実装詳細には関与していない 雰囲気はgRPCと同じ gRPCよりはかなり薄くて、通信の詳細などは規定せずあくまでインターフェース を定義するだけ Swift Distributed Actorsのように、サーバ上のasync関数を呼び出せるようにする try await echoService.hello(request:
.init(name: "Foo")) 20
パッケージ構造 . ├── APIDefinition │ └── Sources │ └── APIDefinition
// 定義だけで実装はなし │ └── Echo.swift ├── APIServer │ └── Sources │ ├── Service // Service の具体的な実装。依存にサーバ用モジュールはなし │ │ └── EchoService.swift │ └── Server // Vapor に依存し、サーバを起動する │ ├── EchoProvider.gen.swift │ └── main.swift ├── ClientApp │ └── Sources │ └── APIClient │ └── EchoStub.gen.swift 21
現在はHTTPの通信にVaporを利用しているが、直接依存しているわけではないので将来 的にVapor以外のフレームワークにも切り替えられる クライアントではただのprotocolとして見えているため、モック実装などへの差し替え が容易 サンプルプロジェクト: https://github.com/sidepelican/CallableKit/tree/main/example 22
Typescript版クライアント CodableToTypeScriptと組み合わせて、TypeScriptクライアントもコード生成 23
Typescript版クライアント 例: TS版クライアント用スタブ実装(生成コード) import { IRawClient } from "./common.gen"; export
interface IEchoClient { hello(request: EchoHelloRequest): Promise<EchoHelloResponse> } class EchoClient implements IEchoClient { rawClient: IRawClient; constructor(rawClient: IRawClient) { this.rawClient = rawClient; } async hello(request: EchoHelloRequest): Promise<EchoHelloResponse> { return await this.rawClient.fetch({}, "Echo/hello") as EchoHelloResponse } } export const buildEchoClient = (raw: IRawClient): IEchoClient => new EchoClient(raw); export type EchoHelloRequest = { name: string; }; export type EchoHelloResponse = { message: string; }; 24
CodableToTypeScript Swiftの型をTypeScriptの型に変換できる CallableKit Swift protocolを任意の言語のinterfaceに変換できる → WebAssembly × TypeScriptにも応用可能 25
WasmCallableKit https://github.com/sidepelican/WasmCallableKit Swiftの型をそのままexportできるWasmライブラリを作成 descriptionなにもなくてごめんなさい 26
WasmCallableKit WasmビルドされたSwift関数をTSから呼び出せる 例: // WasmExports.swift protocol WasmExports { static func
hello(name: String) -> String } // main.swift struct Foo: WasmExports { static func hello(name: String) -> String { "Hello, \(name) from Swift" } } WasmCallableKit.setFunctionList(Foo.functionList) → export type FooExports = { hello: (name: string) => string, }; console.log(swift.hello("world")) // > Hello, world from Swift 27
もちろん、CodableToTypeScriptで変換できるSwiftの型なら何でもやりとりできる protocol WasmExports { static func newGame() -> GameID static
func putFence(game: GameID, position: FencePoint) throws static func movePawn(game: GameID, position: PawnPoint) throws static func aiNext(game: GameID) throws static func currentBoard(game: GameID) throws -> Board static func deleteGame(game: GameID) } ↓ export type WasmLibExports = { newGame: () => GameID, putFence: (game: GameID, position: FencePoint) => void, movePawn: (game: GameID, position: PawnPoint) => void, aiNext: (game: GameID) => void, currentBoard: (game: GameID) => Board, deleteGame: (game: GameID) => void, }; 28
WasmCallableKitの仕組み 文字列をやりとりできるように最低限のランタイムライブラリの用意 Wasmはそのままだと数値型しか直接やりとりできない SwiftTypeReaderとCodableToTypeScriptでTS用の型定義 JS ⇔ Swift間で引数と返り値をJSON文字列としてやりとりする tsランタイム: https://github.com/sidepelican/WasmCallableKit/blob/main/Codegen/Sources/Codegen/templates/SwiftRuntime.ts swiftランタイム:
https://github.com/sidepelican/WasmCallableKit/blob/main/Sources/WasmCallableKit/WasmCallableKit.swift 29
使用例 Swift Quoridor: https://swiftwasmquoridor.iceman5499.work Quoridor(コリドール)というボードゲームとそのAIをSwiftで実装 UIだけReact リポジトリ: https://github.com/sidepelican/SwiftWasmQuoridor 30
JavaScriptKitとの比較? JavaScriptKitはSwiftからJS関数を呼び、SwiftがJSを利用する形になっている。これは Reactのような、JSフレームワークからSwiftを利用したい場合に使いづらかった 単純にやってみたかった 課題 関数を呼び出すたびにJSON文字列との変換が入るのでめちゃくちゃ遅い Reactの場合、1ビルド中に100回程度Swift関数を呼び出すとそのオーバヘッドだけ で遅延を体感できる シリアライズをより軽量な方法で行う、数値型はそのまま渡す、などの工夫が必要そう 31
ここまではブラウザにおける話。 ブラウザからSwiftのWebAPIやWasmのSwift関数を利用できるようになった。 JS上でSwiftを使いたい需要、他には・・・? 32
Cloud Functions for Firebase上でSwift関数を実行 33
Cloud Functions for Firebase上でSwift関数を実行 ブラウザのWasmでSwift関数が使えるなら、Nodeでも動かせるはず サンプル: https://github.com/sidepelican/CFSwiftWasmExample 例: export const
hello = functions.https.onRequest(async (request, response) => { const name = request.query["name"] as string ?? "world"; response.send(swift.hello(name)); }); 34
Cloud Functions for Firebase上でSwift関数を実行 1. WASIのセットアップ Cloud Functions上のNodeではWASIが利用できない( --experimental-wasi- unstablre-preview0
を有効にする方法がない?)ので、 @wasmer/wasi を使っ てWASIを構築する const wasi = new WASI(); 35
2. 通常のWebAssembly利用時のボイラープレート通りにセットアップ const swift = new SwiftRuntime(); const wasmPath =
path.join(__dirname, 'Gen/MySwiftLib.wasm'); const module = new WebAssembly.Module(fs.readFileSync(wasmPath)); const instance = new WebAssembly.Instance(module, { ...wasi.getImports(module), ...swift.callableKitImpodrts, }); swift.setInstance(instance); wasi.start(instance); return bindMySwiftLib(swift); 36
Cloud Functions for FirebaseでSwiftWasmを使うことは実用的か? Webと違い、バイナリサイズを(そこまで)気にしなくて良い NIOがないため、既存のサーバ用Swiftコードの多くが利用できない NIOのWasm対応はかなり厳しいらしい https://github.com/apple/swift-nio/pull/1404#issuecomment-587357512 AsyncHTTPClientなどの基本的なHTTPクライアントが利用できない Firebase
Admin SDKのSwift版がないので、大変 用途はかなり限定されそう 37
まとめ 1. コード生成が気楽にできるようになる(SwiftTypeReader) ↓ 2. TypeScriptからでもSwiftの型が使えるようになる(CodableToTypeScript) 3. SwiftのprotocolでAPI定義できるようになる(CallableKit) ↓ 4.
ブラウザからSwift関数を呼べるようになる(CodableToTypeScript × CallableKit) 5. WasmからSwift関数を呼べるようになる(WasmCallableKit) Swiftがたくさん書けて嬉しい! 38
おわり 39