#event-loop #gui-applications #cross-platform-gui #events #loops #dispatcher #message

thin_main_loop

Thin, cross-platform, main event loop. A building block for native GUI applications, among other use cases.

8 releases

0.2.0 Nov 7, 2019
0.1.7 May 15, 2019
0.1.6 Apr 21, 2019
0.1.5 Mar 2, 2019
0.1.2 Jan 27, 2019

#708 in Asynchronous

Apache-2.0/MIT

55KB
1K SLoC

A thin main loop for Rust

Because Rust's native GUI story starts with the main loop.

(Although this library might be useful for many other use cases too.)

Goals

  • Support both callback based and async/await APIs
  • Cross platform
  • Negligible performance overhead for desktop applications
  • Bind to the best backend on each platform
  • No extra background threads
  • Provide access to raw handles to allow platform specific extensions

Non-goals

  • Avoiding allocations and virtual dispatch at all costs
  • I/O scalability
  • no_std functionality

Status

The library has functions for running code:

  • ASAP (as soon as the main loop gets a chance to run something),
  • after a timeout,
  • at regular intervals,
  • ASAP, but in another thread,
  • when an I/O object is ready of reading or writing.

and it can do so by:

  • Scheduling a callback
  • Scheduling a 0.3 future
  • Scheduling an async fn

Maturity: Just up and running, not battle-tested. It's also a proof-of-concept, to spawn discussion and interest. I e, it's waiting for you to give it a spin, try it out, see what you like and what you don't like, what feature's you're missing, etc!

Unsafe blocks: Only at the backend/FFI level. With the reference (Rust std) backend, there is no unsafe code at all.

Rust version: Latest stable should be fine.

Supported platforms

Currently:

  • Win32 API - compile with --features "win32"
  • Glib - compile with --features "glib"
  • Rust std - reference implementation, does not support I/O.

Wishlist:

  • OS X / Cocoa
  • Wasm / web (limited as we don't control the main loop)
  • QT
  • iOS
  • Android

Examples

Borrowing

If you have access to the mainloop, it supports borrowing closures so you don't have to clone/refcell your data:

// extern crate thin_main_loop as tml;

let mut x = false;
{
    let mut ml = tml::MainLoop::new();
    ml.call_asap(|| {
        x = true; // x is mutably borrowed by the closure
        tml::terminate();
    });
    ml.run();
}
assert_eq!(x, true);

Non-borrowing, and a timer

If you don't have access to the mainloop, you can still schedule 'static callbacks:

// extern crate thin_main_loop as tml;

let mut ml = tml::MainLoop::new();
ml.call_asap(|| {
    // Inside a callback we can schedule another callback.
    tml::call_after(Duration::new(1, 0), || {
        tml::terminate();
    });
})
ml.run();
// After one second, the main loop is terminated.

I/O

Requires features "glib" or "win32".

The following example connects to a TCP server and prints everything coming in.

// extern crate thin_main_loop as tml;

let mut io = TcpStream::connect(/* ..select server here.. */)?;
io.set_nonblocking(true)?;
let wr = tml::IOReader { io: io, f: move |io: &mut TcpStream, x| {
    // On incoming data, read it all
    let mut s = String::new();
    let r = io.read_to_string(&mut s);

    // If we got something, print it
    if s != "" { println!(s); }

    // This is TcpStream's way of saying "connection closed"
    if let Ok(0) = r { tml::terminate(); }
}

let mut ml = MainLoop::new()?;
ml.call_io(wr)?;
ml.run();

Async fn

The following code waits one second, then terminates the program.

// extern crate thin_main_loop as tml;

use std::time::{Instant, Duration};

async fn wait_until(n: Instant) {
    delay(n).await.unwrap();
}

let mut x = tml::futures::Executor::new().unwrap();
let n = Instant::now() + Duration::from_millis(1000);
x.block_on(wait_until(n));

Background

Callbacks

Most of the APIs for native GUIs build on callbacks. When a button is clicked, you get a callback. The problem though, is that callbacks aren't really rustic: if you want to access your button object in two different callbacks, you end up having to Rc/RefCell it. And while that isn't the end of the world, people have been experimenting with other, more rustic, API designs.

But that will be the task of another library. If we first make a thin cross platform library that binds to the native GUI apis, we can then experiment with making a more rustic API on top of that. And before we can make windows and buttons, we need to make a main loop which can process events from these objects. Hence this library.

Other Rust main loops

Mio

Mio is also a cross platform main loop, but Mio has quite different design goals which ultimately makes it unsuitable for native GUI applications. Mio's primary use case is highly scalable servers, as such it binds to IOCP/epoll/etc which can take thousands of TCP sockets without problems, but does not integrate well with native APIs for GUI libraries: IOCP threads cannot process Windows messages, and so on. This library binds to PeekMessage/GMainLoop/etc, which makes it suitable for GUI applications with native look and feel.

Also, Mio is better at avoiding allocations, at the cost of being less ergonomic.

Calloop

Calloop is an event loop with very similar callback-style API to this crate. However, it is built on top of Mio, and so it binds to unsuitable native APIs for native GUI applications.

Winit

Winit includes an event loop, and the crate has the purpose of creating windows. The event loop is not callback based, but enum based (every Event is an enum, which you need to dispatch yourself). Winit's focus is more on getting a window and custom drawing (through OpenGL, Vulcan etc) rather than drawing native GUI widgets, but nonetheless has some common ground with this crate.

IUI / libui

IUI is a Rust binding to libui, which is a cross-platform GUI library written in C. Its event loop offers callbacks, much like this library. In comparison, this library is pure Rust only and binds to native libraries directly, skipping one abstraction level and is therefore easier to build. I also hope that with time this library could offer better Rust integration as well as some more flexibility, being usable for more than pure GUI applications, even if that is the current primary use case.

Dependencies

~0.6–3.5MB
~61K SLoC