Closed GavinRay97 closed 3 years ago
Obviously Rust can't use C++ classes (well, barring cxx/autocxx) so you have these
.cpp
and.hpp
wrappers around classes fromreaper_plugin.h
which "flatten" them. Is the only thing that is required to make a C ABI compatible wrapper to:
- Make a
construct
anddestruct
method to callnew
anddelete
- Make a "flattened" method for every method on the class, which takes the class instance as the first argument
Yes, bringing everything down to C level and then using the C ABI is enough. That's at least my approach to it and it seems to works reliably (it should, it's not magic). I don't know if this generally recommended. If I would have to do this very often, I would look into automating this and maybe cxx could help there (although I have the impression that it's more tailored to use cases where you have a lot of idiomatic C++ code, which REAPER API code is definitely not - it's more like 90% C and 10% C++). But fortunately REAPER doesn't use virtual methods all over the place so it's not much work to do it manually.
- I noticed that some classes and structs are present in your wrappers, like
PCM_source
,PCM_source_transfer_t
,REAPER_PeakGet_Interface
. Is this because you have no use for them or some other reason?
My general strategy with reaper-rs is to only provide what I also use in my own stuff because I think only actually using a library in a real-world app can uncover potential library design flaws. What a coincidence that you mention above data types. I have been working on them during the last days, still on a branch at the time of this writing (https://github.com/helgoboss/reaper-rs/tree/feature/play-preview) but should go on the main branch today or tomorrow. I use it to implement a quite nice feature for ReaLearn, which I think you will like, too ;)
Your
midi.cpp
implementation does not have ctor/dtor methods. Is this because this class only gets returned as a result of calling functions from other classes, indirectly?
Exactly. Only used for references to REAPER-managed data so far. However, the feature/play-preview
branch makes some changes to it. E.g. it makes the MidiEvent
also usable for Rust-owned data - it's just a wrapper around the non-opaque MIDI_event_t
.
My current approach in the branch is to offer for each low-level data structure up to 3 medium-level types (depending on what's necessary):
OwnedPcmSource
).
PcmSource
) or something can only be passed around anyway because it's opaque (e.g. MediaTrack
).unsafe
block.BorrowedPcmSource
).
All the design rationale beyond that is described in the medium-level crate doc.
- I see that in your
bindings.rs
, you have what look like mangled linkage names. I assume this is because you have astruct reaper_functions
which contains all the function pointers in it, and you're building this struct in C/C++ and linking against it?
Those mangled names are irrelevant in this scenario. On Unix/macOS I'm not sure but on Windows I am because the bindings are always generated on Linux and generating them on Windows would generate other linkage names. I just use the signatures in the bindings.rs
file. I parse them (using syn
crate) and generate the reaper.rs
file from it. It's all generated using multiple phases where the first one uses bindgen (see build.rs
).
And if that's the case, that would mean these are empty function pointers until runtime, when they're loaded with
rec->GetFunc()
?extern "C" { #[link_name = "\u{1}_ZN16reaper_functions19AddMediaItemToTrackE"] pub static mut AddMediaItemToTrack: ::std::option::Option< unsafe extern "C" fn(tr: *mut root::MediaTrack) -> *mut root::MediaItem, >; }
Exactly.
To the last point I thought maybe this could be the better way to go but wasn't sure about it (linking against
extern
exports which don't have a value until runtime, and even then areMaybe
's).
Normal reaper_plugin_functions.h
usage involves creation of hundrends of static function pointers which eventually get filled at runtime. Initially I wanted to go that way but because I like things to be scoped and local (and Rust also likes it) I decided to bundle all those function pointer fields in a Rust struct instead and provide an easy way to make that Rust struct static (make_available_globally()
).
What I am doing in D-Lang is basically reflection over members of the struct and calling
rec.GetFunc()
on them and casting the result to the type of the function pointer itself:tmp = cast(typeof(tmp)) rec.GetFunc(name); // Expands to: reaper.Reaper.AddCustomizableMenu = cast(typeof(reaper.Reaper.AddCustomizableMenu)) rec.GetFunc(AddCustomizableMenu);
// D has "extern(C)" and "extern(C++)" for calling convention/ABI so the classes bit is straightforward extern (C++) class ProjectStateContext { public ~this() {} public abstract void AddLine(const(char)* fmt, ...); public abstract int GetLine(char* buf, int buflen); // ... } // "__gshared" is like Rc<Cell<T>> or Arc<Mutex<T>> __gshared struct Reaper { static: void function(void*, size_t, size_t, int function(const(void)*, const(void)*), void*) __mergesort; bool function(const(char)*, const(char)*, const(char)*, bool) AddCustomizableMenu; // ... } void load_reaper_api(reaper.reaper_plugin_info_t* rec) { foreach (name; __traits(allMembers, reaper.Reaper)) { mixin(`alias tmp = reaper.Reaper.`, name); mixin(q{ tmp = cast(typeof(tmp)) rec.GetFunc(name); if (tmp is null) throw new Exception("Failed to load REAPER function: " ~ name); }); } } extern (C) export int ReaperPluginEntry(HINSTANCE hInstance, reaper.reaper_plugin_info_t* rec) { if (!rec || !rec.GetFunc) return 0; load_reaper_api(rec); reaper.Reaper.ShowConsoleMsg("Hello world"); return 1; }
I don't know D-Lang unfortunately. In Rust there's no reflection. Also, I wanted everything to be as fast as the original and don't add any overhead (or only marginal one), so I avoid allocation and dynamic dispatch wherever possible. Some parts of the API are used in real-time threads and there everything must be fast.
I am interested in learning how to make C ABI wrappers and thought I might study + use your example here to write and publish a C ABI compatible
reaper_plugin.h
+reaper_plugin_functions.h
(so that I can use it with codegenerators, which most definitely do not like C++)
That's interesting! Having a totally flat version of the REAPER API sounds good.
All of that said, I'm not an expert in FFI. I just thought a lot about it, read a lot about Rust FFI, fell into traps multiple times and came up with this way of doing things.
Thank you for the comprehensive responses as always!
That's interesting! Having a totally flat version of the REAPER API sounds good.
I think your .cpp
and .hpp
wrappers are essentially this, so I figure it'll largely be me pulling these apart and emulating them to make sure I understand them fully, then just trying to expand the handful of missing classes/structs =)
If I would have to do this very often, I would look into automating this and maybe cxx could help there (although I have the impression that it's more tailored to use cases where you have a lot of idiomatic C++ code, which REAPER API code is definitely not - it's more like 90% C and 10% C++). But fortunately REAPER doesn't use virtual methods all over the place so it's not much work to do it manually.
I might investigate what the results of running reaper_plugins.h
and reaper_plugin_functions.h
through cxx
/autocxx
are. I'm not super hopeful because you often end up needing to whitelist or type-map a bunch of things but the only way you'll ever find that "one golden translator" is by experimenting I suppose.
I decided to bundle all those function pointer fields in a Rust struct instead and provide an easy way to make that Rust struct static (
make_available_globally()
).
I will look into this, I assume it's the same approach as the __gshared static struct {}
thing where it's a struct of empty function pointers that have type signatures for the function pointers, and then they just get loaded in once.
What a coincidence that you mention above data types. I have been working on them during the last days, still on a branch at the time of this writing (https://github.com/helgoboss/reaper-rs/tree/feature/play-preview) but should go on the main branch today or tomorrow. I use it to implement a quite nice feature for ReaLearn, which I think you will like, too ;)
👀
Those mangled names are irrelevant in this scenario. On Unix/macOS I'm not sure but on Windows I am because the bindings are always generated on Linux and generating them on Windows would generate other linkage names. I just use the signatures in the bindings.rs file. I parse them (using syn crate) and generate the reaper.rs file from it. It's all generated using multiple phases where the first one uses bindgen (see build.rs).
Final question -- how is it possible that reaper-rs
works on Windows if the mangled names and linkage stuff are generated for Linux?
(I will donate more for the time you've spent answering these by the way, I know it's a pain to type this much info 😅)
🙏
Final question -- how is it possible that
reaper-rs
works on Windows if the mangled names and linkage stuff are generated for Linux?
Because the mangled names are not important. They are just a byproduct of bindgen, probably I could even omit them using some bindgen config flag. reaper-rs doesn't link to the REAPER functions statically. At runtime (when loading a VST plug-in or on REAPER startup) it queries the addresses of the functions and assigns them to the function pointer variables in the Reaper
struct.
signature (generated by bindgen) + function address (provided by REAPER at runtime) = working function
They are just a byproduct of bindgen, probably I could even omit them using some bindgen config flag. reaper-rs doesn't link to the REAPER functions statically.
Ohhhh
At runtime (when loading a VST plug-in or on REAPER startup) it queries the addresses of the functions and assigns them to the function pointer variables in the Reaper struct.
Ah yes, this is why I was confused -- I thought it was statically linking them somehow despite REAPER plugins only being able to load API functions at runtime. So I thought "Okay well maybe he has some build artifact which is doing something like:"
/* static_reaper_functions.h */
typedef struct {
void* (*ShowConsoleMsg)(const char* text)
} reaper_functions;
And then using bindgen
on + linking this, and initializing it with something like load_reaper_functions_into_struct()
.
But that makes perfect sense and is exactly what I did in D:
signature (generated by bindgen) + function address (provided by REAPER at runtime) = working function
Closing 🥳
Hey Benjamin, sorry to file this under issues (could've mailed you) but thought maybe other people might have similar questions in the future.
I used the code and comments in
reaper-rs
, and the REAPER native SDK to learn C, C++, and how interop/FFI work over the last 2 months.(Learned by the REAPER C++ API to other languages -- currently have translated 100% of classes/structs/functions to D-Lang, and have working examples of writing REAPER extensions in C#, Nim, and Python too).
There were a few questions I had, if you would be kind enough to answer.
.cpp
and.hpp
wrappers around classes fromreaper_plugin.h
which "flatten" them. Is the only thing that is required to make a C ABI compatible wrapper to:construct
anddestruct
method to callnew
anddelete
PCM_source
,PCM_source_transfer_t
,REAPER_PeakGet_Interface
. Is this because you have no use for them or some other reason?midi.cpp
implementation does not have ctor/dtor methods. Is this because this class only gets returned as a result of calling functions from other classes, indirectly?bindings.rs
, you have what look like mangled linkage names. I assume this is because you have astruct reaper_functions
which contains all the function pointers in it, and you're building this struct in C/C++ and linking against it? And if that's the case, that would mean these are empty function pointers until runtime, when they're loaded withrec->GetFunc()
?To the last point I thought maybe this could be the better way to go but wasn't sure about it (linking against
extern
exports which don't have a value until runtime, and even then areMaybe
's).What I am doing in D-Lang is basically reflection over members of the struct and calling
rec.GetFunc()
on them and casting the result to the type of the function pointer itself:I am interested in learning how to make C ABI wrappers and thought I might study + use your example here to write and publish a C ABI compatible
reaper_plugin.h
+reaper_plugin_functions.h
(so that I can use it with codegenerators, which most definitely do not like C++)Thank you =D