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 in Engine
  • Engine quando compila ed esegue un atom deve dargli un riferimento al Runtime, ma questo gli richiede di conoscerne il tipo (non si può neanche realmente usare Box<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,
}