SKSE Plugin Development/Plugin ABI

From the CreationKit Wiki
(Redirected from Plugin ABI)
Jump to navigation Jump to search
Information icon.svgClaude Opus 4.8 was used to generate some content in this article. It has been verified by a human, but may still need work.
(This tag should be removed once confirmed as accurate by multiple editors.)

A plugin and the SKSE runtime are two separate programs, built at different times by different people The runtime DLL was compiled and shipped long before the plugin existed; the plugin is compiled long after, knowing nothing about the runtime's internals. Yet at game start-up they have to meet, agree they're compatible, and start calling each other's code. The thing that lets two independently-built binaries do that is a small contract called the plugin ABI (application binary interface).

The handshake in two phases[edit | edit source]

When the SKSE64 runtime finds a plugin .dll during start-up (see boot-timeline for when this happens), it doesn't blindly run it. If the plugin happened to be incompatible after an update, that would cause many problems. The connection is made in two phases: first, a query where the plugin declares to the SKSE runtime whether it is compatible; then the runtime commits to the actual loading procedure.

Phase 1: Compatibility check[edit | edit source]

In this version of SKSE, the compatibility question is answered without executing a single instruction from the plugin. Instead of calling a function, the runtime reads a block of plain data the plugin exports.

Any plugin exports a filled-in SKSEPluginVersionData struct under the name SKSEPlugin_Version. It contains the plugin name, its version, and which game versions it declares itself compatible with. The runtime opens the DLL as a read-only image (LoadLibraryEx(..., LOAD_LIBRARY_AS_IMAGE_RESOURCE)) and reads that exported block straight out of the file (PluginManager.cpp) - the DLL is loaded as data, not as code, so none of the DLL's functions run yet. It then runs the declaration through a compatibility check (CheckPluginCompatibility) and either accepts the plugin or rejects with a reason. This all happens before faulty code gets a chance to crash.

In this codebase there is no SKSEPlugin_Query function, the "query" phase is a static data declaration. It should be noted that this is not universal across the xSE family. F4SE (Fallout 4, same author) uses the (same mechanism). The older NVSE (New Vegas) does not: a plugin exports an NVSEPlugin_Query function that the loader resolves and calls, so plugin code runs during the check (xNVSE/NVSE).

Phase 2: The load call[edit | edit source]

Once the plugin passes the check, the runtime actually loads the DLL as code and calls the entry point function:

bool SKSEPlugin_Load(const SKSEInterface * skse)

This is the one function a plugin must export (declared as _SKSEPlugin_Load; expected export), found by name (see def-ordinal-exports). The runtime resolves it with GetProcAddress and calls it (PluginManager.cpp), handing the plugin a pointer to an SKSEInterface, which is the gateway to everything the runtime offers (see next section). This is where a plugin does its setup work.

  • A plugin may return false in order to bail out, provided it has not yet registered anything. The runtime will then unload the plugin. (PluginAPI.h).
  • There is also an optional, earlier entry point, SKSEPlugin_Preload, for plugins that need to run before the game has built much of itself. That two-phase preload/load timing is explained on the boot-timeline page; for most plugins, SKSEPlugin_Load is the one that matters.

The SKSEInterface handed to the plugin[edit | edit source]

The single argument to SKSEPlugin_Load is a pointer to the SKSEInterface struct. It carries a few version fields and, more importantly, a handful of function pointers the runtime filled in for plugins to use:

Member What it provides
skseVersion, runtimeVersion, editorVersion, isEditor Which host the plugin runs under (SKSE build, game version, and whether this is the game or the Creation Kit)
QueryInterface(id) The gateway to every sub-system interface (see below)
GetPluginHandle() A unique id for the calling plugin. Note: call it once and save the result. (See the rules of courtesy section.)
GetReleaseIndex() The SKSE build's release index
GetPluginInfo(name) Look up another loaded plugin by name

The struct the runtime actually hands the plugin is a single static instance it builds at start-up, with those function-pointer slots wired to its own internal functions (g_SKSEInterface).

The important field is QueryInterface. On its own, SKSEInterface is small. It doesn't expose Papyrus, serialization, Scaleform, and the rest directly. Instead a plugin calls skse->QueryInterface(id) with one of a fixed set of interface ids, and it hands back a pointer to that sub-system's interface, or null if the id isn't recognized (the dispatch switch).

There are seven interfaces, each exposing one feature the runtime offers to plugins:

Interface id Struct What it opens up
kInterface_Scaleform SKSEScaleformInterface Inject into the game's Scaleform (Flash) UI. Register a callback that builds objects under skse.plugins.<name>, and hook item data as it is extended to the inventory/favorites menus (PluginAPI.h)
kInterface_Papyrus SKSEPapyrusInterface Register native functions callable from Papyrus scripts, into the VM's class registry (PluginAPI.h)
kInterface_Serialization SKSESerializationInterface Read and write information into the SKSE co-save, with save/load/revert callbacks and handle/FormID resolution across saves (PluginAPI.h)
kInterface_Task SKSETaskInterface Queue a task for the game to run later. Either a general task (TaskDelegate) or a UI task (UIDelegate) (PluginAPI.h)
kInterface_Messaging SKSEMessagingInterface Inter-plugin messaging, plus SKSE lifecycle notifications (data-loaded, pre/post load-game, and so on) and access to SKSE's internal event dispatchers (PluginAPI.h)
kInterface_Object SKSEObjectInterface SKSE's managed-object services: the delay-functor manager, object registry, and persistent object storage (PluginAPI.h)
kInterface_Trampoline SKSETrampolineInterface Allocate space from SKSE's shared trampoline pools for code that installs hooks. (branch pool or local pool) (PluginAPI.h)

For a more detailed explanation on the interfaces, see the dedicated interface-catalog page.

Rules of courtesy[edit | edit source]

The ABI contract comes with a few rules of courtesy the runtime documents inline in the header.

  1. GetPluginHandle should be called once and its result cached. It is only valid during query or load, and returns an invalid handle if called at any other time (PluginAPI.h).
  2. Inter-plugin messaging (RegisterListener / Dispatch) should not be attempted during plugin load, because the target plugin may not be loaded yet (PluginAPI.h).

Reading the header comments in full is worthwhile before relying on the interface.