wiki:Technical/PluginAPI

Aegisub Plugin API proposal

This is a proposal for an API dynamic (shared) libraries can follow for Aegisub to be able to discover and load them at runtime, to extend Aegisub in various ways.

Everything in this document is still up for discussion, nothing is decided yet.


File locations and discovery

Plugins can be installed in two locations, a system-wide directory and the user configuration directory.

On Windows, the system-wide directory will be %INSTALLDIR%Plugins.

On UNIX-like systems, the system-wide directory will be $PREFIX/etc/aegisub/plugins/, unless $PREFIX is /usr, then it will be /etc/aegisub/plugins/.

On all platforms, the user plugins directory will be ?user/plugins/, where ?user is the user configuration directory.

In all cases, plugin directories can contain plugin binaries directly, or symlinks to them. It's suggested, on UNIX-like systems, to install the actual plugins $PREFIX/lib/ and name them libaegisub-$PLUGINNAME, and then place symbolic links to them in the plugin directories.

On start up, Aegisub scans all plugin directories and attempts loading all files found in them as plugins. Errors will be logged but not displayed, the user must visit a logging interface (which should be shared with other functions in Aegisub) to see any errors. There might be an UI in Options that displays loaded and failed plugins.

There needs to be a way for the user to disable plugins installed at the system-wide directory without having to manipulate that directory. Suggestions are needed here. One thing to take into account is plugins that might crash the process as soon as they're loaded, disabling a plugins needs to prevent attempting to load it at all.


General API considerations

The API should be pure C, not use any C++ features or C++ linkage. While it might be inconvenient in some regards, it should help in keeping the API cleaner and more accessible to different compilers. (For example, on Windows, Microsoft VC++ and GCC use different decorations and calling conventions for C++, meaning plugins can only be compiled with MSVC if they are to work. This is not desirable.)

The API should generally be extensible and backwards-compatible. A plugin written for an older version of the API should continue to work with newer versions, unless something makes it impossible. There should be a way for a plugin to require a specific version interface to be present.

Because plugins might be written in a different language and/or built with a different compiler, the runtime-library linked by a plugin might not use the same memory manager as Aegisub. Therefore, any heap memory blocks that are transferred in ownership must be allocated by a common allocator. Aegisub should make such a one available. If a plugin allocates all its dynamic memory with this allocator, it can enable Aegisub to monitor and limit the memory consumption of plugins; this needs to be discussed.

All functions should use the same calling convention, which needs to be decided upon. This will probably be stdcall. The calling convention is implicit in all definitions listed in this document.

Interfaces

The API is based upon interfaces. An interface is a struct containing function pointers. By convention, interface-names end with an underscore and the letter i, and are all lowercase, following C identifier naming rules. The interface-name passed as a string to the GetInterface function in the aegisub_plugin_i interface, and the struct declaring it, are the same.

When an interface has been published it does not change. If changes need to be made to an interface to support new functionality, a new interface must be defined.

To test whether required functionality is available in the version of Aegisub loading the plugin, the plugin should attempt to obtain pointers to all required interfaces during initialisation. That way, if any required interfaces are missing in the current version of Aegisub, the plugin can fail initialisation. It is safe to request and "hoard" interface pointers in this way, since obtaining interface pointers does not allocate memory or have other side effects.


Functions plugin library must export

In the hope of making extensibility more explicit and better handle different (older/newer) versions, the function interface exported from the plugin dynamic library will be very simple.

typdef void * aegisub_plugin_handle_t;

extern const char *AegisubPluginInit(const struct aegisub_plugin_i *aegisub_interface, aegisub_plugin_handle_t plugin_handle);
extern void AegisubPluginDone();

AegisubPluginInit

When Aegisub has loaded a dynamic library, it will look for this entry point as well as AegisubPluginDone. If both are found, AegisubPluginInit will be called by Aegisub, passing a pointer to its main plugin interface struct, aegisub_plugin_i. This interface is defined below.

When this function is called, the plugin should initialise any internal data structures it requires for itself. It should also use the function pointers in aegisub_plugin_i to obtain further interfaces to Aegisub and register its own functionality through those.

plugin_handle is an opaque handle value Aegisub uses to identify the plugin internally. The plugin must store this value for its own use. The plugin must not attempt to interpret the handle value in any way, this will invariably cause compatibility problems in the future, the handle must only be used as an opaque value that identifies the plugin to Aegisub.

Aegisub will not support multiple instances of a plugin inside a one process.

It is left undefined what happens if AegisubPluginInit is called more than once without AegisubPluginDone being called in-between.

If the plugin initialises successfully, it must return NULL. If initialisation fails for any reason, the plugin must return a pointer to a nul-terminated string describing the reason. This string may be dynamicaly allocated, but does not need to be. If initialisation fails, Aegisub will still call AegisubPluginDone, allowing the plugin to free any dynamically allocated error string.

AegisubPluginDone

Aegisub calls this function to tell a plugin that it is being unloaded. The plugin can use this opportunity to clean up anything it allocated during init.

It is the responsibility of Aegisub to clean up anything the plugin might have active. For example, if a plugin provides an audio player, and it is in use when the plugin is to be unloaded, it is the responsibility of Aegisub to shut down the audio provider and unregister the factory, before AegisubPluginDone is called.

It is left undefined what happens if AegisubPluginDone is called when the plugin is not initialised. It is left undefined what happens if AegisubPluginDone is called when the plugin is in use.


Base interfaces

Central interfaces that don't categorise, and are more used for management than implementing actual user-visible functionality.

aegisub_plugin_i

The aegisub_plugin_i interface is the main entry point for plugins to access functionality in Aegisub and register themselves.

Implemented by Aegisub. Called by plugin.

#define MAKE_AEGISUB_VERSION(major,minor,release,patch) ((major<<24)|(minor<<16)|(release<<8)|(patch))

enum aegisub_license_level_t {
    LICENSE_LEVEL_LIBERAL     =    0,
    LICENSE_LEVEL_RESTRICTIVE = 1024,
    LICENSE_LEVEL_CLOSED      = 2048
};

struct aegisub_plugin_metadata_t {
    char *name;
    char *author;
    char *copyright;
    char *license;
    char *acknowledgements;
    char *homepage_url;
    char *support_url;
    char *email;
    uint32_t version;
    enum aegisub_license_level license_level;
};

struct aegisub_plugin_i {
    void * (*GetInterface)(const char *interface_name);
    void   (*GetAegisubVersion)(uint32_t *release_version, int *source_revision);
    void   (*RegisterPluginMetadata)(aegisub_plugin_handle_t plugin_handle, const struct aegisub_plugin_metadata_t *metadata);
};

GetInterface

Request a pointer to a named interface. Getting an interface pointer in this way never has any side effects, in particular it does not allocate memory.

If Aegisub supports the requested interface, a pointer to it is returned.

If Aegisub does not support the requested interface, NULL is returned.

GetAegisubVersion

Return the version number of the running Aegisub version.

The plugin should pass pointers to two integers to the function, Aegisub will fill in the actual version numbers in the locations pointed to.

The release_version value is constructed using the MAKE_AEGISUB_VERSION macro defined above. The source_revision value is the source control revision number Aegisub was built from.

The plugin can pass NULL pointers to either parameter if it does not require the corresponding version number.

RegisterPluginMetadata

Register metadata about the plugin with Aegisub, for display in the user interface.

The plugin must pass its plugin_handle value supplied by Aegisub to the AegisubPluginInit function to register its metadata.

It is suggested that the metadata structure is statically allocated. The strings must be nul-terminated and encoded in UTF-8 Unicode with no BOM. The strings should be statically allocated, Aegisub may store pointers to them without making private copies.

Aegisub may cache some of the the metadata obtained between sessions, and make its own mappings between plugin dynamic library filenames and user-friendly names, for example.

Explanations for the fields in aegisub_plugin_metadata_t. Any string field may be NULL if there is no relevant information for it.

  • name: User-friendly name of the plugin, how it will be presented. This should not include version information.
  • author: Name of the author(s) of the plugin. This may be person(s), company or organisation.
  • copyright: Copyright status of the plugin.
  • license: License string for the plugin. If the license text is very long, it is suggested to let this refer to a separate file shipped with the plugin.
  • acknowledgements: Acknowledgements required by the licenses of other libraries the plugin uses.
  • homepage_url: HTTP URL to the official homepage of the plugin. This should be the "front page". This will be made a clickable link.
  • support_url: HTTP URL to the official support means. This may be a forum, knowledge base, issue tracker, FAQ or similar. If this would be the same as the homepage URL, it should be left out. This will be made a clickable link.
  • email: E-mail contact, the exact purpose of this is left undefined for now.
  • version: Version number of the plugin, encoded using the MAKE_AEGISUB_VERSION macro defined elsewhere.
  • license_level: Specifies what class of license the plugin is under. All plugins for which source-code is not publicly available are under "closed" license. If there are restrictions on distribution of source and/or binary versions of the plugin code (this should include the GNU GPL licenses) the license is "restrictive". Licenses placing no special restrictions on redistribution are "liberal".

allocator_i interface

Establishes a common way for Aegisub and plugins to manage dynamic memory. Plugins are encouraged to use this interface

Implemented by Aegisub. Called by plugin.

typedef void * aegisub_allocator_t;

struct allocator_i {
    aegisub_allocator_t Create(aegisub_plugin_handle_t plugin_handle);
    size_t Destroy(aegisub_allocator_t allocator);
    void *calloc(aegisub_allocator_t allocator, size_t nmemb, size_t size);
    void *malloc(aegisub_allocator_t allocator, size_t size);
    void free(aegisub_allocator_t allocator, void *ptr);
    void *realloc(aegisub_allocator_t allocator, void *ptr, size_t size);
    char *strndup(aegisub_allocator_t allocator, const char *s, size_t n);
};

Create

Create a new allocator object.

To identify the memory allocated with the plugin allocating it, the plugin must pass its plugin_handle obtained from AegisubPluginInit.

This function will return a handle to an allocator object, this handle should be treated opaque and plugins should not attempt to interpret its value in any way. Attempting to interpret it as more than an opaque handle will invariably risk compatibility problems.

This function may fail and return NULL, if no creating an allocator was not possible.

Destroy

Destroy an allocator object and free all memory allocated by it.

Plugin authors are discouraged from relying on destroying an allocator to free allocated memory, we consider it good programming practice to explicitly free every allocation you have made.

Returns the number of bytes that were still allocated when the allocator was destroyed. If this number is nonzero, it is considered a memory leak.

calloc

Allocate nmemb blocks of size bytes, and initialise all allocated bytes to binary zero.

Returns a pointer to the first allocated block. The allocated memory is treated as a single unit for the purpose of reallocating or freeing. May return NULL if there was not enough free memory, or if either of nmemb or size was zero.

malloc

Allocate size bytes of memory, leave it uninitialised.

Returns a pointer to the allocated memory. May return NULL if there was not enough free memory, or if either of nmemb or size was zero.

free

Free an allocated block of memory.

realloc

Change the size of an allocation ptr to size bytes. This may create a new allocation and move the contents of the old allocation to the new one.

Returns the new pointer to the reallocated block. This may or may not be different from the original pointer. If the requested size was zero, will return NULL. If new memory could not be allocated, will return NULL.

strndup

Create a copy of nul-terminated string s which is at most n bytes long. The memory for the copy is allocated using the allocator object.

May return NULL if memory could not be allocated.


UI interfaces

Interfaces used to implement user-interface for components. This primarily means graphical interface and logging.

A/V provider-interfaces

Interfaces used to implement audio and video providers =

  • Links to sub-pages

Command management interfaces

For registering and invoking commands to manipulate subtitles.

  • Yet more links to sub-pages

Subtitles manipulation

Stuff used to manipulate subtitles and do undo-management.

  • Sub-page links just won't stop