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
350
元ゲーム開発者が贈る描画パフォーマンス改善 / Rendering performance improvement from a game developer
sidepelican
4
1.8k
わいわいswiftc#19Genericsの特殊化
sidepelican
0
480
わいわいswiftc#17Genericsの特殊化
sidepelican
0
110
SwiftUI: 更新検知と値の生存期間
sidepelican
2
1.2k
クックパッドiOSアプリのパフォーマンス改善
sidepelican
0
810
DispatchQueue.syncが動作するスレッド
sidepelican
0
390
Other Decks in Programming
See All in Programming
えっ!!コードを読まずに開発を!?
hananouchi
0
230
才能?センス?知らん、 続けたもん勝ちだ。-- 結婚・出産・癌を越えてなお、私がプロダクトを創り続ける理由
16bitidol
2
890
ローカルLLMでどこまでコードが書けるか -縮小版 / How much code can be written on a local LLM Shortened
kishida
2
200
Embedded SREと共に達成した会員管理システムのAWS移行 - SRE NEXT 2026 ランチスポンサーセッション
niftycorp
PRO
1
2.8k
ビデオ通話が繋がる0.2秒で何が起きているのか
supurazako
2
150
AWS CDK を「作」ってみた 〜フルスクラッチで見えた CDK の裏側〜 / aws-cdk-from-scratch
gotok365
3
470
The Past, Present, and Future of Enterprise Java
ivargrimstad
0
330
生成AI導入の「期待外れ」を乗り越える ー 開発フロー改革が目指す、真の組織変革
starfish719
0
1.1k
アルゴリズムは何を圧縮しているのか ─ Haskell から育った「圧縮代数」というメンタルモデル
naoya
16
3.5k
AI時代のPHPer生存戦略 ~「言語、もうなんでもよくない?」に本気で向き合う~
vivion
0
120
自作OSでスライド発表する
uyuki234
1
3.9k
Haskell/Servantを通してWebミドルウェアを捉え直す
pizzacat83
1
600
Featured
See All Featured
Why Our Code Smells
bkeepers
PRO
340
58k
[Rails World 2023 - Day 1 Closing Keynote] - The Magic of Rails
eileencodes
38
2.9k
Conquering PDFs: document understanding beyond plain text
inesmontani
PRO
4
2.9k
The Hidden Cost of Media on the Web [PixelPalooza 2025]
tammyeverts
2
360
Leo the Paperboy
mayatellez
8
1.9k
Visualization
eitanlees
152
17k
Testing 201, or: Great Expectations
jmmastey
46
8.2k
No one is an island. Learnings from fostering a developers community.
thoeni
21
3.8k
The #1 spot is gone: here's how to win anyway
tamaranovitovic
3
1.1k
Effective software design: The role of men in debugging patriarchy in IT @ Voxxed Days AMS
baasie
0
450
CSS Pre-Processors: Stylus, Less & Sass
bermonpainter
360
30k
Fashionably flexible responsive web design (full day workshop)
malarkey
408
67k
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