Skip to main content

Cross-Plugin Communication

Plugins are separate DLLs and cannot link against each other. They coordinate through two mechanisms on the plugin context:

  • Services — request/response. One plugin registers an interface; others look it up and call it.
  • Events — fire-and-forget broadcast. One plugin publishes a POD struct; any number of subscribers receive it.

Both are keyed by stable string IDs (InterfaceId / EventId), so no C++ type information crosses the DLL boundary.

Services

A service is a pure abstract interface with a static constexpr const char* InterfaceId. The host registers platform services (IMediaPlayer, IAppWindow, Hotkeys, …) before any plugin installs; feature plugins register their own (currently IHistory).

Looking up a service

Use the typed GetService<T>() helper. It returns nullptr if no provider is registered, so always check:

void OnInstall(IPluginContext& ctx) override
{
if (auto* player = ctx.GetService<IMediaPlayer>())
player->TogglePause();

if (auto* history = ctx.GetService<IHistory>())
Log::Info("resume pos: {:.1f}s", history->GetResumePos("C:/clips/demo.mp4"));
}
Ordering

Platform services exist by the time OnInstall runs. Another plugin's service may not be registered yet if it installs after you. If you need a peer plugin's service, either look it up lazily (when you actually use it) or do the wiring in OnBindHotkeys, which runs after all plugins are installed.

Registering your own service

Implement the interface and register it from OnInstall. You can register one object under several interfaces at once:

class MyThing : public PluginBase, public IMyService
{
protected:
const char* PluginName() const override { return "MyThing"; }
void OnInstall(IPluginContext& ctx) override
{
ctx.RegisterService<IMyService>(this);
}
};

To define your own service interface that other plugins can consume, ship a header with a pure abstract class and a unique InterfaceId:

class IMyService
{
public:
static constexpr const char* InterfaceId = "me.IMyService";
virtual ~IMyService() = default;
virtual void DoTheThing(int n) noexcept = 0;
};

Keep every method POD-only — no STL types across the boundary. See the ABI rules.

tip

When calling across features, depend on the interface (e.g. IHistory), never a concrete plugin type. The interface is all the boundary guarantees.

Events (publish / subscribe)

An event is a POD struct with a static constexpr const char* EventId. The host ships several in <framelift/Events.h>: NotificationEvent, FileOpenedEvent, FileEndedEvent, OpenFileRequestEvent.

Publishing

ctx.Publish<NotificationEvent>({"Saved!"});

Publish<T> is a context method. The payload is copied to subscribers synchronously.

Subscribing

Subscribe is a free helper in <framelift/ContextHelpers.h> that wraps a lambda over the raw fn-ptr ABI and frees the closure automatically on unload:

#include <framelift/ContextHelpers.h>

void OnInstall(IPluginContext& ctx) override
{
framelift::Subscribe<FileEndedEvent>(ctx, [](const FileEndedEvent& e) {
Log::Info("ended {} at {:.1f}s", e.path, e.position);
});
}
Lifetime

const char* fields in an event are valid only during the callback. Subscribers that need the value later must copy it into their own storage.

Defining your own event

Any POD struct with a unique EventId works:

struct MyThingHappened
{
static constexpr const char* EventId = "me.MyThingHappened";
int code = 0;
};

Publish it with ctx.Publish<MyThingHappened>({42}); others subscribe with framelift::Subscribe<MyThingHappened>(ctx, ...).

Services vs events — which to use

Use a service whenUse an event when
You need a return value or to call a specific operation.You are notifying that something happened.
There is exactly one provider.There may be zero, one, or many listeners.
The caller knows who it wants.The publisher should not know or care who listens.

For example: querying the resume position for a file is a service call on IHistory; announcing that a file did open is a FileOpenedEvent.