タコでもわかるDartマクロ作成入門
はじめに
Google I/O 2024でFlutter発表の目玉の一つとして、Dartのマクロ(macros)機能が発表されました。
これまでコード生成に頼っていたJsonのシリアライズ・デシリアライズ、データクラス作成がより効率的に行えるということで注目されています。
より詳しい情報はDart 3.4のMedium記事をどうぞ
マクロを自分でも作ってみたい
マクロはDartのdev版、Flutterのmaster版ですでに利用可能です。
海外のつよつよエンジニアがいくつかマクロを公開しています。
Dart公式のJsonCodableマクロ
Remiさんのマクロ版Freezed
Felangelさんのデータクラスマクロ
これらマクロを試す分にはいいのですが、個人的にはもっと簡単なサンプルがみたいなーと思いました。(作ってる人たちがすごすぎて実装が高度🥹)
簡単なマクロを作ってみた
そういう訳で非常にシンプルなHello World!とプリントするだけのメソッドを生やすマクロを作ってみました。
()
class Sample {}
とするとこういうコードが作られて
augment class Sample {
void hello() => print("Hello, World!");
}
mainメソッドで利用できます。
()
class Sample {}
void main() {
final sample = Sample();
sample.hello();
}
実行時はこのようにすれば、Hello World!と表示されます。
$ dart --enable-experiment=macros run main.dart
$ Hello, World!
作り方
それではこのマクロの作り方です。
dev版のDartを入れる
記事執筆時点ではマクロを使うのにdev版のDartが必要です。
今回は同僚のおかやまんさんが開発しているDVM(Dart Version Management)を使います。
brewかpubでインストール
(pubの場合dvmじゃなくてdvmxなのに注意)
brew install blendfactory/tap/dvm
dart pub global activate dvmx
dev版のインストール
dvm install 3.5.0-164.0.dev
上は執筆時の最新です。以下のコマンドでリリースされているものが確認できます。
dvm list --remote -c dev
プロジェクトのセットアップ
適当にDartプロジェクトを作ります。
僕はVSCodeのコマンドパレットでDart: New Project > Dart packageで作りました。
プロジェクトルートでDVMの有効化をします。
dvm use 3.5.0-164.0.dev
パスの設定
{
"dart.sdkPath": ".dvm/dart_sdk",
}
マクロの依存追加
記事執筆時点の最新のmacrosを依存に追加します。
dependencies:
macros: ^0.1.0-main.5
実態はここにあります。
これがdart sdkに入っている_macros 0.1.5に依存しており、0.1.5はdev版移行にしかまだないので、dev版のDartが必要です。
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
マクロの有効化
analysis_optionsを書き換えます。
analyzer:
# macro有効化
enable-experiment:
- macros
マクロを書く
作成したパッケージのlib/src下のファイルを編集します。
パッケージ名_base.dartというファイルがあるはずです。
Hello Worldを表示したいだけならこのようにします。
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!");',
]
),
);
}
}
ClassDeclarationsMacroインターフェースが定義するbuildDeclarationsForClassに生成処理を書きます。
builderのdeclareInTypeに生成したいhelloメソッドのコードを渡します。
使ってみる
パッケージ作成時に作られたexample/パッケージ名_example.dartで試してみましょう。
以下はk9i_macro_sampleというパッケージ名だった場合の例です。
import 'package:k9i_macro_sample/k9i_macro_sample.dart';
()
class Sample {}
void main() {
final sample = Sample();
sample.hello();
}
実行時は「簡単なマクロを作ってみた」で説明したように--enable-experiment=macrosが必要です。
dvmを使ったコマンドラインからの実行なら
dvm dart --enable-experiment=macros run example/k9i_macro_sample_example.dart
といった感じ
オーグメントクラスを見てみる
マクロで作られたコードはVS Codeの「Go to Augmentation」というCodeLensから確認できます。
こんな感じのコードが生成されているのが見えると思います。
augment library 'file:///略/k9i_macro_sample/example/k9i_macro_sample_example.dart';
augment class Sample {
void hello() => print("Hello, World!");
}
このオーグメントクラス(augment class)がマクロによって生成されたコードです。
オーグメントクラスはマクロ本体を書き換えた時もマクロ利用コードを書き換えたときもリアルタイムで変更されるのが非常に快適に思えました。
まとめ
非常にシンプルなマクロの作り方紹介でした。
以下のリポジトリでコードを公開しているのでスターもらえると嬉しいです🥳
Discussion
declareInLibraryではなくdeclareInTypeを使うのが適切だったため修正しました 🙏
exportされたファイルのマクロを使うとGo to Augmentationが表示されないバグはDartをアプデすれば発生しなくなったので記事を更新しました
村松さん情報ありがとうございます🙇♂️