🐙

タコでもわかるDartマクロ作成入門

に公開
2

はじめに

Google I/O 2024でFlutter発表の目玉の一つとして、Dartのマクロ(macros)機能が発表されました。
これまでコード生成に頼っていたJsonのシリアライズ・デシリアライズ、データクラス作成がより効率的に行えるということで注目されています。

より詳しい情報はDart 3.4のMedium記事をどうぞ
https://medium.com/dartlang/dart-3-4-bd8d23b4462a

マクロを自分でも作ってみたい

マクロはDartのdev版、Flutterのmaster版ですでに利用可能です。
海外のつよつよエンジニアがいくつかマクロを公開しています。

Dart公式のJsonCodableマクロ
https://pub.dev/packages/json

Remiさんのマクロ版Freezed
https://pub.dev/packages/freezed/versions/3.0.0-0.0.dev

Felangelさんのデータクラスマクロ
https://pub.dev/packages/data_class_macro

これらマクロを試す分にはいいのですが、個人的にはもっと簡単なサンプルがみたいなーと思いました。(作ってる人たちがすごすぎて実装が高度🥹)

簡単なマクロを作ってみた

そういう訳で非常にシンプルなHello World!とプリントするだけのメソッドを生やすマクロを作ってみました。

https://github.com/K9i-0/k9i_macro_sample

()
class Sample {}
```text

とするとこういうコードが作られて

```dart
augment class Sample {
  void hello() => print("Hello, World!");
}
```text

mainメソッドで利用できます。

```dart:main.dart
()
class Sample {}

void main() {
  final sample = Sample();
  sample.hello();
}
```text

実行時はこのようにすれば、Hello World!と表示されます。

```zsh
dart --enable-experiment=macros run main.dart 
Hello, World!
```text

## 作り方

それではこのマクロの作り方です。

### dev版のDartを入れる

記事執筆時点ではマクロを使うのにdev版のDartが必要です。

今回は同僚の[おかやまんさん](https://zenn.dev/blendthink)が開発しているDVM(Dart Version Management)を使います。

<https://pub.dev/packages/dvmx>

brewかpubでインストール
(pubの場合dvmじゃなくてdvmxなのに注意)

```zsh:brewの場合
brew install blendfactory/tap/dvm
```text

```zsh:pubの場合
dart pub global activate dvmx
```text

:::message
dvm 0.0.7でpubの場合installで失敗する現象が起きたので現状brewがおすすめです。
:::

dev版のインストール

```zsh
dvm install 3.5.0-164.0.dev
```text

上は執筆時の最新です。以下のコマンドでリリースされているものが確認できます。

```zsh
dvm list --remote -c dev
```text

### プロジェクトのセットアップ

適当にDartプロジェクトを作ります。
僕はVSCodeのコマンドパレットでDart: New Project > Dart packageで作りました。

プロジェクトルートでDVMの有効化をします。

```zsh
dvm use 3.5.0-164.0.dev
```text

パスの設定

```json:.vscode/settings.json
{
    "dart.sdkPath": ".dvm/dart_sdk",
}
```text

#### マクロの依存追加

記事執筆時点の最新のmacrosを依存に追加します。

```yaml:pubspec.yaml
dependencies:
  macros: ^0.1.0-main.5
```text

実態はここにあります。

<https://github.com/dart-lang/sdk/tree/main/pkg/macros>

これがdart sdkに入っている_macros 0.1.5に依存しており、0.1.5はdev版移行にしかまだないので、dev版のDartが必要です。

```yaml:macrosのpubspec.yaml
name: macros
version: 0.1.0-main.5
description: >-
  This package is for macro authors, and exposes the APIs necessary to write
  a macro. It exports the APIs from the private `_macros` SDK vendored package.
repository: https://github.com/dart-lang/sdk/tree/main/pkg/macros

environment:
  sdk: ^3.4.0-256.0.dev

dependencies:
  _macros:
    sdk: dart
    version: 0.1.5
```text

#### マクロの有効化

analysis_optionsを書き換えます。

```yaml:analysis_options.yaml
analyzer:
  # macro有効化
  enable-experiment:
    - macros
```text

### マクロを書く

作成したパッケージのlib/src下のファイルを編集します。
パッケージ名_base.dartというファイルがあるはずです。

:::message
通常パッケージを作るとlib/srcにパッケージ名_base.dartというファイルが生成されそれがexportされるのですが、マクロがまだ不安定だからかlib直下のファイルに直接マクロを定義しないとVSCodeが後述するGo to Augmentationを表示しませんでした。

【追記】
こちらのバグは以下のcommitで修正されたようです。
<https://github.com/dart-lang/sdk/commit/73bdc86dd50e11cedb3bf976c597a02ad209bdb4>

3.5.0-163.0.dev以上を使えば問題は発生しなそうです。
[村松さん調査協力](https://x.com/riscait/status/1793837301652771172)ありがとうございます🙏
:::

Hello Worldを表示したいだけならこのようにします。

```dart:lib/パッケージ名_base.dart
import 'dart:async';

import 'package:macros/macros.dart';

macro class Hello implements ClassDeclarationsMacro {
  const Hello();

  
  FutureOr<void> buildDeclarationsForClass(ClassDeclaration clazz, MemberDeclarationBuilder builder) async {
    final methods = await builder.methodsOf(clazz);
    final hello =
        methods.where((e) => e.identifier.name == 'hello').firstOrNull;

    if (hello != null) return;

    builder.declareInType(
      DeclarationCode.fromParts(
        [
          '  void hello() => print("Hello, World!");',
        ]
      ),
    );
  }
}
```text

ClassDeclarationsMacroインターフェースが定義するbuildDeclarationsForClassに生成処理を書きます。
builderのdeclareInTypeに生成したいhelloメソッドのコードを渡します。

### 使ってみる

パッケージ作成時に作られたexample/パッケージ名_example.dartで試してみましょう。
以下はk9i_macro_sampleというパッケージ名だった場合の例です。

```dart:example/パッケージ名_example.dart
import 'package:k9i_macro_sample/k9i_macro_sample.dart';

()
class Sample {}

void main() {
  final sample = Sample();
  sample.hello();
}
```text

実行時は「簡単なマクロを作ってみた」で説明したように--enable-experiment=macrosが必要です。

dvmを使ったコマンドラインからの実行なら

```zsh
dvm dart --enable-experiment=macros run example/k9i_macro_sample_example.dart
```text

といった感じ

### オーグメントクラスを見てみる

マクロで作られたコードはVS Codeの「Go to Augmentation」というCodeLensから確認できます。

![macros_go_to_augmentation](/images/macros_go_to_augmentation.png =500x)

こんな感じのコードが生成されているのが見えると思います。

```dart
augment library 'file:///略/k9i_macro_sample/example/k9i_macro_sample_example.dart';

augment class Sample {
  void hello() => print("Hello, World!");
}

このオーグメントクラス(augment class)がマクロによって生成されたコードです。
オーグメントクラスはマクロ本体を書き換えた時もマクロ利用コードを書き換えたときもリアルタイムで変更されるのが非常に快適に思えました。

まとめ

非常にシンプルなマクロの作り方紹介でした。
以下のリポジトリでコードを公開しているのでスターもらえると嬉しいです🥳

https://github.com/K9i-0/k9i_macro_sample

GitHubで編集を提案
株式会社ゆめみ

Discussion

K9i - Kota HayashiK9i - Kota Hayashi

exportされたファイルのマクロを使うとGo to Augmentationが表示されないバグはDartをアプデすれば発生しなくなったので記事を更新しました
村松さん情報ありがとうございます🙇‍♂️