helgoboss / reaper-rs

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

plugin_register_add_api_and_def() doesn't seem to make the method available via ReaScript #43

Closed GavinRay97 closed 3 years ago

GavinRay97 commented 3 years ago

I've tried running the example to register a ReaScript function from the tests + internal docs, but I'm unable to get this to appear:

Is it meant to only be usable from within the Rust code by getting a direct pointer to it, or is it supposed to show up in the ReaScript docs and IDE signatures that appear, like this?

image

use std::os::raw::{c_int, c_void};
use std::ptr::null_mut;
use std::error::Error;

use reaper_high::Reaper;
use reaper_low::{PluginContext, Swell};
use reaper_macros::reaper_extension_plugin;
use reaper_medium::ReaperSession;

use c_str_macro::c_str;

extern "C" fn hey_there() {
    Reaper::get().show_console_msg("Hey there!\n");
}

unsafe extern "C" fn hey_there_vararg(_arglist: *mut *mut c_void, _numparms: c_int) -> *mut c_void {
    hey_there();
    null_mut()
}

#[reaper_extension_plugin]
#[allow(clippy::unnecessary_wraps)]
fn plugin_main(context: PluginContext) -> Result<(), Box<dyn Error>> {
    let mut session = ReaperSession::load(context);

    session
        .reaper()
        .show_console_msg("Hello world from reaper-rs medium-level API!");

    Reaper::load(context).setup();
    let medium_reaper = Reaper::get().medium_reaper();

    // Low-level REAPER
    reaper_low::Reaper::make_available_globally(*medium_reaper.low());
    reaper_low::Reaper::make_available_globally(*medium_reaper.low());
    let low = reaper_low::Reaper::get();

    println!("reaper_low::Reaper {:?}", &low);
    unsafe {
        low.ShowConsoleMsg(c_str!("- Hello from low-level API\n").as_ptr());
    }

    // Low-level SWELL
    let swell = Swell::load(*medium_reaper.low().plugin_context());
    println!("reaper_low::Swell {:?}", &swell);
    Swell::make_available_globally(swell);
    let _ = Swell::get();

    // Medium-level REAPER
    reaper_medium::Reaper::make_available_globally(medium_reaper.clone());
    reaper_medium::Reaper::make_available_globally(medium_reaper.clone());

    medium_reaper.show_console_msg("- Hello from medium-level API\n");

    unsafe {
        session
            .plugin_register_add_api_and_def(
                "ReaperRs_HeyThere",
                hey_there as _,
                hey_there_vararg,
                "void",
                "",
                "",
                "Just says hey there.",
            ).map_err(|err| {
                session
                    .reaper()
                    .show_console_msg(format!("Got error registering function: {}", err));
                err
            })?;
    }

    Ok(())
}
helgoboss commented 3 years ago

Haha, out of all functions in the reaper-medium crate you picked exactly the one which is far from being polished and ready ;) I thought once I'm going to need it and then didn't, so I stopped working on it.

However, in your case the problem is another one: ReaperSession needs to stick around. It uses RAII pattern to automatically clean up everything (including your plugin_register registrations) when it goes out of scope. And in your code it does go out of scope as soon as your plugin_main function ends. You need some static to store it in.

The easiest way to do this is to (you didn't hear that from me, okay? ;)) to use the high-level API to bootstrap your plug-in. Just a minimum of it and after that only access reaper-medium.

#[reaper_extension_plugin(
    name = "reaper-rs test extension plug-in",
    support_email_address = "info@helgoboss.org"
)]
fn main() -> Result<(), Box<dyn Error>> {
    let reaper = Reaper::get();
    reaper.medium_session().plugin_register_add_api_and_def(...);
    reaper.medium_reaper().blablabla();
    Ok(())
}

test/test-extension-plugin is a working example.

GavinRay97 commented 3 years ago

You are a gem of a human, thank you once again Benjamin ❤️

helgoboss commented 3 years ago

:) Just for understanding: In the medium-level API, Reaper contains all the stateless functions and functions pointers. This can be freely cloned and is basically the equivalent of the raw C++ API just in a bit more idiomatic Rust. ReaperSession is an addition that releases you from the burden to find a place in memory for all of the things that you want to register (because REAPER itself won't keep these things for you). That's why it needs to stick around.

GavinRay97 commented 3 years ago

Bingo! Thank you again!

Now to figure out whether this ImGui Rust <-> ReaScript <-> ImGui C++ thing will even work 😅

image

GavinRay97 commented 3 years ago

If I could toss ONE last question out there, feel free to answer or not, but this mut* mut* c_void thing:

unsafe extern "C" fn imgui_rs_test_vararg(
    _arglist: *mut *mut c_void,
    _numparms: c_int,
) -> *mut c_void {

}

@cfillion was kind enough to divulge that the arglist stores:

Plus apparently another param with memory addresses it'd like results written to.

How do I go about translating an unknown _"mutable-pointer-to-a-mutable-pointer-to-a c_void, that returns a mutable pointer to a cvoid" into everyday Rust 😅?

I would imagine I could usually just write a Struct and write a serializer/deserializer but with this vararg stuff I don't think it can work that way?

helgoboss commented 3 years ago

Why not use https://github.com/imgui-rs/imgui-rs?

helgoboss commented 3 years ago

Ah, sorry. You already use that. Mmh, you would need to use structs that have the same layout as what's expected in the arg list. How these structs need to look exactly, I don't know. But basically the same way they would look in C?

GavinRay97 commented 3 years ago

It's a little bit complicated -- basically I am trying to see what the experience is like to integrate third-party widgets for cfillion's new "ReaImGui" library which is ImGui for Reaper + ReaScript bindings:

https://github.com/cfillion/reaimgui

However, to manage the REAPER lifecycle and stuff, he has some custom context which wraps the native ImGuiContext*:

https://github.com/cfillion/reaimgui/blob/e490ca4628b2bbbf3c97b5b7f6ee79e974036958/src/context.cpp

I have some prebuilt widgets in C++ that I ported to it through mostly copy paste (spend the last 2 days learning C++ to get through this haha).

So it's probably pointless -- even Christian has said it's better to just ReaImGui's library than try to integrate prebuilt stuff.

But it sounded like it could be fun and a learning experience.

Also I think exposing ReaScript functions from Rust could be really useful in general, so I was glad to have learned more about this.

This is my current draft idea of what I think I am supposed to do 🤔

extern "C" fn imgui_rs_test(ctx: *mut c_void) {
    let ImGuiTextPtr = Reaper::get()
        .medium_reaper()
        .plugin_context()
        .get_func("ImGui_Text");
    assert!(!ImGuiTextPtr.is_null());

    let ImGuiText: Option<extern "C" fn(ctx: *mut c_void)> =
        unsafe { std::mem::transmute(ImGuiTextPtr) };

    let ImGuiText = ImGuiText.ok_or("Couldn't restore function from ReaScript API name and pointer. Make sure it exists and has been loaded.");
    match ImGuiText {
        Ok(method) => method(ctx),
        _ => Reaper::get().medium_reaper().show_console_msg("No bueno"),
    }
}

unsafe extern "C" fn imgui_rs_test_vararg(
    _arglist: *mut *mut c_void,
    _numparms: c_int,
) -> *mut c_void {
    let args = std::slice::from_raw_parts(_arglist, _numparms.try_into().unwrap());
    imgui_rs_test(args[0]);
    null_mut()
}

Where args[0] and ctx here are being used because of:

-- Everything is passed "context", which is an instance of ReaImGui's custom wrapper ImGui_Context pointer
local ctx = reaper.ImGui_CreateContext("My script", 620, 500)
reaper.ImGui_Text(ctx, "Hello!")
GavinRay97 commented 3 years ago

Woo I, got it!!

It seemed the answer to turning *mut *mut c_void array of arguments into something usable lied in:

std::slice::from_raw_parts(_arglist, _numparms.try_into().unwrap())

I think I will make another issue though -- I was about to give up because of the args syntax. It must be EXACTLY the C syntax, with commas and no spaces:

image

Tried many combinations and it never came out quite right. After reading reaper_functions.h and the generated docs + your implementation closer, I took a guess on this.

Will try to submit a PR (if I am capable of writing it) to surface a better interface/docs for this =) Also, I now am curious what can be done if I try to somehow expose and access the direct/underlying ImGuiContext pointer from C++ DLL in Rust DLL. cfillion says it's a terrible idea.

With your help, this was possible -- how amazing! ❤️ ❤️

image

use std::{
    borrow::Cow,
    convert::TryInto,
    os::raw::{c_int, c_void},
};
use std::{ffi::CStr, ptr::null_mut};

use std::error::Error;

use reaper_high::Reaper;
use reaper_macros::reaper_extension_plugin;

extern "C" fn imgui_rs_test(ctx: *mut c_void, text: *mut c_void) {
    let ImGuiTextPtr = Reaper::get()
        .medium_reaper()
        .plugin_context()
        .get_func("ImGui_Text");
    assert!(!ImGuiTextPtr.is_null());

    let ImGuiText: Option<extern "C" fn(ctx: *mut c_void, text: *mut c_void)> =
        unsafe { std::mem::transmute(ImGuiTextPtr) };

    let ImGuiText = ImGuiText.ok_or("Couldn't restore function from ReaScript API name and pointer. Make sure it exists and has been loaded.");
    match ImGuiText {
        Ok(method) => method(ctx, text),
        _ => Reaper::get().medium_reaper().show_console_msg("No bueno"),
    }
}

unsafe extern "C" fn imgui_rs_test_vararg(
    _arglist: *mut *mut c_void,
    _numparms: c_int,
) -> *mut c_void {
    let args = std::slice::from_raw_parts(_arglist, _numparms.try_into().unwrap());
    imgui_rs_test(args[0], args[1]);
    null_mut()
}

#[reaper_extension_plugin(
    name = "reaper-rs test extension plug-in",
    support_email_address = "ray.gavin97@gmail.com"
)]
fn main() -> Result<(), Box<dyn Error>> {
    let reaper = Reaper::get();

    unsafe {
        reaper
            .medium_session()
            .plugin_register_add_api_and_def(
                "ImguiRS_Test",
                imgui_rs_test as _,
                imgui_rs_test_vararg,
                "bool",
                "ImGui_Context*,const char*",
                "ctx,input",
                "A test REAPER function for calling ImGui from ReaScript given an existing context",
            )
            .map_err(|err| {
                reaper.show_console_msg(format!("Got error registering function: {}", err));
                err
            })?;
    }

    Ok(())
}
Boscop commented 3 years ago

@GavinRay97 Btw, if you want to use imgui-rs in your Rust VST that also uses reaper-rs, you can use this: https://github.com/BillyDM/imgui-baseview (You could also combine that with implot-rs.) Here is an example VST: https://github.com/DGriffin91/compressor-plugin There are also round knobs for imgui: https://github.com/DGriffin91/imgui-rs-knobs

GavinRay97 commented 3 years ago

@Boscop Thank you! I will definitely use those for VST's =D

In this case, I was trying to do this:

Unfortunately I know basically NOTHING about either C++ or FFI or how this low level interop and binding/header stuff works so it turned out to be pretty difficult and not sure it's worth it =/