helgoboss / reaper-rs

Rust bindings for the REAPER C++ API
MIT License
78 stars 8 forks source link

Can't Insert Tracks From Main Thread or Spawned Thread #58

Closed jamsoft closed 1 year ago

jamsoft commented 2 years ago

I just found this project and have been looking at creating an extension with it.

I've not used Rust before so this is all very new territory. I just need some very simple functionality. Listen on a UDP port and add some tracks based on file paths passed. I've not really been able to get any of this working so far. During my tinkering I did see a fair number of threading warnings. Even with the move keyword, I never see any results from within the thread, despite calling the handle.join().unwrap(); Which as I understand it, should continue executing the thread to completion. It certainly delays Reaper startup but doesn't actually ever seem to write anything to the console or insert any tracks.

So i stripped it back to basics and decided to just try to add a track. But that's failing too.

use std::error::Error;
// use std::thread;
// use std::time::Duration;
use reaper_macros::reaper_extension_plugin;
use reaper_low::PluginContext;
use reaper_medium::ReaperSession;
use reaper_medium::TrackDefaultsBehavior;

#[reaper_extension_plugin]
fn plugin_main(context: PluginContext) -> Result<(), Box<dyn Error>> {

    let session = ReaperSession::load(context);
    session.reaper().show_console_msg("Hello world from reaper-rs medium-level API! Main");
    session.reaper().insert_track_at_index(1, TrackDefaultsBehavior::AddDefaultEnvAndFx);

    // let handle = thread::spawn(move || {        
    //     for i in 0..10 {
    //         // let session = ReaperSession::load(context);
    //         //session.reaper().insert_track_at_index(i, TrackDefaultsBehavior::AddDefaultEnvAndFx);
    //         thread::sleep(Duration::from_millis(500));
    //     }

    //     //session.reaper().show_console_msg("Hello world from reaper-rs medium-level API! Thread");
    //     //thread::sleep(Duration::from_millis(1000));        
    // });

    //thread::sleep(Duration::from_millis(1000));  
    //handle.join().unwrap();

    Ok(())
}

As far as I can tell from my reading, this thread code should be working. Is this even possible with Rust inside an extension? I managed it in C# at the weekend so I think this is possible.

The plan was to have two threads, one running UDP and one running the reaper session and then use channels to communicate. Does that sound like a reasonable approach? That being said I'm jumping the gun as I can't even get the basics working at the moment.

Many thanks for the work that's gone into this :)

TIA.

helgoboss commented 2 years ago

You can do everything in a Rust extension that you can do with a C++ extension. Listening to UDP and adding tracks in response is absolutely possible. I do that in ReaLearn as well with OSC.

The key issue with your code is in the threading, that's also why you get the warnings in the REAPER console. You are executing REAPER functions in a thread other than the main thread. reaper-rs immediately blocks this by panicking because continuing with it would almost certainly lead to REAPER crashing. Most REAPER functions are only usable from the main thread. That's not a limitation of reaper-rs but of REAPER itself.

The solution is to get yourself a place in the main thread event loop (by registering a hidden control surface and implementing the run method) and then using a channel to send from the UDP thread to the main thread. I show something similar (communication from audio thread to main thread) in this video: https://youtu.be/Y9ypd221FeE

As of why your simple example without threading doesn't work, not sure now. Doesn't it at least show the message? If not, maybe your file name doesn't start with reaper_? Maybe check the video, there I show how to build an extension from scratch.

jamsoft commented 2 years ago

Ahhh, thank you. Will investigate. I suspected as much regarding the threading.

The plugin is working and showing a message.

I did just try to start using the approach here where you mention storing in a static - https://github.com/helgoboss/reaper-rs/issues/49 but as soon as I try to call the method it's not found.

#[reaper_extension_plugin]
fn plugin_main(context: PluginContext) -> Result<(), Box<dyn Error>> {

    let session = ReaperSession::load(context);

    let sessionHigh = reaper_high::Reaper::get();

    session.reaper().show_console_msg("Hello world from reaper-rs medium-level API! Main");
    session.reaper().insert_track_at_index(1, TrackDefaultsBehavior::AddDefaultEnvAndFx);

    let handle = thread::spawn(|| {

        for i in 0..10 {

            let localsession = reaper_high::Reaper::get();
            localsession.medium_session().show_console_msg(""); //.insert_track_at_index(i, TrackDefaultsBehavior::AddDefaultEnvAndFx);

            println!("Loop 2 iteration: {}", i);
            thread::sleep(Duration::from_millis(500));
        }
    });    

    handle.join().unwrap();

    Ok(())
}

It now can't even see show_console_msg method. Anyway, I should just go watch your video. Thank you.

jamsoft commented 2 years ago

I've even stripped it right back to this:

#[reaper_extension_plugin]
fn plugin_main(context: PluginContext) -> Result<(), Box<dyn Error>> {
    let session = ReaperSession::load(context);
    session.reaper().show_console_msg("Hello world from reaper-rs medium-level API! Main");
    session.reaper().insert_track_at_index(1, TrackDefaultsBehavior::AddDefaultEnvAndFx);
    Ok(())
}

Writes the console message at start, doesn't add a track.

Which is odd I would expect this to work. I wrote a C++ and C# plugins over the weekend and both worked doing this in initialisation hook. Hmm ...

jamsoft commented 2 years ago

Whoa! 3.5 hours! :) Thank you!

jamsoft commented 2 years ago

Initialising like this doesn't add the track either, but it does print the message. When reaper starts there is a project already open so I think this should be working.

#[reaper_extension_plugin(name="Somename", support_email_address="some@example.com")]
//fn plugin_main(_context: PluginContext) -> Result<(), Box<dyn Error>> {
fn plugin_main() -> Result<(), Box<dyn Error>> {    

    // let session = ReaperSession::load(context);
    // session.reaper().show_console_msg("Hello world from reaper-rs medium-level API! Main");
    // session.reaper().insert_track_at_index(1, TrackDefaultsBehavior::AddDefaultEnvAndFx);

    let reaper = Reaper::get().medium_reaper();
    reaper.show_console_msg("Hello world from reaper-rs medium-level API! Main");
    reaper.insert_track_at_index(1, TrackDefaultsBehavior::AddDefaultEnvAndFx);

    Ok(())
}
helgoboss commented 2 years ago

I can't try it now but maybe this doesn't work because it's too early. Could be that at the very early time the plug-in is loaded, tracks are not yet addable. Pretty sure it should work if you execute this later, e.g. as an action.

jamsoft commented 2 years ago

Yeah I must admit this crossed my mind too. It did work in the other plugins I wrote but then they aren't this plugin! :)

Hmm ...

jamsoft commented 2 years ago

Just going through your video. I started work on a C++ plugin over the weekend. I got it working but I was so far out of my comfort zone that I had zero confidence in long term management of it and adding more complex features.

I started looking at introducing a UDP library and immediately fell into the compiler hell just including the library.

Rust seems dreamlike in comparison but now I can't make it do anything! LOL.

helgoboss commented 2 years ago

Well, at least it prints a message ;) The track is not added, yes. But the reason is that you call insert_track_at_index too early. That also wouldn't work with a C++ extension.

Below code works. It registers an action. If you really want to add a track as soon as REAPER starts (though I don't think you want to do that), you need to defer execution of the code. You can do it by registering a timer or a hidden control surface.

Disclaimer: Using the high-level API is not recommended, so it's better to build the majority of the code using the medium-level API.

use reaper_high::{ActionKind, Reaper};
use reaper_macros::reaper_extension_plugin;
use reaper_medium::TrackDefaultsBehavior;
use std::error::Error;

#[reaper_extension_plugin(name = "Hello World", support_email_address = "support@example.org")]
fn plugin_main() -> Result<(), Box<dyn Error>> {
    let reaper = Reaper::get();
    reaper.show_console_msg("Hello from reaper-rs!\n");
    reaper.wake_up()?;
    Reaper::get().register_action(
        "MY_ACTION",
        "My action",
        || {
            reaper
                .medium_reaper()
                .insert_track_at_index(1, TrackDefaultsBehavior::AddDefaultEnvAndFx);
        },
        ActionKind::NotToggleable,
    );
    Ok(())
}
jamsoft commented 2 years ago

I've actually made a HUGE amount of progress today.

In fact, I'm 90% complete on the entire job.

UDP is up and running on a separate thread and polling for messages.

JSON messages come in, get deserialised (serde) into a little struct I created, then I have a sync_channel moving the data around between main thread and the UDP polling thread.

HUGE room for improving the code but it's working.

Huge respect to you for creating all this.

Only thing I'm a little hazy on at the moment is orchestrating responses back to the caller in cases where a response is required.

jamsoft commented 2 years ago

Ahhh ...

c_str!("Hello world from reaper-rs low-level API!").as_ptr()

RTFM ...

jamsoft commented 2 years ago

OK. Not having a C++ background - Rust Strings have officially utterly confused me.

I spotted this c_str!("Hello world from reaper-rs low-level API!").as_ptr().

But I couldn't seem to work out how to pull in your utility c_str. Then I found the macro crate and tried that. Same issues. Wow, strings are odd.

jamsoft commented 2 years ago

Wow. I'm actually really embarrassed to admit that despite getting threading and UDP working over a few hours, the hill I actually died on is what would be a trivial string operation in C#.

I've lost count of the number of permutations I've tried. I can't seem to get msg.data as String into the InsertMedia function.

fn run(&mut self) {
    while let Ok(msg) = self.receiver.try_recv() {
        Reaper::get().show_console_msg(msg.data);
        Reaper::get().medium_reaper().insert_track_at_index(1, TrackDefaultsBehavior::AddDefaultEnvAndFx);

        //let mut new_string: String = String::new();
        //new_string.push_str(move |msg: MyMessage| msg.data.as_str());

        //let c_str = CString::new(move |msg| new_string.as_str()).unwrap();
        //let c_world: *const c_char = c_str.as_ptr() as *const c_char;

        let owned = String::from(msg.data.as_str());
        let c_world: *const c_char = owned.as_ptr() as *const c_char;

        unsafe {
            let text: &'static ReaperStr = reaper_str!("Hello REAPER!");
            Reaper::get().medium_reaper().low().InsertMedia(c_world, 1);
        }
    }
}

The error is borrow of moved item on the let owned = String::from(msg.data.as_str()); line.

I've been reading up on it and I've read about clone() and also how it's a bad idea! Any pointers would be fabulous. I must admit I thought this process of having sender / receiver did move ownership?

helgoboss commented 2 years ago

Mmh, to be honest, I think it's pretty hard to learn Rust and reaper-rs at the same time. Even more if you use the low-level API, because that's like approaching Rust from the "wrong" side. It means you need to learn about FFI, which in turn means you have to leave the beautiful world of idiomatic Rust.

Strings in Rust were a revelation to me ... they make everything explicit, which I would expect from a low-level language.

jamsoft commented 2 years ago

Yeah, I have to admit I haven't felt this noob in years. So I've done the same plugin in 3 langs over the last 3 days and Rust is the one I failed! And its the version I most want to use! haha.

helgoboss commented 2 years ago

That particular issue with "borrow of moved item" is because you are passing ownership of msg.data to show_console_msg() further up. You should pass a reference there so msg.data doesn't get consumed.

Welcome to the world of non-GCed languages :)

jamsoft commented 2 years ago

Yeah I jus figured it out myself! 5 seconds ago!! The penny dropped ... I deleted the line and Boom! And you've just posted the same!

jamsoft commented 2 years ago

I'll sleep better this evening ... with a grin! Thanks chap.