PipeWire: SPA Plugins

PipeWire 1.6.8

Loading...
Searching...
No Matches
SPA Plugins

SPA plugins are dynamically loadable objects that contain objects and interfaces that can be introspected and used at runtime in any application.

This document introduces the basic concepts of SPA plugins. It first covers using the API and then talks about implementing new plugins.

# Outline

To use a plugin, the following steps are required:

- Load the shared library.
- Enumerate the available factories.
- Enumerate the interfaces in each factory.
- Instantiate the desired interface.
- Use the interface-specific functions.

In pseudo-code, loading a logger interface looks like this:

handle = dlopen("$SPA_PLUGIN_DIR/support/libspa-support.so")
factory_enumeration_func = dlsym(handle, SPA_HANDLE_FACTORY_ENUM_FUNC_NAME)
spa_log *logger = NULL
while True:
factory = get_next_factory(factory_enumeration_func):
if factory != SPA_NAME_SUPPORT_LOG: # <spa/utils/name.h>
continue
interface_info = get_next_interface_info(factory)
if info->type != SPA_TYPE_INTERFACE_Log: # </spa/support/log.h>
continue
interface = spa_load_interface(handle, interface_info->type)
logger = (struct spa_log *)interface
break
spa_log_error(log, "This is an error message\n")
spa_log_error
#define spa_log_error(l,...)
Definition log.h:309

SPA does not specify where plugins need to live, although plugins are normally installed in /usr/lib64/spa-0.2/ or equivalent. Plugins and API are versioned and many versions can live on the same system.

Note
  The directory the SPA plugins reside in is available through pkg-config --variable plugindir libspa-0.2

The spa-inspect tool provides a CLI interface to inspect SPA plugins:

    $ export SPA_PLUGIN_DIR=$(pkg-config --variable plugindir libspa-0.2)
    $ spa-inspect ${SPA_PLUGIN_DIR}/support/libspa-support.so
    ...
    factory version:                1
    factory name:           'support.cpu'
    factory info:
      none
    factory interfaces:
     interface: 'Spa:Pointer:Interface:CPU'
    factory instance:
     interface: 'Spa:Pointer:Interface:CPU'
    skipping unknown interface
    factory version:                1
    factory name:           'support.loop'
    factory info:
      none
    factory interfaces:
     interface: 'Spa:Pointer:Interface:Loop'
     interface: 'Spa:Pointer:Interface:LoopControl'
     interface: 'Spa:Pointer:Interface:LoopUtils'
    ...

# Open A Plugin

A plugin is opened with a platform specific API. In this example we use dlopen() as the method used on Linux.

A plugin always consists of two parts, the vendor path and then the .so file.

As an example we will load the "support/libspa-support.so" plugin. You will usually use some mapping between functionality and plugin path as we'll see later, instead of hardcoding the plugin name.

To dlopen a plugin we then need to prefix the plugin path like this:

#define SPA_PLUGIN_DIR /usr/lib64/spa-0.2/"
void *hnd = dlopen(SPA_PLUGIN_DIR"/support/libspa-support.so", RTLD_NOW);

The environment variable SPA_PLUGIN_DIR and pkg-config variable plugindir are usually used to find the location of the plugins. You will have to do some more work to construct the shared object path.

The plugin must have exactly one public symbol, called spa_handle_factory_enum, which is defined with the macro SPA_HANDLE_FACTORY_ENUM_FUNC_NAME to get some compile time checks and avoid typos in the symbol name. We can get the symbol like so:

spa_handle_factory_enum_func_t enum_func;
enum_func = dlsym(hnd, SPA_HANDLE_FACTORY_ENUM_FUNC_NAME));
spa_handle_factory_enum_func_t
int(* spa_handle_factory_enum_func_t)(const struct spa_handle_factory **factory, uint32_t *index)
The function signature of the entry point in a plugin.
Definition plugin.h:222
SPA_HANDLE_FACTORY_ENUM_FUNC_NAME
#define SPA_HANDLE_FACTORY_ENUM_FUNC_NAME
Definition plugin.h:226

If this symbol is not available, the library is not a valid SPA plugin.

# Enumerating Factories

With the enum_func we can now enumerate all the factories in the plugin:

uint32_t i;
const struct spa_handle_factory *factory = NULL;
for (i = 0;;) {
if (enum_func(&factory, &i) <= 0)
break;
// check name and version, introspect interfaces,
// do something with the factory.
}
spa_handle_factory
Definition plugin.h:119

A factory has a version, a name, some properties and a couple of functions that we can check and use. The main use of a factory is to create an actual new object from it.

We can enumerate the interfaces that we will find on this new object with the spa_handle_factory_enum_interface_info() method. Interface types are simple strings that uniquely define the interface (see also the type system).

The name of the factory is a well-known name that describes the functionality of the objects created from the factory. <spa/utils/names.h> contains definitions for common functionality, for example:

#define SPA_NAME_SUPPORT_CPU "support.cpu" // A CPU interface
#define SPA_NAME_SUPPORT_LOG "support.log" // A Log interface
#define SPA_NAME_SUPPORT_DBUS "support.dbus" // A DBUS interface

Usually the name will be mapped to a specific plugin. This way an alternative compatible implementation can be made in a different library.

# Making A Handle

Once we have a suitable factory, we need to allocate memory for the object it can create. SPA usually does not allocate memory itself but relies on the application and the stack for storage.

First get the size of the required memory:

struct spa_dict *extra_params = NULL;
size_t size = spa_handle_factory_get_size(factory, extra_params);
spa_handle_factory_get_size
SPA_API_PLUGIN size_t spa_handle_factory_get_size(const struct spa_handle_factory *object, const struct spa_dict *params)
Definition plugin.h:192
spa_dict
Definition dict.h:51

Sometimes the memory can depend on the extra parameters given in _get_size(). Next we need to allocate the memory and initialize the object in it:

handle = calloc(1, size);
spa_handle_factory_init(factory, handle,
NULL, // info
NULL, // support
0 // n_support
);
spa_handle_factory_init
SPA_API_PLUGIN int spa_handle_factory_init(const struct spa_handle_factory *object, struct spa_handle *handle, const struct spa_dict *info, const struct spa_support *support, uint32_t n_support)
Definition plugin.h:198

The info parameter should contain the same extra properties given in spa_handle_factory_get_size().

The support parameter is an array of struct spa_support items. They contain a string type and a pointer to extra support objects. This can be a logging API or a main loop API for example. Some plugins require certain support libraries to function.

# Retrieving An Interface

When a SPA handle is made, you can retrieve any of the interfaces that it provides:

void *iface;
spa_handle_get_interface(handle, SPA_NAME_SUPPORT_LOG, &iface);
spa_handle_get_interface
SPA_API_PLUGIN int spa_handle_get_interface(struct spa_handle *object, const char *type, void **iface)
Definition plugin.h:74
SPA_NAME_SUPPORT_LOG
#define SPA_NAME_SUPPORT_LOG
A Log interface.
Definition names.h:32

If this method succeeds, you can cast the iface variable to struct spa_log * and start using the log interface methods.

struct spa_log *log = iface;
spa_log_warn(log, "Hello World!\n");
spa_log_warn
#define spa_log_warn(l,...)
Definition log.h:310
spa_log
Definition log.h:71
spa_log::iface
struct spa_interface iface
Definition log.h:76

# Clearing An Object

After you are done with a handle you can clear it with spa_handle_clear() and you can unload the library with dlclose().

# SPA Interfaces

We briefly talked about retrieving an interface from a plugin in the previous section. Now we will explore what an interface actually is and how to use it.

When you retrieve an interface from a handle, you get a reference to a small structure that contains the type (string) of the interface, a version and a structure with a set of methods (and data) that are the implementation of the interface. Calling a method on the interface will just call the appropriate method in the implementation.

Interfaces are defined in a header file (for example see <spa/support/log.h> for the logger API). It is a self contained definition that you can just use in your application after you dlopen() the plugin.

Some interfaces also provide extra fields in the interface, like the log interface above that has the log level as a read/write parameter.

See spa_interface for some implementation details on interfaces.

# SPA Events

Some interfaces will also allow you to register a callback (a hook or listener) to be notified of events. This is usually when something changed internally in the interface and it wants to notify the registered listeners about this.

For example, the struct spa_node interface has a method to register such an event handler like this:

static void node_info(void *data, const struct spa_node_info *info)
{
printf("got node info!\n");
}
static struct spa_node_events node_events = {
SPA_VERSION_NODE_EVENTS,
.info = node_info,
};
struct spa_hook listener;
spa_zero(listener);
spa_node_add_listener(node, &listener, &node_event, my_data);
spa_node_add_listener
SPA_API_NODE int spa_node_add_listener(struct spa_node *object, struct spa_hook *listener, const struct spa_node_events *events, void *data)
Definition node.h:713
SPA_VERSION_NODE_EVENTS
#define SPA_VERSION_NODE_EVENTS
Definition node.h:188
spa_zero
#define spa_zero(x)
Definition defs.h:512
spa_hook
A hook, contains the structure with functions and the data passed to the functions.
Definition hook.h:427
spa_node_events
events from the spa_node.
Definition node.h:186
spa_node_info
Node information structure.
Definition node.h:58

You make a structure with pointers to the events you are interested in and then use spa_node_add_listener() to register a listener. The struct spa_hook is used by the interface to keep track of registered event listeners.

Whenever the node information is changed, your node_info method will be called with my_data as the first data field. The events are usually also triggered when the listener is added, to enumerate the current state of the object.

Events have a version field, set to SPA_VERSION_NODE_EVENTS in the above example. It should contain the version of the event structure you compiled with. When new events are added later, the version field will be checked and the new signal will be ignored for older versions.

You can remove your listener with:

spa_hook_remove(&listener);
spa_hook_remove
SPA_API_HOOK void spa_hook_remove(struct spa_hook *hook)
Remove a hook.
Definition hook.h:468

# API Results

Some interfaces provide API that gives you a list or enumeration of objects/values. To avoid allocation overhead and ownership problems, SPA uses events to push results to the application. This makes it possible for the plugin to temporarily create complex objects on the stack and push this to the application without allocation or ownership problems. The application can look at the pushed result and keep/copy only what it wants to keep.

## Synchronous Results

Here is an example of enumerating parameters on a node interface.

First install a listener for the result:

static void node_result(void *data, int seq, int res,
uint32_t type, const void *result)
{
const struct spa_result_node_params *r =
(const struct spa_result_node_params *) result;
printf("got param:\n");
spa_debug_pod(0, NULL, r->param);
}
struct spa_hook listener = { 0 };
static const struct spa_node_events node_events = {
SPA_VERSION_NODE_EVENTS,
.result = node_result,
};
spa_node_add_listener(node, &listener, &node_events, node);
seq
uint32_t int seq
Definition core.h:432
r
uint32_t int int const char int r
Definition core.h:447
res
uint32_t int int res
Definition core.h:433
spa_debug_pod
SPA_API_DEBUG_POD int spa_debug_pod(int indent, const struct spa_type_info *info, const struct spa_pod *pod)
Definition pod.h:248
spa_result_node_params
the result of enum_params or port_enum_params.
Definition node.h:162

Then perform the enum_param method:

int res = spa_node_enum_params(node, 0, SPA_PARAM_EnumFormat, 0, MAXINT, NULL);
spa_node_enum_params
SPA_API_NODE int spa_node_enum_params(struct spa_node *object, int seq, uint32_t id, uint32_t start, uint32_t max, const struct spa_pod *filter)
Definition node.h:733
SPA_PARAM_EnumFormat
@ SPA_PARAM_EnumFormat
available formats as SPA_TYPE_OBJECT_Format
Definition param.h:33

This triggers the result event handler with a 0 sequence number for each supported format. After this completes, remove the listener again:

spa_hook_remove(&listener);

## Asynchronous Results

Asynchronous results are pushed to the application in the same way as synchronous results, they are just pushed later. You can check that a result is asynchronous by the return value of the enum function:

int res = spa_node_enum_params(node, 0, SPA_PARAM_EnumFormat, 0, MAXINT, NULL);
if (SPA_RESULT_IS_ASYNC(res)) {
// result will be received later
...
}
SPA_RESULT_IS_ASYNC
#define SPA_RESULT_IS_ASYNC(res)
Definition result.h:54

In the case of async results, the result callback will be called with the sequence number of the async result code, which can be obtained with:

expected_seq = SPA_RESULT_ASYNC_SEQ(res);
SPA_RESULT_ASYNC_SEQ
#define SPA_RESULT_ASYNC_SEQ(res)
Definition result.h:57

# Implementing A New Plugin

FIXME***