Il trait Any è stato introdotto all'inizio dello sviluppo di Rust per fornire capacità di tipizzazione dinamica, principalmente per scenari di gestione degli errori e di debug in cui le informazioni sui tipi a tempo di compilazione non sono disponibili. Il suo design rispecchia concetti simili in altri linguaggi come typeid di C++ o instanceof di Java, ma il modello di proprietà di Rust impone vincoli unici. Il requisito 'static è emerso dalla necessità di garantire che i riferimenti cancellati per tipo non vivano mai più a lungo dei dati che descrivono, prevenendo errori di uso dopo la liberazione in un linguaggio senza garbage collection.
Senza il vincolo 'static, un tipo cancellato come Any potrebbe contenere riferimenti a dati locali dello stack con una durata limitata. Se l'oggetto trait Any sopravvive a quel frame dello stack, il downcasting e il dereferencing accederebbero a memoria deallocata. Poiché Any opera tramite tabelle dei metodi (vtables) e cancellazione di tipo, il compilatore non può verificare le durate al momento del downcasting; il vincolo 'static funge da garanzia conservativa che il tipo possiede tutti i suoi dati o contiene solo riferimenti statici, garantendo la sicurezza della memoria attraverso il confine della cancellazione.
La definizione del trait Any trait Any: 'static sfrutta il sistema di vincoli dei trait di Rust per imporre questo vincolo a tempo di compilazione. Solo i tipi che non contengono riferimenti non statici possono implementare Any, il che garantisce che qualsiasi &dyn Any o Box<dyn Any> rimanga valido per l'intera durata del programma. Questo consente un downcasting sicuro tramite downcast_ref() e downcast_mut(), poiché i dati sottostanti sono garantiti per non essere invalidati da uscite di scope.
Stavamo costruendo un sistema di plugin per un motore di gioco in cui gli script potevano registrare gestori di eventi restituendo dati arbitrari al motore centrale. Il motore doveva memorizzare questi valori restituiti in una coda eterogenea per un'elaborazione successiva da parte di diversi sottosistemi, richiedendo la cancellazione di tipo per memorizzare diversi tipi in una singola collezione. Tuttavia, alcune interfacce di script tentavano di restituire riferimenti a variabili locali temporanee all'interno del contesto di esecuzione dello script, che sarebbero diventate pendenti una volta completato il frame dello script.
Soluzione 1: Trait personalizzato con parametri di durata
Un approccio prevedeva la creazione di un trait personalizzato PluginResult con un tipo associato per i parametri di durata, consentendo al motore di tracciare le durate attraverso l'oggetto trait. Questo prometteva flessibilità consentendo dati presi in prestito, ma richiedeva annotazioni di durata complesse in tutta la superficie dell'API del plugin. La complessità costringerebbe ogni autore di plugin a comprendere meccaniche avanzate delle durate di Rust, creando una curva di apprendimento inaccettabilmente ripida e aumentando il rischio di bug sottili nelle durate nel codice di terze parti.
Soluzione 2: Trasmutazione di durata non sicura
Un'altra soluzione proponeva di utilizzare codice unsafe per trasmutare via le durate quando si memorizzavano i dati, promettendo fondamentalmente che il motore avrebbe abbandonato tutti i riferimenti prima che il contesto di origine uscisse. Sebbene questo consentisse la desiderata ergonomia dell'API, poneva il carico della sicurezza della memoria interamente sugli sviluppatori del motore. Qualsiasi errore nel tracciare la provenienza dei riferimenti avrebbe portato a vulnerabilità sfruttabili di uso dopo la liberazione, violando le garanzie di sicurezza di Rust e rendendo difficile l'auditing del codice.
Abbiamo scelto di richiedere che tutti i valori di ritorno del plugin implementassero Any con il vincolo 'static, costringendo gli autori degli script a restituire dati posseduti o stato condiviso racchiuso in Arc. Questa decisione ha sacrificato alcuni potenziali benefici prestazionali teorici di riferimenti a zero copie per la garanzia che la coda degli eventi del motore potesse memorizzare e elaborare i dati in modo sicuro in modo asincrono. Il risultato è stata un'API robusta per i plugin senza codice unsafe nell'interfaccia pubblica, sebbene abbia richiesto l'aggiunta di strati di serializzazione per i tipi che un tempo dipendevano da prestiti temporanei.
Perché Any richiede 'static piuttosto che semplicemente la durata del riferimento usato per creare l'oggetto trait?
Il trait Any cancella le informazioni di tipo a tempo di compilazione per produrre una vtable, perdendo tutte le informazioni di durata nel processo. Quando si crea un &dyn Any, il compilatore non può codificare la durata originale 'a nell'oggetto trait in un modo che il meccanismo di downcasting può verificare in seguito. Richiedere 'static è l'unico modo per garantire che il tipo sottostante non contenga puntatori pendenti senza monitoraggio delle durate a tempo di esecuzione. Se Any accettasse durate più brevi, il puntatore vtable stesso dovrebbe portare metadati sulla durata, il che richiederebbe a Rust di implementare tipi dipendenti o controllo degli prestiti a tempo di esecuzione, cambiando fondamentalmente il modello di astrazione a costo zero del linguaggio.
Come interagisce Box<dyn Any> con il vincolo 'static quando il tipo originale contiene riferimenti non statici?
Un tipo come struct Wrapper<'a>(&'a str) non può implementare Any perché non soddisfa il vincolo del trait 'static. Di conseguenza, non puoi creare Box<dyn Any> da un'istanza di Wrapper<'a>. I candidati spesso credono erroneamente che il mettere a box il valore estenda la sua durata; tuttavia, Box possiede solo l'allocazione nel heap, non i dati referenziati dai campi all'interno di quella allocazione. Se i dati referenziati sono locali allo stack, spostare la struttura esterna nell'heap non estende la durata del riferimento, quindi il compilatore rifiuta correttamente la conversione in Box<dyn Any>. Questo previene uno scenario in cui il box allocato nell'heap supera il frame dello stack contenente i dati referenziati.
Puoi implementare in modo sicuro un trait personalizzato Any che allenta il requisito 'static utilizzando codice unsafe e monitoraggio manuale delle durate?
Sebbene sia tecnicamente possibile utilizzare unsafe per trasmutare le durate e vtables personalizzati, una tale implementazione sarebbe insicura perché il sistema dei trait di Rust e il controllore delle referenze non possono verificare le invarianti di durata nel punto di downcast. Dovresti implementare un sistema di tipi parallelo per monitorare le durate a tempo di esecuzione, controllando ad ogni accesso che il contesto originale esista ancora. Questo approccio riproduce essenzialmente un garbage collector o un sistema di conteggio di riferimento, perdendo le garanzie a tempo di compilazione di Rust. Inoltre, qualsiasi implementazione unsafe interagirebbe in modo insicuro con i componenti della libreria standard che si aspettano le invarianti di Any, portando a comportamenti indefiniti quando mescolati con oggetti trait std::any::Any.