Skip to main content

ABI Compatibility

The host ↔ plugin boundary is a COM-like binary ABI. Honoring it is what lets a plugin built with any compatible Windows compiler load into the host regardless of how the host was built, with no shared C++ runtime. Everything in <framelift/Abi.h> exists to enforce it.

The five rules

Every type that crosses the DLL boundary must follow these. Violations cause undefined behavior — crashes or silent corruption — especially when host and plugin use different compilers or runtimes.

1. Interfaces only

Every exchanged object is a pure abstract class: single inheritance, only virtual methods, no data members, no inline non-virtual logic that touches object layout. Declare methods noexcept — exceptions must never propagate across the boundary.

The FRAMELIFT_INTERFACE(ClassName) macro deletes copy/move on a boundary interface; put it at the top of your interface body.

2. POD-only signatures

No std::string, std::vector, std::function, std::variant, std::optional, std::unique_ptr, or by-value non-trivial structs in virtual parameters or return types. Use instead:

NeedPattern
String inputconst char* (NUL-terminated, caller keeps ownership)
String outputint Get(char* buf, int cap) — returns length excl. NUL; buf=nullptr queries size
Collection outputenumeration callback void(*)(const T*, void* ud) + void* ud
Callbackfunction pointer + void* userdata (not std::function)
Non-trivial returnout-pointer parameter
Exchanged dataC POD struct (fixed-size char arrays, numeric fields)

The author-side helpers in <framelift/ContextHelpers.h> / <framelift/HotkeyHelpers.h> wrap these patterns back into lambdas and std::string on the plugin side, where it is safe because that code compiles into your DLL and never crosses the boundary.

3. Explicit IDs, not typeid

Every interface declares static constexpr const char* InterfaceId = "..."; and every event static constexpr const char* EventId = "...";. Service lookup and pub/sub key on these constants. Never use typeid(T).name() across a DLL boundary — it is not stable across compilers.

4. Documented string lifetime

const char* parameters and event fields are valid only for the duration of the call/callback, in both directions. Anything that must outlive the call is copied into the receiver's own storage.

5. Versioned

The ABI is versioned major.minor.patch (FRAMELIFT_PLUGIN_ABI_MAJOR / _MINOR / _PATCH in <framelift/PluginABI.h>). The FRAMELIFT_PLUGIN_EXPORT macro in <framelift/PluginExports.h> bakes the values your plugin was compiled against — alongside its name and own version — into a framelift_plugin_info() export, which the host reads first, before touching any vtable. The host accepts the plugin only when:

plugin.major == host.major   &&   plugin.minor <= host.minor
  • MAJOR — bump on any breaking change: removing/reordering a method, changing a signature, or appending to a host-called plugin interface (IPlugin, IRenderable) or a framelift_* export. The host would otherwise call a new vtable slot on an older, shorter plugin vtable. Old plugins are rejected.
  • MINOR — bump on backward-compatible additions to host-provided surface: a new IPluginContext method, a new service interface, a new optional export. Old plugins (lower minor) keep loading; a plugin built against a newer host than it runs on (higher minor) is correctly rejected.
  • PATCH — ABI-neutral fixes. Carried and logged but never gates the load; it is metadata reserved for future plugin-distribution tooling.

How the version gate plays out

  • At load time: the host reads framelift_plugin_info() and applies the major/minor rule above. An incompatible plugin is skipped and logged with its name and version — never invoked.
  • At configure time: find_package(FrameLiftSdk) is gated on the ABI major version (SameMajorVersion). Building against an incompatible-major SDK fails in CMake, before you ever produce a DLL; the minor rule is enforced by the runtime loader.

Together these mean an out-of-date plugin fails loudly and early instead of corrupting memory at runtime.

Practical implications

  • You do not link imgui, spdlog, stb, or a JSON library — none of those types appear at the boundary. A plugin needs only a C++23 compiler and CMake.
  • You are not required to match the host's compiler or its standard-library build flags, because no standard-library types are shared across the edge.
  • When you define your own services or events, the same rules apply — keep signatures POD and give each type a unique ID.