|
| 1 | +--- |
| 2 | +title: Macros (experimental) |
| 3 | +description: Learn about the experimental macros feature as it develops. |
| 4 | +--- |
| 5 | + |
| 6 | +[The Dart macro system][spec] is a major new language feature |
| 7 | +***currently under development*** which adds support for |
| 8 | +[static meta-programming][motivation] to the Dart language. |
| 9 | + |
| 10 | +A Dart macro is a user-defineable piece of code that takes in other code as parameters |
| 11 | +and operates on it during compile time to create, modify, or add declarations. |
| 12 | + |
| 13 | +You can think about the macro system in two parts: using macros and writing macros. |
| 14 | +This page covers each (at a high level, as ***the feature is still under development***) |
| 15 | +in the following sections: |
| 16 | + |
| 17 | +- [**The `JsonCodable` macro**](#the-jsoncodable-macro): |
| 18 | +A ready-made macro you can try out today (behind an experiment flag) |
| 19 | +that offers a seamless solution to the |
| 20 | +common issue of tedious JSON serialization in Dart. |
| 21 | + |
| 22 | +- [**The macros feature in general**](#the-macros-language-feature): |
| 23 | +Why we're adding macros to Dart, motivating use cases, |
| 24 | +benefits over existing code gen solutions, |
| 25 | +and a cursory overview of how writing macros works. |
| 26 | + |
| 27 | +[spec]: https://github.com/dart-lang/language/blob/main/working/macros/feature-specification.md |
| 28 | +[motivation]: https://github.com/dart-lang/language/blob/main/working/macros/motivation.md |
| 29 | + |
| 30 | +## The `JsonCodable` macro |
| 31 | + |
| 32 | +:::important |
| 33 | +The `JsonCodable` macro is not stable and currently behind an [experiment flag][]. |
| 34 | +Functionality is subject to change. |
| 35 | +::: |
| 36 | + |
| 37 | +The [`JsonCodable`][] macro encodes and decodes |
| 38 | +user-defined Dart classes to JSON maps of type `Map<String, Object?>`. |
| 39 | +It generates two members, a `toJson` serialization method, |
| 40 | +and a `fromJson` deserialization constructor. |
| 41 | + |
| 42 | +[experiment flag]: /tools/experiment-flags |
| 43 | +[`JsonCodable`]: https://github.com/dart-lang/sdk/tree/main/pkg/json |
| 44 | + |
| 45 | +### Set up the experiment |
| 46 | + |
| 47 | +1. [Add the package][] to your pubspec and retrieve |
| 48 | + its dependencies. |
| 49 | +2. [Add the experiment][] to the `analysis_options.yaml` |
| 50 | + file at the root of your project: |
| 51 | + |
| 52 | + ```yaml |
| 53 | + analyzer: |
| 54 | + enable-experiment: |
| 55 | + - macros |
| 56 | + ``` |
| 57 | +3. Import it in the file you plan to use it: |
| 58 | +
|
| 59 | + ```dart |
| 60 | + import 'package:json/json.dart'; |
| 61 | + ``` |
| 62 | + |
| 63 | +4. Run your project with the experiment flag: |
| 64 | + |
| 65 | + ```console |
| 66 | + dart --enable-experiment=macros run bin/my_app.dart |
| 67 | + ``` |
| 68 | + |
| 69 | +[Add the package]: /guides/packages |
| 70 | +[Add the experiment]: /tools/experiment-flags#using-experiment-flags-with-the-dart-analyzer-command-line-and-ide |
| 71 | + |
| 72 | +### Use the macro |
| 73 | + |
| 74 | +To use the `JsonCodable` macro, append the annotation to the class you want to serialize: |
| 75 | + |
| 76 | +```dart |
| 77 | +import 'package:json/json.dart'; |
| 78 | +
|
| 79 | +@JsonCodable() // Macro annotation. |
| 80 | +class User { |
| 81 | + final int? age; |
| 82 | + final String name; |
| 83 | + final String username; |
| 84 | +} |
| 85 | +``` |
| 86 | + |
| 87 | +The macro introspects the `User` class and derives the implementations of |
| 88 | +`fromJson` and `toJson` using the `User` class's fields. |
| 89 | + |
| 90 | +So, without needing to define them yourself, `toJson` and `fromJson` are now |
| 91 | +available to use on objects of the annotated class: |
| 92 | + |
| 93 | +```dart |
| 94 | +void main() { |
| 95 | + // Given some arbitrary JSON: |
| 96 | + var userJson = { |
| 97 | + 'age': 5, |
| 98 | + 'name': 'Roger', |
| 99 | + 'username': 'roger1337' |
| 100 | + }; |
| 101 | +
|
| 102 | + // Use the generated members: |
| 103 | + var user = User.fromJson(userJson); |
| 104 | + print(user); |
| 105 | + print(user.toJson()); |
| 106 | +} |
| 107 | +``` |
| 108 | + |
| 109 | +### View the generated code |
| 110 | + |
| 111 | +You can optionally view the generated code. |
| 112 | +Click on the "**Go to Augmentation**" link that appears under the annotation |
| 113 | +in your IDE (supported in VSCode and IntelliJ) |
| 114 | +to see how the macro generates `toJson` and `fromJson`. |
| 115 | + |
| 116 | +If you change anything in the annotated class, you can watch the generated augmentation |
| 117 | +adjust in real time alongside your application code: |
| 118 | + |
| 119 | + |
| 120 | + |
| 121 | +### Trigger custom diagnostics |
| 122 | + |
| 123 | +The `JsonCodable` macro has built-in diagnostics that are emmitted just like |
| 124 | +diagnostics from the language itself. For example, if you try to manually |
| 125 | +declare a `toJson` method where the macro is applied, the analyzer will emit |
| 126 | +the error: |
| 127 | + |
| 128 | +```dart |
| 129 | +@JsonCodable() |
| 130 | +class HasToJson { |
| 131 | + void [!toJson!]() {} |
| 132 | + // Error: Cannot generate a toJson method due to this existing one. |
| 133 | +} |
| 134 | +``` |
| 135 | + |
| 136 | +You can search "`DiagnosticMessage`" in the [the definition of `JsonCodable`][json] |
| 137 | +for other errors the macro will throw. For example, extending a class that isn't |
| 138 | +also serializable, or if field names don't exactly match the key names in the given JSON. |
| 139 | + |
| 140 | +[the definition of `JsonCodable`]: https://github.com/dart-lang/sdk/blob/master/pkg/json/lib/json.dart |
| 141 | + |
| 142 | +## The macros language feature |
| 143 | + |
| 144 | +Dart macros are a *static* metaprogramming, or code generation, solution. |
| 145 | +Unlike *runtime* code generation solutions (like [build_runner][]), |
| 146 | +macros are fully integrated into the Dart language and executed by the compiler. |
| 147 | +This makes macros much more efficient than relying on an secondary tool: |
| 148 | + |
| 149 | +- **Nothing extra to run**; |
| 150 | + simply call `dart ` / `flutter run <your app>` and the macro builds with your code. |
| 151 | +- **No duplicated work** or constant recompiling hurting performance; |
| 152 | + all the building and code generation happen directly in the compiler, |
| 153 | + automatically. |
| 154 | +- **Not written to disk**, so no part files or pointers to generated references; |
| 155 | + macros directly augment the *existing* class. |
| 156 | +- **No confusing/obfuscated testing**; |
| 157 | + custom diagnostics are emitted like any other message from the analyzer, |
| 158 | + directly in the IDE. |
| 159 | + |
| 160 | +And also far more efficient, and far less error prone, than manually |
| 161 | +writing solutions to these types of problems yourself. |
| 162 | + |
| 163 | +Check out these examples showing the same JSON serialization |
| 164 | +implemented three different ways: |
| 165 | + |
| 166 | +- Using the [`JsonCodable` macro][]. |
| 167 | +- Using the [`json_serializable` code gen package][]. |
| 168 | +- Manually, [with `dart:convert`][]. |
| 169 | + |
| 170 | +[build_runner]: /tools/build_runner |
| 171 | +[`JsonCodable` macro]: https://github.com/mit-mit/sandbox/blob/main/explorations/json/dart_jsoncodable/bin/main.dart |
| 172 | +[`json_serializable` code gen package]: https://github.com/mit-mit/sandbox/blob/main/explorations/json/dart_json_serializable/bin/main.dart |
| 173 | +[with `dart:convert`]: https://github.com/mit-mit/sandbox/blob/main/explorations/json/dart_convert/bin/main.dart |
| 174 | + |
| 175 | +### Use cases |
| 176 | + |
| 177 | +Macros provide reusable mechanisms to address patterns characterized by tedious |
| 178 | +boilerplate, and often times the need to iterate over the fields of a class. |
| 179 | +Some common examples are: |
| 180 | + |
| 181 | +- **Json serialization.** The extra tooling required to serialize JSON, |
| 182 | + like the [json_serializable][] package, isn't as efficient as it should be. |
| 183 | + The `JsonCodable` macro provides a much cleaner way to |
| 184 | + generate serialization code; [try it today](#the-jsoncodable-macro). |
| 185 | + |
| 186 | +- **Data classes.** Dart's [most requested][] feature is for data classes |
| 187 | + that automatically provide a constructor, and implementations of the `==`, |
| 188 | + `hashCode`, and `copyWith()` methods for each field. |
| 189 | + Implementing the solution with macros would mean users can customize the |
| 190 | + their data classes however they see fit. |
| 191 | + |
| 192 | +- **Verbose Flutter patterns.** One example is breaking down a complex `build` |
| 193 | + method into an aggregation of smaller widget classes. It's |
| 194 | + better for performance and makes the code more maintainable. Unfortunately, |
| 195 | + writing all those smaller classes requires tons of boilerplate, which discourages |
| 196 | + users. Macros could potentially provide a solution that iterates over a |
| 197 | + complex `build` method to generates smaller widget classes, |
| 198 | + greatly improving productivity and quality of Flutter code. |
| 199 | + |
| 200 | +[json_serializable]: https://pub.dev/packages/json_serializable |
| 201 | +[most requested]: https://github.com/dart-lang/language/issues/314 |
| 202 | + |
| 203 | +### How macros work |
| 204 | + |
| 205 | +:::important |
| 206 | +The macros language feature is not stable and currently behind an [experiment flag][]. |
| 207 | +Functionality is highly subject to change. This section will remain very high-level until stable. |
| 208 | +::: |
| 209 | + |
| 210 | +To create a macro, you write a macro declaration similar to a class, |
| 211 | +using the `macro` keyword. |
| 212 | +A macro declaration must also include an `implements` clause to define |
| 213 | +which interface the macro can be applied to. |
| 214 | + |
| 215 | +For example, a macro that is applicable to classes, and adds new declarations to the class, |
| 216 | +would implement the `ClassDeclarationsMacro` interface: |
| 217 | + |
| 218 | +```dart |
| 219 | +macro class MyMacro implements ClassDeclarationsMacro { |
| 220 | + const MyMacro(); |
| 221 | +
|
| 222 | +
|
| 223 | + // ... |
| 224 | +} |
| 225 | +``` |
| 226 | + |
| 227 | +While the feature is still in development, you can find the full list of |
| 228 | +macro interfaces [in the source code][types]. |
| 229 | + |
| 230 | +The `MyMacro` constructor in the above example corresponds to the annotation |
| 231 | +you would use to apply the macro to a declaration. |
| 232 | +The syntax is the same as Dart's existing metadata annotation syntax: |
| 233 | + |
| 234 | +```dart |
| 235 | +@MyMacro() |
| 236 | +class A {} |
| 237 | +``` |
| 238 | + |
| 239 | +Within the body of the macro declaration is where you define the code you want |
| 240 | +the macro to [generate](#view-the-generated-code), as well as any |
| 241 | +[diagnostics](#trigger-custom-diagnostics) you want the macro to emit. |
| 242 | + |
| 243 | +At a very high-level, writing macros essentially work by using builder methods |
| 244 | +to piece together the *properties* of a declaration with *identifiers* on those |
| 245 | +properties. The macro gathers this information through deep [introspection][] of |
| 246 | +the program. |
| 247 | + |
| 248 | +Macros are still under development, so that's as much detail we can go into for now. |
| 249 | +If you're curious, or would like to try it yourself behind an experiment flag, |
| 250 | +the best guidance is to take a look at the implementation of exisiting macros: |
| 251 | + |
| 252 | +- Check out the [definition][json] of the `JsonCodable` macro, |
| 253 | +- Or any of the [examples][] available in the language repo. |
| 254 | + |
| 255 | +[types]: https://github.com/dart-lang/sdk/blob/main/pkg/_macros/lib/src/api/macros.dart |
| 256 | +[json]: https://github.com/dart-lang/sdk/blob/master/pkg/json/lib/json.dart |
| 257 | +[augmentation]: https://github.com/dart-lang/language/blob/main/working/augmentation-libraries/feature-specification.md |
| 258 | +[examples]: https://github.com/dart-lang/language/tree/main/working/macros/example |
| 259 | + |
| 260 | +## Timeline |
| 261 | + |
| 262 | +The stable release date for macros is currently unknown. |
| 263 | +This is due to the complexity of their implementation. |
| 264 | + |
| 265 | +Macros work by deeply [introspecting][introspection] the program in which |
| 266 | +they're applied. A macro may end up traversing distant parts of the program |
| 267 | +to gather necessary information on properties and type annotations |
| 268 | +for the declaration it's augmenting. |
| 269 | + |
| 270 | +Considering their application in large code bases, where multiple macros can |
| 271 | +introspect and augment the base continuously in different places, |
| 272 | +the design of [ordering][] and [phases][] of execution is especially challenging |
| 273 | +and requires careful consideration. |
| 274 | + |
| 275 | +We are working towards a stable release of the [`JsonCodable`][] macro |
| 276 | +later this year (2024), and a stable release of the full language feature |
| 277 | +(namely, writing your own macros) early next year (2025). |
| 278 | + |
| 279 | +[introspection]: https://github.com/dart-lang/language/blob/main/working/macros/feature-specification.md#introspection |
| 280 | +[ordering]: https://github.com/dart-lang/language/blob/main/working/macros/feature-specification.md#ordering-in-metaprogramming |
| 281 | +[phases]: https://github.com/dart-lang/language/blob/main/working/macros/feature-specification.md#phases |
0 commit comments