Non è per niente importante, però vi condivido questo “achievement” perché era una roba che volevo fare da un po’ di tempo. Sono finalmente riuscito a estrarre l’engine WebAssembly come generic nel trait di Runtime e nel tipo concreto generico del runtime.
Context:
Il trait Runtime e i tipi che lo implementano sono l’interfaccia a tutte le funzioni di Radon e sono generici in implementazione di Storage, Network, Naming
Le istanze degli atoms (unitĂ di esecuzione di Radon) devono avere un modo di interagire con il runtime in cui si trovano per potere interagire con altri atomi e risorse varie
Dov’è il problema? Nel momento in cui decido di voler supportare diverse tecnologie per eseguire gli atoms, introduco implicitamente una dipendenza circolare nella definizione dei tipi:
Runtimeè generico inEngineEnginequando compila ed esegue un atom deve dargli un riferimento alRuntime, ma questo gli richiede di conoscerne il tipo (non si può neanche realmente usareBox<dyn Runtime>in questo caso perché la catena di tipi che lo compone non è dyn-safe) Di conseguenza per specificare Engine per il runtime devo sapere il tipo di Engine, ma per sapere il tipo di Engine devo sapere il tipo del Runtime, ma il tipo del Runtime dipende da quello di Engine …
Nota: a complicare le cose c’è in mezzo un trait Scheduler costruito sopra Engine, che utilizza la funzione principale di engine per dare funzionalità di più alto livello
Come se ne esce? Dopo tanta sofferenza, GAT to the rescue Step 1: Runtime
pub trait Runtime: Sized + Sync + Send + 'static {
type Storage: Storage;
type Network: Network;
type Resolver: Resolver;
type Scheduler: /* ? */;
fn storage(&self) -> &Self::Storage;
fn network(&self) -> &Self::Network;
fn resolver(&self) -> &Self::Resolver;
fn scheduler(&self) -> &Self::Scheduler;
fn name(&self) -> &str;
}Step 2: Scheduler
pub trait Scheduler<R: Runtime>: Send + Sync {
type Instance: Instance;
fn run_daemon<F: FnOnce(Self::Instance) -> /* ... */>(&self, /* ... */) -> Result<(), RuntimeError>;
fn load_reactive(&self, rt: &Arc<R>, conf: AtomConf) -> Result<(), RuntimeError>;
fn invoke_reactive(&self, /* ... */) -> /* ... */;
}Se implementiamo uno scheduler generico per Engine siamo alla stessa situazione di prima siamo in un ciclo
Step 3: Engine, breaking the cycle
pub trait Engine: Default + Send + Sync {
type Instance<R: Runtime>: Instance + Send + Sync;
fn make_atom<R: Runtime>(
&self,
rt: &Arc<R>,
name: BString,
conf: Arc<AtomConf>,
) -> Result<Self::Instance<R>, RuntimeError>;
}Usando i Generic Associated Types possiamo rompere il ciclo e rendere il trait Engine una singola implementazione per ogni possibile R, a differenza di trait Engine<R> che viene implementato come Engine per un tipo R, e il tipo R può essere generico. Questo secondo modo non rompe il ciclo, perché implementare Engine per un tipo richiede che venga specificato R tramite generico, ma qui c’è il problema: R deve essere generico, ma allo stesso tempo deve essere l’R che ha come Engine il tipo per cui stiamo implementando Engine<R>, che però richiede questa stessa impl per essere valido (!)
Alla fine arriviamo con il trait scritto sopra e questo ci permette di scrivere
pub struct DefaultScheduler<E: Engine, I: Instance> {
engine: E,
registry: DashMap<BString, AtomManager<I>>,
}Che implementa scheduler fissando I con generic constraints e usando il GAT per ottenere il tipo concreto di istanza per lo scheduler in funzione del Runtime
impl<R: Runtime, E: Engine<Instance<R> = I>, I: Instance> Scheduler<R> for DefaultScheduler<E, I> {
type Instance = E::Instance<R>;
// ...
}Quindi infine in runtime possiamo avere
pub trait Runtime: Sized + Sync + Send + 'static {
type Storage: Storage;
type Network: Network;
type Resolver: Resolver;
type Scheduler: Scheduler<Self>;
fn storage(&self) -> &Self::Storage;
fn network(&self) -> &Self::Network;
fn resolver(&self) -> &Self::Resolver;
fn scheduler(&self) -> &Self::Scheduler;
fn name(&self) -> &str;
}Da notare che Scheduler<Self> funziona perché il trait Runtime non è generico in scheduler, ma richiede di specificare un tipo per lo scheduler, quindi no cicli
Quindi si riesce a costruire la struct per il runtime generico finale:
pub struct GenericRuntime<S, N, R, E>
where
S: Storage + Send + Sync + 'static,
N: Network + Send + Sync + 'static,
R: Resolver + Send + Sync + 'static,
E: Engine + Send + Sync + 'static,
{
name: String,
scheduler: DefaultScheduler<E, E::Instance<Self>>,
storage: S,
network: N,
resolver: R,
}