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:
| Need | Pattern |
|---|---|
| String input | const char* (NUL-terminated, caller keeps ownership) |
| String output | int Get(char* buf, int cap) — returns length excl. NUL; buf=nullptr queries size |
| Collection output | enumeration callback void(*)(const T*, void* ud) + void* ud |
| Callback | function pointer + void* userdata (not std::function) |
| Non-trivial return | out-pointer parameter |
| Exchanged data | C 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 aframelift_*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
IPluginContextmethod, 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.