HOWTO: Idiomatic Rust for callbacks with gtk (rust

2019-04-24 01:16发布

I am currently learning Rust and looking to use it for developing a GUI based application with GTK+. My problem relates to registering callbacks to respond to GTK events/signals and mutating state within those callbacks. I have a working but inelegant solution, so I would like to ask if there is a cleaner, more idiomatic solution.

I have implemented my code as a struct with method implementations, where the struct maintains references to the GTK widgets along with other state that it needs. It constructs a closure that is passed to the GtkWidget::connect* functions in order to receive events, draw to a canvas, etc. This can cause problems with the borrow checker, as I will now explain. I have some working but (IMHO) non-ideal code that I will show.

Initial, non-working solution:

#![cfg_attr(not(feature = "gtk_3_10"), allow(unused_variables, unused_mut))]

extern crate gtk;
extern crate cairo;

use gtk::traits::*;
use gtk::signal::Inhibit;
use cairo::{Context, RectangleInt};


struct RenderingAPITestWindow {
    window: gtk::Window,
    drawing_area: gtk::DrawingArea,
    width: i32,
    height: i32
}

impl RenderingAPITestWindow {
    fn new(width: i32, height: i32) -> RenderingAPITestWindow {
        let window = gtk::Window::new(gtk::WindowType::TopLevel).unwrap();
        let drawing_area = gtk::DrawingArea::new().unwrap();
        drawing_area.set_size_request(width, height);
        window.set_title("Cairo API test");
        window.add(&drawing_area);

        let instance = RenderingAPITestWindow{window: window,
            drawing_area: drawing_area,
            width: width,
            height: height,
        };

        instance.drawing_area.connect_draw(|widget, cairo_context| {
            instance.on_draw(cairo_context);
            instance.drawing_area.queue_draw();
            Inhibit(true)
        });

        instance.drawing_area.connect_size_allocate(|widget, rect| {
            instance.on_size_allocate(rect);
        });

        instance.window.show_all();

        return instance;
    }

    fn exit_on_close(&self) {
        self.window.connect_delete_event(|_, _| {
            gtk::main_quit();
            Inhibit(true)
        });
    }


    fn on_draw(&mut self, cairo_ctx: Context) {
        cairo_ctx.save();
        cairo_ctx.move_to(50.0, (self.height as f64) * 0.5);
        cairo_ctx.set_font_size(18.0);
        cairo_ctx.show_text("The only curse they could afford to put on a tomb these days was 'Bugger Off'. --PTerry");
        cairo_ctx.restore();
    }

    fn on_size_allocate(&mut self, rect: &RectangleInt) {
        self.width = rect.width as i32;
        self.height = rect.height as i32;
    }
}


fn main() {
    gtk::init().unwrap_or_else(|_| panic!("Failed to initialize GTK."));
    println!("Major: {}, Minor: {}", gtk::get_major_version(), gtk::get_minor_version());

    let window = RenderingAPITestWindow::new(800, 500);
    window.exit_on_close();
    gtk::main();
}

The above fails to compile as the closures with RenderingAPITestWindow::new that are created and passed to calls to GtkWidget::connect* methods attempt to borrow instance. The compiler states that the closures may outlive the function in which they are declared and that instance is owned by the outer function, hence the problem. Given that GTK may keep a reference to these closures around for an unspecified amount of time, we need an approach in which the lifetime can be determined at runtime, hence my next stab at the problem in which the RenderingAPITestWindow instance is wrapped in Rc<RefCell<...>>.

Wrapping the RenderingAPITestWindow instance compiles but dies at runtime:

#![cfg_attr(not(feature = "gtk_3_10"), allow(unused_variables, unused_mut))]

extern crate gtk;
extern crate cairo;

use std::rc::Rc;
use std::cell::RefCell;
use gtk::traits::*;
use gtk::signal::Inhibit;
use cairo::{Context, RectangleInt};


struct RenderingAPITestWindow {
    window: gtk::Window,
    drawing_area: gtk::DrawingArea,
    width: i32,
    height: i32
}

impl RenderingAPITestWindow {
    fn new(width: i32, height: i32) -> Rc<RefCell<RenderingAPITestWindow>> {
        let window = gtk::Window::new(gtk::WindowType::TopLevel).unwrap();
        let drawing_area = gtk::DrawingArea::new().unwrap();
        drawing_area.set_size_request(width, height);
        window.set_title("Cairo API test");
        window.add(&drawing_area);

        let instance = RenderingAPITestWindow{window: window,
            drawing_area: drawing_area,
            width: width,
            height: height,
        };
        let wrapped_instance = Rc::new(RefCell::new(instance));

        let wrapped_instance_for_draw = wrapped_instance.clone();
        wrapped_instance.borrow().drawing_area.connect_draw(move |widget, cairo_context| {
            wrapped_instance_for_draw.borrow_mut().on_draw(cairo_context);

            wrapped_instance_for_draw.borrow().drawing_area.queue_draw();
            Inhibit(true)
        });

        let wrapped_instance_for_sizealloc = wrapped_instance.clone();
        wrapped_instance.borrow().drawing_area.connect_size_allocate(move |widget, rect| {
            wrapped_instance_for_sizealloc.borrow_mut().on_size_allocate(rect);
        });

        wrapped_instance.borrow().window.show_all();

        return wrapped_instance;
    }

    fn exit_on_close(&self) {
        self.window.connect_delete_event(|_, _| {
            gtk::main_quit();
            Inhibit(true)
        });
    }


    fn on_draw(&mut self, cairo_ctx: Context) {
        cairo_ctx.save();
        cairo_ctx.move_to(50.0, (self.height as f64) * 0.5);
        cairo_ctx.set_font_size(18.0);
        cairo_ctx.show_text("The only curse they could afford to put on a tomb these days was 'Bugger Off'. --PTerry");
        cairo_ctx.restore();
    }

    fn on_size_allocate(&mut self, rect: &RectangleInt) {
        self.width = rect.width as i32;
        self.height = rect.height as i32;
    }
}


fn main() {
    gtk::init().unwrap_or_else(|_| panic!("Failed to initialize GTK."));
    println!("Major: {}, Minor: {}", gtk::get_major_version(), gtk::get_minor_version());

    let wrapped_window = RenderingAPITestWindow::new(800, 500);
    wrapped_window.borrow().exit_on_close();
    gtk::main();
}

The above solution compiles but its not particularly pretty:

  • RenderingAPITestWindow::new returns an Rc<RefCell<RenderingAPITestWindow>> rather than a RenderingAPITestWindow
  • Accessing fields and methods of RenderingAPITestWindow is complicated by the fact that the Rc<RefCell<...>> must be opened up; it now requires wrapped_instance.borrow().some_method(...) rather than just instance.some_method(...)
  • Each closure requires it's own clone of wrapped_instance; attempting to use wrapped_instance would attempt to borrow an object -- the wrapper rather than the RenderingAPITestWindow this time -- that is owned by RenderingAPITestWindow::new as before

While the above compiles, it dies at runtime with:

thread '<main>' panicked at 'RefCell<T> already borrowed', ../src/libcore/cell.rs:442
An unknown error occurred

This is due to the fact that the call to window.show_all() causes GTK to initialise the widget hierarchy, resulting in the drawing area widget receiving a size-allocate event. Accessing the window to call show_all() required that the Rc<RefCell<...>> is opened (hence wrapped_instance.borrow().window.show_all();) and the instance borrowed. Before the borrow ends when show_all() returns, GTK invokes the drawing area's size-allocate event handler, which causes the closure connected to it (4 lines above) to be invoked. The closure attempts to borrow a mutable reference to the RenderingAPITestWindow instance (wrapped_instance_for_sizealloc.borrow_mut().on_size_allocate(rect);) in order to invoke the on_size_allocate method. This attempts to borrow a mutable reference, while the first immutable reference is still in scope. This second borrow causes the run-time panic.

The working but - IMHO - inelegant solution that I have managed to get working so far is to split RenderingAPITestWindow into two structs, with the mutable state that is to modified by the callbacks moved into a separate struct.

Working but inelegant solution that splits the RenderingAPITestWindow struct:

#![cfg_attr(not(feature = "gtk_3_10"), allow(unused_variables, unused_mut))]

extern crate gtk;
extern crate cairo;

use std::rc::Rc;
use std::cell::RefCell;
use gtk::traits::*;
use gtk::signal::Inhibit;
use cairo::{Context, RectangleInt};


struct RenderingAPITestWindowState {
    width: i32,
    height: i32
}

impl RenderingAPITestWindowState {
    fn new(width: i32, height: i32) -> RenderingAPITestWindowState {
        return RenderingAPITestWindowState{width: width, height: height};
    }

    fn on_draw(&mut self, cairo_ctx: Context) {
        cairo_ctx.save();
        cairo_ctx.move_to(50.0, (self.height as f64) * 0.5);
        cairo_ctx.set_font_size(18.0);
        cairo_ctx.show_text("The only curse they could afford to put on a tomb these days was 'Bugger Off'. --PTerry");
        cairo_ctx.restore();
    }

    fn on_size_allocate(&mut self, rect: &RectangleInt) {
        self.width = rect.width as i32;
        self.height = rect.height as i32;
    }
}


struct RenderingAPITestWindow {
    window: gtk::Window,
    drawing_area: gtk::DrawingArea,
    state: Rc<RefCell<RenderingAPITestWindowState>>
}

impl RenderingAPITestWindow {
    fn new(width: i32, height: i32) -> Rc<RefCell<RenderingAPITestWindow>> {
        let window = gtk::Window::new(gtk::WindowType::TopLevel).unwrap();
        let drawing_area = gtk::DrawingArea::new().unwrap();
        drawing_area.set_size_request(width, height);
        window.set_title("Cairo API test");
        window.add(&drawing_area);

        let wrapped_state = Rc::new(RefCell::new(RenderingAPITestWindowState::new(width, height)))
        ;

        let instance = RenderingAPITestWindow{window: window,
            drawing_area: drawing_area,
            state: wrapped_state.clone()
        };
        let wrapped_instance = Rc::new(RefCell::new(instance));

        let wrapped_state_for_draw = wrapped_state.clone();
        let wrapped_instance_for_draw = wrapped_instance.clone();
        wrapped_instance.borrow().drawing_area.connect_draw(move |widget, cairo_context| {
            wrapped_state_for_draw.borrow_mut().on_draw(cairo_context);

            wrapped_instance_for_draw.borrow().drawing_area.queue_draw();
            Inhibit(true)
        });

        let wrapped_state_for_sizealloc = wrapped_state.clone();
        wrapped_instance.borrow().drawing_area.connect_size_allocate(move |widget, rect| {
            wrapped_state_for_sizealloc.borrow_mut().on_size_allocate(rect);
        });

        wrapped_instance.borrow().window.show_all();

        return wrapped_instance;
    }

    fn exit_on_close(&self) {
        self.window.connect_delete_event(|_, _| {
            gtk::main_quit();
            Inhibit(true)
        });
    }
}


fn main() {
    gtk::init().unwrap_or_else(|_| panic!("Failed to initialize GTK."));
    println!("Major: {}, Minor: {}", gtk::get_major_version(), gtk::get_minor_version());

    let wrapped_window = RenderingAPITestWindow::new(800, 500);
    wrapped_window.borrow().exit_on_close();
    gtk::main();
}

While the above code works as required, I would like to find a better way for going forward; I would like to ask if anyone knows a better approach as the above complicates the programming process a fair bit, with the need to use Rc<RefCell<...>> and split structs to satisfy Rust's borrowing rules.

1条回答
仙女界的扛把子
2楼-- · 2019-04-24 01:37

Here's a working version that I came up with:

#![cfg_attr(not(feature = "gtk_3_10"), allow(unused_variables, unused_mut))]

extern crate gtk;
extern crate cairo;

use std::rc::Rc;
use std::cell::RefCell;
use gtk::traits::*;
use gtk::signal::Inhibit;
use cairo::{Context, RectangleInt};


struct RenderingAPITestWindow {
    window: gtk::Window,
    drawing_area: gtk::DrawingArea,
    state: RefCell<RenderingState>,
}

struct RenderingState {
    width: i32,
    height: i32,
}

impl RenderingAPITestWindow {
    fn new(width: i32, height: i32) -> Rc<RenderingAPITestWindow> {
        let window = gtk::Window::new(gtk::WindowType::TopLevel).unwrap();
        let drawing_area = gtk::DrawingArea::new().unwrap();
        drawing_area.set_size_request(width, height);
        window.set_title("Cairo API test");
        window.add(&drawing_area);

        let instance = Rc::new(RenderingAPITestWindow {
            window: window,
            drawing_area: drawing_area,
            state: RefCell::new(RenderingState {
                width: width,
                height: height,
            }),
        });

        {
            let instance2 = instance.clone();
            instance.drawing_area.connect_draw(move |widget, cairo_context| {
                instance2.state.borrow().on_draw(cairo_context);
                instance2.drawing_area.queue_draw();
                Inhibit(true)
            });
        }
        {
            let instance2 = instance.clone();
            instance.drawing_area.connect_size_allocate(move |widget, rect| {
                instance2.state.borrow_mut().on_size_allocate(rect);
            });
        }
        instance.window.show_all();
        instance
    }

    fn exit_on_close(&self) {
        self.window.connect_delete_event(|_, _| {
            gtk::main_quit();
            Inhibit(true)
        });
    }
}

impl RenderingState {
    fn on_draw(&self, cairo_ctx: Context) {
        cairo_ctx.save();
        cairo_ctx.move_to(50.0, (self.height as f64) * 0.5);
        cairo_ctx.set_font_size(18.0);
        cairo_ctx.show_text("The only curse they could afford to put on a tomb these days was 'Bugger Off'. --PTerry");
        cairo_ctx.restore();
    }

    fn on_size_allocate(&mut self, rect: &RectangleInt) {
        self.width = rect.width as i32;
        self.height = rect.height as i32;
    }
}

fn main() {
    gtk::init().unwrap_or_else(|_| panic!("Failed to initialize GTK."));
    println!("Major: {}, Minor: {}", gtk::get_major_version(), gtk::get_minor_version());

    let window = RenderingAPITestWindow::new(800, 500);
    window.exit_on_close();
    gtk::main();
}

I arrived at this through a few observations:

  • The instance is being shared across multiple closures for an undetermined amount of time. Rc is the right answer to that scenario because it provides shared ownership. Rc is very ergonomic to use; it works like any other pointer type.
  • The only part of instance that is actually mutated is your state. Since your instance is being shared, it cannot be borrowed mutably using the standard &mut pointer. Therefore, you must use interior mutability. This is what RefCell provides. Note though, that you only need to use RefCell on the state you're mutating. So this still separates out the state into a separate struct, but it works nicely IMO.
  • A possible modification to this code is to add #[derive(Clone, Copy)] to the definition of the RenderingState struct. Since it can be Copy (because all of its component types are Copy), you can use Cell instead of RefCell.
查看更多
登录 后发表回答