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"));
}
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.
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);
});
}
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 when | Use 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.