PolyMeilex / rfd

Rusty File Dialog
MIT License
609 stars 70 forks source link

crashes on macOS with iced #3

Closed fenhl closed 3 years ago

fenhl commented 3 years ago

I tried to use AsyncFileDialog in an iced app on macOS, but when trying to use the dialog, the app crashes, printing:

fatal runtime error: Rust cannot catch foreign exceptions

This happens on 0.1.2 as well as master.

PolyMeilex commented 3 years ago

That's probably something threading related, I'll see what can be done in this regard.

Some questions:

  1. Does it crash on creation or later?
  2. If it crashes right away, on which tread does it happen? Like... On which thread pick_file fn is called? (you can run simple println!("{:?}", std::thread::current().id()); right before calling rfd to check this)
  3. Simple example would also be helpful.

I was pretty sure that iced always calls update on the main thread as it is direct result of winit event loop callback being called. But maybe I was wrong in this regard. (Judging by examples in iced repo, it is true)

PolyMeilex commented 3 years ago

Is this how you use it in iced?

fn update(&mut self, message: Message) -> Command<Message> {
    match message {
        Message::Open(_) => {
            return Command::perform(
                rfd::AsyncFileDialog::new().pick_file(),
                |ret| Message::Res(ret.map(|f| f.path().into())),
            );
        }
        Message::Res(path) => {
            println!("{:?}", path);
        }
    };
    Command::none()
}

And one more question: Kinda related to previous question, when does the dialogue crashes? Does the dialogue open at all? Does it crash after selecting a file? After closing? etc. If it crashes after close then I probably know what's the problem, otherwise it will be quite tricky one to figure out.

fenhl commented 3 years ago

Here's a minimal example, with the async code being called from a regular Command, as is the usual case in iced apps.

Cargo.toml:

[package]
name = "rfd-crash-example"
version = "0.1.0"
authors = ["Fenhl <fenhl@fenhl.net>"]
edition = "2018"

[dependencies.iced]
git = "https://github.com/hecrj/iced"
rev = "12c0c18d662d2b817b559b94c71d18e122c76990"

[dependencies.rfd]
git = "https://github.com/PolyMeilex/rfd"
rev = "ee4e55f8d2cb0a307cea650a05deaf9d4aa142fc"

main.rs:

use iced::{
    Application,
    Command,
    Element,
    Settings,
    widget::{
        Text,
        button::{
            self,
            Button,
        },
    },
};

#[derive(Debug, Clone)]
enum Message {
    Click,
    Nop,
}

#[derive(Default)]
struct App {
    btn: button::State,
}

impl Application for App {
    type Executor = iced::executor::Default;
    type Message = Message;
    type Flags = ();

    fn new((): ()) -> (App, Command<Message>) { (App::default(), Command::none()) }
    fn title(&self) -> String { format!("rfd crash example") }

    fn update(&mut self, msg: Message) -> Command<Message> {
        match msg {
            Message::Click => async {
                let dialog = rfd::AsyncFileDialog::new();
                println!("{:?}", std::thread::current().id());
                let _ = dialog.pick_file().await;
                Message::Nop
            }.into(),
            Message::Nop => Command::none(),
        }
    }

    fn view(&mut self) -> Element<'_, Message> {
        Button::new(&mut self.btn, Text::new("Pick File"))
            .on_press(Message::Click)
            .into()
    }
}

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

The file dialog window never appears, and the output is:

ThreadId(6)
fatal runtime error: Rust cannot catch foreign exceptions

So the crash happens in pick_file, not new.

PolyMeilex commented 3 years ago

My bad, I should have documented it better, there is no way to spawn dialogs from other threads in Mac OS, you have to spawn it on the main thread and await on it in the threaded executor. That's how Mac OS system API is designed nothing that I can do about it. So in this case dialog should be created in the update, and returned future should be moved into the async block where it can be awaited.

It is shown in async example

My solution:

PolyMeilex commented 3 years ago

@fenhl Does it solve your problem? If so, I'll proceed to add some additional checks and docs for this and close the issue

fenhl commented 3 years ago

It fixes the minimal example above, I'll check if it works in the actual app I'm writing too.

PolyMeilex commented 3 years ago

Ok, thanks for the info.

I found a way to work around it, so if everything goes well, soon it will be possible to spawn async dialogues anywhere.

But it is only possible in windowed applications (those have internal macOS event loop, that I can use) So I don't want to make it a defacto/recommended way of doing things, as I want the API to be the same for all use-cases.

I will try to integrate it anyway, just in case main-thread limitation is unacceptable for someone (for example, there can be a GUI framework out there that does not allow users to control on which thread their code is executed)

fenhl commented 3 years ago

Seems to be working fine now.

PolyMeilex commented 3 years ago

Nice, so for now I will just add a helpful error message, and proper documentation of this behaviour somewhere.

PolyMeilex commented 3 years ago

Version 0.2.0 released, RFD can now handle dialog spawn in other threads in iced, and in any other framework that uses winit or SDL2, and friendly panic message was added in case someone is trying to do this in unsupported (non-windowed) env.