diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 18:28:17 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 18:28:17 +0000 |
commit | 7a46c07230b8d8108c0e8e80df4522d0ac116538 (patch) | |
tree | d483300dab478b994fe199a5d19d18d74153718a /doc/spa-plugins.dox | |
parent | Initial commit. (diff) | |
download | pipewire-upstream.tar.xz pipewire-upstream.zip |
Adding upstream version 0.3.65.upstream/0.3.65upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'doc/spa-plugins.dox')
-rw-r--r-- | doc/spa-plugins.dox | 360 |
1 files changed, 360 insertions, 0 deletions
diff --git a/doc/spa-plugins.dox b/doc/spa-plugins.dox new file mode 100644 index 0000000..af14d5e --- /dev/null +++ b/doc/spa-plugins.dox @@ -0,0 +1,360 @@ +/** \page page_spa_plugins SPA Plugins + +\ref spa_handle "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: + +\code{.py} +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") +\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_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' +... +\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 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: + +\code{.c} +#define SPA_PLUGIN_DIR /usr/lib64/spa-0.2/" +void *hnd = dlopen(SPA_PLUGIN_DIR"/support/libspa-support.so", RTLD_NOW); +\endcode + +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: + +\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. + +See \ref 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: + +\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*** + + + +\addtogroup spa_handle + +See: \ref page_spa_plugins + +*/ |