diff --git a/doc/meson.build b/doc/meson.build index 032d343ad..277880033 100644 --- a/doc/meson.build +++ b/doc/meson.build @@ -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', diff --git a/doc/spa-design.dox b/doc/spa-design.dox index da6646f28..7a90707a8 100644 --- a/doc/spa-design.dox +++ b/doc/spa-design.dox @@ -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 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. `` 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 -`` 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 `` and a +number of utility functions, see \ref spa_utils */ diff --git a/doc/spa-index.dox b/doc/spa-index.dox index 3321cd35f..df5ec914b 100644 --- a/doc/spa-index.dox +++ b/doc/spa-index.dox @@ -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 +#include + +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 diff --git a/doc/spa-plugins.dox b/doc/spa-plugins.dox new file mode 100644 index 000000000..7beebaca1 --- /dev/null +++ b/doc/spa-plugins.dox @@ -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: # + continue + + interface_info = get_next_interface_info(factory) + if info->type != SPA_TYPE_INTERFACE_Log: # + 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. `` 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 +`` 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 + +*/ diff --git a/spa/include/spa/support/log.h b/spa/include/spa/support/log.h index d02775b5f..7ebdd8f35 100644 --- a/spa/include/spa/support/log.h +++ b/spa/include/spa/support/log.h @@ -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 */