mirror of
https://gitlab.freedesktop.org/pipewire/pipewire
synced 2024-09-06 00:40:34 +00:00
docs: add some doc about SPA design
This commit is contained in:
parent
2b7e6e19dc
commit
8696ad4480
243
doc/spa/design.md
Normal file
243
doc/spa/design.md
Normal file
|
@ -0,0 +1,243 @@
|
|||
# SPA Design
|
||||
|
||||
SPA (Simple Plugin API) is an extensible API to implement alls 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 dependecies
|
||||
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.
|
||||
|
||||
# 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:
|
||||
|
||||
```c
|
||||
#define SPA_PLUGIN_PATH /usr/lib64/spa-0.2/"
|
||||
void *hnd = dlopen(SPA_PLUGIN_PATH"/support/libspa-support.so", RTLD_NOW);
|
||||
```
|
||||
|
||||
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:
|
||||
|
||||
```c
|
||||
spa_handle_factory_enum_func_t enum_func;
|
||||
enum_func = dlsym(hnd, SPA_HANDLE_FACTORY_ENUM_FUNC_NAME));
|
||||
```
|
||||
|
||||
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:
|
||||
|
||||
```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. */
|
||||
}
|
||||
```
|
||||
|
||||
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:
|
||||
|
||||
```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 */
|
||||
```
|
||||
|
||||
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:
|
||||
|
||||
```c
|
||||
size_t size = spa_handle_factory_get_size(factory, NULL /* extra params */);
|
||||
```
|
||||
|
||||
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:
|
||||
|
||||
```c
|
||||
handle = calloc(1, size);
|
||||
spa_handle_factory_init(factory, handle,
|
||||
NULL, /* info */
|
||||
NULL, /* support */
|
||||
0 /* n_support */);
|
||||
```
|
||||
|
||||
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 and interface
|
||||
|
||||
When a SPA handle is made, you can retrieve any of the interfaces that
|
||||
it provides:
|
||||
|
||||
```c
|
||||
void *iface;
|
||||
spa_handle_get_interface(handle, SPA_NAME_SUPPORT_LOG, &iface);
|
||||
```
|
||||
|
||||
If this method succeeds, you can cast the `iface` variable to
|
||||
`struct spa_log *` and start using the log interface methods.
|
||||
|
||||
## 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 apropriate 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) to
|
||||
receive a set 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:
|
||||
|
||||
```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);
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
You can remove your listener with:
|
||||
|
||||
```c
|
||||
spa_hook_remove(&listener);
|
||||
```
|
||||
|
||||
## 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.
|
||||
|
||||
|
||||
## Asynchronous results
|
||||
|
||||
Asynchronous results are pushed to the application in the same way as
|
||||
synchronous results, they are just pushed later.
|
||||
|
||||
|
||||
|
||||
# Implementing a new plugin
|
Loading…
Reference in a new issue