目录

PipeWire 音频设计与实现分析二——SPA 插件

SPA 插件

PipeWire 的绝大部分功能组件都设计为可动态加载的,可动态加载的对象分为两类,一是插件,默认位于 /usr/lib/aarch64-linux-gnu/spa-0.2 这样的目录下;二是模块,默认位于 /usr/lib/aarch64-linux-gnu/pipewire-0.3 这样的目录下。通过动态加载库实现的组件甚至包括 CPU 访问、系统访问和事件循环。

PipeWire 的插件依照它的 SPA (Simple Plugin API) 机制实现。PipeWire 的插件实现中定义 spa_handle_factory_enum() 函数,它是插件的入口点,这个函数的声明 (位于 pipewire/spa/include/spa/support/plugin.h) 如下:

复制代码
/**
 * The function signature of the entry point in a plugin.
 *
 * \param factory a location to hold the factory result
 * \param index index to keep track of the enumeration
 * \return 1 on success
 *         0 when there are no more factories
 *         -EINVAL when factory is NULL
 */
typedef int (*spa_handle_factory_enum_func_t) (const struct spa_handle_factory **factory,
					       uint32_t *index);

#define SPA_HANDLE_FACTORY_ENUM_FUNC_NAME "spa_handle_factory_enum"

/**
 * The entry point in a plugin.
 *
 * \param factory a location to hold the factory result
 * \param index index to keep track of the enumeration
 * \return 1 on success
 *	   0 when no more items are available
 *	   < 0 errno type error
 */
int spa_handle_factory_enum(const struct spa_handle_factory **factory, uint32_t *index);

PipeWire 的 SPA 框架加载插件时,通过 dlopen() 函数打开动态链接库文件,通过 dlsym() 函数找到这个函数,调用这个函数迭代插件提供的 struct spa_handle_factorystruct spa_handle_factory 提供统一的接口来创建 struct spa_handle(插件实例),实现功能组件的动态加载和实例化。struct spa_handle_factory 类型定义 (位于 pipewire/spa/include/spa/support/plugin.h) 如下:

复制代码
struct spa_handle_factory {
	/** The version of this structure */
#define SPA_VERSION_HANDLE_FACTORY	1
	uint32_t version;
	/**
	 * The name of the factory contains a logical name that describes
	 * the function of the handle. Other plugins might contain an alternative
	 * implementation with the same name.
	 *
	 * See utils/names.h for the list of standard names.
	 *
	 * Examples include:
	 *
	 *  api.alsa.pcm.sink: an object to write PCM samples to an alsa PLAYBACK
	 *			device
	 *  api.v4l2.source: an object to read from a v4l2 source.
	 */
	const char *name;
	/**
	 * Extra information about the handles of this factory.
	 */
	const struct spa_dict *info;
	/**
	 * Get the size of handles from this factory.
	 *
	 * \param factory a spa_handle_factory
	 * \param params extra parameters that determine the size of the
	 * handle.
	 */
	size_t (*get_size) (const struct spa_handle_factory *factory,
			    const struct spa_dict *params);

	/**
	 * Initialize an instance of this factory. The caller should allocate
	 * memory at least size bytes and pass this as \a handle.
	 *
	 * \a support can optionally contain extra interfaces or data items that the
	 * plugin can use such as a logger.
	 *
	 * \param factory a spa_handle_factory
	 * \param handle a pointer to memory
	 * \param info extra handle specific information, usually obtained
	 *        from a spa_device. This can be used to configure the handle.
	 * \param support support items
	 * \param n_support number of elements in \a support
	 * \return 0 on success
	 *	   < 0 errno type error
	 */
	int (*init) (const struct spa_handle_factory *factory,
		     struct spa_handle *handle,
		     const struct spa_dict *info,
		     const struct spa_support *support,
		     uint32_t n_support);

	/**
	 * spa_handle_factory::enum_interface_info:
	 * \param factory: a #spa_handle_factory
	 * \param info: result to hold spa_interface_info.
	 * \param index: index to keep track of the enumeration, 0 for first item
	 *
	 * Enumerate the interface information for \a factory.
	 *
	 * \return 1 when an item is available
	 *	   0 when no more items are available
	 *	   < 0 errno type error
	 */
	int (*enum_interface_info) (const struct spa_handle_factory *factory,
				    const struct spa_interface_info **info,
				    uint32_t *index);
};

spa_handle_factory 类型的几个字段,说明如下:

  • name:工厂的名称,包含一个逻辑名称,它描述 handle 的功能。其它插件可能提供相同功能的不同实现,也可以具有相同的名称。工厂的名称如 api.alsa.pcm.sink
  • info:包含这个工厂的 handles 的额外信息。
  • get_size():获得这个工厂的 struct spa_handle(插件实例)的大小。
  • init():初始化这个工厂的 struct spa_handle(插件实例)。
  • enum_interface_info():迭代工厂的接口信息。

PipeWire 定义的工厂名称有如下 (位于 pipewire/spa/include/spa/utils/names.h) 这些:

复制代码
/** for factory names */
#define SPA_NAME_SUPPORT_CPU		"support.cpu"			/**< A CPU interface */
#define SPA_NAME_SUPPORT_DBUS		"support.dbus"			/**< A DBUS interface */
#define SPA_NAME_SUPPORT_LOG		"support.log"			/**< A Log interface */
#define SPA_NAME_SUPPORT_LOOP		"support.loop"			/**< A Loop/LoopControl/LoopUtils
									  *  interface */
#define SPA_NAME_SUPPORT_SYSTEM		"support.system"		/**< A System interface */

#define SPA_NAME_SUPPORT_NODE_DRIVER	"support.node.driver"		/**< A dummy driver node */

/* control mixer */
#define SPA_NAME_CONTROL_MIXER		"control.mixer"			/**< mixes control streams */

/* audio mixer */
#define SPA_NAME_AUDIO_MIXER		"audio.mixer"			/**< mixes the raw audio on N input
									  *  ports together on the output
									  *  port */
#define SPA_NAME_AUDIO_MIXER_DSP	"audio.mixer.dsp"		/**< mixes mono audio with fixed input
									  *  and output buffer sizes. supported
									  *  formats must include f32 and
									  *  optionally f64 and s24_32 */

/** audio processing */
#define SPA_NAME_AUDIO_PROCESS_FORMAT	"audio.process.format"		/**< processes raw audio from one format
									  *  to another */
#define SPA_NAME_AUDIO_PROCESS_CHANNELMIX	\
					"audio.process.channelmix"	/**< mixes raw audio channels and applies
									  *  volume change. */
#define SPA_NAME_AUDIO_PROCESS_RESAMPLE		\
					"audio.process.resample"	/**< resamples raw audio */
#define SPA_NAME_AUDIO_PROCESS_DEINTERLEAVE	\
					"audio.process.deinterleave"	/**< deinterleave raw audio channels */
#define SPA_NAME_AUDIO_PROCESS_INTERLEAVE	\
					"audio.process.interleave"	/**< interleave raw audio channels */


/** audio convert combines some of the audio processing */
#define SPA_NAME_AUDIO_CONVERT		"audio.convert"			/**< converts raw audio from one format
									  *  to another. Must include at least
									  *  format, channelmix and resample
									  *  processing */
#define SPA_NAME_AUDIO_ADAPT		"audio.adapt"			/**< combination of a node and an
									  *  audio.convert. Does clock slaving */

#define SPA_NAME_AEC				"audio.aec"				/**< Echo canceling */

/** video processing */
#define SPA_NAME_VIDEO_PROCESS_FORMAT	"video.process.format"		/**< processes raw video from one format
									  *  to another */
#define SPA_NAME_VIDEO_PROCESS_SCALE	"video.process.scale"		/**< scales raw video */

/** video convert combines some of the video processing */
#define SPA_NAME_VIDEO_CONVERT		"video.convert"			/**< converts raw video from one format
									  *  to another. Must include at least
									  *  format and scaling */
#define SPA_NAME_VIDEO_ADAPT		"video.adapt"			/**< combination of a node and a
									  *  video.convert. */
/** keys for alsa factory names */
#define SPA_NAME_API_ALSA_ENUM_UDEV	"api.alsa.enum.udev"		/**< an alsa udev Device interface */
#define SPA_NAME_API_ALSA_PCM_DEVICE	"api.alsa.pcm.device"		/**< an alsa Device interface */
#define SPA_NAME_API_ALSA_PCM_SOURCE	"api.alsa.pcm.source"		/**< an alsa Node interface for
									  *  capturing PCM */
#define SPA_NAME_API_ALSA_PCM_SINK	"api.alsa.pcm.sink"		/**< an alsa Node interface for
									  *  playback PCM */
#define SPA_NAME_API_ALSA_SEQ_DEVICE	"api.alsa.seq.device"		/**< an alsa Midi device */
#define SPA_NAME_API_ALSA_SEQ_SOURCE	"api.alsa.seq.source"		/**< an alsa Node interface for
									  *  capture of midi */
#define SPA_NAME_API_ALSA_SEQ_SINK	"api.alsa.seq.sink"		/**< an alsa Node interface for
									  *  playback of midi */
#define SPA_NAME_API_ALSA_SEQ_BRIDGE	"api.alsa.seq.bridge"		/**< an alsa Node interface for
									  *  bridging midi ports */
#define SPA_NAME_API_ALSA_ACP_DEVICE	"api.alsa.acp.device"		/**< an alsa ACP Device interface */

/** keys for bluez5 factory names */
#define SPA_NAME_API_BLUEZ5_ENUM_DBUS	"api.bluez5.enum.dbus"		/**< a dbus Device interface */
#define SPA_NAME_API_BLUEZ5_DEVICE	"api.bluez5.device"		/**< a Device interface */
#define SPA_NAME_API_BLUEZ5_A2DP_SINK	"api.bluez5.a2dp.sink"		/**< a playback Node interface for A2DP profiles */
#define SPA_NAME_API_BLUEZ5_A2DP_SOURCE	"api.bluez5.a2dp.source"	/**< a capture Node interface for A2DP profiles */
#define SPA_NAME_API_BLUEZ5_SCO_SINK	"api.bluez5.sco.sink"		/**< a playback Node interface for HSP/HFP profiles */
#define SPA_NAME_API_BLUEZ5_SCO_SOURCE	"api.bluez5.sco.source"		/**< a capture Node interface for HSP/HFP profiles */

/** keys for codec factory names */
#define SPA_NAME_API_CODEC_BLUEZ5_A2DP	"api.codec.bluez5.a2dp"		/**< Bluez5 A2DP codec plugin */

/** keys for v4l2 factory names */
#define SPA_NAME_API_V4L2_ENUM_UDEV	"api.v4l2.enum.udev"		/**< a v4l2 udev Device interface */
#define SPA_NAME_API_V4L2_DEVICE	"api.v4l2.device"		/**< a v4l2 Device interface */
#define SPA_NAME_API_V4L2_SOURCE	"api.v4l2.source"		/**< a v4l2 Node interface for
									  *  capturing */

/** keys for libcamera factory names */
#define SPA_NAME_API_LIBCAMERA_ENUM_CLIENT	"api.libcamera.enum.client"	/**< a libcamera client Device interface */
#define SPA_NAME_API_LIBCAMERA_ENUM_MANAGER	"api.libcamera.enum.manager"	/**< a libcamera manager Device interface */
#define SPA_NAME_API_LIBCAMERA_DEVICE		"api.libcamera.device"		/**< a libcamera Device interface */
#define SPA_NAME_API_LIBCAMERA_SOURCE		"api.libcamera.source"		/**< a libcamera Node interface for
									  *  capturing */

/** keys for jack factory names */
#define SPA_NAME_API_JACK_DEVICE	"api.jack.device"		/**< a jack device. This is a
									  *  client connected to a server */
#define SPA_NAME_API_JACK_SOURCE	"api.jack.source"		/**< a jack source */
#define SPA_NAME_API_JACK_SINK		"api.jack.sink"			/**< a jack sink */

/** keys for vulkan factory names */
#define SPA_NAME_API_VULKAN_COMPUTE_SOURCE	\
					"api.vulkan.compute.source"	/**< a vulkan compute source. */

PipeWire 的 SPA 框架获得 spa_handle_factory 对象,创建 struct spa_handle(插件实例)时,首先调用 get_size() 获得工厂的 struct spa_handle 的大小,然后基于这个大小分配内存,之后调用 init() 初始化 struct spa_handle。SPA 框架中,避免插件内部执行分配内存的操作,以避免不同的内存分配机制可能引入的分配内存的时间消耗不可控的问题。

struct spa_handle 类型定义 (位于 pipewire/spa/include/spa/support/plugin.h) 如下:

复制代码
struct spa_handle {
	/** Version of this struct */
#define SPA_VERSION_HANDLE	0
	uint32_t version;

	/**
	 * Get the interface provided by \a handle with \a type.
	 *
	 * \a interface is always a struct spa_interface but depending on
	 * \a type, the struct might contain other information.
	 *
	 * \param handle a spa_handle
	 * \param type the interface type
	 * \param interface result to hold the interface.
	 * \return 0 on success
	 *         -ENOTSUP when there are no interfaces
	 *         -EINVAL when handle or info is NULL
	 */
	int (*get_interface) (struct spa_handle *handle, const char *type, void **interface);
	/**
	 * Clean up the memory of \a handle. After this, \a handle should not be used
	 * anymore.
	 *
	 * \param handle a pointer to memory
	 * \return 0 on success
	 */
	int (*clear) (struct spa_handle *handle);
};

SPA 插件的使用者通过 struct spa_handleget_interface() 获得它提供的接口。

PipeWire 的 SPA 框架提供了 pw_load_spa_handle() 接口从特定的动态链接库文件中加载特定的 spa_handle,这个接口定义 (位于 pipewire/src/pipewire/pipewire.c) 如下:

复制代码
struct plugin {
	struct spa_list link;
	char *filename;
	void *hnd;
	spa_handle_factory_enum_func_t enum_func;
	struct spa_list handles;
	int ref;
};

struct handle {
	struct spa_list link;
	struct plugin *plugin;
	char *factory_name;
	int ref;
	struct spa_handle handle SPA_ALIGNED(8);
};
 . . . . . .
static struct plugin *
find_plugin(struct registry *registry, const char *filename)
{
	struct plugin *p;
	spa_list_for_each(p, &registry->plugins, link) {
		if (spa_streq(p->filename, filename))
			return p;
	}
	return NULL;
}

static struct plugin *
open_plugin(struct registry *registry,
	    const char *path, size_t len, const char *lib)
{
	struct plugin *plugin;
	char filename[PATH_MAX];
	void *hnd;
	spa_handle_factory_enum_func_t enum_func;
	int res;

        if ((res = spa_scnprintf(filename, sizeof(filename), "%.*s/%s.so", (int)len, path, lib)) < 0)
		goto error_out;

	if ((plugin = find_plugin(registry, filename)) != NULL) {
		plugin->ref++;
		return plugin;
	}

        if ((hnd = dlopen(filename, RTLD_NOW)) == NULL) {
		res = -ENOENT;
		pw_log_debug("can't load %s: %s", filename, dlerror());
		goto error_out;
        }
        if ((enum_func = dlsym(hnd, SPA_HANDLE_FACTORY_ENUM_FUNC_NAME)) == NULL) {
		res = -ENOSYS;
		pw_log_debug("can't find enum function: %s", dlerror());
		goto error_dlclose;
        }

	if ((plugin = calloc(1, sizeof(struct plugin))) == NULL) {
		res = -errno;
		goto error_dlclose;
	}

	pw_log_debug("loaded plugin:'%s'", filename);
	plugin->ref = 1;
	plugin->filename = strdup(filename);
	plugin->hnd = hnd;
	plugin->enum_func = enum_func;
	spa_list_init(&plugin->handles);

	spa_list_append(&registry->plugins, &plugin->link);

	return plugin;

error_dlclose:
	dlclose(hnd);
error_out:
	errno = -res;
	return NULL;
}
 . . . . . .
static const struct spa_handle_factory *find_factory(struct plugin *plugin, const char *factory_name)
{
	int res = -ENOENT;
	uint32_t index;
        const struct spa_handle_factory *factory;

        for (index = 0;;) {
                if ((res = plugin->enum_func(&factory, &index)) <= 0) {
                        if (res == 0)
				break;
                        goto out;
                }
		if (factory->version < 1) {
			pw_log_warn("factory version %d < 1 not supported",
					factory->version);
			continue;
		}
                if (spa_streq(factory->name, factory_name))
                        return factory;
	}
	res = -ENOENT;
out:
	pw_log_debug("can't find factory %s: %s", factory_name, spa_strerror(res));
	errno = -res;
	return NULL;
}
 . . . . . .
static struct spa_handle *load_spa_handle(const char *lib,
		const char *factory_name,
		const struct spa_dict *info,
		uint32_t n_support,
		const struct spa_support support[])
{
	struct support *sup = &global_support;
	struct plugin *plugin;
	struct handle *handle;
	const struct spa_handle_factory *factory;
	const char *state = NULL, *p;
	int res;
	size_t len;

	if (factory_name == NULL) {
		res = -EINVAL;
		goto error_out;
	}

	if (lib == NULL)
		lib = sup->support_lib;

	pw_log_debug("load lib:'%s' factory-name:'%s'", lib, factory_name);

	plugin = NULL;
	res = -ENOENT;

	if (sup->plugin_dir == NULL) {
		pw_log_error("load lib: plugin directory undefined, set SPA_PLUGIN_DIR");
		goto error_out;
	}
	while ((p = pw_split_walk(sup->plugin_dir, ":", &len, &state))) {
		if ((plugin = open_plugin(&sup->registry, p, len, lib)) != NULL)
			break;
		res = -errno;
	}
	if (plugin == NULL)
		goto error_out;

	pthread_mutex_unlock(&support_lock);

	factory = find_factory(plugin, factory_name);
	if (factory == NULL) {
		res = -errno;
		goto error_unref_plugin;
	}

	handle = calloc(1, sizeof(struct handle) + spa_handle_factory_get_size(factory, info));
	if (handle == NULL) {
		res = -errno;
		goto error_unref_plugin;
	}

	if ((res = spa_handle_factory_init(factory,
					&handle->handle, info,
					support, n_support)) < 0) {
		pw_log_debug("can't make factory instance '%s': %d (%s)",
				factory_name, res, spa_strerror(res));
		goto error_free_handle;
	}

	pthread_mutex_lock(&support_lock);
	handle->ref = 1;
	handle->plugin = plugin;
	handle->factory_name = strdup(factory_name);
	spa_list_append(&plugin->handles, &handle->link);

	return &handle->handle;

error_free_handle:
	free(handle);
error_unref_plugin:
	pthread_mutex_lock(&support_lock);
	unref_plugin(plugin);
error_out:
	errno = -res;
	return NULL;
}

SPA_EXPORT
struct spa_handle *pw_load_spa_handle(const char *lib,
		const char *factory_name,
		const struct spa_dict *info,
		uint32_t n_support,
		const struct spa_support support[])
{
	struct spa_handle *handle;
	pthread_mutex_lock(&support_lock);
	handle = load_spa_handle(lib, factory_name, info, n_support, support);
	pthread_mutex_unlock(&support_lock);
	return handle;
}

PipeWire 的 SPA 框架,在内部用 struct handle 对象描述 spa_handle,用 struct plugin 对象描述插件。一个插件文件中可能可以提供多个 spa_handle,在 struct plugin 对象中,该插件文件提供的多个 spa_handle 由一个链表串起来。

pw_load_spa_handle() 接口是 load_spa_handle() 函数的线程安全封装。load_spa_handle() 函数的执行过程如下:

  1. 库路径是插件的动态链接库文件相对于插件目录的路径,但去掉 .soload_spa_handle() 函数接受传入的 SPA 插件库路径为空,当为空时,库路径取支持库 的路径,即默认从支持库 插件加载 spa_handle

  2. global_support 获得插件目录。

  3. 遍历插件目录,调用 open_plugin() 函数尝试打开插件目录下的插件库文件,直到打开成功或遍历完了所有插件目录。open_plugin() 函数打开插件文件的过程如下:

    • 根据插件目录路径和库路径获得插件库文件的完整路径。
    • 根据插件库文件的完整路径,在 global_supportregistry 中查找插件,如果找到则增加插件的引用计数并返回,否则继续执行。
    • 通过 dlopen() 函数打开动态链接库文件,通过 dlsym() 函数找到 spa_handle_factory_enum 函数。
    • 创建 struct plugin 对象,并根据前面获得的 spa_handle_factory_enum 函数指针等信息初始化其各个字段。
    • struct plugin 对象挂进 global_supportregistry 中。
  4. 调用 find_factory() 函数,通过插件库文件内的 spa_handle_factory_enum 函数迭代查找请求的工厂。查找失败时报错并返回,否则继续执行。

  5. spa_handle 分配内存空间。内存空间的大小实际为 struct handle 对象的大小加上通过工厂的 get_size() 获得的 spa_handle 实现的大小。spa_handle_factory_get_size() 是个宏,是对工厂的 get_size() 的包装。

  6. 通过工厂的 init() 初始化 spa_handle 实例。spa_handle_factory_init() 是个宏,是对工厂的 init() 的包装。

  7. 进一步初始化 struct handle 对象,包括工厂名称等。

  8. struct handle 对象挂进 struct plugin 的链表里。

struct handlestruct plugin 都没有保存对 struct spa_handle_factory 的引用。

PipeWire 的 SPA 框架提供了 pw_unload_spa_handle() 接口卸载特定的 spa_handle,这个接口定义 (位于 pipewire/src/pipewire/pipewire.c) 如下:

复制代码
static void
unref_plugin(struct plugin *plugin)
{
	if (--plugin->ref == 0) {
		spa_list_remove(&plugin->link);
		pw_log_debug("unloaded plugin:'%s'", plugin->filename);
		if (!global_support.in_valgrind)
			dlclose(plugin->hnd);
		free(plugin->filename);
		free(plugin);
	}
}
 . . . . . .
static void unref_handle(struct handle *handle)
{
	if (--handle->ref == 0) {
		spa_list_remove(&handle->link);
		pw_log_debug("clear handle '%s'", handle->factory_name);
		pthread_mutex_unlock(&support_lock);
		spa_handle_clear(&handle->handle);
		pthread_mutex_lock(&support_lock);
		unref_plugin(handle->plugin);
		free(handle->factory_name);
		free(handle);
	}
}
 . . . . . .
static struct handle *find_handle(struct spa_handle *handle)
{
	struct registry *registry = &global_support.registry;
	struct plugin *p;
	struct handle *h;

	spa_list_for_each(p, &registry->plugins, link) {
		spa_list_for_each(h, &p->handles, link) {
			if (&h->handle == handle)
				return h;
		}
	}
	return NULL;
}

SPA_EXPORT
int pw_unload_spa_handle(struct spa_handle *handle)
{
	struct handle *h;
	int res = 0;

	pthread_mutex_lock(&support_lock);
	if ((h = find_handle(handle)) == NULL)
		res = -ENOENT;
	else
		unref_handle(h);
	pthread_mutex_unlock(&support_lock);

	return res;
}

pw_load_spa_handle() 接口调用 find_handle() 函数在 global_supportregistry 中查找 spa_handle,它遍历 registry 的各个插件,并遍历各个插件的 spa_handle 来查找,查找失败时报错返回,否则继续调用 unref_handle() 函数卸载特定的 spa_handle

unref_handle() 函数递减 spa_handle 的引用计数,引用计数减到 0 时执行清理动作,具体包括将 spa_handlestruct plugin 的链表中取下来,调用 spa_handleclear() 清理 spa_handle 具体实现的资源,调用 unref_plugin() 函数递减所属的 struct plugin 的引用计数,释放 spa_handle 占用的内存。

unref_plugin() 函数递减 struct plugin 的引用计数,引用计数减到 0 时执行清理动作,具体包括将 struct pluginregistry 的链表中取下来,关闭动态链接库的 handle,释放 struct plugin 占用的内存。

我们在 pw_init() 函数中看到调用 add_interface() 添加 CPU 和日志功能组件,add_interface() 函数定义 (位于 pipewire/src/pipewire/pipewire.c) 如下:

复制代码
static void *add_interface(struct support *support,
		const char *factory_name,
		const char *type,
		const struct spa_dict *info)
{
	struct spa_handle *handle;
	void *iface = NULL;
	int res = -ENOENT;

	handle = load_spa_handle(support->support_lib,
			factory_name, info,
			support->n_support, support->support);
	if (handle == NULL)
		return NULL;

	pthread_mutex_unlock(&support_lock);
	res = spa_handle_get_interface(handle, type, &iface);
	pthread_mutex_lock(&support_lock);

	if (res < 0 || iface == NULL) {
		pw_log_error("can't get %s interface %d: %s", type, res,
				spa_strerror(res));
		return NULL;
	}

	support->support[support->n_support++] =
		SPA_SUPPORT_INIT(type, iface);
	return iface;
}

add_interface() 函数调用 load_spa_handle()支持库 插件加载 spa_handle,调用 spa_handleget_interface() 从其中获得指定接口的对象,成功时将其保存在 support 的支持功能组件列表。

本文是转载文章,点击查看原文
如有侵权,请联系 xyy@jishuzhan.net 删除
相关推荐
ylfhpy1 小时前
Java面试黄金宝典30
java·数据库·算法·面试·职场和发展
明.2441 小时前
DFS 洛谷P1123 取数游戏
算法·深度优先
简简单单做算法3 小时前
基于mediapipe深度学习和限定半径最近邻分类树算法的人体摔倒检测系统python源码
人工智能·python·深度学习·算法·分类·mediapipe·限定半径最近邻分类树
Tisfy4 小时前
LeetCode 2360.图中的最长环:一步一打卡(不撞南墙不回头) - 通过故事讲道理
算法·leetcode··题解
LuckyAnJo4 小时前
Leetcode-100 链表常见操作
算法·leetcode·链表
双叶8366 小时前
(C语言)虚数运算(结构体教程)(指针解法)(C语言教程)
c语言·开发语言·数据结构·c++·算法·microsoft
工一木子6 小时前
大厂算法面试 7 天冲刺:第5天- 递归与动态规划深度解析 - 高频面试算法 & Java 实战
算法·面试·动态规划
invincible_Tang7 小时前
R格式 (15届B) 高精度
开发语言·算法·r语言
独好紫罗兰8 小时前
洛谷题单2-P5715 【深基3.例8】三位数排序-python-流程图重构
开发语言·python·算法
zhslhm8 小时前
Moo0 VideoResizer,简单高效压缩视频!
音视频·视频压缩技巧·视频文件瘦身·数字媒体优化