Feature guide » Loading and using plugins

Extending Magnum with additional functionality.

The base Magnum libraries contain math support, scene graph implementation and are able to interact with graphics and audio hardware. However, they don't provide any functions for dealing with specific file formats on their own, as there are simply too many formats to include all of them directly. Instead, the libraries provide plugin APIs using which you can load a plugin that's able to understand a particular file format. Plugins are distributed as additional libraries and loaded either dynamically from the filesystem or, in case of embedded systems that don't support dynamic loading, built statically into the executable. Depending on the target platform requirements, different set of plugins can be deployed for different platforms.

The core Magnum repository contains a few basic plugins for opening simple formats, such as TGA images or WAV audio files. These plugins are included because the file formats are so simple that no 3rd party libraries are needed for loading their contents and thus they are suitable for quick demos and prototyping without needing to deal with additional dependencies.

Additional plugins (such as importers for PNG and JPEG images, glTF scenes, TrueType fonts etc.) are available in the Magnum Plugins repository. Majority of these plugins depends on external libraries, thus not all of them might be available on all platforms.

Plugin interfaces

Plugin functionality is exposed via an interface that's common for all plugins of given type. Magnum provides these plugin interfaces:

  • Trade::AbstractImporter — importers for general 2D and 3D scene, mesh, material, texture, image and animation data. See *Importer classes in the Trade namespace for available importer plugins.
  • Trade::AbstractImageConverter — conversion among various image formats. See *ImageConverter classes in the Trade namespace for available image converter plugins.
  • Trade::AbstractSceneConverter — conversion among various scene formats, mesh optimization etc. See *SceneConverter classes in the Trade namespace for available scene converter plugins.
  • Text::AbstractFont — font loading and glyph layout. See *Font classes in the Text namespace for available font plugins.
  • Text::AbstractFontConverter — font and glyph cache conversion. See *FontConverter classes in the Text namespace for available font converter plugins.
  • Audio::AbstractImporter — importers for audio formats. See *Importer classes in the Audio namespace for available audio importer plugins.
  • ShaderTools::AbstractConverter — shader conversion, compilation, optimization and validation. See *Converter classes in the ShaderTools namespace for available shader converter plugins.

Loading and instantiating plugins

To load a plugin, you need to instantiate Corrade::PluginManager::Manager for given plugin interface, for example for the Trade::AbstractImporter. In order to find plugins on the filesystem, plugin interfaces have a bunch of plugin search paths hardcoded — see documentation of the pluginSearchPaths() functions provided by each plugin interface. It usually just works, but you can also pass a plugin directory to the manager constructor explicitly.

Once the plugin manager is instantiated, you can start creating instances of particular plugins. Keep in mind that the manager instance has to exist for whole lifetime of all plugin instances created using it.

{
    PluginManager::Manager<Trade::AbstractImporter> manager;
    Containers::Pointer<Trade::AbstractImporter> importer =
        manager.loadAndInstantiate("TgaImporter");
    if(!importer) Fatal{} << "Cannot load the TgaImporter plugin";

    // Use the plugin...

    /* At the end of the scope the importer instance gets deleted and then
       the manager automatically unloads the plugin on destruction */
}

Plugin dependencies

Some plugins have dependencies, for example the TinyGltfImporter plugin uses AnyImageImporter to load texture data. The dependency loading is done automatically, but in some cases it's across different plugin interfaces (for example the MagnumFont font* plugin depends on the TgaImporter importer plugin) — there you need to create manager instances for all required plugin interfaces and connect them together using registerExternalManager():

PluginManager::Manager<Trade::AbstractImporter> importerManager;
PluginManager::Manager<Text::AbstractFont> fontManager;
fontManager.registerExternalManager(importerManager);

// As a side effect TgaImporter is loaded by importerManager
fontManager.load("MagnumFont");

Static plugins

By default, on desktop systems at least, all plugins are built as dynamic ones, i.e. they are a separate binary which gets linked in at runtime. This is good for reducing memory consumption, as the code is loaded in memory only for the time it is actually needed. However, if you need to port to a platform which does not support dynamic linking or you simply want to have the plugin loaded at all times, you can use static plugins.

The plugins are built as static if the MAGNUM_BUILD_PLUGINS_STATIC CMake option is enabled (see Downloading and building and Downloading and building plugins for more information). The actual usage in the code is basically the same as above, but you need to explicitly find the plugin and link it into the executable. If you use CMake, it would look like this:

find_package(MagnumPlugins REQUIRED
    TinyGltfImporter
    StbTrueTypeFont)

add_executable(my-app my-app.cpp)
target_link_libraries(my-app PRIVATE
    MagnumPlugins::TinyGltfImporter
    MagnumPlugins::StbTrueTypeFont)

The only user-visible behavioral change in the code will be that PluginManager::AbstractManager::load() will return PluginManager::LoadState::Static instead of LoadState::Loaded, but there is no need to change anything in the C++ code — it will work for both dynamic and static case.

Plugin aliases and "any" plugins

There's usually more than one plugin available to achieve a particular goal — for example in order to open a PNG file, you can choose among PngImporter, StbImageImporter or DevIlImageImporter plugins. Rather than this being an unnecessary redundancy, it allows you to pick a particular performance / portability / feature tradeoff — a plugin with no external dependencies for a web build or, on the other hand, the fastest possible implementation for a tool that does heavy image processing.

To make this simpler in the code and defer the decision to the buildsystem or app deployment, all plugins that support a particular format provide a common alias — in case of PNG images, you can just load a "PngImporter" plugin and if PngImporter is not available, it will pick up any of the other candidates.

For greater control you can also use setPreferredPlugins(), giving a prioritized candidate list for a particular alias. This is especially useful in case of font plugins, where you can get advanced layout capabilities if the HarfBuzzFont plugin is available or at least a faster and smoother glyph rendering if you can get the FreeTypeFont plugin. If none of the suggestions is available, it falls back to whatever is left (which can be, for example, the StbTrueTypeFont plugin).

PluginManager::Manager<Text::AbstractFont> manager;
manager.setPreferredPlugins("TrueTypeFont", {"HarfBuzzFont", "FreeTypeFont"});

To make your life even simpler, there are various "any format" plugins — these detect format based on file extension or its contents and then proxy the rest of the work to a concrete plugin suited for given format:

Containers::Pointer<Trade::AbstractImporter> importer =
    manager.instantiate("AnyImageImporter");

/* Delegates to the DdsImporter plugin, if it's available */
importer->openFile("texture.dds");

Besides convenience for the end user, this allows the scene importer plugins to load arbitrary image formats without having to explicitly deal with and depend on multitude of image importer plugins.

So far, the following plugins have the "any format" ability:

Editing plugin-specific configuration

Because it's not possible for a general statically typed plugin API to expose all possible knobs and switches that a file format could support, the plugins have a possibility to supply additional configuration via the configuration() function. Plugins that offer such possibility then show how the default configuration looks like and document the purpose of each option. For example, in case of the AssimpImporter plugin you can toggle various import options and postprocessing steps — this is how a subset of its default configuration file looks like:

[configuration]

mergeAnimationClips=false

[configuration/postprocess]

PreTransformVertices=false

and this is how you can edit it using Utility::Configuration:

Containers::Pointer<Trade::AbstractImporter> importer =
    manager.instantiate("AssimpImporter");
importer->configuration().setValue("mergeAnimationClips", true);
importer->configuration().group("postprocess")->setValue("PreTransformVertices", true);

Besides affecting a single plugin instance, you can also change the configuration globally via PluginManager::PluginMetadata::configuration(). That will affect all subsequently created plugin instances. Resetting the global configuration back to the initial state can be done by recreating the plugin manager.

A special case is the "Any" plugins described above — these don't contain any implicit configuration on their own, but will propagate anything you set to the concrete plugin implementation. So you could do the above with "AnySceneImporter" as well, and if AssimpImporter would end up being used, it correctly receives the options you have set.

Developing your own plugins

See class documentation of particular plugin interfaces for more information about subclassing. The Corrade's plugin management tutorial contains more information about plugin compilation and registering.