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:
carb::graphics::IGraphics
interface abstracts the low level graphics API. It has two implementations:carb.graphics-direct3d.plugin
andcarb.graphics-vulkan.plugin
. There could be multiple plugins which usecarb::graphics::IGraphics
interface and even expose and pass pointers across the interface boundaries. For example one plugin can create aFramebuffer
and pass it to another plugin which will use it internally. In this case we want to be sure that both plugins acquire the samecarb::graphics::IGraphics
implementation.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 callloadPlugins
on some folder, load all dynamic libraries, ask for plugins which implementITextEditorExtension
(usingFramework::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.