Framework, Plugins, Interfaces

The key ingredient of Carbonite is what we call carb::Framework. It is the core entity which manages plugins and their interfaces. This guide describes key concepts of Carbonite.

Key Concepts

The Carbonite SDK is centered around the following concepts:

  • ABI-Stable Interfaces

  • Plugins

  • Versions

  • Client

In the following sections, we cover these concepts in detail.

Interface

Carbonite is built on versioned interfaces. An interface, in general terms, is a well-defined boundary across which plugins exchange information. All the connections between plugins and the application go through interfaces. In practice, an interface is just a struct of function pointers.

struct MyInterface
{
    CARB_PLUGIN_INTERFACE("my::MyInterface", 1, 0)

    size_t (*getItemCount)();
};

The example above declares an interface, my::MyInterface, at version 1.0. It contains one interface function: getItemCount. The necessary call to the CARB_PLUGIN_INTERFACE inserts boiler-plate code needed by carb::Framework to identify the struct as an interface.

Warning

This document presents Carbonite Interfaces, an interface strategy implemented early in Omniverse’s history. As time passed, Omniverse moved to using Omniverse Native Interfaces, whose design embraced the lessons learned from Carbonite interfaces.

New interfaces should use Omniverse Native Interfaces, not the interfaces outlined in this doc. This document is provided for historical reasons.

Plugin

A plugin is an entity which provides an implementation of one or more interfaces. In practice, the most common plugins are implemented as shared libraries (.dll/.so files), though statically complied plugins are also supported.

Plugin Name

Each plugin has a author assigned name. This name must be globally unique. The name can match the interface name if only one implementation exists. If you have multiple implementations of the same interface, the general rule is to uniquely name each plugin by adding a qualifier after the interface name. This approach also results in the restriction that you can’t have multiple implementations of the same interface in the same plugin.

Framework

The Carbonite carb::Framework is the central system which handles plugins and interfaces. It allows the user to load plugins, acquire existing interfaces, write new plugins, and check interface versions. It encapsulates the dirty work and everything else is built on top of it.

The Carbonite carb::Framework is implemented in carb.dll (Windows) and libcarb.so (Linux). In the spirit of Carbonite, it has its own versioned interface in Framework.h.

Note that this dynamic library is not a Carbonite plugin. Furthermore, carb::Framework interface, even though similar, is not a Carbonite interface. The Carbonite carb::Framework is a fundamental piece that needs to be loaded first before you can take advantage of Carbonite interfaces and plugins.

Version

We use semantic versioning. For interfaces, only major and minor versions are used. Patch version is stored inside of a plugin, as PluginImplDesc::build string. Version is stored on interface structs as compile time information. Notice CARB_PLUGIN_INTERFACE on MyInterface above.

To understand how this works, let’s consider a typical scenario.

Let’s say we have an application which was compiled against a particular version of interface IFoo, say ver. 1.2. That means that 1.2 is actually compiled into the application binary.

When this application will receive a pointer to an IFoo, it expects certain function pointers in certain places.

When a plugin which implements IFoo is loaded by carb::Framework it knows which version of the interface it provides. So when we request IFoo of ver 1.2 from Carbonite, it will check against the actual version(s) it has. Major versions must match, while the minor versions of the provided interface must be equal or higher. Essentially this means that when you ask for version 1.2 of IFoo, carb::Framework is allowed to return 1.4, because higher minor version is backwards compatible with lower minor versions within the same major version. An example of a backwards compatible change is to add new functions at the end of the interface struct.

This means that as long as you acquire interfaces from carb::Framework you are safe.

Warning

You shouldn’t pass an interface acquired in one plugin as a pointer to another plugin. Such usage of interfaces could lead to undefined behavior when those plugins were built against different versions of that interface.

The general rule is to acquire interfaces from carb::Framework. As long as the interface was properly versioned, you can drop in different releases of plugins (.dll/.so) into your folder and expect carb::Framework to do all the verification.

Warning

See Omniverse Native Interfaces for a detailed description on how the Carbonite Interface’s versioning scheme presented in this document is broken in practice.

Client

A client is any entity which uses the same instance of carb::Framework. This includes:

  • Plugin

  • Application

  • Other shared libraries (e.g. scripting bindings)

The client name allows carb::Framework to uniquely identify clients and store which client acquired which interface. For plugins, the plugin name is used as the client name. For applications and bindings, it is recommended to come up with a reasonable unique name as the client name.

Starting Carbonite

The core plugin loading framework in Carbonite is found in carb.dll. It is up to you to link, either implicitly or explicitly, to carb.dll.

The Carbonite carb::Framework must be initialized. While there are a myriad of ways to do this, it’s recommended to call the following in main.cpp:

#include <omni/OmniInit.h>

OMNI_APP_GLOBALS("example.helloworld", "Greets the world from Omniverse.");

int main(int argc, char** argv)
{
    OMNI_CORE_INIT(argc, argv);

    carb::Framework* framework = carb::getFramework();

    // make the world a better place

    return 0;
}

Note

Developers may encounter older applications using the startup sequence below. It’s recommended to transition to the initialization code above.

#include <carb/StartupUtils.h>

CARB_GLOBALS_EX("example.helloworld", "Example application.");

int main(int argc, char** argv)
{
    carb::Framework* framework = carb::acquireFrameworkAndRegisterBuiltins();

    carb::StartupFrameworkDesc startupParams = carb::StartupFrameworkDesc::getDefault();
    startupParams.argv = argv;
    startupParams.argc = argc;
    carb::startupFramework(startupParams);

    // make the world a better place

    carb::shutdownFramework();
    carb::releaseFrameworkAndDeregisterBuiltins();

    return 0;
}

By default, plugins located in the same directory as the executable are loaded into the process space of the application. This behavior can be changed via configuration files or explicit code.

Once carb::Framework is loaded, you can “acquire” interfaces. As an example, let’s acquire the IInput interface and start using it. This is done with the carb::Framework::acquireInterface function.

carb::input::IInput* input = framework->acquireInterface<carb::input::IInput>();

It may surprise you that we use a template function, while an interface is just a struct with function pointers. What actually happens is that we use a header only helper function (carb::Framework::acquireInterface which calls the actual interface function: carb::Framework::acquireInterfaceWithClient(const char*).

You can call carb::Framework::acquireInterfaceWithClient(const char*) directly, but it will be a bit more verbose. You need to provide the interface name, version and the name of the client who calls it. Since all the interface details are compiled into interface macro CARB_PLUGIN_INTERFACE, and client name is usually stored in a global variable, we can take advantage and simplify the code.

It is important to realize that this header only code is compiled into the client (an application in our case) so it doesn’t break our binary compatibility if it changes. The key here is that the inline code calls into the interface’s ABI safe function pointer.

When we call carb::Framework::acquireInterface, carb::Framework will search among registered plugins for a plugin which implements the requested interface of compatible version. It will load the plugin (if it was not yet loaded), which will trigger initialization of a plugin and carbOnPluginStartup call. Thanks to the client name passed into the acquire function, carb::Framework will know the client that acquired this interface and make sure it won’t be unloaded until all clients that have acquired will explicitly release the interface.

If carb::Framework couldn’t find a matching plugin with the interface requested, it will log an error and return nullptr. If you don’t want this case to be considered an error, you can use carb::Framework::tryAcquireInterface instead. It returns nullptr without logging any errors.

At this point we’ve covered the minimal code required to actually load your first plugin and use it. Next we will go into more advanced topics, starting with how we handle the scenario of multiple plugins implementing the same interface.

Default Plugins

It is a totally valid situation to have multiple implementations of the same interface. But there are different use cases for that. Lets consider two scenarios:

  1. carb::graphics::IGraphics interface abstracts the low level graphics API. It has two implementations: carb.graphics-direct3d.plugin and carb.graphics-vulkan.plugin. There could be multiple plugins which use carb::graphics::IGraphics interface and even expose and pass pointers across the interface boundaries. For example one plugin can create a Framebuffer and pass it to another plugin which will use it internally. In this case we want to be sure that both plugins acquire the same carb::graphics::IGraphics implementation.

  2. Imagine we have an interface ITextEditorExtension, whose main purpose is to extend a text editor with dynamic modules (plugins in a common sense). What we probably want to do is to call loadPlugins on some folder, load all dynamic libraries, ask for plugins which implement ITextEditorExtension (using Framework::getCompatiblePlugins) and explicitly control which ones to use and how. We could have a UI which lists all extensions and turns them on and off.

For the first scenario we have a concept called default plugins. When you call acquire interface for the carb::graphics::IGraphics interface for the first time:

carb::graphics::IGraphics* graphics = framework->acquireInterface<carb::graphics::IGraphics>();

it selects one of the interface implementations and locks the selection. You can control which one would be selected with Framework::setDefaultPluginEx function. It must be called before first acquire call happens. The default plugin cannot be changed until all clients that acquired this interface have released it (or they have been unloaded).

For the second scenario you can bypass this mechanism by explicitly passing the plugin name into acquire function.

ITextEditorExtension* ext = framework->acquireInterface<ITextEditorExtension>("text-colorizer.plugin");

This call will give you what you want and won’t trigger or use default plugin locking.

carb::Framework::tryAcquireInterface works like carb::Framework::acquireInterface but doesn’t produce an error when nullptr is returned.

Plugin Dependencies

Every plugin can specify interfaces (names and versions) it depends on. They are listed in the PluginRegistryEntry, but typically specified with macro in a plugin:

CARB_PLUGIN_IMPL_DEPS(carb::assets::IAssets, carb::imaging::IImaging)

This enables carb::Framework to check if there are registered plugins which implement those dependent interfaces before loading and starting your plugin. That allows failing early and catching errors before you will actually call carb::Framework::acquireInterface and use the interface somewhere later in code.

We also have a command line tool plugin.inspector, which for a particular plugin (.dll/.so file) can list which interfaces it depends on. Here is an example of such command output on carb.assets.plugin.dll:

plugin.inspector.exe carb.assets.plugin.dll

Its output:

{
    "carb.assets.plugin": {
        "name": "carb.assets.plugin",
        "path": "d:/Projects/Carbonite/_build/windows-x86_64/debug/carb.assets.plugin.dll",
        "description": "Assets.",
        "author": "NVIDIA",
        "hot-reload": "Disabled",
        "build": "dev",
        "interfaces": [
            {
                "name": "carb::assets::IAssets",
                "major": 0,
                "minor": 2
            }
        ],
        "dependencies": [
            {
                "name": "carb::tasking::ITasking",
                "major": 0,
                "minor": 1
            },
            {
                "name": "carb::datasource::IDataSource",
                "major": 0,
                "minor": 3
            }
        ]
    }
}

Here we can see that carb.assets.plugin.dll uses the ITasking and IDataSource interfaces. It should be noted that this list is based on the plugin implementer listing out the dependencies, it is therefore not guaranteed to be correct. When a plugin is loaded Carbonite will complain if the plugin ever acquires interfaces that weren’t listed - but this is still an honor system.

Note

If you call framework->acquireInterface<IFoo>() but haven’t listed IFoo as a dependency, carb::Framework will produce a warning. acquireInterface expresses your intent to always get a valid interface.

If you can handle the absence of the requested interface, and it is valid scenario, use framework->tryAcquireInterface. This also means that the dependency should not be listed as a required dependency.

The important takeaway here is that dependencies are per plugin. Different implementations for the same interface can have different dependencies.

Circular dependencies are disallowed and checked for.

Releasing Plugins

When you acquired a particular interface, carb::Framework tracks this using client name. Usually you don’t need to explicitly release an interface you use. When the plugin is unloaded, all interfaces it acquired are released.

That being said, you can explicitly release with the carb::Framework::releaseInterface function when an interface is not needed anymore.

When an interface was acquired by a client this information is stored in carb::Framework. Multiple acquire calls do not add up. This means that:

IFoo* foo = framework->acquireInterface<IFoo>();
IFoo* foo2 = framework->acquireInterface<IFoo>();
IFoo* foo3 = framework->acquireInterface<IFoo>();
framework->releaseInterface(foo);

will release all foo, foo2, foo3, which actually all contain the same pointer value.

Remember that a plugin can implement multiple interfaces. Every interface can be used by multiple clients. Once all interfaces are released by all clients (explicitly or automatically) the plugin is unloaded.

Reloading

From the previous, section one takeaway is that you can actually explicitly reload a plugin if no one else is using it.

You can acquire an interface, this will load a plugin (call LoadLibrary() / dlopen()). Then when you release it will unload the plugin. You can now replace the .dll/.so file with another one and acquire again. The carb::Framework will do all the checks: maybe new plugin contains other interfaces, or other versions of interfaces etc. And will provide you with new valid interface if possible.

This explicit reloading can be used for example for text editor extensions we mentioned before. The nice thing about this approach is that as a plugin implementer you don’t care about reloading. It is interface user’s job to properly release and recreate all the data.

Static Plugins

Besides plugins as shared libraries, one can also register a static plugin with carb::Framework::registerPlugin. The mechanism is exactly the same, except instead of exporting certain functions (like OnPluginRegisterFn) you are passing function pointers.

This allows you to put all your plugins into a single binary, mock and replace certain plugins thus providing more flexibility.

It is important to note that carb::Framework doesn’t control the lifetime of static plugins (doesn’t load, unload or track as dynamic plugins). So if your register a static plugin from your plugin, but your plugin will be unloaded, those static plugins will become invalid. You would need to call carb::Framework::unregisterPlugin for your static plugin to avoid this catastrophe.