~rleek/website

d2d4a94408ed0c642de5b8dc3197958e0c37afd9 — Richard Leek 2 months ago 8afe90e
new eobot post
A content/posts/theads-in-rust/mockup.png => content/posts/theads-in-rust/mockup.png +0 -0
A content/posts/theads-in-rust/post.md => content/posts/theads-in-rust/post.md +151 -0
@@ 0,0 1,151 @@
---
title: "GUIs and Threads in Rust"
date: 2020-11-03T11:41:32-05:00
draft: false
tags: [
    "rust",
    "threads",
    "gui",
    "eo",
]
---

One thing I've really been wanting to accomplish with my [eobot](https://git.sr.ht/~rleek/eobot) project
is a GUI. I explored a few options for getting a GUI up and running. I'd heard a lot of projects go the
electron route but I didn't really want to pull in all the bloat for this.

A slimmer option that seemed appealing was the [web-view](https://github.com/Boscop/web-view) crate.
Its purpose is to open a native window + web view using the system's default web render stack. I set
up a project and went along trying to write some react+webpack front-end but I could not get it to work
on my system (Windows 10 LTSC 2019) at all. From what I could tell it was trying to run the code in an
IE11 window and I couldn't get it to use Edge.

A few weeks ago I decided I'd just go native. I went with the [native-windows-gui](https://github.com/gabdube/native-windows-gui) crate for this. Here's the first mockup I created with the crate.

![mockup](../mockup.png)

The idea is that on the world tab you'll get a view into the game world of [eo](https://game.eoserv.net).
The map will show you other players/npcs/drops/warps/chests/etc around the logged in player. The lists on
the side will give you a more detailed list of npcs, drops, and other players around you. The packets tab will give you a detailed summary of each packet sent to and from the server. This will be useful for debugging and just be fun to look at.
Finally the settings tab will just be a place to easily modify the config.toml file for the bot.

So once I had decided on a GUI it came time to actually try getting the bot running with it. I knew I was
going to need a separate thread to run the bot on. I couldn't believe how easy it really was once I did a
little bit of searching and read a few documents on the rust website. Below is a snippet of code from the
GUI programs main.rs file.

```rust
fn main() {
    let (tx, rx) = mpsc::channel();
    nwg::init().expect("Failed to init Native Windows GUI");
    nwg::Font::set_global_family("Segoe UI").expect("Failed to set default font");
    let mut _app = EOBot::build_ui(Default::default()).expect("Failed to build UI");

    thread::spawn(move || {
        let mut client = Client::new();

        // snipped configuration

        match client.load_pub_files() {
            Ok(_) => {}
            Err(e) => {
                println!("Error Encountered loading pub files: {:?}", e);
            }
        }

        if client.connect().unwrap() {
            client.send_init_request();
            const UPDATE_INTERVAL_MILLIS: u64 = 500;
            const SLEEP_INTERVAL_MILLIS: u64 = 50;
            let mut update_timer = 0;
            while client.connected {
                match client.receive_and_process() {
                    Ok(_) => {}
                    Err(e) => {
                        if e.kind() != io::ErrorKind::WouldBlock {
                            println!("Error Encountered, closing: {:?}", e);
                            client.connected = false;
                        }
                    }
                }
                if update_timer == 0 {
                    let mut chat_log: Vec<String> = Vec::with_capacity(client.chat_log.len());
                    chat_log.append(&mut client.chat_log);

                    let mut packet_log: Vec<(message::PacketSender, packet::Packet)> =
                        Vec::with_capacity(client.packet_log.len());
                    packet_log.append(&mut client.packet_log);

                    tx.send(message::Message {
                        chat: chat_log,
                        packets: packet_log,
                        drops: client.drops.clone(),
                        characters: client.characters.clone(),
                        npcs: client.npcs.clone(),
                    })
                    .unwrap();
                    update_timer = UPDATE_INTERVAL_MILLIS;
                } else {
                    update_timer -= SLEEP_INTERVAL_MILLIS;
                }
                thread::sleep(time::Duration::from_millis(SLEEP_INTERVAL_MILLIS));
            }
        }
    });
}
```

To pass data between the threads I decided to use a multiple producer, single consumer channel.
It's created like this. [Rust book chapter on the subject](https://doc.rust-lang.org/book/ch16-02-message-passing.html)
```rust
let (tx, rx) = mpsc::channel();
```

The eobot thread runs ever 50ms and I coded a sort of timer into it so that it'll update the GUI thread
every 500ms. I created a message struct inside the eobot library that contains the data we want
to send back to the consuming application (in this case the GUI app).
```rust
#[derive(Debug)]
pub struct Message {
    pub chat: Vec<String>,
    pub packets: Vec<(PacketSender, Packet)>,
    pub drops: Vec<Drop>,
    pub characters: Vec<Character>,
    pub npcs: Vec<Npc>,
}

#[derive(Debug)]
pub enum PacketSender {
    Client,
    Server,
}
```

As of right now the chat, and packets members only contain a pending Vec of items. This means the consumer can
append them to the view as it comes in. The other three members are the complete list of drops/characters/npcs
in the world view. I might change these to all be separate messages and actually send to the consumer as the events
are happening in the main loop, but this is working for now.

The one thing that really let this all come together was this function from the nwg crate
```rust
nwg::dispatch_thread_events_with_callback(move || {
    if let Ok(m) = rx.try_recv() {

        // this takes the message struct from above and updates the gui
        _app.process_bot_state(m);
    }

    // cpu runs like crazy without this
    thread::sleep(time::Duration::from_millis(1));
});
```

The default `nwg::dispatch_thread_events` function doesn't have a callback so you wouldn't
be able to run your own code on every cycle of the event loop. I had to add that `thread::sleep` call at
the bottom when I noticed my program was using 30%+ of cpu while running.

Anywho, I'm quite happy with the results so far. The last thing I got working was the packet log.
But soon I'd like to add popup windows to the GUI so when you click a packet you get a view of
all the data, sequence numbers, encoding, etc.

There's still a ton left to do on this project but it's a lot of fun to work on.

A content/posts/theads-in-rust/video.mp4 => content/posts/theads-in-rust/video.mp4 +0 -0