#BOOK LINK https://danielkeep.github.io/tlborm/book/README.html
Procedural macros in Rust are a powerful metaprogramming tool that allows you to generate or transform code at compile time. They operate on Rust's abstract syntax tree (AST), enabling code generation, custom derives, and more. Here's a structured guide to understanding procedural macros:
There are three types:
- Derive Macros: Add traits to structs/enums via
#[derive(MyMacro)]. - Attribute-like Macros: Attach custom attributes to items, e.g.,
#[my_attribute]. - Function-like Macros: Invoked like regular macros, e.g.,
my_macro!(...).
- Crate Configuration: Procedural macros must reside in a separate crate with
proc-macro = trueinCargo.toml:[lib] proc-macro = true
- Dependencies: Use
syn(parsing Rust code),quote(generating code), andproc-macro2(bridging between proc-macro and non-proc-macro code):[dependencies] syn = "2.0" quote = "1.0" proc-macro2 = "1.0"
Goal: Create a derive macro Builder to generate a builder pattern for a struct.
// lib.rs
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput};
#[proc_macro_derive(Builder)]
pub fn derive_builder(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = &input.ident;
// Generate a builder struct name (e.g., "MyStruct" → "MyStructBuilder")
let builder_name = format!("{}Builder", name);
let builder_ident = syn::Ident::new(&builder_name, name.span());
// Process struct fields (assuming a struct with named fields)
let fields = if let syn::Data::Struct(syn::DataStruct {
fields: syn::Fields::Named(ref fields),
..
}) = input.data {
&fields.named
} else {
panic!("Builder derive only works on structs with named fields");
};
// Generate setter methods and builder struct fields
let setters = fields.iter().map(|field| {
let field_name = &field.ident;
let field_type = &field.ty;
quote! {
pub fn #field_name(&mut self, value: #field_type) -> &mut Self {
self.#field_name = Some(value);
self
}
}
});
let builder_fields = fields.iter().map(|field| {
let field_name = &field.ident;
let field_type = &field.ty;
quote! { #field_name: Option<#field_type> }
});
// Generate the `build` method
let build_fields = fields.iter().map(|field| {
let field_name = &field.ident;
quote! {
#field_name: self.#field_name.take().ok_or(format!("field {} not set", stringify!(#field_name)))?
}
});
// Combine everything into generated code
let expanded = quote! {
// Define the builder struct
struct #builder_ident {
#(#builder_fields),*
}
impl #builder_ident {
#(#setters)*
pub fn build(&mut self) -> Result<#name, String> {
Ok(#name {
#(#build_fields),*
})
}
}
impl #name {
pub fn builder() -> #builder_ident {
#builder_ident {
#(#field_names: None),*
}
}
}
};
TokenStream::from(expanded)
}// In another crate
#[derive(Builder)]
struct MyStruct {
field1: String,
field2: i32,
}
fn main() {
let builder = MyStruct::builder()
.field1("hello".into())
.field2(42);
let my_struct = builder.build().unwrap();
}- TokenStream: Input and output of procedural macros. Use
proc_macro2::TokenStreamfor compatibility. - Parsing with
syn: ConvertTokenStreaminto a structured AST (e.g.,DeriveInput). - Code Generation with
quote: Write Rust code as a template and interpolate variables with#var. - Error Handling: Use
syn::Errororcompile_error!to report errors during macro expansion.
Use trybuild to test macro expansion:
[dev-dependencies]
trybuild = "1.0"Create tests in tests/:
#[test]
fn test_builder() {
let t = trybuild::TestCases::new();
t.pass("tests/pass/*.rs"); // Valid cases
t.compile_fail("tests/fail/*.rs"); // Invalid cases
}- Validation: Check input structure (e.g., ensure
Builderis applied to a struct). - Hygiene: Use
quote!’s hygiene to avoid variable name collisions. - Performance: Keep macros efficient to minimize compile times.
- Documentation: Provide examples and explain generated code.
- Span Tracking: Preserve source code locations for better error messages using
.span(). - Attribute Macros: Process attributes and their arguments:
#[proc_macro_attribute] fn my_attribute(_attr: TokenStream, item: TokenStream) -> TokenStream { /* ... */ }
- Function-like Macros:
#[proc_macro] fn my_macro(input: TokenStream) -> TokenStream { /* ... */ }
- Official Documentation: Rust Book: Macros
- Syn and Quote Guides: Syn Documentation
- Example Repositories: Study macros in
serde,tokio, orclap.
By mastering procedural macros, you can automate boilerplate, enforce patterns, and extend Rust's syntax for domain-specific needs. Start with simple derive macros and gradually explore more complex use cases!
##Explain me this: input as DeriveInput. How many type can I do?
The DeriveInput type (from the syn crate) represents the parsed structure of the Rust item (e.g., a struct, enum, or union) that your procedural macro is applied to. When you write parse_macro_input!(input as DeriveInput), you’re parsing the incoming TokenStream into a structured AST (Abstract Syntax Tree) that you can inspect and manipulate.
A DeriveInput has the following structure:
pub struct DeriveInput {
pub attrs: Vec<Attribute>, // Attributes like `#[...]`
pub vis: Visibility, // Visibility (e.g., `pub`)
pub ident: Ident, // Name of the struct/enum/union
pub generics: Generics, // Generic parameters (e.g., `<T>`)
pub data: Data, // The body of the item (fields, variants, etc.)
}The key part is data, which tells you what kind of item the macro is applied to. The Data enum has three variants:
Represents a struct with fields. The fields can be:
- Named fields (e.g.,
struct Foo { x: i32 }):Data::Struct(DataStruct { fields: Fields::Named(NamedFields { named: vec![...] }), .. })
- Tuple fields (e.g.,
struct Foo(i32, String)):Data::Struct(DataStruct { fields: Fields::Unnamed(UnnamedFields { unnamed: vec![...] }), .. })
- Unit struct (e.g.,
struct Foo;):Data::Struct(DataStruct { fields: Fields::Unit, .. })
Represents an enum with variants. Each variant can have its own fields (named, tuple, or unit):
Data::Enum(DataEnum {
variants: vec![Variant, ...], // List of enum variants
..
})Example:
enum MyEnum {
A, // Unit variant
B(i32), // Tuple variant
C { x: String }, // Named variant
}Represents a union (Rust’s unsafe C-like union):
Data::Union(DataUnion {
fields: Fields::Named(NamedFields { named: vec![...] }),
..
})Example:
union MyUnion {
a: i32,
b: f32,
}In your procedural macro, you typically pattern-match on input.data to handle different cases. For example:
use syn::{Data, DataStruct, DataEnum, DataUnion};
#[proc_macro_derive(MyMacro)]
pub fn my_macro(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
match &input.data {
// Handle structs
Data::Struct(data_struct) => {
// Process fields (named, tuple, or unit)
let fields = &data_struct.fields;
// ... generate code ...
}
// Handle enums
Data::Enum(data_enum) => {
for variant in &data_enum.variants {
// Process each variant (e.g., `A`, `B(i32)`, etc.)
}
}
// Handle unions
Data::Union(data_union) => {
let fields = &data_union.fields;
}
}
// Generate output TokenStream
// ...
}#[proc_macro_derive(MyDerive)]
pub fn my_derive(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
if let Data::Struct(data_struct) = &input.data {
if let Fields::Named(named_fields) = &data_struct.fields {
// Iterate over the named fields
for field in &named_fields.named {
let field_name = &field.ident; // e.g., `x`, `y`
let field_type = &field.ty; // e.g., `i32`, `String`
// ... generate code ...
}
}
}
quote! { /* generated code */ }.into()
}- Three Possible Types: Your macro can be applied to
struct,enum, oruniontypes. - Pattern Matching: Use
matchoninput.datato handle each case. - Field Variants: Structs/enums/unions can have named, tuple, or unit fields.
- Generics/Attributes:
DeriveInputalso gives you access to generics, visibility, and attributes (e.g.,#[...]).
- Structs: Most common (e.g., derive
serde::Serializefor data structures). - Enums: Useful for code generation based on variants (e.g.,
strumcrate for enum utilities). - Unions: Rare, but possible (e.g., custom derive for unsafe code).
By understanding DeriveInput, you can write flexible macros that work across different Rust types!
Absolutely! Here’s a deeper dive into key concepts, tools, and pitfalls you need to know to work effectively with procedural macros in Rust:
Procedural macros can unintentionally introduce variable name collisions. Use hygienic identifiers and track spans (source code locations) for better error messages.
-
Hygiene: The
quote!macro handles hygiene by default, but for dynamic identifiers, useformat_ident!:use quote::format_ident; let method_name = format_ident!("set_{}", field_name); quote! { fn #method_name(...) { ... } }
-
Spans: Preserve spans from the original code for meaningful errors:
let field_span = field.ident.span(); quote_spanned! { field_span => // Generated code inherits the span of `field` }
Procedural macros can’t return Result, but you can emit compile errors using syn::Error:
use syn::Error;
fn process(input: DeriveInput) -> Result<TokenStream, Error> {
if /* invalid input */ {
return Err(Error::new_spanned(input, "Custom error message"));
}
// ...
}
#[proc_macro_derive(MyMacro)]
pub fn my_macro(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
process(input).unwrap_or_else(|e| e.to_compile_error().into())
}Testing macros is tricky because they operate at compile-time. Use these tools:
trybuild: Tests whether code compiles (or fails) as expected.Example test:[dev-dependencies] trybuild = "1.0"
#[test] fn test_macro() { let t = trybuild::TestCases::new(); t.pass("tests/pass/*.rs"); // Valid cases t.compile_fail("tests/fail/*.rs"); // Invalid cases }
cargo expand: Inspect macro-expanded code:cargo install cargo-expand cargo expand --bin my_crate
Attribute macros take additional arguments. Parse them using syn::AttributeArgs:
#[proc_macro_attribute]
pub fn my_attr(attr: TokenStream, item: TokenStream) -> TokenStream {
let args = parse_macro_input!(attr as AttributeArgs); // Parses `#[my_attr(arg1, arg2)]`
let item = parse_macro_input!(item as ItemStruct); // Parses the struct/enum/etc.
// ...
}These macros take a TokenStream and return code. Example:
#[proc_macro]
pub fn my_macro(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as MyCustomParser);
// Generate code...
}Use syn::parse::Parser to parse custom syntax:
let parser = syn::meta::parser(|meta| {
// Parse meta (e.g., `my_macro!(key = value)`).
});
let parsed = parser.parse(input)?;Procedural macros run during compilation. Optimize them by:
- Avoiding expensive operations (e.g., network calls).
- Using
lazy_staticoronce_cellfor heavy initialization. - Minimizing recursion in generated code.
Debugging procedural macros is challenging. Use these techniques:
eprintln!: Print debug info during macro expansion (visible withcargo build -vv).dbg!macros: Usesyn::dump::dumpto inspect AST nodes:eprintln!("{:#?}", syn::dump::dump(&input));
cargo expand: See the expanded code to debug generation logic.
- Infinite Recursion: Avoid generating code that re-invokes the same macro.
- Unhygienic Code: Use
quote!’s hygiene unless intentional. - Panics: Never panic in a macro—use
syn::Errorinstead. - Overly Complex Macros: Keep macros focused to avoid compile-time bloat.
- Custom Parsing: Use
syn::parsewith custom parsers for DSL-like syntax. - Code Injection: Generate
implblocks, traits, or modules dynamically. - Interacting with Other Macros: Chain macros or process existing attributes.
- Conditional Code: Use
cfgattributes in generated code:quote! { #[cfg(target_os = "linux")] impl MyTrait for #name { ... } }
- Document Generated APIs: Use
///doc comments in generated code.quote! { /// This is a generated method. pub fn foo() { ... } }
- Versioning: Treat procedural macros as public APIs—follow semver.
- Security: Macros can execute arbitrary code at compile time. Audit dependencies!
syn: Parsing Rust syntax into ASTs.quote: Generating Rust code from templates.proc-macro2: Bridging betweenproc_macroand non-macro code.darling: Simplify attribute parsing with a derive-based API.paste: Generate concatenated identifiers.
Study macros in popular crates:
serde: DeriveSerialize/Deserialize.tokio: Attribute macros for async runtime.thiserror: DeriveErrorfor enums.clap: Derive command-line parsers.
- Start small: Write a derive macro for a simple struct.
- Use
cargo expandreligiously to debug generated code. - Test edge cases (e.g., generics, lifetimes, nested macros).
- Read the
synandquotedocs—they’re your best friends!
Procedural macros are powerful but complex. With practice, you’ll unlock Rust’s full metaprogramming potential! 🚀
##imagine this declarative macro:
#[macro_export]
macro_rules! vec {
( $(
Let’s break down the macro and its repetition logic:
#[macro_export]
macro_rules! vec {
( $( $x:expr ),* ) => {
{
let mut temp_vec = Vec::new();
$(
temp_vec.push($x);
)*
temp_vec
}
};
}The key syntax $( ... )* is a repetition operator in Rust macros. Here’s how it works:
-
Pattern Matching:
-
$( $x:expr ),*matches a comma-separated list of expressions (e.g.,1, 2, 3). -
$xcaptures each individual expression in the list. -
,is the separator between items (optional for the last item). -
*means "zero or more repetitions".
-
-
Code Expansion:
-
$( temp_vec.push($x); )*repeats thetemp_vec.push($x);line once for each captured$x. - The number of repetitions equals the number of expressions passed to the macro.
-
Let’s see how the macro expands for different inputs:
- Input: 3 expressions (
1,2,3). - Expanded Code:
{ let mut temp_vec = Vec::new(); temp_vec.push(1); temp_vec.push(2); temp_vec.push(3); temp_vec }
- Repetitions: The
pushline repeats 3 times.
- Input: 0 expressions.
- Expanded Code:
{ let mut temp_vec = Vec::new(); // No push statements (zero repetitions). temp_vec }
- Repetitions: The
pushline repeats 0 times.
- Input: 2 expressions (
foo(),bar). - Expanded Code:
{ let mut temp_vec = Vec::new(); temp_vec.push(foo()); temp_vec.push(bar); temp_vec }
- Repetitions: The
pushline repeats 2 times.
-
Separator Flexibility:
- The
,in$( $x:expr ),*allows trailing commas (e.g.,vec![1, 2, 3,]is valid). - If you use
$( $x:expr ),+(with+instead of*), the macro requires at least one expression.
- The
-
Zero Repetitions:
- If there are no expressions (e.g.,
vec![]), thepushcode is omitted entirely.
- If there are no expressions (e.g.,
-
Order of Execution:
- The code inside
$( ... )*is generated in the order the expressions appear in the input.
- The code inside
For vec![a, b, c], the macro expands like this:
Start with:
let mut temp_vec = Vec::new();
Then repeat for each $x (a, b, c):
temp_vec.push(a);
temp_vec.push(b);
temp_vec.push(c);
Finally, return:
temp_vec
- The
*operator in$( ... )*tells the macro to repeat the enclosed code for each captured input. -
$xis substituted with the actual value of each expression during expansion.
-
Missing Separators: If you forget the
,in the pattern$( $x:expr ),*, the macro won’t match comma-separated inputs. -
Misplaced
*: The*must directly follow the)closing the repetition group.
By understanding repetition operators (* and +), you can create macros that generate code dynamically based on the input!
##explain this lines of your procedural mecro code: // Process struct fields (assuming a struct with named fields) let fields = if let syn::Data::Struct(syn::DataStruct { fields: syn::Fields::Named(ref fields), .. }) = input.data { &fields.named } else { panic!("Builder derive only works on structs with named fields"); };
is it "if let syn::Data::Struct(syn::DataStruct { fields: syn::Fields::Named(ref fields), .. }) = input.data" a pattern match
Yes, this is a pattern match using Rust's if let syntax. Let’s break it down step by step:
This code checks if the input to the procedural macro is a struct with named fields (e.g., struct Foo { x: i32, y: String }). If not, it panics with an error message.
The code matches against the nested structure of input.data (which is of type syn::Data). Here’s how it works:
-
Outer Match:
syn::Data::Struct(...)- Checks if
input.datais a struct (not an enum or union). - Extracts the inner
syn::DataStructdata.
- Checks if
-
Inner Match:
syn::DataStruct { fields: syn::Fields::Named(ref fields), .. }- Destructures the
DataStructto check itsfields. syn::Fields::Named(ref fields)ensures the struct has named fields (e.g.,{ x: i32 }, not a tuple struct like(i32, String)).ref fieldsbinds a reference to theNamedFieldsdata (accessible viafieldsvariable)...ignores other fields inDataStruct(likestruct_tokenorsemi_token).
- Destructures the
-
Condition:
- If the pattern matches (i.e., the input is a struct with named fields),
&fields.namedis assigned tofields(a&Vec<Field>of the struct’s fields). - If it doesn’t match (e.g., an enum or tuple struct), the code panics.
- If the pattern matches (i.e., the input is a struct with named fields),
syn::Data::Struct: An enum variant representing a struct.syn::DataStruct: A struct holding details about the struct (fields, generics, etc.).syn::Fields::Named: An enum variant for named fields (e.g.,{ x: i32 }).ref fields: Binds a reference to theNamedFieldsdata (avoids moving ownership).
#[derive(Builder)]
struct ValidStruct {
a: i32,
b: String,
}- Pattern matches:
Data::Struct→Fields::Named. fieldsbecomes&[Field for "a", Field for "b"].
#[derive(Builder)]
struct InvalidStruct(u8, u8); // Tuple struct (unnamed fields)- Pattern fails (fields are
Fields::Unnamed, notNamed). - Macro panics with the error message.
The if let syntax is used because we only care about one specific case (structs with named fields). Other cases (enums, unions, tuple structs) are explicitly rejected.
For clarity, here’s what the code would look like using match:
let fields = match input.data {
syn::Data::Struct(syn::DataStruct {
fields: syn::Fields::Named(ref fields),
..
}) => &fields.named,
_ => panic!("Builder derive only works on structs with named fields"),
};The if let version is more concise for this single-case check.
Procedural macros need to validate their input. This pattern match ensures the Builder macro is only applied to valid structs, preventing cryptic compiler errors downstream.
- Pattern Matching: Rust’s
if letis used to destructure nested enums/structs. - Validation: Ensures the macro is only used on structs with named fields.
- Error Handling: Panics with a helpful message for invalid inputs.
This is a common pattern in procedural macros to enforce input constraints! 🦀
##in this following code "// lib.rs use proc_macro::TokenStream; use quote::quote; use syn::{parse_macro_input, DeriveInput};
#[proc_macro_derive(Builder)] pub fn derive_builder(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); let name = &input.ident;
// Generate a builder struct name (e.g., "MyStruct" → "MyStructBuilder")
let builder_name = format!("{}Builder", name);
let builder_ident = syn::Ident::new(&builder_name, name.span());
// Process struct fields (assuming a struct with named fields)
let fields = if let syn::Data::Struct(syn::DataStruct {
fields: syn::Fields::Named(ref fields),
..
}) = input.data {
&fields.named
} else {
panic!("Builder derive only works on structs with named fields");
};
// Generate setter methods and builder struct fields
let setters = fields.iter().map(|field| {
let field_name = &field.ident;
let field_type = &field.ty;
quote! {
pub fn #field_name(&mut self, value: #field_type) -> &mut Self {
self.#field_name = Some(value);
self
}
}
});
let builder_fields = fields.iter().map(|field| {
let field_name = &field.ident;
let field_type = &field.ty;
quote! { #field_name: Option<#field_type> }
});
// Generate the `build` method
let build_fields = fields.iter().map(|field| {
let field_name = &field.ident;
quote! {
#field_name: self.#field_name.take().ok_or(format!("field {} not set", stringify!(#field_name)))?
}
});
// Combine everything into generated code
let expanded = quote! {
// Define the builder struct
struct #builder_ident {
#(#builder_fields),*
}
impl #builder_ident {
#(#setters)*
pub fn build(&mut self) -> Result<#name, String> {
Ok(#name {
#(#build_fields),*
})
}
}
impl #name {
pub fn builder() -> #builder_ident {
#builder_ident {
#(#field_names: None),*
}
}
}
};
TokenStream::from(expanded)
}"
is #(#builder_fields),* work as a for loop or how does it know that how many times it will be repeated?
The #(#builder_fields),* syntax in the quote! macro works similarly to a loop, but it’s a repetition operator that automatically repeats the code based on the number of items in the iterator builder_fields. Here’s a detailed breakdown:
The syntax #(...)* is part of the quote! macro’s templating system. It repeats the enclosed code for each item in the iterator, inserting separators (like commas) between iterations.
#(...): Wraps the code to repeat.*: Specifies "repeat for all items in the iterator".,: The separator (optional) placed after)*(e.g.,#(...),*adds commas between repetitions).
In your code:
let builder_fields = fields.iter().map(|field| {
let field_name = &field.ident;
let field_type = &field.ty;
quote! { #field_name: Option<#field_type> }
});builder_fieldsis an iterator ofTokenStreamelements, where each element corresponds to a struct field in the formatfield_name: Option<field_type>.- When you write
#(#builder_fields),*, thequote!macro:- Iterates over all items in
builder_fields. - For each item, inserts the generated code (e.g.,
field1: Option<Type1>). - Inserts a comma
,between each item (due to the,after)*).
- Iterates over all items in
Suppose the input struct has two fields:
struct MyStruct {
a: i32,
b: String,
}-
builder_fieldsIterator:- Generates two items:
quote! { a: Option<i32> } quote! { b: Option<String> }
- Generates two items:
-
Expansion of
#(#builder_fields),*:struct MyStructBuilder { a: Option<i32>, b: Option<String>, // Trailing comma allowed in Rust }
- The code is repeated once per field, with commas separating the struct fields.
-
Setters:
#(#setters)*
- Repeats the
pub fn field_name(...)methods with no separators (valid in animplblock).
- Repeats the
-
Build Fields:
#(#build_fields),*
- Repeats fields in the
Ok(#name { ... })struct constructor, with commas separating each field.
- Repeats fields in the
The repetition count is determined by the length of the iterator:
builder_fieldsis created by mapping overfields.iter().- The number of fields in the original struct (
fields) dictates how many times the code is repeated.
For example:
- If the struct has 3 fields,
builder_fieldswill have 3 items, and#(#builder_fields),*will repeat 3 times. - If the struct has 0 fields (unlikely here, since the macro panics for non-named fields), it would repeat 0 times.
- Dynamic Repetition: The
#(...)*syntax repeats code based on the iterator’s length. - Separators: The
,in#(...),*adds commas between repetitions (crucial for struct/array syntax). - No Explicit Loops: The
quote!macro handles repetition internally—no need for manualforloops.
This pattern is common in procedural macros to generate boilerplate code dynamically! 🚀
##Can it be replaced by for loop?
No, you cannot directly replace #(...)* with a for loop in the procedural macro code. Here’s why:
quote!Repetition (#(...)*):- Is a compile-time code generation mechanism.
- Expands to static code during macro execution (e.g., inserting all struct fields at once).
- Works with token streams, not runtime values.
forLoop:- Executes at runtime (when your program runs).
- Cannot generate code dynamically during compilation.
Suppose you try to write:
let mut builder_fields_code = quote! {};
for field in &builder_fields {
builder_fields_code.extend(field);
}This would not work because:
quote!requires all code to be generated upfront at compile time (during macro expansion).- A
forloop would attempt to iterate at runtime, but the macro needs to generate code before runtime.
The #(...)* syntax is not a loop—it’s a template directive for the quote! macro. It tells the macro to repeat the enclosed code for each item in the iterator, inserting separators (like ,) where specified.
For example:
quote! {
struct Builder {
#(#builder_fields),*
}
}Expands to:
struct Builder {
field1: Option<Type1>,
field2: Option<Type2>,
// ...
}This expansion happens during compilation, not at runtime.
You’d get a compiler error like:
error: expected expression
Because the quote! macro expects static code, not runtime logic.
You can manually concatenate TokenStreams, but it’s more verbose and error-prone. For example:
let builder_fields_code = builder_fields
.map(|field| quote! { #field })
.collect::<Vec<_>>();
quote! {
struct Builder {
#( #builder_fields_code ),*
}
}This is essentially what #(...)* does internally, but with more boilerplate.
Use #(...)* (the repetition operator) in quote! for compile-time code generation. It’s the idiomatic, concise way to handle repetitions in procedural macros. for loops are for runtime logic and cannot replace this. 🦀