Rust: Difference between revisions

From bibbleWiki
Jump to navigation Jump to search
Line 270: Line 270:


==Copy==
==Copy==
When we copy something me make a new thing. They is not the same a let a = b, which is assignment. Copy means we duplicate the underlying data of the type. For primitives a copy is implemented by default. This is because the primitive has a know size. E.g. u32, bool etc. If you want to be able to copy a non primitive you need to add the derive macro. Note Clone must also be specified
<syntaxhighlight lang="rust">
#[derive(Copy, Clone)]
enum Direction {
    North,
    East,
    South,
    West,
}
#[derive(Copy, Clone)]
struct RoadPoint {
    direction: Direction,
    index: i32,
}
</syntaxhighlight>
==Clone==
==Clone==
==References==
==References==

Revision as of 02:05, 3 January 2024

Terms

    • Fat Pointer Contains Address actual data and length

Cargo

Sample file

[package]
name = "hello_world"
version = "0.0.1"
authors = [ "Iain Wiseman iwiseman@bibble.co.nz" ]

Sample commands

 cargo new hello_world --bin
 cargo build
 cargo run

Fundamental Data Types

Primitive types

Cam declare with size of type

 let a:u8 = 123; // unsigned int 8 bits number immutable
 let a:i8 = 123; // signed int 8 bits number immutable
 let mut a:u8 = 123; // unsigned int 8 bits number mutable

Or without e.g.

 let mut c = 123456789 // 32-bit signed i32
 println!("c = {}", c);

Now variable based on OS e.g.

 let z:isize = 123 // signed 64 bit if on 64 bit OS

Decimal

let e:f64 = 2.5 // double-precision, 8 bytes or 64-bits

Char

let x:char = 'x' // Note 4 bytes unicode

boolean

let g:bool = false; // Note 4 bytes unicode

Operators

Does not support -- and ++ but does support

a -= 2;

Remainder can be calculated using

a%3

Bitwise

let c = 1 | 2 // | OR

Shift

let two_to_10 = 1 << 10; // 1024

Logical of standard e.g.

let pi_less_4 = std::f64::consts::PI < 4.0; // true

Scope and shadowing

Curly braces keep scope

 fn test()
 {
   {
     let a = 5; 
   }
   println!("Broken {a}");
 }

Shadowing is fine though

 fn test()
 {
   let a = 5; 
   {
     let a = 10; 
     println!("10 {a}");
   }
   println!("5 {a}");
 }

Constants

Standard const

 const MEANING_OF_LIFE:u8 = 42;

Static const

 static Z:i32 = 123;

Stack and Heap

Same a c++ i.e.

 let y = Box::new(10);
 println!("y = {}", *y);

Types

Tuples

Eezy peezy lemon squeezy

fn sum_and_product(x:i32,y:i32) -> (i32, i32)
{
 (x+y, x*y)
}

fn main()
{
  let sp = sum_and_product(3,4);
  let (a,b) = sp;
  let sp2 = sum_and_product(4,5);
   
  // combine
  let combined = (sp, sp2);
  let ((c,d), (e,f)) = combined;
}

Arrays

Array sizes cannot grow in rust

Simple

let mut a:[i32;5] = [1,2,3,4,5];
// Or 
let mut a = [1,2,3,4,5];
// Length
 a.len()
// Assignment
 a[0] = 321
// Printing
 println!("{:?}", )
// Testing
  if a == [1,2,3,4,5]
  {
  }
// Initialise
  let b = [1,10]; // 10 array initialised to 1

Multi Dimension

Here is a two dimension array

let mtx:[[f32;3];2] =
[
  [1.0, 0.0, 0.0],
  [0.0, 2.0, 0.0],
];

Vectors

Same a c++

let mut a = Vec::new()
a.push(1);
a.push(2);
a.push(3);
// Print
println!("a[0] {}", a[0]);

// We can create vector with initial capacity
let mut b = Vec::<i32>::with_capacity(2);

// We can initialize using an iterator values of 0-4
let c: Vect<i32> = (0..5).collect();

// Using get returns a option
match a.get(3333)
{
...
}

// Removing, pop returns an option   
let last_elem = a.pop();

// Using the option type iterating over vector to print it
while let Some(x) = a.pop()
{
   println!("x = {}",x);
}

Slices

A slice is a non-owning pointer to a block of memory. For example

// Create a vector
let v: Vec<i32> = {0..5}.collect();

// Now create a slice (reference)
let sv: &[i32]= &v;

// We create a slice with only some elements
let sv1: &[i32]= &v[2..4];

// Printing these will produce the same result
println!("{:?}",v);
println!("{:?}",sv);

// And the range
println!("{:?}",sv1);

Get the first 3 elements of an array

fn use_slice(slice: &mut[i32])
{

}

fn test()
{
  let mut data = [1,2,3,4,5];
  // Passes element 1-3 to use_slice as a reference
  use_slice( &mut data[1..4]); 
}

Strings

Basic String

let name = String::from("Iain");

Two types, static string and string type

let s = "hello";
// Cannot do
// let h = s[0]
// You can iterate as a sequence using chars e.g.
for c in s.chars()
{
  println!("{}", c);
}

And now the mutable string in rust essentially an vector // Create a string

let mut letters = String::new();

Add a char

let a = 'a' as u8;
letters.push(a as char);

String to str

let u:%str = &letters;

Concatenation

let z = lettters + &letters

Other examples

let mut abc = "hello world".to_string()'
abc.remove(0);
abc.push_str("!!!");
abc.replace("ello","goodbye")

Rust Principles

Ownership

Move

Move is when you assign a value to another variable. If we try and use a variable after the move we will get an error.

let v = vec![1,2,3]
let v2 = v;
println!("{:?}",v2)
println!("{:?}",v) // Error

Copy

When we copy something me make a new thing. They is not the same a let a = b, which is assignment. Copy means we duplicate the underlying data of the type. For primitives a copy is implemented by default. This is because the primitive has a know size. E.g. u32, bool etc. If you want to be able to copy a non primitive you need to add the derive macro. Note Clone must also be specified

#[derive(Copy, Clone)]
enum Direction {
    North,
    East,
    South,
    West,
}

#[derive(Copy, Clone)]
struct RoadPoint {
    direction: Direction,
    index: i32,
}

Clone

References

Structs

General

There are 3 types of structs, name, tuple and unit structs

    • Named
    • Tuples
    • Unit

Name Struct

struct User
{
  active: bool,
  username: String,
  sign_in_count: u32
}

let user1 = User{active: true, username: String::from("Biil"),
  sign_in_count: 0};
println!("{}", user1.username);

...

fn build_user(username: String) -> User {
   User {
     username,
     active:true,
     sign_in_count: 1
   }
}

Tuple Struct

Tuple structs use the order in which declared to assign.

   struct Coordinates{i32,i32,i32};
   let coords = Coordinates{1,2,3};

Unit Struct

These are used to mark the existence of something

struct UnitStruct;
let a = UnitStruct{}

The example shown was when you are implementing a trait (interface) but the properties were not required for this type. So given a trait for Area, Square uses size but Point does not have an area as it is zero

trait AreaCalculator {
  fn calc_area(&self) => f64
}

struct Square {
  size: f64
}

struct Point;

impl AreaCalculator for Square { 
  fn calc_area(&self) -> f64 {
    self.size * self.size
  } 
} 

impl AreaCalculator for Point { 
  fn calc_area(&self) -> f64 {
    0.0
  } 
}

We can use it for error

struct DivideByZero;

fn divide(nom: f64, den: f64) -> Result<f64, DivideByZero> {
   if den != 0.0 {
       Ok(nom/den)
   } else {
       Err(DivideByZero)
   }
}

Example Structs

struct Point
{
  x: f64,
  y: f64
}

fn main()
{
  let p = Point { x: 30.0, y: 4.0 };
  println!("point is at ({},{})", p.x, p.y)
}

Methods on Structs

Methods on struct require the first argument to be self

Example Method

Add method len to struct

struct Line
{
 start: Point,
 end: Point
}

// Declare impl using the keyword impl. Not ends with no semi colon.
impl Line
{
  fn len(&self) -> f64
  {
    let dx = self.start.x - self.end.x;
    let dy = self.start.y - self.end.y;
    (dx*dx+dy*dy).sqrt()
  }
}

Changing an attribute

To change an attribute and ensure you do not break the borrowing rules we do

struct Square {
   width: u32,
   height: u32
}

impl Square
{
  fn area(&self) -> u32 {
     self.width * self.height
  }

  fn change_width(&mut self, new_width: u32) -> Self
  {
    self.width = new_width;
  }
}

...
main() {
...
   let mut sq = Square(width:5, height: 5);
   sq.change_point(10) 
}

Control Flow

if statement

Same as C++ except no brackets

 if temp > 30 
 {
    println!("Blah");
 }
 else if temp < 10 
 {
    println!("Blah"); 
 }
 else
 {
    println!("Blah"); 
 }

Elvis is like

  let a = if temp > 30 {"sunny"} else {"cloud"}

While and Loop

While

Same as C++ except no brackets

 while x < 1000
 {
 }

There is support for continue and break

Loop

Loop is while true

 loop
 {
    if y == 1 << 10 { break; }
 }

For Loop

A bit like kotlin loops (I think)

 for x in 1..11
 {
    println!("x = {}",x);
 }

You can get position in series as well

 for (pos,x) in (1..11).enumerate()
 {
    println!("x = {}, pos = {}",x, pos);
 }

Match

Match can be used like case

 let country = match country_code
 {
    44 => "uk",
    46 => "sweden",
    7 => "russia"
    1...999 => "unknown" // other triple dot inclusive
    _ => "invalid" // invalid
 };


Lifetime

Ownership

Only one thing can have ownership

let v = vec![1,2,3]
let v2 = v;
println!("{:?}",v)

Only v can own the vector so this will not compile. Upon usage you get the error "use of moved value"

Clone

This is a way to duplicate the value to fix this. Obviously this has a cost

let v = vec![1,2,3]
let v2 = v.cone();
println!("{:?}",v)
println!("{:?}",v2)

References

We can pass references to an object using mut to modify a string.

...
   let mut s = String::from("hello");
   change_string(&mut s);
   printlin!("{}",s)
}

fn change_string(some_string: &mut String) {
   some_thing.push_str(", world!");
}

Borrowing

This fails because we are have two mutable accesses to a

let mut a = 40;
let b &mut a
*b += 2;
println!("a = {}", a);

This fails because we are have two mutable accesses to a

Same fix as before

let mut a = 40;
{
  let b &mut a
  *b += 2;
}
println!("a = {}", a);

Dangling References

We cannot do the code below as r refers to something which has gone out of scope

let r;
{
  let x = 5;
  r = &x;
}
println!("{}",r);

Lifetime Annotations

Introduction

This was a bit confusing but here goes. Here is a function without lifetime annotations. This fails to compile as we need to let the compiler know how long the return value will live for.

fn longest(x: &str, y: &str) -> &str {
   if x.len() > y.len() {
     x 
   }
   else {
     y 
   }
}

Adding Annotations to Methods

To do this we specify annotations. The extension in vscode does this for us but clearly it is important we understand. But note, they are only annotation and do not change the lifetime of anything only assist the compiler.

fn main() {
    println!("Hello, world!");
    let string1: String = String::from("abcd");
    let string2: String = String::from("xyz");
    let result = longest(&string1, &string2);
    println!("The longest string is {}", result)
}

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
   if x.len() > y.len() {
     x 
   }
   else {
     y 
   }
}

Making it Break

Lets make a dangling reference

fn main() {
    println!("Hello, world!");
    let string1: String = String::from("abcd");
    let result: &str;
    {
        let string2: String = String::from("xyz");
        result = longest(&string1, &string2);
    }
    println!("The longest string is {}", result)
}
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
   if x.len() > y.len() {
     x 
   }
   else {
     y 
   }
}

This fails because the string2 does not have a long enough life and is highlighted in the editor and compiler

  |
6 |         let string2: String = String::from("xyz");
  |             ------- binding `string2` declared here
7 |         result = longest(&string1, &string2);
  |                                    ^^^^^^^^ borrowed value does not live long enough
8 |     }
  |     - `string2` dropped here while still borrowed
9 |     println!("The longest string is {}", result)
  |                                          ------ borrow later used here

Returning a Reference

If we are returning a reference we must be returning a parameter as all local variables are destroyed. (Clearly Rust is not going to allow new MyMemory(6502)

Three Rules of Lifetimes

  • Each Parameter that is a reference gets its own lifetime parameter
  • If there is exactly one input lifetime parameter, that lifetime is assigned to all output lifetime parameters
  • If there are multiple input lifetime parameters, but one of them is &self or &mut self the lifetime is assigned to all output lifetime parameters

Lifetime Annotations for Structs

Struct can also have lifetime annotations as they can have a reference. Again this is for the borrower and does not change anything.

struct MyString<'a> {
  text: &'a str,
}

fn main() {

    let string1: String = String::from("This is my String);
    let x = MyString(text: str1.as_str());
}

Static Lifetimes

We can also have lifetimes for statics.

let s: &'static str = "I live forever";

People these for error messages which would be stored in the binary.

Enums And Pattern Matching

Standard Enums

enum Color {
  Red,
  Green,
  Blue
}

fn main()
{
  let c:Color = Color::Red;
  match c
  {
     Color::Red => prinln!("Color is Red");
     Color::Green => prinln!("Color is Green");
  }
}

Enum with Types

enum Color {
  Red,
  Green,
  Blue,
  RgbColor(u8,u8,u8) // Tuple
  CmykColor{cyan:u8, magenta:u8, yellow:u8, black:u8,} // Struct
}

fn main()
{
  let c:Color = Color::RgbColor(10,0.0);
  match c
  {
     Color::Red => prinln!("Color is Red");
     Color::Green => prinln!("Color is Green");
     Color::RgbColor(0,0,0) => prinln!("Color is Black");
     Color::RgbColor(r,g,b) => prinln!("Color is {},{},{}", r,g,b);
  }

  let d:Color = Color::CmykColor(cyan:0, magenta:0, yellow:0, black:0);
  match d
  {
     Color::Red => prinln!("Color is Red");
     Color::Green => prinln!("Color is Green");
     Color::RgbColor(0,0,0) => prinln!("Color is Black");
     Color::CmykColor(cyan:_, magenta:_, yellow:_, black:255) => prinln!("Black");
  }
}

Option <T> and if let

Used to avoid null or invalid values. This was used in things where the value might be present. Maybe command line arguments where some were provide or none were provided. Lets to the classic divide by zero.

let x = 3.0
let y = 0.0 // Divide by zero

let result:Option<f64> = 
   if y != 0.0 { Some(x/y) } else { None };

// Using match
match result {
   Some(z) => println!("Goody result"),
   None => println!("No result")
}

// Using if let
if let Some(z) = result { println!("z = {}", z); }

More if let

Here is another example

let mut stack = Vec:new();
stack.push(1);
stack.push(2);
stack.push(3);
while let Some(top) = stack.pop() {
   println!("{}", top);
}

Pattern Matching

Various pattern matching

match x
{
  0 => "zero"
  1 | 2 => "one or two"
  9...11 => "lots of"
  _ if(blahh) => "something"
  _ => "all others"
}

Using two dots .. with pattern matching allows you to ignore other values except those specified.

Generics

Looks like templates

struct Point<T>
{
  x: T,
  y: T
}

fn generics()
{
  let a:Point<i32> = Point {x: 0, y: 4}
}

Functions

Functions and Arguments

No surprises

fn print_value(x: i32)
{
  println("value = {}", x);
}

Pass by reference

fn increase(x: &mut i32)
{
  *x = 1;
}

Return value, note no semicolon

fn product(x: i32, y: i32) -> i32
{
  x * y
}

Return two values, note no semicolon

fn product(x: i32) -> i32
{
  if x == 10 {
     123
  }
  
  321
}

Closures

Closures are delegates. Here is an inline one

let plus_one = |x:i32| -> i32 { x + 1};
let a = 6;
println!("{}, + 1 = {}",a plus_one(a));

// another approach
let plus_to = |x|
{
 let mut z = x;
 z += 2;
 z
};

We need to ensure scope is executed before using variables again e.g.

let mut two = 2;

let plus_to = |x|
{
 let mut z = x;
 z += two;
 z
};

let borrow_two = &mut two;

This will not compile as two could be modified so you need to add bracket to ensure scope.

let mut two = 2;
{
  let plus_to = |x|
  {
   let mut z = x;
   z += two;
   z
  };
}
let borrow_two = &mut two;

Higher-order functions

Not sure what this is, seems like just a way to chain written functions together like lamba. Here is the given example.

fn is_even(x: u32) -> bool 
{
   x%2 == 0 
}

fn main()
{
// Method without HOF
  let limit = 500;
  let mut sum = 0;

  for i in 0..
  {
    let isq = i*i;

    if isq > limit { break; }
    else if is_even(isq) { sum += isq; }
  }

  println!("loop sum = {}", sum);

// HOF way

  let sum2 =
     (0...).map(|x| x*x)
        .take_while(|&x| x < limit)
        .filter(|x| is_even(*x))
        .fold(0, |sum,x| sum+x);

  println!("hof sum = {}", sum2);
}

Traits

Traits are interfaces from maybe c#

trait Animal
{
  fn create(name:&'static str);

  fn name(&self) => &'static str;

  fn talk(&self)
  {
     println!("{} cannot talk",self.name()); 
  }
}

struct Human
{
   name: &'static str;
}

// Implement interface
impl Animal for Human
{
  fn create(name:&'static str) -> Human
  {
    Human{name: name}
  }

  fn name(&self) -> &'static str
  { 
     self.name
  }
  // override default
  fn talk(&self)
  {
     println!("{} can talk",self.name()); 
  }
}

struct Cat
{
   name: &'static str;
}

// Implement interface
impl Animal for Cat
{
  fn create(name:&'static str) -> Cat
  {
    Cat{name: name}
  }

  fn name(&self) -> &'static str
  { 
     self.name
  }
  // override default
  fn talk(&self)
  {
     println!("{} says meeow",self.name()); 
  }
}

// Create doing

let h:Human = Animal::create("John");
let c:Cat = Animal::create("John");

Odds and Ends

Consuming Crates

Crates is like nuget [1]

[package]
name = "mypackage" 
version = "0.1.0"
author = " ["Iain Wiseman iwiseman@bibble.co.nz"]

[dependencies]
rand = "0.3.12"

And the usage

extern crate rand; // Package
use rand::Rng; // Namespace

fn main() {
  let mut rng = rand::thread_rng();
  let b:bool = rng.gen();
}

Building Crates and Modules

Module example e.g. src/lib.rs

pub mod greetings
{
   pub mod english
   {
      pub fn hello() -> {"hello"->to_string() }
      pub fn goodbye() -> {"goodbye"->to_string() }
   }
   pub mod french
   {
      pub fn hello() -> {"bonjour"->to_string() }
      pub fn goodbye() -> {"au revoir"->to_string() }
   }
}

For modules within modules you can make directories. For above this would be greetings\english and greetings\french. So we could have greetings\english\english.rs

  pub fn hello() -> {"hello"->to_string() }
  pub fn goodbye() -> {"goodbye"->to_string() }

greetings\lib.rs

pub mod greetings
{
   pub mod english;
   pub mod french
   {
      pub fn hello() -> {"bonjour"->to_string() }
      pub fn goodbye() -> {"au revoir"->to_string() }
   }
}

We need a package file for it

[package]
name = "phrases" 
version = "0.1.0"
author = " ["Iain Wiseman iwiseman@bibble.co.nz"]

And then cargo build should work. To use the package you will need to specify the path to the package in the cargo file where used. e.g.

[package]
name = "mypackage" 
version = "0.1.0"
author = " ["Iain Wiseman iwiseman@bibble.co.nz"]

[dependencies]
rand = "0.3.12"
phrases = { path = "../Phrases" }

Testing

Rush comes with it's own assert library marked by #[test] e.g. for the above

#[test]
fn english_greeting_correct()
{
   assert_eq!("hello", greetings::english::hello());
}

Documentation

rustdoc is used to generate.

//! This module comment
//! and this
//! #Examples are compiled with back ticks 
//! ```
//! let username = "John";
//! println!("{}", english::hello());
//! ```

/// This is for code
/// In this case, it's our `hello()` function.
pub fn hello() -> String {" hello".to_string() }

Installing

Do not use apt as it does not set rustup correctly and then vscode extension will not work with "Couldn't start client Rust Language Server"

apt-get install curl build-essential make gcc -y
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

VS Code setup

I ended up installing. Still cannot find a in built package manager for cargo

Date Handling

Found the on twitter (not all bad) @orhanbalci Type conversion and dates are the worst in most languages so thought I would keep this.

fn main() {
    // you can use chrono crate for date time operations.
    // chrono crate is more capable than standard library std::time module
    use chrono::prelude::*;
    use chrono::Duration;
    
    // you can retrieve current date in Utc timezone as follows
    let current_date = Utc::today();
    println!("Utc current date: {}", current_date);
    
    // you can retrieve current date in local timezone as follows
    let local_current_date = Local::today();
    println!("Local current date: {}", local_current_date);
    
    // you can retrieve current time in UTC as follows
    let current_time_utc = Utc::now();
    println!( "Utc current time: {}", current_time_utc);
    
    // you can retrieve current time in local time zone as follows
    let current_time_local = Local::now();
    println!("Local current time: {}", current_time_local);
    
    // you can add some duration to a chrono::Date
    // succ method gets succeeding date
    
    let today = Utc::today();
    
    let tomorrow = today + Duration::days(1);
    assert_eq!(today.succ(), tomorrow);
    
    // you can subtract some duration from a chrono
    // pred method gets previous date
    
    let today = Utc::today();
    
    let yesterday = today - Duration::days(1);
    assert_eq!(today.pred(), yesterday);
    
    // you can get UNIX timestamp (epoch) value for a datetime using
    
    // timestamp method of chrono::offset::TimeZone trait.
    
    // since timestamps have numeric representation, they are easy to store in db
    // and send through network. You can aldo prefer this notation in your APIs
    let dt = Utc.ymd(1970, 1, 1).and_hms_milli(0, 0, 1, 0);
    assert_eq!(dt.timestamp(), 1);
    
    //you can also get timestamp value of a datetime in milliseconds
    let dt = Utc.ymd(1970, 1, 1).and_hms_milli(0, 0, 1, 500);
    assert_eq!(dt.timestamp_millis(), 1500);
    
    //you can convert create a chrono::DateTime from a timestamp seconds
    let timestamp = 15;
    
    let datetime = Utc.timestamp(timestamp, 0);
    assert_eq!(datetime.timestamp(), 15);
    
    //you can get difference of two date times as follows
    let first = Utc.ymd(1970, 1, 1).and_hms_milli(0, 0, 1, 0);
    let second = Utc.ymd(1970, 1, 1).and_hms_milli(0, 0, 2, 0);
    
    let difference: Duration = second.signed_duration_since(first);
    assert_eq!(difference, Duration::seconds(1));
    
    // you can also add and subtract duration from a DateTime struct
    
    let now = Utc::now();
    
    let yesterday_at_the_same_time = now - Duration::days(1);
    println!("Yesterday at the same time: {}", yesterday_at_the_same_time);
    
    //you can compare durations
    assert_eq! (Duration::days(1), Duration::hours(24));
    
    //you can format your DateTime struct using strftime formatting options
    let now = Utc::now();
    println!("{}", now. format( "%d.%m.%Y %H:%M"));
    
    //you can parse DateTime from a formatted string as below
    let some_time = NaiveDateTime::parse_from_str("01.10.2021 14:21", "%d.%m.%Y %H:%M").unwrap( );
    assert_eq!(some_time, NaiveDate::from_ymd(2021,10,1).and_hms(14,21,0));
}

Macros

Declarative Macros

This seems similar to C++ with improvements. Need to learn the syntax but this should be enough for me to get going. Here is a basic macro.

#[macro_export]
macro_rules! my_macro {
    () => {
        { // Do Stuff }
    };
}

Export allows the macro to be seen, macro_rules! is the syntax to say I am a macro then the name of the macro. We can now write an arrow function similar to typescript. This has curly braces around whatever we are returning.
This example appends a "# " to the text passed. The function arguments are $var and in this case the type is literal. These are known as fragment specifiers. And a list can be found [here]

#[macro_export]
macro_rules! header {
    ($var:literal) => {
        {concat!("# ", $var)}
    };
}

fn main() {
    let fred = header!("fred");
    println!("Hello, world! {}", fred);
}

We can test this using the testing framework offered by rust and running cargo test

#[cfg(test)]
mod tests {
    #[test]
    fn test_header() {
        let val = header!("Hello");

        assert_eq!(val, "# Hello"); 
    }
}

A more complicated macro maybe one that takes a list of values. E.g. ulist!["a","b","c"]. First we define a parameter with multiple inputs, to do this start with $(), separator , and repetition operator +. A list of repetition operators may be found [here]

macro_rules! ulist {
   ($(),+) => {
   };
}

To emphasise the separator I have changed it to a % to make it clear how it is used.

#[macro_export]
macro_rules! ulist {
    ($($var:literal)%+) => {
       { concat!( $("item:", $var, "\n",)+ ).trim_end() }
    };
 }

Basically the example concatenates the values together and using .trim_end() removes the last "\n". Left it in as it may remind me how to do this. And here is the test.

    #[test]
    fn test_ulist() {
        let val = ulist!("foo"%"bar");

        assert_eq!(val, "item:foo\nitem:bar");
    }

Procedural Macros

The previous macros were like C++ macros which match against patterns. Procedural macros are like functions, they take code as input, operate on it and then output code. There are three types of these

  • Custom #[derive] macros.
  • Attribute-like macros.
  • Function-like macros

We are going to look at the derived as this is what I have come across and need to know
This type of macro takes code as input and outputs code.

  • Create a library crate called billprod_macro
  • Create a procedural macro crate inside billprod_macro (billprod_macro_derive)

Here is the code structure.

Macro Library

This just defines the trait we are going to build. In the lib.rs we declare this trait (interface)

pub trait BillProdMacro {
    fn bill_prod_macro();
}

Macro Derive Library

This lives inside of the macro library. For the procedural macro we need to define the Cargo.toml as a proc-macro and add dependencies syn and quote.

[package]
name = "billprod_macro_derive"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
proc-macro = true

[dependencies]
syn = "1.0.75"
quote = "1.0.9"

For the code, we have something called TokenStream which is capable of parsing rust code. We write a function to receive the code, do something, and a function to output the code. My understanding is these are the same for most macros. We annotate it with the name of the macro #[proc_macro_derive(BillProdMacro)]

extern crate proc_macro;

use proc_macro::TokenStream;
use quote::quote;
use syn;

#[proc_macro_derive(BillProdMacro)]
pub fn billprod_macro_derive(input: TokenStream) -> TokenStream {

    // Construct a representation of Rust code as a syntax tree
    // that we can manipulate
    let ast = syn::parse(input).unwrap();

    // Build the trait implementation
    impl_billprod_macro(&ast)
}

The implementation takes the code (ast) prints out text with the type.

fn impl_billprod_macro(ast: &syn::DeriveInput) -> TokenStream {
    let name = &ast.ident;
    let gen = quote! {
        impl BillProdMacro for #name {
            fn bill_prod_macro() {
                println!("Hello, world! My name is {}", stringify!(#name));
            }
        }
    };
    gen.into()
}

Using the Macro

We need both packages. As they are not published we must reference them on disk

...
[dependencies]
billprod_macro = { path = "../billprod_macro" }
billprod_macro_derive = { path = "../billprod_macro/billprod_macro_derive" }

So now we can use them in the code.

use billprod_macro::BillProdMacro;
use billprod_macro_derive::BillProdMacro;

#[derive(BillProdMacro)]
struct BillCake;
....

fn main() {
    BillCake::bill_prod_macro();
}

This outputs

Hello, world! My name is BillCake