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

Allow dart messages to be easily handled as bevy events #349

Merged
merged 10 commits into from
Jun 30, 2024

Conversation

Deep-co-de
Copy link
Contributor

@Deep-co-de Deep-co-de commented Jun 5, 2024

Changes

I have added a feature bevy, and added bevy as an optional dependency for this feature. (I'm not sure if I did everything right in Cargo.toml).
I also added the derive macro optional for DartSignal. This would make it possible to use ecs in rust with bevy as follows and use the events:

async fn main() {    
    let mut app = App::new()
        .add_plugins(MinimalPlugins)
        // This function is defined like mentioned here: https://github.com/bevyengine/bevy/issues/8983#issuecomment-1623432049
        .add_event_channel(SmallText::get_dart_signal_receiver())
        .add_systems(Update, handle_smalltext)
        .run()
}

fn receive_smalltext(
    mut smalltexts: EventReader<SmallText>
) {
    for smalltext in smalltexts.read() {
        println!("{}", smalltext.text);
    }
}

With this feature users would still need to manually add these two lines to the generated message files:

use bevy_ecs::event::Event;
#[derive(Event)]
pub struct SmallText {
...

I didn't know how to accomplish this automatically with prost-build.

I'm not sure if this option is in your favour at all. I thought it would make these two great projects wonderfully combinable and open up new possibilities.

solution: add a bevy feature and use derive macro for event
@temeddix
Copy link
Member

temeddix commented Jun 6, 2024

Hi @Deep-co-de , thanks for the idea :)

I'm not sure yet that Bevy, as a game engine, will benefit from being used together with Rinf. Perhaps it's because of my little knowledge about Bevy.

Could you elaborate how Rinf will be used in combination with Bevy? Would Rinf be showing the output from Bevy on the screen? I wonder if Bevy doesn't have its own GUI solution.

@Deep-co-de
Copy link
Contributor Author

Thank you very much for your answer.
Yes, bevy is a game engine, but I wanted to use bevy's ecs for state management. That's why I only used the bevy_ecs crate.
One advantage of entity component systems is the modularity, so different concerns can be easily separated and developed independently. This makes it easy to implement clean architecture by using one system for each concern in bevy. This can then be exchanged at a later date without affecting the others.
I had imagined that stateful widgets could be partially replaced by stateless widgets, as the state is influenced by messages (-> StreamBuilder). This would make the structure more of a reactive one.
I hope I was able to make my idea clear, please feel free to ask if you have any questions.
I don't have that much time next week, but I could program a small example application to illustrate this.

@temeddix
Copy link
Member

temeddix commented Jun 7, 2024

I see. I think this bevy feature can make it into Rinf. I don't think a small app would be needed, but may I ask you for some example code snippets here? I (naively) assume that Bevy is event-driven, but am still curious about how it is used in async tokio environment.

/// This is a mutable cell type that can be shared across threads.
pub type SharedCell<T> = OnceLock<Mutex<RefCell<Option<T>>>>;

/// This contains a message from Dart.
/// Optionally, a custom binary called `binary` can also be included.
/// This type is generic, and the message
/// can be of any type declared in Protobuf.
/// If the bevy feature is used, every message can be received as an event in bevy.
Copy link
Member

@temeddix temeddix Jun 7, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A small request here, do you mind change the sentence to:

If the bevy feature is used, every message can be received as an event in Bevy.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just did it. I hope everything fits.

@Deep-co-de
Copy link
Contributor Author

Deep-co-de commented Jun 7, 2024

Please find below a few code snippets:

lib.rs

mod flutter_events;
...
async fn main() {    
    let mut app = App::new()
        .add_plugins(MinimalPlugins)
        // This function is defined like mentioned here: https://github.com/bevyengine/bevy/issues/8983#issuecomment-1623432049
        .add_event_channel(SmallText::get_dart_signal_receiver())
        .add_systems(Update, handle_smalltext)

        // for purpose see below
        .add_systems(Update, (
            initialize_database_connection
                .run_if(in_state(MyAppState::LoadingScreen)),
           ))
        .run()
}

fn receive_smalltext(
    mut smalltexts: EventReader<SmallText>
) {
    for smalltext in smalltexts.read() {
        println!("{}", smalltext.text);
    }
}

flutter_events.rs

...
#[derive(Resource, Deref, DerefMut)]
struct ChannelReceiver<T>(Mutex<UnboundedReceiver<DartSignal<T>>>);
...
// This method is used to add a dart signa as event for usage in bevy
fn add_event_channel<T: Event>(&mut self, receiver: UnboundedReceiver<DartSignal<T>>) -> &mut Self {
        assert!(
            !self.world.contains_resource::<ChannelReceiver<T>>(),
            "this event channel is already initialized",
        );

        self.add_event::<T>();
        self.insert_resource(ChannelReceiver(Mutex::new(receiver)));
        println!("ChannelReceiver added");
        self.add_systems(PreUpdate,
            channel_to_event::<T>
                .after(event_update_system::<T>),
        );
        self
    }
...

somewhere in rust: (global state, or even for each page)

#[derive(States, Default, Debug, Clone, PartialEq, Eq, Hash)]
enum MyAppState {
    LoadingScreen,
    #[default]
    SimplePage,
    SettingsPage,
}

main.dart

...
StreamBuilder<GlobalState>(
          stream: GlobalState.rustSignalStream,
          builder: (BuildContext context, AsyncSnapshot<GlobalState> snapshot) {            
              if (snapshot.hasData) {
                // e.g. GlobalState is a message from rust code
                // with field state representing percentage of 
                // initialized backend procedures
                if (snapshot.data.state != 1.0) {
                  return CircularProgressIndicator(value: snapshot.data.state);
                } else {
                  return Text("Finished Loading");
                }
              } else {
                return CircularProgressIndicator();
              }
            
          },
        )
...

There would be a system in rust that would be loaded at startup, which could then repeatedly increase the value of the CircularprogressIndicator:
rust:

fn setup_tasks() {
    GlobalState{state: 0.1}.send_signal_to_dart();
    ...
    GlobalState{state: 0.2}.send_signal_to_dart();
}

I have just realised that if the generated rust code of the messages were not just #[derive(Event)] but #[derive(Resource)], then you could save the messages on the rust side in the state.
But here I don't know much about the generation of the automatic code (if you should want custom derives as a second feature from the dart side at all)

@temeddix
Copy link
Member

temeddix commented Jun 7, 2024

Thanks for the details, I think I now understand how things work(maybe).

I did come up with one concern: Rinf spawns its own async multithreaded tokio runtime. It looks like Bevy has its own event loop(or runtime). As a consequence there will be tokio threads from Rinf(which is doing nothing), alongside Bevy threads. Wouldn't this be inefficient? Since each of the thread has to make its stack inside memory.

Maybe we can discuss a little further about the combination of tokio system and Bevy system

@Deep-co-de
Copy link
Contributor Author

Deep-co-de commented Jun 7, 2024

I see. I would try one more thing, after looking at the generated code again and the output of rinf::write_interface!();, it should be possible to implement the functions pub extern ‘C’ fn start_rust_logic_extern() { and . .stop.. without tokio threads but with bevy, whereby type SignalHandlers = OnceLock<Mutex<HashMap<i32, Box<dyn Fn(Vec<u8>, Vec<u8>) + Send>>>>; would then be saved as a resource in bevy. This would then require App::new(), but would itself have to be saved as OnceLock<>. So that when using the feature bevy rinf::write_interface!(); generates slightly different code. I will try again and post the snippet here.
Nevertheless, tokio would be used, only managed by bevy.

@temeddix
Copy link
Member

temeddix commented Jun 7, 2024

Yeah, rinf::write_interface! is the point.

FYI, the codebase will undergo some refactoring for one or more weeks, so I recommend diving in a bit later :)

@Deep-co-de
Copy link
Contributor Author

All right, thanks for the advice!

@Deep-co-de
Copy link
Contributor Author

I just wanted to ask how far you've got with refactoring the code?

@temeddix
Copy link
Member

The big changes are almost done, but there would be a bit more tweaking about building the tokio runtime and spawning the main function.

I would say it will take a week at most :)

rust_crate/src/interface.rs Outdated Show resolved Hide resolved
@temeddix
Copy link
Member

temeddix commented Jun 30, 2024

Also, could you add the following information in the documentation/docs/configuration.md docs?

@Deep-co-de
Copy link
Contributor Author

Deep-co-de commented Jun 30, 2024

I now have to make a small adjustment to enable the exit of the app without Ctrl+C. Since bevy runs in a loop that is not interrupted by stop_rust_logic_extern(), if bevy is configured in a kind of EventLoop.
For this, the following would have to exist in a location accessible for both native/hub/src/lib.rs and rinf/rust_crate/src/interface_os.rs: pub static ASYNC_WORLD: OnceLock<AsyncWorld> = OnceLock::new();. I would have placed it in interface.rs, as it would also be necessary for the target web, as far as I know.
But I wonder to what extent stop_rust_logic_extern() is/will be implemented for the web, or whether it would then only be implemented for the feature bevy.

@Deep-co-de
Copy link
Contributor Author

Deep-co-de commented Jun 30, 2024

It would look like this:

#[no_mangle]
pub extern "C" fn stop_rust_logic_extern() {
    #[cfg(feature = "bevy")]
    {
        match crate::ASYNC_WORLD.get() {
            Some(world) => {
                use bevy_app::AppExit;
                futures::executor::block_on(async {world.send_event(AppExit::default()).await;});
            },
            None => {}
        }
    }
    let sender_lock = SHUTDOWN_SENDER.get_or_init(move || ThreadLocal::new(|| RefCell::new(None)));
    let sender_option = sender_lock.with(|cell| cell.take());
    if let Some(shutdown_sender) = sender_option {
        // Dropping the sender tells the tokio runtime to stop running.
        // Also, it blocks the main thread until
        // it gets the report that tokio shutdown is dropped.
        drop(shutdown_sender);
    }
}

@temeddix
Copy link
Member

temeddix commented Jun 30, 2024

stop_rust_logic_extern() does nothing on the web, because it's not possible to detect closing of browser tabs.

However, you can utilize the shutdown logic of Rinf to properly exit ASYNC_WORLD, which is why I don't think the additional changes in stop_rust_logic_extern is needed.

If you take a look at this docs section above, you can understand how to properly run your finalization logic(as well as shutting down the whole tokio runtime before closing the app. )

Also, starting from Rinf 6.13 the default tokio runtime is single-threaded by default, so it wouldn't be too inefficient even if you have a separate 'Bevy world' threads now.

@Deep-co-de
Copy link
Contributor Author

I have found a solution that does not require any further adjustments. You can find a small example that has not yet been tidied up here: mwp Basically, bevy is in a loop that keeps synchronising with the main thread and is thus also terminated during shutdown, which was not the case before.

let mut app = App::new();
// do setup stuff
loop {
    // run app
    app.update();
    std::thread::sleep(Duration::from_millis(100)); // or slower for less performance critical apps
    tokio::task::yield_now().await;
}

I think everything should work this way, I would code a few examples next anyway, but would consider this feature finished.

@temeddix temeddix merged commit ca89648 into cunarist:main Jun 30, 2024
22 checks passed
@temeddix
Copy link
Member

Thank you very much for your contribution :)

@Deep-co-de
Copy link
Contributor Author

You're welcome

@Deep-co-de Deep-co-de deleted the bevy_feature branch June 30, 2024 17:10
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

Successfully merging this pull request may close these issues.

2 participants