doc: rework the SPA plugin documentation

Add more info to the main SPA page and split the design vs plugin pages up,
together with some more documentation to ideally lower make this easier to
understand on a glance.

Most of the actual plugin loading documentation are unmodified.
This commit is contained in:
Peter Hutterer 2021-06-24 10:49:22 +10:00 committed by Wim Taymans
parent 8a25076c4e
commit 0f0565175e
5 changed files with 421 additions and 317 deletions

View file

@ -64,6 +64,7 @@ extra_docs = [
'tutorial5.dox',
'tutorial6.dox',
'spa-index.dox',
'spa-plugins.dox',
'spa-design.dox',
'spa-pod.dox',
'spa-buffer.dox',

View file

@ -1,37 +1,5 @@
/** \page page_spa_design SPA Design
SPA (Simple Plugin API) is an extensible API to implement all kinds of plugins.
It is inspired by many other plugin APIs, mostly LV2 and GStreamer.
Plugins are dynamically loadable objects that contain objects and interfaces that
can be introspected and used at runtime in any application.
SPA provides the following functionality:
- enumeration of object factories and the interfaces provided by the objects
- creation of objects (AKA a handle)
- retrieve interfaces to perform actions on the objects
SPA was designed with the following goals in mind:
- No dependencies, SPA is shipped as a set of header files that have no dependencies
except for the standard c library.
- Very efficient both in space and in time.
- Very configurable and usable in many different environments. All aspects of
the plugin environment can be configured and changed, like logging, poll loops,
system calls etc.
- Consistent API
- Extensible, new API can be added with minimal effort, existing API can be
updated and versioned.
The original user of SPA is PipeWire, which uses SPA to implement the low-level
multimedia processing plugins, device detection, mainloops, CPU detection and
logging, among other things. SPA however can be used outside of PipeWire with
minimal problems.
This document introduces the basic concepts of SPA plugins. It first covers using
the API and then talks about implementing new Plugins.
# Conventions
## Types
@ -61,288 +29,7 @@ event.
## Useful macros
SPA comes with some useful macros defined in `<spa/utils/defs.h>`.
# SPA Plugin
The SPA plugin is the starting point for the API. A plugin is an OS specific
shared object that needs to be loaded/opened in an OS specific way. 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.
## 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 2 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:
\code{.c}
#define SPA_PLUGIN_PATH /usr/lib64/spa-0.2/"
void *hnd = dlopen(SPA_PLUGIN_PATH"/support/libspa-support.so", RTLD_NOW);
\endcode
The environment variable `SPA_PLUGIN_PATH` is 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 has (should 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:
\code{.c}
spa_handle_factory_enum_func_t enum_func;
enum_func = dlsym(hnd, SPA_HANDLE_FACTORY_ENUM_FUNC_NAME));
\endcode
If this symbol is not available, this is not a valid SPA plugin.
## Enumerating factories
With the `enum_func` we can now enumerate all the factories in the plugin:
\code{.c}
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.
}
\endcode
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:
\code{.c}
#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
\endcode
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:
\code{.c}
struct spa_dict *extra_params = NULL;
size_t size = spa_handle_factory_get_size(factory, extra_params);
\endcode
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:
\code{.c}
handle = calloc(1, size);
spa_handle_factory_init(factory, handle,
NULL, // info
NULL, // support
0 // n_support
);
\endcode
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:
\code{.c}
void *iface;
spa_handle_get_interface(handle, SPA_NAME_SUPPORT_LOG, &iface);
\endcode
If this method succeeds, you can cast the `iface` variable to
`struct spa_log *` and start using the log interface methods.
\code{.c}
struct spa_log *log = iface;
spa_log_warn(log, "Hello World!\n");
\endcode
## 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.
## 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:
\code{.c}
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);
\endcode
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:
\code{.c}
spa_hook_remove(&listener);
\endcode
## 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:
\code{.c}
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);
\endcode
Then perform the `enum_param` method:
\code{.c}
int res = spa_node_enum_params(node, 0, SPA_PARAM_EnumFormat, 0, MAXINT, NULL);
\endcode
This triggers the result event handler with a 0 sequence number for each
supported format. After this completes, remove the listener again:
\code{.c}
spa_hook_remove(&listener);
\endcode
### 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:
\code{.c}
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
...
}
\endcode
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:
\code{.c}
expected_seq = SPA_RESULT_ASYNC_SEQ(res);
\endcode
# Implementing a new plugin
FIXME
SPA comes with some useful macros defined in `<spa/utils/defs.h>` and a
number of utility functions, see \ref spa_utils
*/

View file

@ -1,10 +1,77 @@
/** \page page_spa SPA (Simple Plugin API)
SPA (Simple Plugin API) is an extensible API to implement all kinds of
plugins. It is inspired by many other plugin APIs, mostly LV2 and
GStreamer.
plugins.
It is inspired by many other plugin APIs, mostly LV2 and
GStreamer. SPA provides two parts:
- a header-only API with no external dependencies
- a set of support libraries ("plugins") for commonly used functionality
The usual approach is that PipeWire and PipeWire clients can use the
header-only functions to interact with the plugins. Those plugins are
usually loaded at runtime (through `dlopen(3)`.
## Motivation
SPA was designed with the following goals in mind:
- No dependencies, SPA is shipped as a set of header files that have no dependencies except for the standard c library.
- Very efficient both in space and in time.
- Very configurable and usable in many different environments. All aspects
of the plugin environment can be configured and changed, like logging,
poll loops, system calls etc.
- Consistent API
- Extensible, new API can be added with minimal effort, existing API can be updated and versioned.
The original user of SPA is PipeWire, which uses SPA to implement the
low-level multimedia processing plugins, device detection, mainloops, CPU
detection and logging, among other things. SPA however can be used outside
of PipeWire with minimal problems.
## The SPA header-only API
A very simple example on how SPA headers work are the \ref spa_utils, a set
of utilities commonly required by C projects. SPA functions use the `spa_`
namespace and are easy to identify.
\code
/* cc $(pkg-config --cflags libspa-0.2) -o spa-test spa-test.c */
#include <stdint.h>
#include <spa/utils/string.h>
int main(int argc, char **argv) {
uint32_t val;
if (spa_atoi32(argv[1], &val, 16))
printf("argv[1] is hex %#x\n", val);
else
printf("argv[1] is not a hex number\n");
return 0;
}
\endcode
## SPA Plugins
SPA plugins are shared libraries (`.so` files) that can be loaded at
runtime. Each library provides one or more "factories", each of which may
implement several "interfaces". Code that uses SPA plugins then uses those
interfaces (through SPA header files) to interact with the plugin.
For example, the PipeWire daemon can load the normal `printf`-based logger
or a systemd journal-based logger. Both of those provide the \ref spa_log
interface and once instantiated, PipeWire no longer has to differentiate
between the two logging facilities.
Please see \ref page_spa_plugins for the details on how to use SPA plugins.
## Further details
- \subpage page_spa_design
- \subpage page_spa_plugins
- \subpage page_spa_pod
- \subpage page_spa_buffer

347
doc/spa-plugins.dox Normal file
View file

@ -0,0 +1,347 @@
/** \page page_spa_plugins SPA Plugins
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:
\code{.py}
handle = dlopen("$SPA_PLUGIN_PATH/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")
\endcode
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:
\verbatim
$ export SPA_PLUGIN_PATH=$(pkg-config --variable plugindir libspa-0.2)
$ spa-inspect ${SPA_PLUGIN_PATH}/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'
...
\endverbatim
## 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 2 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:
\code{.c}
#define SPA_PLUGIN_PATH /usr/lib64/spa-0.2/"
void *hnd = dlopen(SPA_PLUGIN_PATH"/support/libspa-support.so", RTLD_NOW);
\endcode
The environment variable `SPA_PLUGIN_PATH` 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:
\code{.c}
spa_handle_factory_enum_func_t enum_func;
enum_func = dlsym(hnd, SPA_HANDLE_FACTORY_ENUM_FUNC_NAME));
\endcode
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:
\code{.c}
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.
}
\endcode
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:
\code{.c}
#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
\endcode
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:
\code{.c}
struct spa_dict *extra_params = NULL;
size_t size = spa_handle_factory_get_size(factory, extra_params);
\endcode
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:
\code{.c}
handle = calloc(1, size);
spa_handle_factory_init(factory, handle,
NULL, // info
NULL, // support
0 // n_support
);
\endcode
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:
\code{.c}
void *iface;
spa_handle_get_interface(handle, SPA_NAME_SUPPORT_LOG, &iface);
\endcode
If this method succeeds, you can cast the `iface` variable to
`struct spa_log *` and start using the log interface methods.
\code{.c}
struct spa_log *log = iface;
spa_log_warn(log, "Hello World!\n");
\endcode
## 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.
## 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:
\code{.c}
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);
\endcode
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:
\code{.c}
spa_hook_remove(&listener);
\endcode
## 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:
\code{.c}
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);
\endcode
Then perform the `enum_param` method:
\code{.c}
int res = spa_node_enum_params(node, 0, SPA_PARAM_EnumFormat, 0, MAXINT, NULL);
\endcode
This triggers the result event handler with a 0 sequence number for each
supported format. After this completes, remove the listener again:
\code{.c}
spa_hook_remove(&listener);
\endcode
### 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:
\code{.c}
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
...
}
\endcode
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:
\code{.c}
expected_seq = SPA_RESULT_ASYNC_SEQ(res);
\endcode
# Implementing a new plugin
FIXME
*/

View file

@ -174,6 +174,8 @@ static inline void spa_log_trace_fp (struct spa_log *l, const char *format, ...)
#endif
/** \fn spa_log_error foo */
/** keys can be given when initializing the logger handle */
#define SPA_KEY_LOG_LEVEL "log.level" /**< the default log level */
#define SPA_KEY_LOG_COLORS "log.colors" /**< enable colors in the logger */