Dance: Plugins

May 14, 2022

In the first post of this series, we covered the design and implementation of Dance’s graphical application. We talked about some of our supporting technologies, and we mused about a couple abstractions that I thought were kinda clever. All of that naturally segues us into the plugin system. Dance’s plugin system is the keystone of the project; it allows us to dynamically host external visualizers (in their own DLL files) by hooking them into our runtime and render pipeline. There’s quite a bit of nuance though, so let’s dive right in!

The API

To get started, we’re going to need a standardized interface for visualizers. This interface should provide graphical and runtime hooks as well as types for a static constructor and destructor. The Visualizer class, available in the shared files that all Dance subprojects have access to.

class Visualizer
{
public:
    // What we expect to be passed to the visualizer constructor
    struct Dependencies
    {
        // ...
    };

    // Make destructor virtual for children classes.
    virtual ~Visualizer() {}

    // Allocation and deallocation of size-dependent resources
    virtual HRESULT Unsize() = 0;
    virtual HRESULT Resize(const RECT& size) = 0;

    // Runtime hooks
    virtual void Render() = 0;
    virtual void Update(double delta) = 0;

    // Signatures
    using Constructor = std::function<Visualizer* (const Visualizer::Dependencies&)>;
    using Destructor = std::function<void(Visualizer*)>;

    // ...
};

Visualizer is concise, but it packs a punch.

  • The virtual destructor is required for proper deallocation with polymorphism.
  • The Unsize and Resize functions are called by the Dance application before and after the host window is resized, respectively. These hooks are used for reallocating graphical resources as well as making the visualizer’s contents responsive.
  • Render is invoked on WM_PAINT by the window. All graphics stuff should be done here.
  • Update is called in the main thread with the elapsed time since the last call. If you’re familiar with game engines, you’ll recognize this as part of a traditional game loop pattern. Any simulation should be done here.
  • Lastly, Constructor and Destructor types are outlined here for continuity. They’re going to be used in the plugin system header and source, which we’ll talk about next.

Plugins

While working on an early version of this project, I realized that half the fun of it would be writing your own visualizers, not to mention running other developers’ creations. Baking a couple visualizers directly into the application wouldn’t be nearly as cool as loading visualizers dynamically. This necessitated a plugin system.

The workflow was simple: compile your visualizer into a DLL, drop that DLL in Dance’s Visualizers folder, and when you run the program, you’ll be able to activate your visualizer on command. The implementation? Not so much.

Early Days

The first thing to note is that Windows’ DLL toolkit is…sparse. You have ::LoadLibrary and ::FreeLibrary, which are self-explanatory, and you have ::GetProcAddress, which searches for a symbol in the DLL by name. There’s no enumeration, no metadata, no hints, no nothing. My first attempt at the plugin system treated each .dll file in the Visualizers folder as a visualizer factory, requiring that it implemented:

extern "C" __declspec(dllexport) Visualizer* Factory
(
    const Visualizer::Dependencies& dependencies,
    const std::filesystem::path& path
);

extern "C" __declspec(dllexport) std::wstring Name();

These two methods would be found by ::GetProcAddress and stored in a container for reference.

While this worked in my development sandbox, I quickly realized it’s not guaranteed that every DLL in the Visualizers folder is an actual visualizer plugin. Some DLLs might be dependencies. Fuck it, some might just be junk, it’s not like we can trust the user. To address this, I considered working some super secret symbol into each visualizer DLL, e.g. void Dance(), but this is super unsatisfying. For any given symbol, an arbitrary DLL that’s not a visualizer might accidentally implement it.

Another issue with this approach was that we still kinda have to validate the symbols ::GetProcAddress turns up. What if Factory has a different signature than what we expect? As far as I know, this is impossible since the signature is completely stripped from the DLL. If we happen to find any old Factory and Name, do we just send it and hope for the best?

The last nail in the coffin is that this solution is just…ugly. If we’re always calling into their methods, visualizers can never know where their original DLL is located in the file system. A critical consequence of this is that they can’t reference files relative to themselves; for example, the CubeVisualizer requires an adjacent HLSL shader that’s compiled on the fly. In short, we were forced to keep track of DLL paths in order to pass them to the visualizer constructors.

The first version of this system was neither simple nor clean nor congruent, and none of the technical aspects listed above justified that in any way.

Late Nights

Fortunately, after doing a bit of research and asking for thoughts on Reddit, I made a breakthrough. Instead of selectively calling into DLLs that appear to be visualizers, I would indiscriminately load everything in the visualizers folder and let any actual visualizer plugins register themselves. Thus, I arrived at my current solution.

The first ingredient in this recipe is a bit of trivia: if you didn’t know, Windows DLLs have a main function. It’s aptly documented by Microsoft. DllMain is invoked when a DLL is loaded by a process, when it’s loaded into a thread, when it’s unloaded from a thread, and it’s when unloaded from a process. Note that code run in DllMain can locate itself in the filesystem, circumventing our path issues.

Next up is our underlying registration logic, which is made available from the main Dance process. The details aren’t particularly important, but feel free to dig through the header and source.

// Provide version info, etc.
extern "C" __declspec(dllexport) Dance::API::About _Dance();

// Externally visible registration method.
extern "C" __declspec(dllexport) void _Register
(
    const std::wstring& name,
    const Visualizer::Constructor& constructor,
    const Visualizer::Destructor& destructor
);

These functions are underscored because we don’t expose them statically. Instead, we make sure they’re ambiently available and provide wrapper methods that find them use the same ::GetProcAddress trick from earlier but on the main executable instead. Back to the shared Visualizer header:

template<typename Return, typename ...Args>
static inline Return Defer(LPCSTR to, Args ...args)
{
    using Signature = Return __cdecl(Args...);
    FARPROC pointer = ::GetProcAddress(::GetModuleHandle(nullptr), to);
    Signature* actual = reinterpret_cast<Signature*>(pointer);
    return actual(args...);
}

static inline About Dance()
{
    return Defer<About>("_Dance");
}

static inline void Register
(
    const std::wstring& name,
    const Visualizer::Constructor& constructor,
    const Visualizer::Destructor& destructor
) {
    Defer
    <
        void, 
        const std::wstring&,
        const Visualizer::Constructor&,
        const Visualizer::Destructor&
    >(
        "_Register",
        name,
        constructor,
        destructor
    );
}

As a side note, Defer is kinda clever, no? ::GetModuleHandle(nullptr) returns the address of host executable, so ::GetProcAddress finds our hosted _Dance and _Register symbols no matter where it’s called from. Next, reinterpret_cast casts the resulting pointer as the function type declared by the template arguments. Function in hand, we’re finally ready to register our visualizer (or visualizers plural, for that matter). I thought it was especially cool that you can pass void as Return and have it elide the return in the wrapper.

Anyhow, now, registering our visualizers is as simple as adding the following DllMain to each visualizer subproject. This example is taken from the bars visualizer. Note that the overload of Register we’re using here provides a naive new constructor and delete destructor.

BOOL WINAPI DllMain(HINSTANCE instance, DWORD reason, LPVOID reserved)
{
    if (reason == DLL_PROCESS_ATTACH)
    {
        Dance::API::Register<BarsVisualizer>(L"Bars");
    }
    return TRUE;
}

Wrapping Up

Now that we’ve figured out the appropriate abstraction, everything else falls into place. The remainder of the implementation is fairly simple; if you’re interested, you can read through the rest of the Plugin source and the VisualizerWindow source, specifically VisualizerWindow::Switch. Next up, we’ll be taking a look at audio analysis, visualizer plugin build dependencies, and the BarsVisualizer as a case study.