Shopping cart

0

Cart

  • 0 item

Nessun prodotto nel carrello.

All categories

SE VUOI PRENDERE LA CERTIFICAZIONE PER QUESTO CORSO CLICCA QUI

Corso su Le Macro

Abbiamo utilizzato macro come println! durante tutto questo corso, ma non abbiamo esplorato appieno cosa siano e come funzionino. Il termine macro si riferisce a una famiglia di funzionalità in Rust: le macro dichiarative con macro_rules! e tre tipi di macro procedurali:

  • Macro #[derive] personalizzate che specificano codice aggiunto con l’attributo derive usato su strutture ed enum.
  • Macro simili ad attributi che definiscono attributi personalizzati utilizzabili su qualsiasi elemento.
  • Macro simili a funzioni che assomigliano a chiamate di funzioni ma operano sui token specificati come loro argomento.

Parleremo di ognuna di queste, ma prima, vediamo perché abbiamo bisogno di macro quando abbiamo già le funzioni.

La Differenza tra Macro e Funzioni

Fondamentalmente, le macro sono un modo per scrivere codice che scrive altro codice, noto come metaprogrammazione. Nell’Appendice C, discutiamo dell’attributo derive, che genera un’implementazione di vari trait per te. Abbiamo anche utilizzato le macro println! e vec! durante tutto il corso. Tutte queste macro si espandono per produrre più codice rispetto a quello che hai scritto manualmente.

La metaprogrammazione è utile per ridurre la quantità di codice che devi scrivere e mantenere, il che è anche uno dei ruoli delle funzioni. Tuttavia, le macro hanno alcune potenzialità aggiuntive rispetto alle funzioni.

Una firma di funzione deve dichiarare il numero e il tipo di parametri della funzione. Le macro, d’altra parte, possono prendere un numero variabile di parametri: possiamo chiamare println!(“ciao”) con un argomento o println!(“ciao {}”, nome) con due argomenti. Inoltre, le macro vengono espande prima che il compilatore interpreti il significato del codice, quindi una macro può, ad esempio, implementare un trait su un dato tipo. Una funzione non può farlo, perché viene chiamata a tempo di esecuzione e un trait deve essere implementato a tempo di compilazione.

Il downside nell’implementare una macro invece di una funzione è che le definizioni delle macro sono più complesse delle definizioni delle funzioni perché stai scrivendo codice Rust che scrive codice Rust. A causa di questa indirezione, le definizioni delle macro sono generalmente più difficili da leggere, capire e mantenere rispetto alle definizioni delle funzioni.

Un’altra differenza importante tra macro e funzioni è che devi definire le macro o portarle in scope prima di chiamarle in un file, a differenza delle funzioni che puoi definire ovunque e chiamare ovunque.

Le Macro Dichiarative con macro_rules! per la Metaprogrammazione Generale

La forma più utilizzata di macro in Rust è la macro dichiarativa. Queste sono anche talvolta chiamate “macro per esempio”, “macro macro_rules!” o semplicemente “macro”. Fondamentalmente, le macro dichiarative ti consentono di scrivere qualcosa di simile a un’espressione match di Rust. Come discusso nel Capitolo 6, le espressioni match sono strutture di controllo che prendono un’espressione, confrontano il valore risultante dell’espressione con i pattern e quindi eseguono il codice associato al pattern corrispondente. Anche le macro confrontano un valore con pattern che sono associati a un particolare codice: in questa situazione, il valore è il codice sorgente Rust letterale passato alla macro; i pattern vengono confrontati con la struttura di quel codice sorgente; e il codice associato a ciascun pattern, quando corrispondente, sostituisce il codice passato alla macro. Tutto questo accade durante la compilazione.

Per definire una macro, si utilizza il costrutto macro_rules!. Esploriamo come utilizzare macro_rules! guardando come è definita la macro vec!. Nel Capitolo 8 abbiamo visto come possiamo usare la macro vec! per creare un nuovo vettore con valori particolari. Ad esempio, la seguente macro crea un nuovo vettore contenente tre interi:

rust

let v: Vec<u32> = vec![1, 2, 3];

Potremmo anche usare la macro vec! per creare un vettore di due interi o un vettore di cinque sottostringhe. Non potremmo usare una funzione per fare lo stesso perché non sapremmo il numero o il tipo di valori anticipatamente.

Il Listato 19-28 mostra una definizione leggermente semplificata della macro vec!.

rust

#[macro_export]
macro_rules! vec {
( $( $x:expr ),* ) => {
{
let mut temp_vec = Vec::new();
$(
temp_vec.push($x);
)*
temp_vec
}
};
}

Listato 19-28: Una versione semplificata della definizione della macro vec!

rust

Nota: La definizione effettiva della macro vec! nella libreria standard include codice per allocare anticipatamente la quantità corretta di memoria. Questo codice è un'ottimizzazione che non includiamo qui per rendere l'esempio più semplice.

L’annotazione #[macro_export] indica che questa macro dovrebbe essere resa disponibile ogni volta che il crate in cui è definita la macro viene portato in scope. Senza questa annotazione, la macro non può essere portata in scope.

Iniziamo quindi la definizione della macro con macro_rules! e il nome della macro che stiamo definendo senza il punto esclamativo. Il nome, in questo caso vec, è seguito dalle parentesi graffe che indicano il corpo della definizione della macro.

La struttura nel corpo di vec! è simile alla struttura di un’espressione match. Qui abbiamo un ramo con il pattern ( $( $x:expr ),* ), seguito da => e dal blocco di codice associato a questo pattern. Se il pattern corrisponde, il blocco di codice associato verrà emesso. Dato che questo è l’unico pattern in questa macro, c’è solo un modo valido per corrispondere; qualsiasi altro pattern produrrà un errore. Le macro più complesse avranno più di un ramo.

La sintassi del pattern valido nelle definizioni delle macro è diversa dalla sintassi del pattern coperta nel Capitolo 18 perché i pattern delle macro sono abbinati alla struttura del codice Rust anziché ai valori. Vediamo cosa significano i pezzi del pattern nel Listato 19-28; per la sintassi completa del pattern della macro, consultare il Riferimento Rust.

Prima di tutto, utilizziamo un set di parentesi per racchiudere l’intero pattern. Usiamo un segno di dollaro ($) per dichiarare una variabile nel sistema delle macro che conterrà il codice Rust che corrisponde al pattern. Il dollaro rende chiaro che si tratta di una variabile di macro rispetto a una variabile Rust regolare. Viene quindi un set di parentesi che cattura i valori che corrispondono al pattern all’interno delle parentesi per l’uso nel codice di sostituzione. All’interno di $() c’è $x:expr, che corrisponde a qualsiasi espressione Rust e dà all’espressione il nome $x.

La virgola che segue $() indica che un carattere di separatore di virgola letterale potrebbe opzionalmente apparire dopo il codice che corrisponde al codice in $(). Il * specifica che il pattern corrisponde a zero o più di quanto precede il *.

Quando chiamiamo questa macro con vec![1, 2, 3];, il pattern $x corrisponde tre volte con le tre espressioni 1, 2 e 3.

Ora guardiamo il pattern nel corpo del codice associato a questo ramo: temp_vec.push() all’interno di $()* viene generato per ogni parte che corrisponde a $() nel pattern zero o più volte a seconda di quante volte il pattern corrisponde. Il $x viene sostituito con ogni espressione corrispondente. Quando chiamiamo questa macro con vec![1, 2, 3];, il codice generato che sostituisce questa chiamata macro sarà il seguente:

rust

{
let mut temp_vec = Vec::new();
temp_vec.push(1);
temp_vec.push(2);
temp_vec.push(3);
temp_vec
}

Abbiamo definito una macro che può prendere qualsiasi numero di argomenti di qualsiasi tipo e può generare codice per creare un vettore contenente gli elementi specificati.

Per saperne di più su come scrivere macro, consulta la documentazione online o altre risorse, come “The Little Book of Rust Macros” iniziato da Daniel Keep e continuato da Lukas Wirth.

Corso su le Macro

Le Macro Procedurali per Generare Codice da Attributi

La seconda forma di macro sono le macro procedurali, che agiscono più come una funzione (e sono un tipo di procedura). Le macro procedurali accettano del codice come input, operano su quel codice e producono del codice come output anziché confrontare con pattern e sostituire il codice con altro codice come fanno le macro dichiarative. Le tre tipologie di macro procedurali sono derivate personalizzate, simili ad attributi e simili a funzioni, e tutte funzionano in modo simile.

Nel creare macro procedurali, le definizioni devono risiedere nel proprio crate con un tipo di crate speciale. Questo per complesse ragioni tecniche che speriamo di eliminare in futuro. Nel Listato 19-29, mostriamo come definire una macro procedurale, dove some_attribute è un segnaposto per l’utilizzo di una specifica varietà di macro.

Nome file: src/lib.rs

rust

use proc_macro;

#[some_attribute]
pub fn some_name(input: TokenStream) -> TokenStream {
}

Listato 19-29: Un esempio di definizione di una macro procedurale

La funzione che definisce una macro procedurale prende un TokenStream come input e produce un TokenStream come output. Il tipo TokenStream è definito dal crate proc_macro incluso con Rust e rappresenta una sequenza di token. Questo è il cuore della macro: il codice sorgente su cui la macro sta operando costituisce l’input TokenStream, e il codice che la macro produce è l’output TokenStream. La funzione ha anche un attributo ad essa allegato che specifica quale tipo di macro procedurale stiamo creando. Possiamo avere più tipi di macro procedurali nello stesso crate.

Esaminiamo le diverse tipologie di macro procedurali. Inizieremo con una macro derivata personalizzata e poi spiegheremo le piccole differenze che rendono le altre forme diverse.

Come Scrivere una Macro Derivata Personalizzata

Creiamo un crate chiamato hello_macro che definisce un trait chiamato HelloMacro con una funzione associata chiamata hello_macro. Piuttosto che far implementare ai nostri utenti il trait HelloMacro per ciascuno dei loro tipi, forniremo una macro procedurale in modo che gli utenti possano annotare il loro tipo con #[derive(HelloMacro)] per ottenere un’implementazione predefinita della funzione hello_macro. L’implementazione predefinita stamperà Hello, Macro! Il mio nome è NomeTipo! dove NomeTipo è il nome del tipo su cui è stato definito questo trait. In altre parole, scriveremo un crate che consenta a un altro programmatore di scrivere codice come il Listato 19-30 utilizzando il nostro crate.

Nome file: src/main.rs

[Questo codice non compila!] use hello_macro::HelloMacro; use hello_macro_derive::HelloMacro;

#[derive(HelloMacro)] struct Pancakes;

fn main() { Pancakes::hello_macro(); }

Listato 19-30: Il codice che un utente del nostro crate potrà scrivere quando utilizza la nostra macro procedurale

Questo codice stamperà Hello, Macro! Il mio nome è Pancakes! quando avremo finito. Il primo passo è creare un nuovo crate di libreria, come segue:

shell

$ cargo new hello_macro --lib

Successivamente, definiremo il trait HelloMacro e la sua funzione associata:

Nome file: src/lib.rs

rust

pub trait HelloMacro {
fn hello_macro();
}

Abbiamo un trait e la sua funzione. A questo punto, un utente del nostro crate potrebbe implementare il trait per ottenere la funzionalità desiderata, così:

rust

use hello_macro::HelloMacro; struct Pancakes; impl HelloMacro for Pancakes {
fn hello_macro() {
println!("Hello, Macro! Il mio nome è Pancakes!");
}
}

fn main() {
Pancakes::hello_macro();
}

Tuttavia, dovrebbero scrivere il blocco di implementazione per ogni tipo che desiderano utilizzare con hello_macro; vogliamo risparmiargli questo lavoro.

Inoltre, al momento non possiamo fornire alla funzione hello_macro un’implementazione predefinita che stampi il nome del tipo su cui è implementato il trait: Rust non ha capacità di reflection, quindi non può cercare il nome del tipo a tempo di esecuzione. Abbiamo bisogno di una macro per generare codice a tempo di compilazione.

Il prossimo passo è definire la macro procedurale. Al momento della stesura di questo testo, le macro procedurali devono essere nel proprio crate. In futuro, questa restrizione potrebbe essere revocata. La convenzione per la strutturazione dei crate e dei macro crate è la seguente: per un crate chiamato foo, un crate di macro procedurale chiamato foo_derive. Creiamo quindi un nuovo crate chiamato hello_macro_derive all’interno del nostro progetto hello_macro:

shell

$ cargo new hello_macro_derive --lib

I nostri due crate sono strettamente correlati, quindi creiamo il crate di macro procedurale all’interno della directory del nostro crate hello_macro. Se cambiamo la definizione del trait in hello_macro, dovremo cambiare anche l’implementazione del macro procedurale in hello_macro_derive. I due crate dovranno essere pubblicati separatamente, e i programmatori che utilizzano questi crate dovranno aggiungere entrambi come dipendenze e portarli entrambi in scope. Potremmo invece fare in modo che il crate hello_macro utilizzi hello_macro_derive come dipendenza e rieporti il codice del macro procedurale. Tuttavia, il modo in cui abbiamo strutturato il progetto rende possibile per i programmatori utilizzare hello_macro anche se non vogliono la funzionalità di derivazione.

Dobbiamo dichiarare il crate hello_macro_derive come un crate di macro procedurale. Avremo anche bisogno di funzionalità dai crate syn e quote, come vedrai tra un attimo, quindi dobbiamo aggiungerli come dipendenze. Aggiungi quanto segue al file Cargo.toml per hello_macro_derive:

Nome file: hello_macro_derive/Cargo.toml

toml

[lib]
proc-macro = true

[dependencies]
syn = "1.0"
quote = "1.0"

Per iniziare a definire la macro procedurale, inserisci il codice nel Listato 19-31 nel file src/lib.rs per il crate hello_macro_derive. Nota che questo codice non verrà compilato finché non aggiungiamo una definizione per la funzione impl_hello_macro.

Nome file: hello_macro_derive/src/lib.rs

[Questo codice non compila!] use proc_macro::TokenStream; use quote::quote; use syn;

#[proc_macro_derive(HelloMacro)] pub fn hello_macro_derive(input: TokenStream) -> TokenStream { // Costruisci una rappresentazione del codice Rust come un albero di sintassi // che possiamo manipolare let ast = syn::parse(input).unwrap();

scss

// Costruisci l'implementazione del trait
impl_hello_macro(&ast)

}

vbnet


Listato 19-31: Codice che la maggior parte dei crate di macro procedurali richiederà per elaborare il codice Rust
Nota che abbiamo suddiviso il codice nella funzione hello_macro_derive, che è responsabile del parsing del TokenStream, e nella funzione impl_hello_macro, che è responsabile della trasformazione dell'albero di sintassi: ciò rende più comodo scrivere una macro procedurale. Il codice nella funzione esterna (hello_macro_derive in questo caso) sarà lo stesso per quasi ogni crate di macro procedurale che vedrai o creerai. Il codice che specificate nel corpo della funzione interna (impl_hello_macro in questo caso) sarà diverso a seconda dello scopo della vostra macro procedurale. Abbiamo introdotto tre nuovi crate: proc_macro, syn e quote. Il crate proc_macro viene fornito con Rust, quindi non abbiamo bisogno di aggiungerlo alle dipendenze in Cargo.toml. Il crate proc_macro è l'API del compilatore che ci consente di leggere e manipolare il codice Rust dal nostro codice. Il crate syn analizza il codice Rust da una stringa in una struttura dati su cui possiamo eseguire operazioni. Il crate quote trasforma le strutture dati syn nuovamente in codice Rust. Questi crate semplificano molto il parsing di qualsiasi tipo di codice Rust che potremmo voler gestire: scrivere un parser completo per il codice Rust non è affatto semplice. La funzione hello_macro_derive verrà chiamata quando un utente della nostra libreria specifica #[derive(HelloMacro)] su un tipo. Questo è possibile perché abbiamo annotato la funzione hello_macro_derive qui con proc_macro_derive e specificato il nome HelloMacro, che corrisponde al nome del nostro trait; questa è la convenzione che seguono la maggior parte delle macro procedurali. La funzione hello_macro_derive converte prima l'input da un TokenStream in una struttura dati che possiamo quindi interpretare ed eseguire operazioni su di essa. Qui entra in gioco syn. La funzione parse in syn prende un TokenStream e restituisce una struct DeriveInput che rappresenta il codice Rust analizzato. Il Listato 19-32 mostra le parti rilevanti della struct DeriveInput che otteniamo dal parsing della struct Pancakes. ```rust
DeriveInput {
// --snip--

ident: Ident {
ident: "Pancakes",
span: #0 bytes(95..103)
},
data: Struct(
DataStruct {
struct_token: Struct,
fields: Unit,
semi_token: Some(
Semi
)
}
)
}

Listato 19-32: L’istanza DeriveInput che otteniamo quando parsiamo il codice che ha l’attributo della macro nel Listato 19-30

I campi di questa struct mostrano che il codice Rust che abbiamo analizzato è una struct unit con l’ident (identificatore, ossia il nome) di Pancakes. Ci sono più campi su questa struct per descrivere tutti i tipi di codice Rust; controlla la documentazione syn per DeriveInput per ulteriori informazioni.

Presto definiremo la funzione impl_hello_macro, dove costruiremo il nuovo codice Rust che vogliamo includere. Ma prima di farlo, nota che l’output per la nostra macro di derivazione è anche un TokenStream. Il TokenStream restituito viene aggiunto al codice che i nostri utenti del crate scrivono, quindi quando compilano il loro crate, otterranno la funzionalità aggiuntiva che forniamo nel TokenStream modificato.

Potresti aver notato che chiamiamo unwrap per far sì che la funzione hello_macro_derive entri in panico se la chiamata alla funzione syn::parse fallisce qui. È necessario che la nostra macro procedurale entri in panico sugli errori perché le funzioni proc_macro_derive devono restituire TokenStream anziché Result per conformarsi all’API delle macro procedurali. Abbiamo semplificato questo esempio utilizzando unwrap; nel codice di produzione, dovresti fornire messaggi di errore più specifici su cosa sia andato storto utilizzando panic! o expect.

Ora che abbiamo il codice per trasformare il codice Rust annotato da un TokenStream in un’istanza DeriveInput, generiamo il codice che implementa il trait HelloMacro sul tipo annotato, come mostrato nel Listato 19-33.

Nome file: hello_macro_derive/src/lib.rs

rust

fn impl_hello_macro(ast: &syn::DeriveInput) -> TokenStream {
let name = &ast.ident;
let gen = quote! {
impl HelloMacro for #name {
fn hello_macro() {
println!("Hello, Macro! Il mio nome è {}!", stringify!(#name));
}
}
};
gen.into()
}

Listato 19-33: Implementazione del trait HelloMacro utilizzando il codice Rust analizzato

Otteniamo un’istanza della struct Ident contenente il nome (identificatore) del tipo annotato utilizzando ast.ident. La struct nel Listato 19-32 mostra che quando eseguiamo la funzione impl_hello_macro sul codice nel Listato 19-30, l’ident che otteniamo avrà il campo ident con un valore di “Pancakes”. Quindi, la variabile name nel Listato 19-33 conterrà un’istanza della struct Ident che, quando stampata, sarà la stringa “Pancakes”, il nome della struct nel Listato 19-30.

La macro quote! ci consente di definire il codice Rust che vogliamo restituire. Il compilatore si aspetta qualcosa di diverso dal risultato diretto dell’esecuzione della macro quote!, quindi dobbiamo convertirlo in un TokenStream. Facciamo questo chiamando il metodo into, che consuma questa rappresentazione intermedia e restituisce un valore del tipo TokenStream richiesto.

La macro quote! fornisce anche meccaniche di templating molto cool: possiamo inserire #name e quote! lo sostituirà con il valore nella variabile name. Possiamo persino fare una certa ripetizione simile al modo in cui funzionano le macro regolari. Dai un’occhiata alla documentazione del crate quote per una introduzione approfondita.

Vogliamo che la nostra macro procedurale generi un’implementazione del nostro trait HelloMacro per il tipo annotato dall’utente, che possiamo ottenere utilizzando #name. L’implementazione del trait ha la funzione hello_macro, il cui corpo contiene la funzionalità che vogliamo fornire: stampare Hello, Macro! Il mio nome è e poi il nome del tipo annotato.

La macro stringify! usata qui è incorporata in Rust. Prende un’espressione Rust, come 1 + 2, e a tempo di compilazione trasforma l’espressione in una stringa letterale, come “1 + 2”. Questo è diverso da format! o println!, macro che valutano l’espressione e quindi trasformano il risultato in una String. C’è la possibilità che l’input #name potrebbe essere un’espressione da stampare letteralmente, quindi usiamo stringify!. Usare stringify! risparmia anche una allocazione convertendo #name in una stringa letterale a tempo di compilazione.

A questo punto, cargo build dovrebbe completarsi correttamente sia in hello_macro che in hello_macro_derive. Collega questi crate al codice nel Listato 19-30 per vedere la macro procedurale in azione! Crea un nuovo progetto binario nella tua directory dei progetti utilizzando cargo new pancakes. Dobbiamo aggiungere hello_macro e hello_macro_derive come dipendenze nel file Cargo.toml del crate pancakes. Se stai pubblicando le tue versioni di hello_macro e hello_macro_derive su crates.io, saranno dipendenze regolari; se no, puoi specificarle come dipendenze del percorso in questo modo:

toml

hello_macro = { path = "../hello_macro" }
hello_macro_derive = { path = "../hello_macro/hello_macro_derive" }

Metti il codice nel Listato 19-30 dentro a src/main.rs e esegui cargo run: dovrebbe stampare Hello, Macro! Il mio nome è Pancakes! L’implementazione del trait HelloMacro dalla macro procedurale è stata inclusa senza che il crate pancakes debba implementarla; #[derive(HelloMacro)] ha aggiunto l’implementazione del trait.

Successivamente, esploreremo come le altre tipologie di macro procedurali differiscono dalle macro di derivazione personalizzate.

Macro Simili ad Attributi

Le macro simili ad attributi sono simili alle macro di derivazione personalizzate, ma invece di generare codice per l’attributo di derivazione, ti consentono di creare nuovi attributi. Sono anche più flessibili: derive funziona solo per strutture ed enum; gli attributi possono essere applicati anche ad altri elementi, come le funzioni. Ecco un esempio di utilizzo di una macro simile ad attributo: diciamo che hai un attributo chiamato route che annota le funzioni quando si utilizza un framework di applicazioni web:

rust

#[route(GET, "/")]
fn index() {

Questo #[route] attributo sarebbe definito dal framework come una macro procedurale. La firma della funzione di definizione della macro sarebbe così:

rust

#[proc_macro_attribute]
pub fn route(attr: TokenStream, item: TokenStream) -> TokenStream {

Qui abbiamo due parametri di tipo TokenStream. Il primo è per il contenuto dell’attributo: la parte GET, “/”. Il secondo è il corpo dell’elemento a cui è allegato l’attributo: in questo caso, fn index() {} e il resto del corpo della funzione.

A parte questo, le macro simili ad attributi funzionano allo stesso modo delle macro di derivazione personalizzate: crei un crate con il tipo di crate proc-macro e implementi una funzione che genera il codice che desideri!

Macro Simili a Funzioni

Le macro simili a funzioni definiscono macro che assomigliano a chiamate di funzioni. Similmente alle macro macro_rules!, sono più flessibili delle funzioni; ad esempio, possono accettare un numero sconosciuto di argomenti. Tuttavia, le macro macro_rules! possono essere definite solo utilizzando la sintassi simile a quella di match che abbiamo discusso nella sezione “Macro Dichiarative con macro_rules! per la Metaprogrammazione Generale” in precedenza. Le macro simili a funzioni prendono un parametro TokenStream e la loro definizione manipola quel TokenStream utilizzando codice Rust come fanno gli altri due tipi di macro procedurali. Un esempio di una macro simile a funzione è una macro sql! che potrebbe essere chiamata così:

rust

let sql = sql!(SELECT * FROM posts WHERE id=1);

Questa macro analizzerebbe l’istruzione SQL al suo interno e verificherebbe che sia sintatticamente corretta, il che è un’elaborazione molto più complessa rispetto a quanto può fare una macro macro_rules!. La macro sql! sarebbe definita così:

rust

#[proc_macro]
pub fn sql(input: TokenStream) -> TokenStream {

Questa definizione è simile alla firma della macro di derivazione personalizzata: riceviamo i token che sono all’interno delle parentesi e restituiamo il codice che volevamo generare.

Sommario

Uff! Ora hai alcune caratteristiche di Rust nel tuo arsenale che probabilmente non userai spesso, ma saprai che sono disponibili in circostanze molto particolari. Abbiamo introdotto diversi argomenti complessi in modo che quando li incontri nei suggerimenti dei messaggi di errore o nel codice di altre persone, sarai in grado di riconoscere questi concetti e sintassi. Usa questo capitolo come riferimento per guidarti verso soluzioni.

Dopo, metteremo in pratica tutto ciò di cui abbiamo discusso nel libro e faremo un altro progetto!

1 Comment

Leave a Reply

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *

Releated Posts