protocのプラグインをSwiftで書く

# protoc のプラグインを Swift で書く
* protobuf の魅力の一つに Third-Party 製の Add-on を容易に作れることがあります
* 実際に沢山の Add-on が公開されています

https://github.com/protocolbuffers/protobuf/blob/main/docs/third_party.md

* 今回は、protoc のプラグインを Swift で実装してみました

# protoc のプラグイン
* protoc は proto ファイルからスキーマを解析して、任意のファイルを生成してくれます
* 公式では、C++ や Java、Python、C# などに対応しています
* プラグインを実装することで、これ以外の言語やファイルを出力させる事が可能です
* 例えば、[protoc-gen-doc](https://github.com/pseudomuto/protoc-gen-doc) を利用すると、proto 定義から HTML のドキュメントを生成してくれます
    * plugin を利用する場合は、--plugin で明示的に指定できます

```makefile
// protoc-gen-doc の利用例
protoc \
  --plugin=protoc-gen-doc \
  --doc_out=./doc \
  --doc_opt=html,index.html \
  proto/*.proto
```

# protoc のプラグインの実装に必要なこと

* 実装に必要なことはとてもシンプルです
* 標準入力から `CodeGeneratorRequest` のバイナリを受け取って

https://github.com/protocolbuffers/protobuf/blob/520c601c99012101c816b6ccc89e8d6fc28fdbb8/src/google/protobuf/compiler/plugin.proto#L68

* 標準出力に `CodeGeneratorResponse` のバイナリを出力するだけです

https://github.com/protocolbuffers/protobuf/blob/520c601c99012101c816b6ccc89e8d6fc28fdbb8/src/google/protobuf/compiler/plugin.proto#L99

* あとは適当にエラーハンドリングします

# protoc のプラグインを Swift で実装する
* 実際に Swift でプラグインを実装します
* 今回は、proto定義からフィールド名の一覧のテキストファイルを出力するプラグイン `protoc-gen-sfield` を実装してみました

```make
$ cat proto/SearchRequest.proto
syntax = "proto3";

message SearchRequest {
  string query = 1;
  int32 page_number = 2;
  int32 result_per_page = 3;
}

$ protoc \
    --plugin=protoc-gen-sfield \
    --sfield_out=out\
    proto/SearchRequest.proto

$ cat out/fields.txt
query, page_number, result_per_page
```

## protoc-gen-sfield の実装
* Swift Package を利用して作ります
* `protoc-gen-sfield` という名前で、executable target を作成します

```shell
$ swift package init --type=executable --name=protoc-gen-sfield
Creating executable package: protoc-gen-sfield
Creating Package.swift
Creating README.md
Creating .gitignore
Creating Sources/
Creating Sources/protoc-gen-sfield/main.swift
Creating Tests/
Creating Tests/protoc-gen-sfieldTests/
Creating Tests/protoc-gen-sfieldTests/protoc_gen_sfieldTests.swift
```

* Package.swiftを開き、依存関係を追加します

```swift
let package = Package(
    name: "protoc-gen-sfield",
    dependencies: [
        .package(url: "https://github.com/apple/swift-protobuf.git", from: "1.19.0"),
    ],
    targets: [
        .executableTarget(
            name: "protoc-gen-sfield",
            dependencies: [
                .product(name: "SwiftProtobuf", package: "swift-protobuf"),
                .product(name: "SwiftProtobufPluginLibrary", package: "swift-protobuf"),
            ]),
    ]
)
```

* main.swift を開き、適当に入出力を書きます
* readDataToEndOfFile で EOF まで読み込み、適当に入力をコネコネした結果を write しているだけです
* Google_Protobuf_Compiler_CodeGeneratorRequest など、plugin に必要な定義は、SwiftProtobufPluginLibrary に入っています

```swift
import Foundation
import SwiftProtobuf
import SwiftProtobufPluginLibrary

let requestData = FileHandle.standardInput.readDataToEndOfFile()
let request = try! Google_Protobuf_Compiler_CodeGeneratorRequest(serializedData: requestData)
let fields: [[String]] = request.protoFile
    .map { (file: Google_Protobuf_FileDescriptorProto) in
        let res = file.messageType.map { (descriptor: Google_Protobuf_DescriptorProto) in
            descriptor.field.map { (field: Google_Protobuf_FieldDescriptorProto) in
                field.name
            }
        }
        return Array(res.joined())
    }

var file = Google_Protobuf_Compiler_CodeGeneratorResponse.File()
file.name = "fields.txt"
file.content = fields
    .map { $0.joined(separator: ", ") }
    .joined(separator: "\n")

let response = Google_Protobuf_Compiler_CodeGeneratorResponse(files: [file])
let responseData = try! response.serializedData()
FileHandle.standardOutput.write(responseData)
```

## protoc-gen-sfield の動作確認
* 実装した protoc-gen-sfield をビルドします

```shell
swift build --product protoc-gen-sfield
```

* `.build/debug/protoc-gen-sfield` が作成されるので、これを plugin として指定して生成します
* 事前に出力先のディレクトリを用意しておく必要があります

```make
$ mkdir out
$ protoc \
	  --plugin=.build/debug/protoc-gen-sfield \
	  --sfield_out=out\
	  proto/SearchRequest.proto
```

* `out/fields.txt` を確認すると、フィールド名の一覧が作成されています

```fields.text
query, page_number, result_per_page
```

* これで、protoc のプラグインを Swift で実装できました

# CommandPlugin を実装する
* せっかくなので、Swiift 5.6 から追加された CommandPlugin にしてみます
* CommandPlugin にすることで、他のプロジェクトで使う回す事が容易になります

## protoc の artifactbundle の作成
* [protobuf の relase 一覧](https://github.com/protocolbuffers/protobuf/releases)から任意のバージョンのバイナリをダウンロードしてきて、Artifacts/protoc.artifactbundle 以下に配置します

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214313/b210063c-e9a2-58a4-11f3-51cf190c71ff.png)

* Artifacts/protoc.artifactbundle 以下に Info.jon を作成し、バイナリへのパスを記述します

```json
{
    "schemaVersion": "1.0",
    "artifacts": {
        "swiftgen": {
            "type": "executable",
            "version": "3.20.1",
            "variants": [
                {
                    "path": "protoc-3.20.1-osx-x86_64/bin/protoc",
                    "supportedTriples": ["x86_64-apple-macosx"]
                }
            ]
        }
    }
}
```

## Package.swift の編集
* binaryTarget と plugin をターゲットに追加しています

```swift
import PackageDescription

let package = Package(
    name: "protoc-gen-sfield",
    dependencies: [
        .package(url: "https://github.com/apple/swift-protobuf.git", from: "1.19.0"),
    ],
    targets: [
        .binaryTarget(
            name: "protoc",
            path: "Artifacts/protoc.artifactbundle"),
        .executableTarget(
            name: "protoc-gen-sfield",
            dependencies: [
                .product(name: "SwiftProtobuf", package: "swift-protobuf"),
                .product(name: "SwiftProtobufPluginLibrary", package: "swift-protobuf"),
            ]),
        .plugin(
            name: "SwiftFieldCommand",
            capability: .command(intent: .custom(verb: "sfield", description: "swift field command")),
            dependencies: [
                .target(name: "protoc"),
                .target(name: "protoc-gen-sfield"),
            ]),
    ]
)
```

## SwiftFieldCommand の実装
* Plugins/SwiftFieldCommand/SwiftFieldCommand.swift を作成します

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214313/5989106f-ee4e-583d-ae95-d9ce0e26be45.png)

* 依存関係から、protoc と protoc-gen-sfield のパスを取得し、Process.run で実行します
* 元々の arguments に plugin の設定を追加しています
* 上で protoc を artifactbundle にしたため、`context.tool` でパスを取得できています

```swift
import Foundation
import PackagePlugin

@main struct SwiftFieldCommand: CommandPlugin {
    func performCommand(context: PluginContext, arguments: [String]) async throws {
        let protocTool = try context.tool(named: "protoc")
        let protocExec = URL(fileURLWithPath: protocTool.path.string)
        let pluginTool = try context.tool(named: "protoc-gen-sfield")
        let additionalArguments = [
            "--plugin", pluginTool.path.string,
        ]
        let process = try Process.run(protocExec, arguments: arguments + additionalArguments)
        process.waitUntilExit()
    }
}
```

## Makefile
* make で実行できるようにします
* `--allow-writing-to-directory` で明示的に plugin に書き込み先を指定する必要があります
* sfield に渡す引数は、protoc への引数と同様に指定可能です

```Makefile:Makefile
PROTO_PATH=proto
OUTPUT_PATH=out

sfield:
	mkdir -p ${OUTPUT_PATH}
	swift package plugin \
		--allow-writing-to-directory ${OUTPUT_PATH} \
		sfield \
		--proto_path=${PROTO_PATH} \
		--sfield_out=${OUTPUT_PATH} \
		${PROTO_PATH}/*.proto
```

## 動作確認
* 先ほど作成した make を実行してみます

```shell
make sfield
```

* `out/fields.txt` を確認すると、フィールド名の一覧が作成されています

```fields.text
query, page_number, result_per_page
```

# 終わりに
* protoc のプラグインを Swift で実装する話でした
* かなり簡単にプラグインを実装できて驚きました
* ドキュメント生成や、lint など応用先の幅が広がりそうです