Expanders across Plugins?

Is it possible to have an expander in one plugin that works on modules from another plugin?

Note: In this scenario I can make modifications the code of both plugins.

Yes, see Stoermelder STRIP for instance.

Ah yeah that is a good example. I should have been more clear that I’m looking for something deeper than what STRIP does.

STRIP expands any VCV rack module by calling the common methods on the base Module structure in the VCV code. Like save, load, enable, etc.

What I am looking for is the ability to access specific functionality inside a different plugin.

Since it sounds like you are the author of both plugins, you can share header files so that the struct/class type declarations are in sync across both plugins. I think the main risk is making any breaking changes to a struct/class type can cause crashes or other bad behavior, because there is no way to force users to upgrade both plugins to keep them in sync.

1 Like

I was dealing with the same idea.

@ldlework answered my question in this thread

I managed to find my module and cast it to access its members, but I had no idea how to deal with the threading problems so reverted back to a message bus.

Edit: Come to think of it… perhaps Little Utils Teleport In has the answer to your problem.

just yesterday I was wondering if the Rainbow Expanders would work on the GlassPane, and discovered that nope, so scratched that idea, and now just saw this, so I vote for that feature :wink:

What exactly is it that you wish to accomplish? I’ve an unfinished but functional module that modifies modules in other plugins so I might be of further help depending on what you’re trying to do.

A simple mutex can go a long way.

While not specifically what I’m working on, @fractalgee’s idea is a good illustration.


I am working on a new collection of premium sequencers that have some reusable expanders. I would like to release free versions of the modules that can work with the premium expanders.

So as an example I have an expander that provides triggers for shifting the sequence on the parent module. That expander will live in the premium collection. But I would like for that expander to also be able to work on the free modules too.

1 Like

@cosinekitty I will have to try this out. My c++ knowledge is lacking here. My intuition says that just sharing headers across plugins wont allow for data interoperability, but maybe I am wrong. I will try it out.

never, ever, ever try to acquire a mutex on the process thread! I mention it in this old article: Demo/efficient-plugins.md at main · squinkylabs/Demo · GitHub

But just google “audio click priority inversion” and you should get a ton of stuff.

Working on a similar thing. A minimal sequencer ala addr, where all additional features, (glide, shift, scramble, rot, ratchet, etc) are added using generic reusable expanders.

Not premium of course.

2 Likes

It will work if you are careful. If you define a struct in a header file and #include the same header from both plugins, then you will be able to typecast a pointer from a base type to the derived type, even though the pointer comes from another compiled binary.

But once you get that working, it should give you nightmares if you ever have to make a change. One thing that can help is by knowing ahead of time that you want to share types across separately compiled binaries, you can at the very least put a couple of things at the front of your struct to help sanity-check it at runtime:

  • the structure’s own size in bytes
  • a magic number that would be very unlikely to be there accidentally

for example, something like

const uint32_t MYSIG = 0x50536574;   // ASCII "PSet"
struct MyModule : Module
{
    size_t sizeInBytes = sizeof(MyModule);
    uint32_t signature = MYSIG;
};

Then be paranoid after you cast a pointer to it…

void *ptr = ...;
MyModule *module = (MyModule *)ptr;
if (module->sizeInBytes == sizeof(MyModule) && module->signature == MYSIG)
{
    // assume we found the correct type
}
else
{
    // Oh no! Something is wrong. Ignore module pointer.
}

Also, you can maybe put a version number in there also, to plan for checking which version of the struct you are dealing with at runtime. Always add new fields to the end. Never change or re-order existing fields.

4 Likes

Yes. all this is good advice. But also there will be threading issues, as the two plugins could easily be running on different threads.

1 Like

That should not be a problem if you are working in the audio threads, as long as you use the expander API. The left and right expander data should be stable during sample processing, and the flip method synchronises between samples.

1 Like

I often develop new modules in a private experimental plugin rather than the CM plugin and have had no issues with using the expanders from one plugin with the modules in another.

1 Like

As to api stability and headers changing I suppose someone could propose a com like base class with a get Id that we all could adopt for functions beyond the module base class. Then dyn cast to the base and query the interface api and so on. Or dyn cast and never change your interfaces could also work I guess

I took that approach, a common base class across modules, mainly for the Expander use case. Then in the common base class’s onExpanderChange method one can do a dynamic_cast to see if it matches. There’s a bit of a pros/cons with it but it seemed to fit the use case well enough.

Yeah dybamic casts work as long as you make your fast targets static once released

An intermediate expander? Like so it really just works as an adapter to a generic protocol. Then all plugins could present or subscribe to the expander busses. And then say access any of the 256 CV floats, the boolean[256] of used/connected, and a slug string. Then all modules would effectively use the same float[256]; bool[256], char* interface.

EDIT: I think it should be called the Super EXpander Interface.