Plugins are a way to extend a software’s capabilties. It makes the software more modular (when used properly). After writing a plugin for my UI library that I’m working on, I suddenly realized, everything can be thought of as a plugin on your computer system. Some individuals might already think this way, but it’s my first time thinking of modern operating systems and software this way.

In this blog post, we’ll cover what is a plugin, how to write it and then I’ll show you my perspective, of why I feel that everything on modern operating system is a plugin!

What Is A Plugin?

Think of plugins as the plug that you connect to a socket. The socket remains the same, but you (can) connect plugs of different electric/electronic appliances. To the same socket, you can connect an electronic iron, a refridgerator, a header, your induction, laptop/mobile phone charger, etc…

Why is this possible? It’s because the plug and socket agree on some things. If the socket has 3 pins, the plug can be of two or three pins only. Also, the dimensions must match as well.

Similar approach is followed in software systems as well! A plugin as multiple API it exposes, that the software agrees to. You can think of these exposed APIs as multiple pins of the plug. The software (socket) can use a plugin as long as the plugin (plug) and the software (socket) agree on some simple ground rules.

How To Write A Plugin?

So, as I said, I had to write a plugin when I was working on my own GUI library. The main idea was to implement the UI rendering backend as a plugin, that can be written in different Graphics APIs, and the user can select one of readily available ones, or provide their own! This also allows one to use my GUI library on older hardwares that don’t have any modern GPU drivers, meaning, the rendering will be done on the CPU, as long as user has a plugin for that!

How Plugins Are Loaded

First we need to understand how plugins are loaded by the software. As I said multiple times, there’s some kind of agreement between the software and plugin which is used to make the plugin work. One such agreement is the plugin data, and it’s symbol, giving us address to where the plugin data is locaded.

So, plugins are written as normal libraries, like .dll (Dynamically Linked Library) or .so (Shared Object) or .dylib or some other type of library files. In the library, there exists a symbol, that the plugin user can search to get information about this plugin. This symbol can only be searched by those softwares which know the exact name of it! (This is the first, foremost and required agreement). Now, the software can use the operating system (or loader) provided API to load the library file, and then again, using the operating system (or loader) provided API, to find this known symbol.

The loader will return the address where the data pointed by the symbol resides. This data can be a struct, an array, or a function. As long as the agreement is not broken, everything works fine.

Here are some reference methods that you can take a look at before reading further :

After loading the plugin, and finding symbol, the plugin is initialized, and used as agreed upon.

My Plugin Implementation

In my case, the agreement is to draw initialize a graphics API, create a graphics context, draw some 2D shapes, etc…

/**
 * @b Generic data needs to be type-casted to plugin-specific data,
 * depending on @c type of plugin in @x XuiPlugin object.
 * */
typedef void *XuiPluginGenericData;

typedef Bool (*XuiPluginInit)();
typedef Bool (*XuiPluginDeinit)();

typedef struct XuiPluginVersion {
    Uint16 year;
    Uint8  month;
    Uint8  date;
} XuiPluginVersion;

typedef enum XuiPluginType {
    XUI_PLUGIN_TYPE_NONE     = 0,
    XUI_PLUGIN_TYPE_GRAPHICS = 1, /**< @b Defined in Plugin/Graphics */
    XUI_PLUGIN_TYPE_MAX
} XuiPluginType;

typedef Uint8 XuiPluginPlatform;

typedef enum XuiPluginPlatformMask : XuiPluginPlatform {
    XUI_PLUGIN_PLATFORM_MASK_NONE    = 0, /**< @b No platform supported. (Must not be the case) */
    XUI_PLUGIN_PLATFORM_MASK_LINUX   = (1 << 0), /**< @b Plugin can work on Linux. */
    XUI_PLUGIN_PLATFORM_MASK_WINDOWS = (1 << 1), /**< @b Plugin can work on windows. */
    XUI_PLUGIN_PLATFORM_MASK_MAC     = (1 << 2), /**< @b Plugin can work on MacOS */
    XUI_PLUGIN_PLATFORM_MASK_ANDROID = (1 << 3), /**< @b Plugin can work on android */
    XUI_PLUGIN_PLATFORM_MASK_ALL =
        (XuiPluginPlatform)-1                    /**< @b The plugin works on all platforms */
} XuiPluginPlatfomMask;

typedef struct XuiPlugin {
    XuiPluginType     type;                /**< @b Type of plugin */
    CString           name;                /**< @b Name of plugin. */
    XuiPluginVersion  version;             /**< @b Plugin version. */
    CString           license;             /**< @b License information about plugin.*/
    XuiPluginPlatform supported_platforms; /**< @b Bitmask of supported platforms by the plugin */
    XuiPluginGenericData plugin_data;      /**< @b Data of plugin dependent on plugin type. */
    XuiPluginInit        init;             /**< @b Initialize plugin. */
    XuiPluginDeinit      deinit;           /**< @b De-initialize plugin. */
    void                *plugin_handle;    /**< @b Loaded plugin library handle. */
} XuiPlugin;

XuiPlugin *xui_plugin_load (CString plugin_name);
void       xui_plugin_unload (XuiPlugin *plugin);

In my application, I require any plugin (not just graphics ones), to define a variable in global scope named xui_plugin and must be of type XuiPlugin. This data structure will then inform me various important things about the plugin that I think is important (for now atleast).

This is just first level of agreement between CrossGui and Renderer Plugin. The next level of agreement, is dependent on type of plugin. For now there’s only one plugin type, so here is what agreement looks like in code :

/**
 * @b Opaque structure, defined by the plugin.
 *
 * Rendered image presentation method is different in different rendering APIs.
 * To abstract this away, I've decided to use a GraphicsContext, that's plugin
 * dependent. This needs to be passed to plugin whenever we need to render something.
 *
 * One can think of GraphicsContext having one-to-one correspondence with window.
 * */
typedef struct XuiGraphicsContext XuiGraphicsContext;

/**
 * @b Create a graphics context for given window.
 *
 * @param window 
 *
 * @return @c XuiGraphicsContext opaque object pointer on success.
 * @return @c Null otherwise.
 * */
typedef XuiGraphicsContext *(*XuiGraphicsContextCreate) (XwWindow *window);

/**
 * @b Destroy the given graphics context.
 *
 * @param gctx GraphicsContext object to be destroyed.
 * */
typedef void (*XuiGraphicsContextDestroy) (XuiGraphicsContext *gctx);

/**
 * @b Plugin must render given 2D shape.
 *
 * This is a per-vertex draw call.  
 *
 * @param graphics_context The @c XuiGraphicsContext object created for @x xwin.
 * @param xwin @c XwWindow object used to create @c graphics context.
 * @param vertices Array of 2D vertices.
 * @param vertex_count Number of vertices.
 *
 * @return True if draw was successful.
 * @return False otherwise.
 *
 * @sa XuiGraphicsDrawIndexed2D
 * */
typedef Bool (*XuiGraphicsDraw2D) (
    XuiGraphicsContext *graphics_context,
    XwWindow           *xwin,
    Vertex2D           *vertices,
    Size                vertex_count
);

/**
 * @b Plugin must render given 2D shape, along with indices data.
 *
 * This is an indexed, per-vertex call. Indices define the actual shape.
 * Refer to indexed drawing method for more detail.
 *
 * @param graphics_context The @c XuiGraphicsContext object created for @x xwin.
 * @param xwin @c XwWindow object used to create @c graphics context.
 * @param vertices Array of 2D vertices.
 * @param vertex_count Number of vertices.
 * @param indices Array of 2D vertices.
 * @param index_count Number of vertices.
 *
 * @return True if draw was successful.
 * @return False otherwise.
 *
 * @sa XuiGraphicsDrawIndexed2D
 * */
typedef Bool (*XuiGraphicsDrawIndexed2D) (
    XuiGraphicsContext *graphics_context,
    XwWindow           *xwin,
    Vertex2D           *vertices,
    Size                vertex_count,
    Uint32              indices,
    Size                index_count
);

Each of these method types define an agreement between the graphics plugin and CrossGui. The plugin data is now just a collection of these agreements like this :

/**
 * @b Defines set of callbacks to be used to interact with the plugin.
 * */
typedef struct XuiGraphicsPlugin {
    XuiGraphicsContextCreate context_create;
    XuiGraphicsContextDestroy context_destroy;
    XuiGraphicsDraw2D        draw_2d;
    XuiGraphicsDrawIndexed2D draw_indexed_2d;
} XuiGraphicsPlugin;

Now, that we have all the agreements setup, we can now write the graphics plugin like this :


/* Describe callbacks in graphics plugin data */
static XuiGraphicsPlugin vulkan_graphics_plugin_data = {
    .context_create  = graphics_context_create,
    .context_destroy = graphics_context_destroy,
    .draw_2d         = draw_2d,
    .draw_indexed_2d = Null
};

/**
 * @b Vulkan Graphics Plugin
 * */
XuiPlugin xui_plugin = {
    .type                = XUI_PLUGIN_TYPE_GRAPHICS,
    .name                = "Vulkan Graphics Plugin",
    .version             = {.date = 20, .month = 4, .year = 2024},
    .license             = "BSD 3-Clause License",
    .supported_platforms = XUI_PLUGIN_PLATFORM_MASK_LINUX,
    .plugin_data         = &vulkan_graphics_plugin_data,
    .init                = (XuiPluginInit)init,
    .deinit              = (XuiPluginDeinit)deinit
};

All the methods used in plugin data are defined here. I also streamed this whole implementation live on Youtube

At 1:45:45, you can see the plugin loading, works and the plugin name printed on terminal.

The plugin in the end is loaded and used like this :

XuiPlugin *xui_plugin_load (CString plugin_name) {
    RETURN_VALUE_IF (!plugin_name, Null, ERR_INVALID_ARGUMENTS);

    void *plugin_handle = dlopen (plugin_name, RTLD_NOW);
    RETURN_VALUE_IF (!plugin_handle, Null, "Failed to open plugin %s\n", plugin_name);

    XuiPlugin *plugin = (XuiPlugin *)dlsym (plugin_handle, "xui_plugin");
    GOTO_HANDLER_IF (
        !plugin,
        PLUGIN_NOT_FOUND,
        "Failed to find plugin info \"xui_plugin\". Did you define it publicly in the plugin?\n"
    );

    plugin->plugin_handle = plugin_handle;

    PRINT_ERR (
        "Loaded plugin \"%s\" Version %u.%u.%u\n",
        plugin->name,
        plugin->version.date,
        plugin->version.month,
        plugin->version.year
    );

    return plugin;
PLUGIN_NOT_FOUND:
    dlclose (plugin_handle);
    return Null;
}

void xui_plugin_unload (XuiPlugin *plugin) {
    RETURN_IF (!plugin, ERR_INVALID_ARGUMENTS);

    dlclose (plugin->plugin_handle);
}

int main (Int32 argc, CString *argv) {
    RETURN_VALUE_IF (argc < 2, EXIT_FAILURE, "%s <plugin path>\n", argv[0]);

    XuiPlugin *plugin = xui_plugin_load (argv[1]);
    RETURN_VALUE_IF (!plugin, EXIT_FAILURE, "Failed to load plugin\n");

    plugin->init();
    XuiGraphicsPlugin *gplug = (XuiGraphicsPlugin *)plugin->plugin_data;

    XwWindow           *xwin = xw_window_create (Null, 540, 360, 0, 0);
    XuiGraphicsContext *gctx = gplug->context_create (xwin);
    RETURN_VALUE_IF (!gctx, EXIT_FAILURE, "Failed to create graphics context\n");

    XwEvent e;
    Bool    is_running = True;
    while (is_running) {
        while (xw_event_poll (&e)) {
            is_running = e.type != XW_EVENT_TYPE_CLOSE_WINDOW;
        }
        gplug->draw_2d (gctx, xwin, (Vertex2D *)1, 1);
    }

    gplug->context_destroy (gctx);
    xw_window_destroy (xwin);
    plugin->deinit();

    xui_plugin_unload (plugin);

    return EXIT_SUCCESS;
}

Everything Is A Plugin

Now, we come to the post title. Everything is a plugin. Any hardware or software you run on your system, has to come to some agreement with other components of the whole machine to make itself of some use. The only difference between these plugin types is on how and to what they agree to. I’ll show this by following examples.

Applications As Plugins

If you have some detailed knowledge about how an application is launched, once you double click it, or run it in a terminal, then it’ll be relatively easy to convince you about this plugin type.

The process I’m going to explain might be specific to linux, but the general idea is the same on all operating systems. So, when an application launches, first it requests the operating system a loader to be used to load the selected executable file. The loader loads the executable file (just as we loaded the plugin), and also library files requested by the executable.

The loader then searches for a specific symbol named main on linux, WinMain on windows, and so on… The signature of this main method is already agreed upon, by the executable and the loader. The loader, will then call this main or WinMain to actually run your applications.

This is also where other amazing things happen. Things like initialization of static data/objects. Calling constructor methods (marked as __attribute__((constructor)) in C and C++).

This is not only for executable files, but for other compiled languages as well, that are loaded. Take Java for example. If you don’t define the humongous public static void main(), then your program probably won’t compile.

If you consider scripting languages as exception, then don’t forget that they instead run using an interpreter that is an executable! So, the main method agreement lies in that executable rather than your script. Python allows you to define your own main function in the script itself!

Libraries As Plugins

Libraries agree upon the exposed (or exported) symbols. The applications that use them does not to find any symbol or any plugin data because the loader will do this for you even before you start using anything.

Here also we see the agreement of signature on the signature/type of library functions/

Operating System As Plugin

If you really extrapolate the idea, then you can think of operating systems as plugins as well. If you ever sit to write an operating system for x86 (I did some time ago), you’ll have to go through a 5000+ page manual by intel to know about all the agreements between the operating system and your hardware.

The operating system may change as long as it complies with these agreements.

End Note

That’s it! I hope if not the idea of this post, you find the code provided useful. See you next time! ;-)


comments powered by Disqus