Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Iced sample code keeps crashing at every lib reload #25

Open
lucatrv opened this issue Nov 29, 2022 · 5 comments
Open

Iced sample code keeps crashing at every lib reload #25

lucatrv opened this issue Nov 29, 2022 · 5 comments

Comments

@lucatrv
Copy link
Contributor

lucatrv commented Nov 29, 2022

I've been experimenting with the following Iced sample code, but it keeps crashing at every lib reload. I can't figure out if I'm mistaking something or if it's due to a hot-lib-reloader-rs' issue, can you please help me out?

I tested the attached code both on Windows 10 and Arch Linux, with rustc version 1.65.0 and 1.67.0-nightly, and I attempted also using cargo run --features iced/glow with no luck.

commands:
  bin: |
    cargo run
  lib: |
      cargo watch -w lib -x 'build -p lib'

bin

[workspace]
members = ["lib"]

[package]
name = "bin"
version = "0.1.0"
edition = "2021"

[dependencies]
hot-lib-reloader = "0.6.4"
iced = "0.5.2"
lib = { path = "lib" }
#[hot_lib_reloader::hot_module(dylib = "lib")]
mod hot_lib {
    use iced::{Element, Theme};
    pub use lib::*;
    hot_functions_from_file!("lib/src/lib.rs");
}

use hot_lib::*;
use iced::{Element, Sandbox, Settings, Theme};

fn main() -> iced::Result {
    App::run(Settings::default())
}

#[derive(Debug, Default)]
struct App {
    state: State,
}

impl Sandbox for App {
    type Message = Message;

    fn new() -> Self {
        Self::default()
    }

    fn title(&self) -> String {
        title()
    }

    fn update(&mut self, message: Message) {
        update(&mut self.state, message)
    }

    fn view(&self) -> Element<Message> {
        view(&self.state)
    }

    fn theme(&self) -> Theme {
        theme(&self.state)
    }
}

lib

[package]
name = "lib"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["rlib", "dylib"]

[dependencies]
iced = "0.5.2"
use iced::widget::{
    button, column, horizontal_rule, horizontal_space, radio, row, text, vertical_space,
};
use iced::{Alignment, Element, Length, Theme};

#[derive(Debug)]
pub struct State {
    text_size: u16,
    theme: Theme,
}

impl Default for State {
    fn default() -> Self {
        Self {
            text_size: 100,
            theme: Theme::default(),
        }
    }
}

#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum ThemeType {
    Light,
    Dark,
}

#[derive(Debug, Clone)]
pub enum Message {
    ThemeChanged(ThemeType),
    IncrementTextSizePressed,
    DecrementTextSizePressed,
}

#[no_mangle]
pub fn title() -> String {
    String::from("Hello World - HotIce")
}

#[no_mangle]
pub fn update(state: &mut State, message: Message) {
    let size_change = 10;
    match message {
        Message::ThemeChanged(theme) => {
            state.theme = match theme {
                ThemeType::Light => Theme::Light,
                ThemeType::Dark => Theme::Dark,
            }
        }
        Message::IncrementTextSizePressed => {
            if state.text_size <= 200 - size_change {
                state.text_size += size_change;
            }
        }
        Message::DecrementTextSizePressed => {
            if state.text_size >= size_change {
                state.text_size -= size_change;
            }
        }
    }
}

#[no_mangle]
pub fn view(state: &State) -> Element<Message> {
    let choose_theme = [ThemeType::Light, ThemeType::Dark].into_iter().fold(
        column![text("Choose a theme:")].spacing(10),
        |column, theme| {
            column.push(radio(
                format!("{:?}", theme),
                theme,
                Some(match state.theme {
                    Theme::Light => ThemeType::Light,
                    Theme::Dark => ThemeType::Dark,
                    Theme::Custom { .. } => panic!(),
                }),
                Message::ThemeChanged,
            ))
        },
    );

    column![
        row![choose_theme, horizontal_space(Length::Fill)],
        horizontal_rule(20),
        vertical_space(Length::Fill),
        text("zHello, world!").size(state.text_size),
        vertical_space(Length::Fill),
        row![
            button("Increment Text Size").on_press(Message::IncrementTextSizePressed),
            button("Decrement Text Size").on_press(Message::DecrementTextSizePressed),
        ]
        .spacing(20)
    ]
    .spacing(20)
    .padding(20)
    .align_items(Alignment::Center)
    .into()
}

#[no_mangle]
pub fn theme(state: &State) -> Theme {
    state.theme.clone()
}
@rksm
Copy link
Owner

rksm commented Dec 17, 2022

Hi, thanks or the report. I tested it on macos initially and can confirm that it still works there. I'll give it a try with Linux.

@rksm
Copy link
Owner

rksm commented Dec 17, 2022

Yeah on Ubuntu I get a crash, too. Seems that the issue is in iced_native::user_interface::UserInterface::update, the reload might mess with the data that gets ManuallyDroped there. I'll take a look over the holidays if this can be solved with either a custom command or a manual run loop.

@rksm
Copy link
Owner

rksm commented Dec 30, 2022

Sorry, didn't get enough time to fix this. Happy to accept contributions.

Not quite sure why this works in macos but not under Windows/Linux.

The issue manifests as a segmentation fault when dealing with the root and overlay Elements at UserInterface::update when wrapping those with a ManuallyDrop. I think the ManuallyDrop isn't the root cause, something about the rendering state might become invalid through the reload.

For now I'll add a note to the example and maybe someone with deeper knowledge of iced than me can look at this?!

rksm added a commit that referenced this issue Dec 30, 2022
@Imberflur
Copy link

Imberflur commented Apr 4, 2023

Hi, I investigated this a bit.

Here is what happens:

  • view (in the dynamically loaded lib) returns a Element<Message> (internally this is a Widget trait object).
  • The internals of Application::run will call Application::view and then hold onto the returned Element<Message> for a while (e.g. view is only called again when it determines that the interface is outdated, see the calls to build_user_interface in https://github.com/iced-rs/iced/blob/0.5/winit/src/application.rs#L235).
  • So whenever UserInterface::update runs it will use this element without necessarily calling view again.
  • When the lib is reloaded, the old version is unloaded. However, the trait object in Element<Message> will contain a vtable pointer pointing into old unloaded version.
  • Then, in UserInterface::update it tries to call the overlay method on the trait object, this tries to read the unloaded vtable and segfaults.

Potential solutions:

  • Just don't unload the lib, you can sort of hack this in by adding a thread local with a drop impl in the dynamically loaded lib (and make sure to initialize it). Although, it seem like it would be useful for hot-lib-reloader to offer an option to not unload old versions. The only downside (afaik) is leaking the lib when you need to reload but that seems worth it to have hotreloading work.
// e.g. add this in the view method in the dynamic lib
thread_local! {
    static KEEP_LOADED: Box<u8> = Box::new(0);
}
KEEP_LOADED.with(|_| {}); // initialize
  • There might be some way to listen for reload events and then convince iced that the interface is outdated but I don't really know how viable this is. It seems complex and prone to errors and there could be other issues!

(note: this is all on the iced version used above (0.5.2) so some details could have changed)

(edit: I would guess that there might already be something preventing the lib from being unloaded on macos, but I can't test that myself)

@rksm
Copy link
Owner

rksm commented Apr 12, 2023

Thanks! Gonna take a look at this soon.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants