Article #1771: fltk-rs: Rust bindings for FLTK

GitHub FLTK Project   FLTK News RSS Feed  
  FLTK Apps      FLTK Library      Forums      Links     Login 
 Home  |  Articles & FAQs  |  Bugs & Features  |  Documentation  |  Download  |  Screenshots  ]
 

Return to Articles | Show Comments | Submit Comment ]

Article #1771: fltk-rs: Rust bindings for FLTK

Created at 05:12 Sep 07, 2021 by Mo_Al_

Last modified at 07:00 Sep 08, 2021

Assuming you know FLTK, and you're interested in (or have heard of) Rust, this introduction is for you.

fltk-rs is a Rust crate (Rust-term for a library), which is hosted on github and crates.io (Rust's central crate registry). If you had never heard of Rust. A quick intro: Rust is a relatively new programming language, originally developed at Mozilla, which aims to target the same niches as C and C++, but with the added benefit of memory and thread safety out of the box. This is done via built-in static analysis in the Rust compiler, assisted by lifetime annotations in the language itself. The language also offers an escape-hatch, that's the "unsafe" block, which allows using raw pointers and bypassing some of the static analysis. That said, the most compelling feature of Rust (in my opinion) is the package manager (Cargo). Which allows adding a dependency without having to know how to build/install the dependency, and without having to deal with build/meta-build systems like CMake or autotools.

fltk-rs, as such, can be added to a project by simply adding a single line to your Rust project's Cargo.toml file (under the dependencies section):

fltk = "1"

This builds FLTK 1.4 from source (so you still need to have a C++ compiler and CMake). For certain platforms (Windows x86_64, MacOS x86_64 and Linux x86_64), a bundle is provided which makes it possible to use fltk-rs without the need for CMake or a C++ compiler:

fltk = { version = "1", features = ["fltk-bundled"] }

Architecture

Before delving into how the code looks like, a simple overview of the architecture of fltk-rs:
  • Basically fltk-rs is composed of 3 sub-crates (fltk-sys, fltk-derive and fltk).
  • The fltk-sys crate, as is the Rust convention of <crate>-sys crates, is the low-level (unsafe) binding of FLTK. It can be used directly, however, everything will have to be wrapped in a large unsafe block. Rust can't call C++ code directly, so FLTK itself is wrapped in a C api which Rust can call. The C api can be found here. The C api is not pretty to look at since it's mostly composed of macros. The idea is that those macros wrap FLTK types and methods and are expanded to cover most of FLTK's public api. fltk-sys is also auto-generated by a tool called rust-bindgen.
  • The fltk-derive crate, is made up of Rust macros, which finally expand to also cover most of FLTK's public api but on the Rust side and with a higher-level (safe) api.
  • the fltk crate, which basically uses fltk-sys and fltk-derive to tie everything together and actually present the higher-level api to the developer.

So what does the api look like? Even though fltk-rs targets FLTK 1.4, the api resembles more FLTK 2. All symbols are under an fltk namespace. Similar widgets are grouped under a namespace as well. And widgets don't have the "Fl_" prefix, and instead use a PascalCase naming convention. So the button namespace contains:

  • Button
  • ToggleButton
  • CheckButton ...etc. The full list of Widgets can be found here.

Class methods are mostly similar, however, since Rust doesn't have method/function overloading, setters are preceded by the "set_" prefix. Another difference is the preference of using closures for callbacks. Another distinguishing feature, is that since Rust reserves both Box and box symbols, these are replaced with Frame and frame respectively.

The code

So starting with a simple program:

use fltk::{
    app,
    prelude::{GroupExt, WidgetBase, WidgetExt},
    window,

};

fn main() {
    let a = app::App::default();
    let mut wind = window::Window::new(100, 100, 400, 300, "My Window");
    wind.end();
    wind.show();
    a.run().expect("Couldn't run app");

}

You'll already notice how similar C++ and Rust are:

  • Both use semicolons and braces.
  • Both have type-inference (in C++ using C++11 auto).
  • Both use double colons for namespace/module resolution.

Some differences:

  • Rust is module based (C++20 is also getting modules), so no headers. Modules are imported using the `use` keyword.
  • `main` doesn't take arguments, although these can be accessed via std::env::args().

On the FLTK side, you'll notice:

  • The ::new constructor basically takes the same arguments.
  • `prelude::{GroupExt, WidgetBase, WidgetExt}` in the imports, Rust requires explicit imports of traits (akin to C++ abstract classes), to be able to use any of their methods.
  • the App struct, this just ensures proper initialization of styles and images. It also ultimately calls Fl::lock() to enable multithreaded support.
  • `a.run().expect("...")` is similar to `Fl::run()`, however Rust encourages returning Result types (more explicit than error codes, errno ..etc) and enforces handling errors, bubbling up to the caller or just unwrapping which will panic on error. Panicking is akin to C++ exceptions, which also unwind the stack, unless compiled with `panic = "abort"`, which is akin to `-fno-exceptions` (gcc/clang). And since we provide no panic hook/handler, this aborts main with the message specified printed to stderr.

Back to the imported traits, the window's `end` method for example comes from GroupExt, which is a trait carrying Fl_Group's methods. The Group widget also implements the GroupExt trait. Similarly `show` comes from WidgetExt, and the constructor `::new` comes from WidgetBase. There are 11 widget traits which can be found here.

Events

fltk-rs offers the same mechanisms of handling events as FLTK:
  • Using callbacks via the set_callback() method. The trigger can be changed using the set_trigger() method (like FLTK's when()).
  • Handling events in the handle method. This also allows defining new custom events
  • Using channels (based on FLTK's `awake(message)` and `thread_message()`).

We'll just touch on the set_callback method which is probably the easiest. Since fltk-rs prefers using closures (even though function pointers are also supported), a basic example:

use fltk::{
    app, button, enums, frame, group,
    prelude::{GroupExt, WidgetBase, WidgetExt},
    window, };

fn main() {
    let a = app::App::default().with_scheme(app::Scheme::Gtk);
    app::get_system_colors();
    let mut wind = window::Window::new(100, 100, 400, 300, "My Window");
    let mut pack = group::Pack::default().with_size(200, 80).center_of_parent();
    pack.set_spacing(10);
    pack.set_frame(enums::FrameType::BorderFrame);
    pack.set_color(enums::Color::Red.inactive());
    let mut frame = frame::Frame::default().with_size(0, 30);
    let mut btn = button::Button::default()
        .with_size(0, 30)
        .with_label("Click");
    pack.end();
    wind.end();
    wind.show();

    btn.set_callback(move |b| {
        frame.set_label("Button clicked");
        b.set_label("Clicked");
    });

    a.run().unwrap(); }

There might be a lot here all of a sudden, but lets look first at the set_callback() method towards the end:

  • The move keyword indicates all closed on items are captured by value, which in Rust indicates copying for trivially copyable types, and moves for non-trivially copyable types. This is like C++ lambda syntax `[=](ArgType arg) {}`, where the capture is `[some_arg = std::move(some_arg)]` for movable types.
  • The symbol between the 2 pipes, is the closure's argument, which in this case, is the same type as widget calling set_callback(). Whereas in FLTK, the function pointer's main argument is an Fl_Widget pointer which might need casting to access methods not available to Fl_Widget.
  • The closed on objects represent the data void pointer, without the need to cast objects back and forth.

Looking back at the beginning of the code. We import the modules that we need, and in this example, we use a button::Button, group::Pack, enums (FLTK's Enumerations).

Some differences to the previous code:

  • We set the scheme for our App object.
  • We use the get_system_colors() free function from the app module.
  • We construct the Pack using the ::default() constructor, which basically inits everything to zero, then we set the size and the position using a builder design pattern, using some convenience methods provided by fltk-rs. The ::new constructor still can be used.
  • We set the box() type of the Pack widget, using set_frame().
  • The box types and colors are in the enums module (under the enums namespace).
  • Rust enums are like C++ scoped enums (notice the double colons).
  • Rust allows uniform function call syntax for methods. i.e we can use both `Color::inactive(&Color::Red)` and `Color::Red.inactive()`.
  • The Frame is as previously mentioned an Fl_Box.
  • Both frame and btn are constructed using the builder pattern (the ::new() constructor can still be used).
  • In the end we just unwrap() on run failure, this will print to stderr the fltk-rs defined error type on run failure.

Conclusion

The Rust api is meant to be easy for people familiar with FLTK, it also makes things easier when returning to the FLTK main documentation. It's also meant to be easy for new Rust users, there are no lifetime annotations since widgets manage the lifetime of their children. There are more examples in the fltk-rs repo, as well as a wiki with more information. The fltk-rs repo is under the fltk-rs github organization, which also contains demos and other crates forming a small ecosystem around fltk-rs.

Listing ]


Comments

Submit Comment ]
 
 

Comments are owned by the poster. All other content is copyright 1998-2021 by Bill Spitzak and others. This project is hosted by The FLTK Team. Please report site problems to 'erco@seriss.com'.