Skip to content

Commit

Permalink
Update Rust proc macro article, since the r3bl-open-core macros crate…
Browse files Browse the repository at this point in the history
… has changed
  • Loading branch information
nazmulidris committed Sep 29, 2024
1 parent a1fc0f8 commit b41012a
Show file tree
Hide file tree
Showing 6 changed files with 13 additions and 83 deletions.
41 changes: 3 additions & 38 deletions _posts/2022-03-30-rust-proc-macro.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,6 @@ categories:
<!-- /TOC -->

## What are procedural macros
<a id="markdown-what-are-procedural-macros" name="what-are-procedural-macros"></a>

**Procedural macros** are a way for you to extend the Rust compiler and provide plugins that you can
use to extend the language. They are really powerful and require some more work to setup in an
Expand All @@ -71,7 +70,6 @@ declared in the `lib.rs` file). Here are the key benefits of procedural macros:
Kotlin and babel and JavaScript.

### Declarative macros have limitations
<a id="markdown-declarative-macros-have-limitations" name="declarative-macros-have-limitations"></a>

For example they can't work with generics. They are easier to write than procedural
macros. If you have simple use cases they work great, since they are so easy to write.
Expand All @@ -83,7 +81,6 @@ Here are some resources to help you w/ learning declarative macros.
4. [Great YT video on declarative macros](https://youtu.be/q6paRBbLgNw)

### Summary of proc vs decl macros
<a id="markdown-summary-of-proc-vs-decl-macros" name="summary-of-proc-vs-decl-macros"></a>

| Macro type | Capabilities & limitations |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
Expand All @@ -93,11 +90,11 @@ Here are some resources to help you w/ learning declarative macros.
| Procedural - attribute | Like function-like, replaces token stream of annotated item (not just `struct` or `enum`) |

### Source code examples
<a id="markdown-source-code-examples" name="source-code-examples"></a>

1. You can find "real world" examples of both declarative and procedural macros in the
`r3bl-open-core` [repo](https://github.com/r3bl-org/r3bl-open-core).
- [proc macros](https://github.com/r3bl-org/r3bl-open-core/tree/main/macro)
- [proc macros](https://github.com/r3bl-org/r3bl-open-core-archive/tree/main/macro)
- [decl macros](https://github.com/r3bl-org/r3bl-open-core/search?q=macro_rules)
2. You can find all the pedagogical examples of macros shown in this article in this
[repo](https://github.com/nazmulidris/rust_scratch/blob/main/macros/).
Expand All @@ -107,7 +104,6 @@ Here are some resources to help you w/ learning declarative macros.
> by Will Crichton demonstrating Typed Design Patterns with Rust.
## How to add a proc macro lib crate to your existing project
<a id="markdown-how-to-add-a-proc-macro-lib-crate-to-your-existing-project" name="how-to-add-a-proc-macro-lib-crate-to-your-existing-project"></a>

Rust has two kinds of macros: declarative and procedural. Declarative macros are made using
`macro_rules!` inline in your code w/out creating a new lib crate. This article is about procedural
Expand Down Expand Up @@ -192,7 +188,6 @@ r3bl_rs_utils = "*"
> and then runs the test you give that script as an argument.
### Add an internal or core crate
<a id="markdown-add-an-internal-or-core-crate" name="add-an-internal-or-core-crate"></a>
There are situations where you will need to share code between your public crate and your procedural
macro crate. In this case you can add an internal or core crate to your project. The shared files
Expand Down Expand Up @@ -222,7 +217,6 @@ and run `cargo publish` on each and every dependent crate and they will be their
crate on crates.io.
## What does a syn AST look like?
<a id="markdown-what-does-a-syn-ast-look-like%3F" name="what-does-a-syn-ast-look-like%3F"></a>
Before writing macros, let's talk about how we need to think about things:
Expand Down Expand Up @@ -308,7 +302,6 @@ the string `"fn foo() -> u32 { 42 }"` using
> [dump-syntax](https://github.com/dtolnay/syn/blob/master/examples/dump-syntax/src/main.rs).
## How to write a proc macro of any kind
<a id="markdown-how-to-write-a-proc-macro-of-any-kind" name="how-to-write-a-proc-macro-of-any-kind"></a>
There are 3 kinds of proc macros. Once you've created a new library crate for them inside your
project, you write macros like the ones shown below.
Expand Down Expand Up @@ -352,7 +345,6 @@ pub fn log_entry_and_exit(args: TokenStream, input: TokenStream) -> TokenStream
```
### Strategy
<a id="markdown-strategy" name="strategy"></a>
The rough idea is that we will have to parse "things" into this `proc_macro2::TokenStream` in order
to manipulate them. They can be parsed into this AST from:
Expand All @@ -375,7 +367,6 @@ In order to do this parsing you have to use the
an `ItemFn` AST and then you can work w/ the fields provided by `ItemFn` after that.
### Examples
<a id="markdown-examples" name="examples"></a>
So here are some examples of what this looks like.
Expand Down Expand Up @@ -410,7 +401,6 @@ So here are some examples of what this looks like.
implement which allows you greater control over the parsing process.
### Writing your own Parse trait impl in different ways
<a id="markdown-writing-your-own-parse-trait-impl-in-different-ways" name="writing-your-own-parse-trait-impl-in-different-ways"></a>
This might not be intuitive, but you can parse the **same** `TokenStream` using various different
parsers. You can parse a `TokenStream` as a `Type` or `Ident` or whatever else depending on what you
Expand Down Expand Up @@ -503,7 +493,6 @@ impl Parse for ManagerOfThingInfo {
> team is also quite helpful.
## Eg 1 - Function-like macro that dumps the AST
<a id="markdown-eg-1---function-like-macro-that-dumps-the-ast" name="eg-1---function-like-macro-that-dumps-the-ast"></a>
Let's start our procedural macro journey w/ something very simple. It's a macro that doesn't really
emit any token stream. It just prints out the AST of the input as debug. So we won't be using
Expand Down Expand Up @@ -615,7 +604,6 @@ fn test_proc_macro() {
> parse a custom syntax.
## Eg 2 - Function-like macro that parses custom syntax
<a id="markdown-eg-2---function-like-macro-that-parses-custom-syntax" name="eg-2---function-like-macro-that-parses-custom-syntax"></a>
There are times when you need to create your own syntax or domain specific language. Examples of
this are JSX for React. Or DAO generators for a database. In these cases, it's not just about
Expand All @@ -629,12 +617,8 @@ procedural macro will do the rest.
- For the implementors, it ends up generating imperative code.
> 📦 To see a real world example of a custom syntax parser, please check out
> [`manager_of_things.rs`](https://github.com/r3bl-org/r3bl-rs-utils/blob/main/my_proc_macros_lib/src/manager_of_things.rs).
> [`manager_of_things.rs`](https://github.com/r3bl-org/r3bl-open-core-archive/blob/main/macro/src/manager_of_things.rs).
>
> - This is part of the [`r3bl_rs_utils`](https://crates.io/crates/r3bl_rs_utils) crate.
> - You can also look at the
> [tests](https://github.com/r3bl-org/r3bl-rs-utils/blob/main/tests/test_manager_of_things_macro.rs)
> to see how this macro is used.
> - You can create your own custom keywords using syn via the
> [`syn::custom_keyword!()`](https://docs.rs/syn/latest/syn/macro.custom_keyword.html) macro.
> - The code that's generated also uses [async traits](https://github.com/dtolnay/async-trait) which
Expand All @@ -645,7 +629,6 @@ procedural macro will do the rest.
> ideas on custom syntax parsing and creating custom error messages for the compiler.
### Desired syntax and behavior
<a id="markdown-desired-syntax-and-behavior" name="desired-syntax-and-behavior"></a>
Let's say that we want to parse a custom syntax like the following, which basically is a declaration
of how a manager for the struct `HashMap<K, V>` should be created.
Expand Down Expand Up @@ -705,7 +688,6 @@ where
```
### Implementing the syntax parser
<a id="markdown-implementing-the-syntax-parser" name="implementing-the-syntax-parser"></a>
So how would we implement this macro? The first thing is to create a custom parser for the syntax.
There are 2 main things we have to do:
Expand Down Expand Up @@ -809,7 +791,6 @@ impl Parse for ManagerOfThingInfo {
````
### Implementing the code generator
<a id="markdown-implementing-the-code-generator" name="implementing-the-code-generator"></a>
In this example almost all the work goes into parsing the custom syntax. The code generator we are
going to implement is trivial. Here's what it looks like.
Expand Down Expand Up @@ -852,7 +833,6 @@ pub fn fn_proc_macro_impl(input: TokenStream) ->:TokenStream {
`./cargo-watch-one-test.fish test_fn_macro_custom_syntax`
## Eg 3 - Derive macro that adds a method to a struct
<a id="markdown-eg-3---derive-macro-that-adds-a-method-to-a-struct" name="eg-3---derive-macro-that-adds-a-method-to-a-struct"></a>
We are going to come up w/ a made-up derive macro called `Describe` just for our pedagogical
purposes.
Expand All @@ -862,7 +842,6 @@ purposes.
2. We will then extend this derive macro to handle generics.
### Test for expected output
<a id="markdown-test-for-expected-output" name="test-for-expected-output"></a>
Here are some simple cases that we should be able to handle in our initial implementation.
Expand Down Expand Up @@ -902,7 +881,6 @@ fn test_proc_macro() {
> `./cargo-watch-one-test.fish test_derive_macro_describe`.
### Watch macro expansion
<a id="markdown-watch-macro-expansion" name="watch-macro-expansion"></a>
As we are developing this macro it is really useful not only to have the tests running (in watch
mode) but also have the macro expansion running in watch mode.
Expand All @@ -913,7 +891,6 @@ mode) but also have the macro expansion running in watch mode.
> `./cargo-watch-macro-expand-one-test.fish test_derive_macro_describe`.
### Naive implementation
<a id="markdown-naive-implementation" name="naive-implementation"></a>
Let's implement this derive macro in a naive way. We won't handle generics, that will happen
[later](#better-implementation-that-handles-generics).
Expand Down Expand Up @@ -1026,7 +1003,6 @@ The `quote!` macro is incredibly powerful and it has a lot of smarts built into
when we implement generics support next.
### Better implementation that handles generics
<a id="markdown-better-implementation-that-handles-generics" name="better-implementation-that-handles-generics"></a>
Here's an example of what a simple `Generics` object looks like when generated from
`struct Point<T> { ... }`.
Expand Down Expand Up @@ -1116,7 +1092,6 @@ This might provide some insight into how the `Generics` object itself is structu
need to do any of this, since `quote!()` is awesome 🤯.
### Using quote!
<a id="markdown-using-quote!" name="using-quote!"></a>
Here's a mental model for using `quote!()`:
Expand Down Expand Up @@ -1184,7 +1159,6 @@ Here are some tips and tricks for using `quote!()`:
generating `TokenStream`s manually, and then passing them to `quote!()`.
## Eg 4 - Derive macro that generates a builder
<a id="markdown-eg-4---derive-macro-that-generates-a-builder" name="eg-4---derive-macro-that-generates-a-builder"></a>
Now that we have seen a relatively simple derive procedural macro, let's look at a more complex one
that implements the builder pattern and supports generics. There are two things this macro has to
Expand All @@ -1202,7 +1176,6 @@ do:
> [here](https://github.com/nazmulidris/rust_scratch/blob/main/macros/tests/test_derive_macro_builder.rs).
### Stub out the implementation
<a id="markdown-stub-out-the-implementation" name="stub-out-the-implementation"></a>
We need to make an entry in `lib.rs` for it, like so:
Expand Down Expand Up @@ -1288,7 +1261,6 @@ pub fn derive_proc_macro_impl(input: proc_macro::TokenStream) -> proc_macro::Tok
```
### Testing the macro
<a id="markdown-testing-the-macro" name="testing-the-macro"></a>
Here's the test for the derive macro, `test_derive_macro_builder.rs`. They have to cover all the
different kinds of structs that we might encounter, some that have generics, some that don't.
Expand Down Expand Up @@ -1352,7 +1324,6 @@ fn test_proc_macro_generics() {
```
### Implementation details
<a id="markdown-implementation-details" name="implementation-details"></a>
Now that we have the skeleton of the entire thing, let's look at some details of how this is
implemented. It's worth taking a closer look at the
Expand Down Expand Up @@ -1431,7 +1402,6 @@ fn add_trait_bounds_to_existing_where_clause_ts(
> `./cargo-watch-macro-expand-one-test.fish test_derive_macro_builder`
## Eg 5 - Attribute macro that adds logging to a function
<a id="markdown-eg-5---attribute-macro-that-adds-logging-to-a-function" name="eg-5---attribute-macro-that-adds-logging-to-a-function"></a>
[Attribute procedural macros](https://doc.rust-lang.org/reference/procedural-macros.html#attribute-macros)
are very similar to derive procedural macros, with a few key differences.
Expand Down Expand Up @@ -1475,7 +1445,6 @@ might try out. So let's just make 2 macros.
```
### Create entry in lib.rs
<a id="markdown-create-entry-in-lib.rs" name="create-entry-in-lib.rs"></a>
Let's start by creating an entry in `lib.rs` for these attribute macros.
Expand Down Expand Up @@ -1535,7 +1504,6 @@ pub fn attrib_proc_macro_impl_2(
> `./cargo-watch-one-test.fish test_attribute_macro_logger`
### How to parse item?
<a id="markdown-how-to-parse-item%3F" name="how-to-parse-item%3F"></a>
How do we parse the `item` parameter? We can use `syn::ItemFn` and `parse_macro_input!()` to parse
it into something usable. Here's an example.
Expand All @@ -1555,7 +1523,6 @@ pub fn attrib_proc_macro_impl_2(args: TokenStream, item: TokenStream) -> TokenSt
```
### How to parse args containing attributes for variant 1?
<a id="markdown-how-to-parse-args-containing-attributes-for-variant-1%3F" name="how-to-parse-args-containing-attributes-for-variant-1%3F"></a>
How do we parse `args` parameter into something we can use? We can use
[`syn::AttributeArgs`](https://docs.rs/syn/latest/syn/type.AttributeArgs.html) along w/
Expand Down Expand Up @@ -1653,7 +1620,6 @@ fn key() -> &'static str {
```
### How to parse args containing set of identifiers for variant 2?
<a id="markdown-how-to-parse-args-containing-set-of-identifiers-for-variant-2%3F" name="how-to-parse-args-containing-set-of-identifiers-for-variant-2%3F"></a>
We can also provide our own custom implementation of the `Parse` trait if we want to. Here's an
example of this based on syn's
Expand Down Expand Up @@ -1753,7 +1719,6 @@ pub fn foo() -> &'static str {
> [`trace-var`](https://github.com/dtolnay/syn/tree/master/examples/trace-var).
## Learning resources
<a id="markdown-learning-resources" name="learning-resources"></a>
- Overview
- [Excellent overview video](https://youtu.be/g4SYTOc8fL0)
Expand All @@ -1780,12 +1745,12 @@ pub fn foo() -> &'static str {
1. You can find "real world" examples of both declarative and procedural macros in the
`r3bl-open-core` [repo](https://github.com/r3bl-org/r3bl-open-core).
- [proc macros](https://github.com/r3bl-org/r3bl-open-core/tree/main/macro)
- [proc macros](https://github.com/r3bl-org/r3bl-open-core-archive/tree/main/macro)
- [decl macros](https://github.com/r3bl-org/r3bl-open-core/search?q=macro_rules)
2. You can find all the pedagogical examples of macros shown in this article in this
[repo](https://github.com/nazmulidris/rust_scratch/blob/main/macros/).
## Build with Naz video series on developerlife.com YouTube channel
<a id="markdown-build-with-naz-video-series-on-developerlife.com-youtube-channel" name="build-with-naz-video-series-on-developerlife.com-youtube-channel"></a>
> If you have comments and feedback on this content, or would like to request new content
> (articles & videos) on developerlife.com, please join our [discord
Expand Down
Loading

0 comments on commit b41012a

Please sign in to comment.