1

ça doit faire quelque chose comme 3-4 semaines a total que je me suis mis au Rust et plus j'avance plus je me prend des bâtons dans les roues.

Actuellement j'ai du code qui fait ça:

d6udE7s.png

Globalement, c'est architecturé en dessinant le triangle indépendamment de l'interface (l'interface se dessine par dessus). Mais tout ce qu'il faut retenir c'est que la logique de rendu de l'interface est séparée dans un objet, et celle du triangle aussi.
Jusque là pourquoi pas ça marche plutôt bien.

Maintenant on va rajouter un peu d'interactivité: quand l'utilisateur remplit le textbox correctement, un tableau s'affiche. Là encore, ça marche très bien, parce que c'est un comportement qui se limite à l'interface:
o8TwaHZ.png

Vous noterez que y a un bouton sur la ligne du tableau. Et maintenant, moi, je veux détecter le clic sur le bouton. Evidemment je peux ajouter ça dans l'objet qui gère le dessin de l'interface (ça fait partie de l'état de l'interface après tout), mais par contre l'action correspondante (à savoir aller lire des fichiers binaires etc) n'a rien a faire dans l'interface, vu qu'il s'agit de charger des données qui sont utilisables par n'importe quel composant de rendu (en gros, le triangle peut aussi en avoir besoin).

Du coup je me dis que je peux faire un event listener classique tout con, et en plus j'ai même pas besoin de m'emmerder avec de la synchro ou quoi puisque j'ai juste à enregistrer les listeners à l'allocation des structures; une fois que l'appli est setup visuellement ça ne bouge plus.

#[derive(Default)] pub struct InterfaceState { installation_path : String, // La chaîne de caractères de la textbox active_tab : Tab, // event handlers event_handlers : Vec<Box<dyn Fn(&InterfaceEvent)>>, } impl InterfaceState { pub fn publish_event(&mut self, event : InterfaceEvent) { for handler in &mut self.event_handlers { handler(&event) } } pub fn add_event_listener<F>(&mut self, handler : F) where F : Fn(&InterfaceEvent) + 'static { self.event_handlers.push(Box::new(handler)); } }
(Il manque le Rust dans la coloration syntaxique ^^)

A priori pas de problème, par contre les emmerdes commencent quand on essaie de s'en servir.

fn setup(app : &mut Application, window : Window) -> ApplicationData { let renderer = RendererOptions::default() .line_width(DynamicState::Fixed(1.0f32)) .multisampling(vk::SampleCountFlags::TYPE_4); let mut renderer = Renderer::builder(app.context.clone()) .build(renderer, window, vec![ash::khr::swapchain::NAME.to_owned()]); let mut slf = ApplicationData { geometry : GeometryRenderer::new(&mut renderer, false), interface : { let _theme = theming::themes::StandardDark{}; let style = egui::Style::default(); // _theme.custom_style(); let mut fonts = FontDefinitions::default(); load_fonts(&mut fonts, &None, "./assets/fonts"); let options = InterfaceOptions::default(|ctx, state : &mut InterfaceState| state.render(ctx)) .fonts(fonts) .style(style); InterfaceRenderer::new(&renderer.swapchain, &renderer.context, true, options) }, renderer, fs : None, }; slf.interface.state.add_event_listener(|event| { match event { InterfaceEvent::InstallationSelected { path, cdn, build, .. } => { slf.load_game_install(cdn, build, path) }, }; }); slf } // Pour référence... pub struct ApplicationData { renderer : Renderer, geometry : GeometryRenderer, interface : InterfaceRenderer<InterfaceState>, // An accessor around the game files fs : Option<FileSystem>, } impl ApplicationData { fn load_game_install(&mut self, cdn : &str, build: &str, path : &PathBuf) -> Result<(), Error> { let fs = match FileSystem::open(path, cdn, build) { Ok(fs) => Some(fs), Err(err) => return Err(err), }; self.fs = fs; Ok(()) } }
Pas d'erreur dans l'IDE, par contre quand on essaie de compiler...

error[E0596]: cannot borrow `slf` as mutable, as it is a captured variable in a `Fn` closure
  --> wowedit\src/main.rs:81:17
   |
81 |                 slf.load_game_install(cdn, build, path)
   |                 ^^^ cannot borrow as mutable

En gros, l'appel à add_event_listener travaille sur un mutable (slf est déclaré mut, donc slf.interface.state est accédé mutablement); je stocke une lambda qui elle aussi a besoin d'accéder à slf mutablement, et c'est la merde: Rust interdit d'avoir deux borrow mutables en même temps (pour des raisons que je ne comprend pas très bien encore, mais admettons).

Sauf que moi j'ai pas particulièrement envie de stocker des états dans tous les sens, avoir des évènements comme ça ça me plaît bien, ça m'évite de stocker des trucs sans raison et d'en plus devoir gérer le cas où l'utilisateur a pas encore cliqué (globalement je devrais avoir un Option<...> et pas juste les données) L'event est lancé une seule fois, j'initialise le filesystem, et on en parle plus.

Mais non, faut que le borrow checker me casse les couilles, et là les "rustacéens" (ces gens qui se croient meilleurs que les autres, pour le peu que j'ai parlé avec eux) me sortent qu'il faut passer par un Rc<RefCell<T>> pour avoir de la "mutabilité intérieure". et moi je dis: pourquoi je dois activement m'emmerder avec ce truc là?

Bref ça m'énerve, plus j'avance plus je me rend compte que les cas simples ça marche très bien mais dès qu'on essaie un truc avec un tout petit peu de complexité c'est la merde parce que le borrow checker nous casse activement les couilles.

le code est ici
GitHub - Warpten/rust-pg: rust playing ground, don't look too much into thisGitHubrust playing ground, don't look too much into this - Warpten/rust-pg
(sur une branche)

2

Warpten (./1) :
les "rustacéens" (ces gens qui se croient meilleurs que les autres, pour le peu que j'ai parlé avec eux)
Ah, toi aussi tu as remarqué l'effet que ce langage semble avoir sur ceux qui l'utilisent ? tongue
avatar
Zeroblog

« Tout homme porte sur l'épaule gauche un singe et, sur l'épaule droite, un perroquet. » — Jean Cocteau
« Moi je cherche plus de logique non plus. C'est surement pour cela que j'apprécie les Ataris, ils sont aussi logiques que moi ! » — GT Turbo

3

J'allais justement te proposer comme solution de passer par Rc<RefCell<T>>, ce que d'autre on visiblement déjà fait.

Comme pour tous les langages, en Rust il y a des spécificités à apprendre, et en effet ce n'est certainement pas le langage le plus simple. Quand on fait du Rust, la première difficulté est en effet d'apprendre à composer avec le borrow checker qui est très énervant, surtout au début car c'est une mécanique restrictive qui n'est pas commune avec les autres langages. C'est le compromis à accepter pour avoir à la fois les performances et la sécurité.
La restriction d'avoir un seul borrow mutable a la fois, c'est la base de ce qui permet de garantir l'absence de data race et d'erreur mémoire à la compilation: pour toute la durée de vie d'une variable on est certain que son contenu ne peut être modifié de manière imprévue par une autre partie du code. C'est sur que c'est contraignant, mais c'est ce qui fait que l'on a rarement de mauvaises surprises à partir du moment où ça compile.
avatar

4

Mais Rc<RefCell<T>> introduit de la complexité pour rien, j'ai pas besoin de refcounter ici, y a qu'un seul owner sur ApplicationData, à savoir l'appelant à setup().

je me retrouve à devoir faire slf.borrow_mut().load_game_install(...), pour moi ça c'est un mur, parce que maintenant faut que je garde en tête que y a un RefCell entre deux.

J'ai juste l'impression que ce truc (la "mutabilité intérieure") ça a été inventé parce qu'on s'est rendu compte que sinon on arrivait à rien

5

Warpten (./4) :
J'ai juste l'impression que ce truc (la "mutabilité intérieure") ça a été inventé parce qu'on s'est rendu compte que sinon on arrivait à rien
C'est indiqué clairement dans la doc de std::cell . Le but de la mutabilité intérieure est bien de relaxer les règle de base sans compromettre la sécurité.
avatar

6

Bon, et tant que j'y suis, comment je suis sensé faire quand j'ai une opération qui prend du temps qui est lancée au clic sur un élément d'interface, et que l'objet retourné construit doit être stocké quand il est construit?

avec tokio ça me donne un truc comme ça

    pub fn async_load_game_install(self : &SharedState, runtime: &Runtime, cdn : String, build : String, path : PathBuf) -> oneshot::Receiver<Option<FileSystem>> {
        let (rx, tx) = oneshot::channel();
        runtime.spawn(async move {
            match FileSystem::open(path, build, cdn) {
                Ok(fs) => rx.send(Some(fs)),
                Err(_) => rx.send(None),
            }
        });

        tx
    }

Mais ensuite je veux pas await sur le receiver je veux juste que ça mutate self (qui est derrière un Arc<Mutex<>> maintenant); si j'await sur le receiver le problème reste le même, l'UI va stall

Un peu comme les futures en java avec du then, on_complete, etc

edit: de façon générale je comprend pas grand chose à l'async en rust, ça m'a l'air d'être le même enfer que les lifetime à polluer tout le code et tout rendre async sans raison

7

J'ai pas trop joué avec l'async pour le moment mais je ne pense pas que ça soit le mieux pour ce que tu veux faire. Si tu n'a pas besoin de faire des milliers d'appels à la seconde, ça n'est probablement pas nécessaire, un thread classique devrait être suffisant et t'éviterais une dépendance envers Tokio.
Clairement l'asynchrone va t'obliger à avoir un runtime jusqu'auquel doivent remonter les appels de fonction async.
avatar

8

j'vois pas comment faire autrement, c'est pas comme si je pouvais charger "un petit peu" à chaque frame.

Le problème c'est que j'ai globalement besoin d'un &mut self dans le thread sauf que ... ben le language veut pas