Monday, July 25, 2022

C++ API Design Best Practices - C++ EXTENDING VIA PLUGINS

                                                                        Image source

Plugin Model Overview

Many examples of commercial software packages allow their core functionality to be extended

through the use of C/C++ plugins. 

For example, 

  • The Apache Web server supports C-based “modules,”
  • Adobe Photoshop supports a range of plugin types to manipulate images, and Web browsers such as Firefly, Chrome, 
  • Opera supports the Netscape Plugin API (NPAPI) for the creation of browser plugins such as the Adobe Flash or PDF Reader plugins.
  • The Qt toolkit can also be extended via the QPluginLoader class. (A server-based plugin API such as Apache’s module interface is sometimes referred to as a Server API or SAPI.)

The benefits of adopting a plugin model in your API are as follows.

  • Greater versatility. Your API can be used to solve a greater range of problems, without requiring you to implement solutions for all of those problems.
  • Community catalyst. By giving your users the ability to solve their own problems within the framework of your API, you can spark a community of user-contributed additions to your base design.
  • Smaller updates. Functionality that exists as a plugin can be updated easily independently of the application by simply dropping in a new version of the plugin. This can often be a much smaller update than distributing a new version of the entire application. 
  • Future-proofing. Your API may reach a level of stability where you feel that no further updates are necessary. However, further evolution of the functionality of your API can continue through the development of plugins, allowing the API to maintain its usefulness and relevance for a greater period of time. For example, the NPAPI has changed little in recent years, but it is still a popular method to write plugins for many Web browsers.
  • Isolating risk. Plugins can be beneficial for in-house development too by letting engineers change functionality without destabilizing the core of your system.

Plugin System Design  



The Plugin Manager lives in the Core API. It discovers and loads plugins that have been built against the Plugin API.

  • The Plugin API: This is the API that your users must compile and link against in order to create a plugin. I differentiate this from your Core API, which is the larger code base into which you are adding the plugin system.
  • The Plugin Manager: This is an object (often a singleton) in the Core API code that manages the life cycle of all plugins, that is, loading, registration, and unloading. This object can also be called the Plugin Registry.

C versus C++. 

The C++ specification does not define a specific ABI. Therefore, different compilers, and even different versions of the same compiler, can produce code that is binary incompatible. The implication for a plugin system is that plugins developed by clients using a compiler with a different ABI may not be loadable. In contrast, the ABI for plain C code is well-defined and will work across platforms and compilers.

Implementing Plugins in C++

  • Supporting C++ plugins can be difficult due to cross-platform and cross-compiler ABI problems. However, because this is a book about C++ API design, let’s take a few more moments to present some solutions that let you use C++ plugins more robustly.
  • One can use a binding technology for your plugins, for example, an IPC solution such as COM on Windows,  creating script bindings for your API and letting users write extensions using a cross-platform scripting language such as Python or Ruby (as If you absolutely need to use C++ plugins for maximum performance or you feel that creating a COM or script binding is too heavyweight for your needs, there are still ways that you can use C++ more safely in plugins.

The following list offers several best practices

  • Use abstract base classes. Implementing virtual methods of an abstract base class can insulate a plugin from ABI problems because a virtual method call is usually represented as an index into a class’s vtable.
  • Use C linkage for free functions. All global functions in your Plugin API should use C linkage to avoid C++ ABI issues, that is, they should be declared with extern "C". Similarly, function callbacks that a plugin passes to the Core API should also use C linkage for maximum portability
  • Avoid STL and exceptions. Different implementations of STL classes such as std::string and std::vector may not be ABI compatible. It is therefore best to avoid these containers in any function calls between the Core API and Plugin API. Similarly, because the ABI for exceptions tends to be unstable across compilers, these should be avoided in your Plugin API.
  • Don’t mix allocators. It’s possible for plugins to be linked against a different memory allocator than your API. For example, on Windows it’s common for debug builds to use a different allocator than release builds. The implication for the design of our plugin system is that either the plugin must allocate and free all of its objects or the plugin should pass control to the Core API to create and destroy all objects. However, your Core API should never free objects that were allocated by a plugin, and vice versa

The Plugin API

  • The Plugin API is the interface that you provide to your users to create plugins. the header file will contain functionality that allows plugins to communicate with the Core API.
  •  When the Core API loads a plugin, it needs to know which functions to call or symbols to access in order to let the plugin do its work. This means that you should define specifically named entry points in the plugin that your users must provide. 
  • There are several different ways that you can do this. For example, when writing a GIMP plugin, you must define a variable called PLUG_IN_INFO that lists the various callbacks defined in the plugin.
  • The two most basic callbacks that a plugin should provide are an initialization function and a cleanup function. As noted earlier, these functions should be declared with C linkage to avoid name mangling differences between compilers.
  • In a cross-platform plugin, system development, you will also have to deal with correctly using __declspec(dllexport) and __declspec(dllimport) decorators on Windows.

                // pluginapi.h

                #include "defines.h"

                #include "renderer.h"

                #define CORE_FUNC extern "C" CORE_API

                #define PLUGIN_FUNC extern "C" PLUGIN_API

                #define PLUGIN_INIT() PLUGIN_FUNC int PluginInit()

                #define PLUGIN_FREE() PLUGIN_FUNC int PluginFree()

                typedef IRenderer *(*RendererInitFunc)();

                typedef void (*RendererFreeFunc)(IRenderer *);

                CORE_FUNC void RegisterRenderer(const char *type,

                                                                 RendererInitFunc init_cb,

                                                                RendererFreeFunc free_cb);

  • Macros to define the initialization and cleanup functions for a plugin: PLUGIN_INIT() and PLUGIN_FREE(),
  • PLUGIN_FUNC() macro let plugins export functions for the Core API to call, as well as the CORE_FUNC() macro that exports Core API functions for plugins to call.
  • Function, RegisterRenderer(), which allows plugins to register new IRenderer classes with the Core API. Note that a plugin must provide both an init function and a free function for their new IRenderer classes to ensure that allocations and frees happen within the plugin
  • CORE_API and PLUGIN_API define. These let us specify the correct DLL export/import decorators under Windows. CORE_API is used to decorate functions that are part of the Core API, and PLUGIN_API is used for functions that will be defined in plugins. The definition of these macros is contained in the definitions.
            // defines.h
            #ifdef _WIN32
                #ifdef BUILDING_CORE
                        #define CORE_API __declspec(dllexport)
                        #define PLUGIN_API __declspec(dllimport)
                #else
                        #define CORE_API __declspec(dllimport)
                        #define PLUGIN_API __declspec(dllexport)
                #endif
            #else
                #define CORE_API
                #define PLUGIN_API
            #endif

            //  plugin.cpp
            #include "pluginapi.h"
            #include <iostream>

            class OpenGLRenderer : public IRenderer
            {
            public:
                        ~OpenGLRenderer() {}
                        bool LoadScene(const char *filename) { return true; }
                        void SetViewportSize(int w, int h) {}
                        void SetCameraPosition(double x, double y, double z) {}
                        void SetLookAt(double x, double y, double z) {}
                        void Render() { std::cout << "OpenGL Render" << std::endl; }
            };

            PLUGIN_FUNC IRenderer *CreateRenderer()
            {
                        return new OpenGLRenderer();
            }
            PLUGIN_FUNC void DestroyRenderer(IRenderer *r)
            {
                        delete r;
            }
            PLUGIN_INIT()
            {
                        RegisterRenderer("opengl", CreateRenderer, DestroyRenderer);
                        return 0;
            }

  • PLUGIN_INIT() function, which will get run whenever the plugin is loaded. This registers our OpenGLRenderer factory function, CreateRenderer(), and the associated destruction function, DestroyRenderer(). 
  • These are both defined using PLUGIN_FUNC to ensure that they are exported correctly with C linkage.
  • The RegisterRenderer() function essentially just calls the RendererFactory::RegisterRenderer()

Reasons for an explicit registration function to the Plugin API rather than letting plugins register themselves directly with the RendererFactory. 

  • One reason is simply to give us a layer of abstraction so that you could change RendererFactory in the future without breaking existing plugins. 
  • Another reason is to avoid plugins calling methods that use STL strings: note that RegisterRenderer uses a const char * to specify the renderer name.

The Plugin Manager

  • Now that you have a Plugin API and you can build plugins against this API, you need to be able to load and register those plugins into the Core API. This is the role of the Plugin Manager. Specifically, the Plugin Manager needs to handle the following tasks.

Load metadata for all plugins
  • These metadata can either be stored in a separate file (such as an XML file) or be embedded within the plugins themselves. In the latter case, the Plugin Manager will need to load all available plugins to collate metadata for all plugins. These metadata let you present the user with a list of available plugins for them to choose between.

Load a dynamic library into memory 
  • provide access to the symbols in that library, and unload the library if necessary. This involves using dlopen(), dlclose(), and dlsym() on UNIX platforms (including Mac OS X) and LoadLibrary(), FreeLibrary(), and GetProcAddress() on Windows. 

Pugin’s initialization and cleanup 
  • Call the plugin’s initialization routine when the plugin is loaded, and call the cleanup routine when the plugin is unloaded. These functions are defined by PLUGIN_INIT() and PLUGIN_FREE() within the plugin.
  • Because the Plugin Manager provides a single point of access to all of the plugins in the system, it is often implemented as a singleton. In terms of design, the Plugin Manager can be thought of as a collection of Plugin Instances, where each Plugin Instance represents a single plugin and offers functionality to load and unload that plugin. Here is an example implementation for a Plugin Manager:

            // pluginmanager.cpp
            #include "defines.h"
            #include <string>
            #include <vector>
            class CORE_API PluginInstance
            {
            public:
                        explicit PluginInstance(const std::string &name);
                        ~PluginInstance();
                        bool Load();
                         bool Unload();
                        bool IsLoaded();
                        std::string GetFileName();
                        std::string GetDisplayName();
            private:
                        PluginInstance(const PluginInstance &);
                        const PluginInstance &operator =const PluginInstance &);
                        class Impl;
                        Impl *mImpl;
            };

            class CORE_API PluginManager
            {
            public:
                        static PluginManager &GetInstance();
                        bool LoadAll();
                        bool Load(const std::string &name);
                        bool UnloadAll();
                        bool Unload(const std::string &name);
                        std::vector<PluginInstance *> GetAllPlugins();
            private:
                        PluginManager();
                        ~PluginManager();
                        PluginManager(const PluginManager &);
                        const PluginManager &operator =(const PluginManager &);
                        std::vector<PluginInstance *> mPlugins;
            };

Metadata 

  • The above design decouples the ability to access metadata for all plugins from the need to load those plugins. That is, if metadata such as the plugin’s display name is stored in an external file, you can call PluginManager::GetAllPlugins() without loading the actual plugins.
  • However, if metadata is stored in the plugins, then GetAllPlugins() can simply call LoadAll() first. The following example presents a sample external metadata file based on an XML syntax:

                <?xml version="1.0" encoding=’UTF-8’?>
                                <plugins>
                                                <plugin filename="oglplugin">
                                                <name>OpenGL Renderer</name>
                                </plugin>
                                 <plugin filename="dxplugin">
                                                <name>DirectX Renderer</name>
                                </plugin>
                                <plugin filename="mesaplugin">
                                                <name>Mesa Renderer</name>
                                </plugin>
                </plugins>

  • Irrespective of the approach to storing plugin metadata within an external file or embedded within each plugin, the following code outputs the display name for all available plugins:
                std::vector<PluginInstance *> plugins = PluginManager::GetInstance().GetAllPlugins();
                std::vector<PluginInstance *>::iterator it;
               
               for (it = plugins.begin(); it != plugins.end();++it)
                {
                                PluginInstance *pi = *it;
                                std::cout << "Plugin: " << pi->GetDisplayName() << std::endl;
                }

References 

  • API Design for C++ Book by Martin Reddy

No comments:

Post a Comment

LeetCode C++ Cheat Sheet June

🎯 Core Patterns & Representative Questions 1. Arrays & Hashing Two Sum – hash map → O(n) Contains Duplicate , Product of A...