linux alsa-lib snd_pcm_open函数源码分析(三)

欢迎直接到博客

linux alsa-lib snd_pcm_open函数源码分析(三)

系列文章其他部分:

linux alsa-lib snd_pcm_open函数源码分析(一)

linux alsa-lib snd_pcm_open函数源码分析(二)

linux alsa-lib snd_pcm_open函数源码分析(四)

linux alsa-lib snd_pcm_open函数源码分析(五)

linux alsa-lib snd_pcm_open函数源码分析(六)

解析配置的最后一个超复杂子函数

1. snd_config_hooks

核心函数之一,配置hooks功能,其中使用hooks功能加载并解析了配置文件中引用的其他配置文件。
所谓的其他配置文件通常为"/etc/asound.conf"及"~/.asoundrc",也就是用户经常自定义修改的配置文件。

static int snd_config_hooks_call(snd_config_t *root, snd_config_t *config, snd_config_t *private_data)
{void *h = NULL;snd_config_t *c, *func_conf = NULL;char *buf = NULL;const char *lib = NULL, *func_name = NULL;const char *str;int (*func)(snd_config_t *root, snd_config_t *config, snd_config_t **dst, snd_config_t *private_data) = NULL;int err;//在配置树中通过id寻找某个节点//实际通常这里func后会跟一个load,比如alsa.conf中的func load//目的是加载其他配置//见下文详细分析err = snd_config_search(config, "func", &c);if (err < 0) {SNDERR("Field func is missing");return err;}//功能及实现都很简单//返回string类型节点的string值//本质上就是直接把节点的值返回一下//见下文分析err = snd_config_get_string(c, &str);if (err < 0) {SNDERR("Invalid type for field func");return err;}assert(str);//配置树中查找"hook_func"节点//详细分析见下文err = snd_config_search_definition(root, "hook_func", str, &func_conf);if (err >= 0) {snd_config_iterator_t i, next;if (snd_config_get_type(func_conf) != SND_CONFIG_TYPE_COMPOUND) {SNDERR("Invalid type for func %s definition", str);err = -EINVAL;goto _err;}snd_config_for_each(i, next, func_conf) {snd_config_t *n = snd_config_iterator_entry(i);const char *id = n->id;if (strcmp(id, "comment") == 0)continue;if (strcmp(id, "lib") == 0) {err = snd_config_get_string(n, &lib);if (err < 0) {SNDERR("Invalid type for %s", id);goto _err;}continue;}if (strcmp(id, "func") == 0) {err = snd_config_get_string(n, &func_name);if (err < 0) {SNDERR("Invalid type for %s", id);goto _err;}continue;}SNDERR("Unknown field %s", id);}}if (!func_name) {int len = 16 + strlen(str) + 1;buf = malloc(len);if (! buf) {err = -ENOMEM;goto _err;}//这里即可拼接出函数的名字snprintf(buf, len, "snd_config_hook_%s", str);buf[len-1] = '\0';func_name = buf;}//对dlopen的包装,打开库,如果指定的库不存在,则打开默认的库h = snd_dlopen(lib, RTLD_NOW);//对dlsym的包装,从动态库中解析函数符号,即通过字符串查找到函数func = h ? snd_dlsym(h, func_name, SND_DLSYM_VERSION(SND_CONFIG_DLSYM_VERSION_HOOK)) : NULL;err = 0;if (!h) {SNDERR("Cannot open shared library %s", lib);err = -ENOENT;} else if (!func) {SNDERR("symbol %s is not defined inside %s", func_name, lib);snd_dlclose(h);err = -ENXIO;}_err:if (func_conf)snd_config_delete(func_conf);if (err >= 0) {snd_config_t *nroot;//这里执行了根据字符串查找出来的函数err = func(root, config, &nroot, private_data);if (err < 0)SNDERR("function %s returned error: %s", func_name, snd_strerror(err));snd_dlclose(h);if (err >= 0 && nroot)err = snd_config_substitute(root, nroot);}free(buf);if (err < 0)return err;return 0;
}

1.1 snd_config_search

在配置树中通过id查找一个子节点。id可以是一个或多个,中间以圆点(.)分隔。
如果是多个id的情况,则每个id需要依次指定前一级的复合节点。
比如在下面的配置树中,假设config是复合节点的句柄,
每个节点后的注释为找到这个节点需要用到的key

  config {a 42               # "a"b {                # "b"c "cee"        # "b.c"d {            # "b.d"e 2.71828  # "b.d.e"}}}

函数原型如下:

int snd_config_search(snd_config_t *config, const char *key, snd_config_t **result)
{SND_CONFIG_SEARCH(config, key, result, );
}

实际主要是使用了SND_CONFIG_SEARCH宏,这类宏有多个,后面会对所有类型的宏都做详细的分析。

1.1.1 SND_CONFIG_SEARCH

此宏是相对来说最简单的宏,实现的目的比较单一,即通过Key查找节点。
其中主要调用了_snd_config_search函数,此函数在上一篇中已经分析过。

#define SND_CONFIG_SEARCH(config, key, result, extra_code) \
{ \snd_config_t *n; \int err; \const char *p; \assert(config && key); \while (1) { \//如果不是复合节点直接报错。//这是因为传入的config本身即为父节点,如果不是复合节点则本身就不应该在一个单身节点下面查找子节点if (config->type != SND_CONFIG_TYPE_COMPOUND) \return -ENOENT; \//这里执行extra_code,这也是为什么宏中可以添加代码的原因。{ extra_code ; } \//此函数返回key中第一次出现字符'.'的位置,如果每找到,则返回nullp = strchr(key, '.'); \if (p) { \//如果找到,则返回(.)的位置,注意此处的p-key//由于返回的是.的位置,即.的地址,key为字符串最开头的地址//这样p-key即为字符串开始到.的字符的个数//于是搜索key字符串的前p-key个字符,则变成了搜索key字符串第一个点前的字符串//比如搜索a.b.c的话,此时相当于搜索aerr = _snd_config_search(config, key, p - key, &n); \if (err < 0) \return err; \//注意这里,如果搜索到了返回n,则把n赋值为config,下个循环则从n开始搜索config = n; \//p+1的目的是跳过(.),这里的1其实就是这个(.)//这样一来,在下个循环的时候,相当于从刚刚搜索到的节点开始,搜索(.)后面的内容key = p + 1; \} else \//如果没有(.)则传下来的key就是要搜索的内容return _snd_config_search(config, key, -1, result); \} \
}

1.2 snd_config_get_string

非常简单,返回string类型节点的string值。

int snd_config_get_string(const snd_config_t *config, const char **ptr)
{assert(config && ptr);if (config->type != SND_CONFIG_TYPE_STRING)return -EINVAL;*ptr = config->u.string;return 0;
}

1.3 snd_config_search_definition

在配置树中查找节点,函数允许传入的参数为别名(alias),同时如果传入的参数为别名,函数会还把别名展开。
如果传入的名字中包含冒号(😃,则冒号(:)后则为snd_config_expand展开所用的参数。

int snd_config_search_definition(snd_config_t *config,const char *base, const char *name,snd_config_t **result)
{snd_config_t *conf;char *key;//查找":"//如果返回参数作为char*,则实际是找到的:的地址const char *args = strchr(name, ':');int err;if (args) {//如果找到了:,则自增1,目的是跳过:这个字符args++;//args - name则等于:前的字符长度+1,多出的1个字符刚好作为\0key = alloca(args - name);//注意这里需要把多出来的\0字符的位置减掉//实际刚好为:前的内容memcpy(key, name, args - name - 1);//最后的位置为\0key[args - name - 1] = '\0';} else {//找不到则直接就是keykey = (char *) name;}/**  if key contains dot (.), the implicit base is ignored*  and the key starts from root given by the 'config' parameter*/snd_config_lock();//如果key中有(.),则传入的base会被忽略//否则如果没有找到(.),说明没有指明base,则采用传入的base//函数查找子结点,如果传入的是别名(alias)则可以展开别名//详细见下文分析err = snd_config_search_alias_hooks(config, strchr(key, '.') ? NULL : base, key, &conf);if (err < 0) {snd_config_unlock();return err;}//展开节点,并执行对应的函数//见下文分析err = snd_config_expand(conf, config, args, NULL, result);snd_config_unlock();return err;
}

1.3.1 snd_config_search_alias_hooks

在配置树下使用别名及hooks查找节点。主要用来实现snd_config_search_definition的功能。
对别两个函数,snd_config_search_definition的第三个参数name可以包含’:‘,
如果有’:‘则’:‘后的内容为snd_config_expand的参数,而snd_config_search_alias_hooks的第三个参数更纯粹,
就是key,无法包含’:'用于snd_config_expand

int snd_config_search_alias_hooks(snd_config_t *config,const char *base, const char *key,snd_config_t **result)
{SND_CONFIG_SEARCH_ALIAS(config, base, key, result,snd_config_searcha_hooks,snd_config_searchva_hooks);
}

这里出现了SND_CONFIG_SEARCH_ALIAS宏,与此类似的宏由多个,这些宏分别实现类似的功能,但是彼此之间有差异。
这些宏分析起来及其复杂,因为大部分都涉及到递归处理,以及相互嵌套。
这些函数中最基础的有两个,在此做介绍,其他类似的函数的函数只做功能说明.

1.3.2 snd_config_searcha

通过key在配置树中查找节点,展开别名。注意与1.1 snd_config_search的区别。

  config {a {b bb}}root {bb {c cc}cc cccccc {d {x "icks"}}}

在上面的配置树中,使用snd_config_searcha(root, config, "a.b.c.d", &result);则最终返回d节点。

int snd_config_searcha(snd_config_t *root, snd_config_t *config, const char *key, snd_config_t **result)
{SND_CONFIG_SEARCHA(root, config, key, result, snd_config_searcha, );
}

1.3.2.1 SND_CONFIG_SEARCHA

此宏主要用来实现snd_config_searcha,使用key查找一个配置节点,同时在root下查找是否有别名并展开。
注意传入的fcn为调用者本身,意味着此处会有递归处理。

#define SND_CONFIG_SEARCHA(root, config, key, result, fcn, extra_code) \
{ \snd_config_t *n; \int err; \const char *p; \assert(config && key); \while (1) { \//如果不是复合类型,通常意味着搜索结束或者错误if (config->type != SND_CONFIG_TYPE_COMPOUND) { \//获取string类型的字符串值if (snd_config_get_string(config, &p) < 0) \return -ENOENT; \//递归处理err = fcn(root, root, p, &config); \if (err < 0) \return err; \} \{ extra_code ; } \//此处是查看key中是否有(.)p = strchr(key, '.'); \if (p) { \err = _snd_config_search(config, key, p - key, &n); \if (err < 0) \return err; \//把找到的n赋值给config,相当于从root逐步往下查找config = n; \key = p + 1; \} else \return _snd_config_search(config, key, -1, result); \} \
}

此类函数的功能,所使用的宏,以及主要作用总结在下表:

函数名功能说明使用到的宏
snd_config_search在配置树中根据key查找节点SND_CONFIG_SEARCH
snd_config_searcha在配置树中根据key查找节点,展开别名。别名从root下查找SND_CONFIG_SEARCHA
snd_config_searchv在配置树中根据key查找节点;key可以是一系列的多个keySND_CONFIG_SEARCHV
snd_config_searchva在配置树中根据key查找节点,展开别名;key可以是连续多个keySND_CONFIG_SEARCHVA
snd_config_search_alias在配置树中根据key查找节点,展开别名.与snd_config_searcha类似,但是只能在config下查找。如果config下找不到id,则函数会尝试寻找base.idSND_CONFIG_SEARCH_ALIAS
snd_config_search_hooks在配置树中根据key查找节点,并且展开hooks。与snd_config_search类似,但是搜索的任何包含hooks的节点都会被各自的hooks函数修改SND_CONFIG_SEARCH
snd_config_searcha_hooks在配置树中根据key查找节点,并且展开alias与hooksSND_CONFIG_SEARCHA
snd_config_searchva_hooks在配置树中根据key查找节点,并且展开alias与hooks。与snd_config_searcha_hooks类似但是key可以是一系列的keySND_CONFIG_SEARCHVA
snd_config_search_alias_hooks在配置树中根据key查找节点,并且展开alias与hooks。与snd_config_search_alias相似,并且展开hooks与snd_config_search_hooks相似SND_CONFIG_SEARCH_ALIAS

1.3.2 snd_config_expand

使用参数及函数展开一个配置节点。如果传入的这个节点中有参数(通过一个id为@args的子节点定义),
则这个函数会用各自的参数值,或默认的参数值或者空来取代任何以$开头的string节点。
而且任何函数都会被评估(参考snd_config_evaluate),结果的副本将会在result中返回。
这里评估(evaluated)的意思比较模糊,从代码分析上看应该是所有的函数都被执行了。

int snd_config_expand(snd_config_t *config, snd_config_t *root, const char *args,snd_config_t *private_data, snd_config_t **result)
{int err;snd_config_t *defs, *subs = NULL, *res;//寻找参数//前面已分析过err = snd_config_search(config, "@args", &defs);if (err < 0) {if (args != NULL) {SNDERR("Unknown parameters %s", args);return -EINVAL;}//创建config的副本到res,注意是深层拷贝//也就是说如果config是复合节点//它的子节点也会被拷贝//详细见下文分析err = snd_config_copy(&res, config);if (err < 0)return err;} else {//如果找到参数,则直接创建一个top节点//前文已分析err = snd_config_top(&subs);if (err < 0)return err;//把defs里面的"default“节点添加到空的subs里面//详细见下文分析err = load_defaults(subs, defs);if (err < 0) {SNDERR("Load defaults error: %s", snd_strerror(err));goto _end;}//解析参数args//太太太复杂err = parse_args(subs, args, defs);if (err < 0) {SNDERR("Parse arguments error: %s", snd_strerror(err));goto _end;}//在运行时评估一个配置节点err = snd_config_evaluate(subs, root, private_data, NULL);if (err < 0) {SNDERR("Args evaluate error: %s", snd_strerror(err));goto _end;}             }err = snd_config_walk(config, root, &res, _snd_config_expand, subs);if (err < 0) {SNDERR("Expand error (walk): %s", snd_strerror(err));goto _end;}}err = snd_config_evaluate(res, root, private_data, NULL);if (err < 0) {SNDERR("Evaluate error: %s", snd_strerror(err));snd_config_delete(res);goto _end;}*result = res;err = 1;_end:if (subs)snd_config_delete(subs);return err;
}

1.3.2.1 snd_config_evaluate

在运行时评估一个函数,此函数会评估配置树中的任何一个函数(@func),
并用各自函数的结果替换这些节点。这里的评估应该时计算的意思。

int snd_config_evaluate(snd_config_t *config, snd_config_t *root,snd_config_t *private_data, snd_config_t **result)
{/* FIXME: Only in place evaluation is currently implemented */assert(result == NULL);return snd_config_walk(config, root, result, _snd_config_evaluate, private_data);
}

1.3.2.2 snd_config_walk

这里面传入的回调函数为_snd_config_evaluate,函数本身又会有递归,
大概目的就是一步一步查找func,找到并执行,并且由于创建了新的配置树,
会把执行函数后的节点信息替换掉原来的节点。

static int snd_config_walk(snd_config_t *src,snd_config_t *root,snd_config_t **dst,snd_config_walk_callback_t callback,snd_config_t *private_data)
{int err;snd_config_iterator_t i, next;switch (snd_config_get_type(src)) {case SND_CONFIG_TYPE_COMPOUND:err = callback(src, root, dst, SND_CONFIG_WALK_PASS_PRE, private_data);if (err <= 0)return err;snd_config_for_each(i, next, src) {snd_config_t *s = snd_config_iterator_entry(i);snd_config_t *d = NULL;err = snd_config_walk(s, root, (dst && *dst) ? &d : NULL,callback, private_data);if (err < 0)goto _error;if (err && d) {err = snd_config_add(*dst, d);if (err < 0)goto _error;}}err = callback(src, root, dst, SND_CONFIG_WALK_PASS_POST, private_data);if (err <= 0) {_error:if (dst && *dst)snd_config_delete(*dst);}break;default:err = callback(src, root, dst, SND_CONFIG_WALK_PASS_LEAF, private_data);break;}return err;
}

1.3.2.2 _snd_config_evaluate

具体执行函数,太复杂,即有循环,又有递归,注意里面的snd_dlopen
snd_dlsym,会从动态库中根据func符号找到函数,并且执行。
所以func其实是执行了,evaluate可以理解为计算的意思。

static int _snd_config_evaluate(snd_config_t *src,snd_config_t *root,snd_config_t **dst ATTRIBUTE_UNUSED,snd_config_walk_pass_t pass,snd_config_t *private_data)
{int err;if (pass == SND_CONFIG_WALK_PASS_PRE) {char *buf = NULL;const char *lib = NULL, *func_name = NULL;const char *str;int (*func)(snd_config_t **dst, snd_config_t *root,snd_config_t *src, snd_config_t *private_data) = NULL;void *h = NULL;snd_config_t *c, *func_conf = NULL;err = snd_config_search(src, "@func", &c);if (err < 0)return 1;err = snd_config_get_string(c, &str);if (err < 0) {SNDERR("Invalid type for @func");return err;}assert(str);err = snd_config_search_definition(root, "func", str, &func_conf);if (err >= 0) {snd_config_iterator_t i, next;if (snd_config_get_type(func_conf) != SND_CONFIG_TYPE_COMPOUND) {SNDERR("Invalid type for func %s definition", str);err = -EINVAL;goto _err;}snd_config_for_each(i, next, func_conf) {snd_config_t *n = snd_config_iterator_entry(i);const char *id = n->id;if (strcmp(id, "comment") == 0)continue;if (strcmp(id, "lib") == 0) {err = snd_config_get_string(n, &lib);if (err < 0) {SNDERR("Invalid type for %s", id);goto _err;}continue;}if (strcmp(id, "func") == 0) {err = snd_config_get_string(n, &func_name);if (err < 0) {SNDERR("Invalid type for %s", id);goto _err;}continue;}SNDERR("Unknown field %s", id);}}if (!func_name) {int len = 9 + strlen(str) + 1;buf = malloc(len);if (! buf) {err = -ENOMEM;goto _err;}snprintf(buf, len, "snd_func_%s", str);buf[len-1] = '\0';func_name = buf;}h = snd_dlopen(lib, RTLD_NOW);if (h)func = snd_dlsym(h, func_name, SND_DLSYM_VERSION(SND_CONFIG_DLSYM_VERSION_EVALUATE));err = 0;if (!h) {SNDERR("Cannot open shared library %s", lib);err = -ENOENT;goto _errbuf;} else if (!func) {SNDERR("symbol %s is not defined inside %s", func_name, lib);snd_dlclose(h);err = -ENXIO;goto _errbuf;}_err:if (func_conf)snd_config_delete(func_conf);if (err >= 0) {snd_config_t *eval;err = func(&eval, root, src, private_data);if (err < 0)SNDERR("function %s returned error: %s", func_name, snd_strerror(err));snd_dlclose(h);if (err >= 0 && eval) {/* substitute merges compound members *//* we don't want merging at all */err = snd_config_delete_compound_members(src);if (err >= 0)//替换节点err = snd_config_substitute(src, eval);}}_errbuf:free(buf);if (err < 0)return err;return 0;}return 1;
}

至此,snd_config_expand的功能为展开节点,从节点中搜索函数,逐个执行,并最终用执行结果替换掉原来的节点。
snd_config_search_definition则查找某个定义,查找到后使用snd_config_expand去展开执行。
根据读取alsa的默认配置文件alsa.conf,通常会构建一个func load函数,再用构造出的函数去load配置文件。
里面又是一大堆递归。总之alsa配置的目的就是通过在配置文件中修改配置,即可控制运行时的函数执行。
为了实现这个目标,alsa丧心病狂的实现了众多嵌套,递归,为了实现代码复用,采用了众多的宏,整体让alsa变得及其复杂。

比如alsa.conf中的配置:

@hooks [{func loadfiles ["/etc/alsa/conf.d""/etc/asound.conf""~/.asoundrc"]errors false}
]

通过这个hooks,实际上构造了snd_config_hooks_load函数,运行时会解析符号并从动态库中找到此函数,
用来加载接下来的三个配额文件,"/etc/alsa/conf.d""/etc/asound.conf","~/.asoundrc"

至此snd_config_update_r的大致功能已经比较清晰了,从配置文件中读取配置文件,解析为配置树,
执行所有的hooks,其中可能又会包含读取配置文件,解析为配置树。执行所有的hooks函数,重新更新配置树。
返回最后的配置树。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/462032.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

从源码到成品应用:互联网医院系统与在线问诊APP的开发全解析

今天将全面解析互联网医院系统和在线问诊APP的开发过程&#xff0c;从源码到成品应用&#xff0c;帮助您理解其中的关键技术和实施策略。 一、系统架构设计 互联网医院系统和在线问诊APP的开发首先需要一个合理的系统架构。通常&#xff0c;系统架构分为前端和后端两个部分。…

2024年【危险化学品生产单位安全生产管理人员】考试内容及危险化学品生产单位安全生产管理人员作业考试题库

题库来源&#xff1a;安全生产模拟考试一点通公众号小程序 危险化学品生产单位安全生产管理人员考试内容是安全生产模拟考试一点通生成的&#xff0c;危险化学品生产单位安全生产管理人员证模拟考试题库是根据危险化学品生产单位安全生产管理人员最新版教材汇编出危险化学品生…

企业出海网络:SD-WAN与专线混合组网方案

随着越来越多的国内企业进入海外市场&#xff0c;包括出海电商、游戏、社交网络和区块链等领域&#xff0c;它们通常需要使用海外服务器。同时&#xff0c;这些企业在国内也会拥有自己的机房、IDC或依赖其他云服务提供商的机房。在这种情况下&#xff0c;如何实现国内外之间的高…

vue 果蔬识别系统百度AI识别vue+springboot java开发、elementui+ echarts+ vant开发

编号&#xff1a;R03-果蔬识别系统 简介&#xff1a;vuespringboot百度AI实现的果蔬识别系统 版本&#xff1a;2025版 视频介绍&#xff1a; vuespringboot百度AI实现的果蔬识别系统前后端java开发&#xff0c;百度识别&#xff0c;带H5移动端&#xff0c;mysql数据库可视化 1 …

深度了解flink Flink 本地运行Standalone模式

环境准备 IDEA 必须git 必须maven 必须jdk 1.8 必须scala 2.12.7 源码下载 如果能访问github&#xff0c;下载github的源码(flink的代码托管网站) git clone https://github.com/apache/flink.git 如果不能访问github&#xff0c;可以通过码云下载(国内的代码托管平台) g…

【C语言】宏封装的实用总结

在C语言的广阔天地中&#xff0c;宏&#xff08;Macro&#xff09;犹如一门神秘的内功&#xff0c;掌握它&#xff0c;你将能够以不变应万变&#xff0c;以简洁驾驭复杂。今天&#xff0c;我们将深入探讨C语言宏封装的高级技巧&#xff0c;并通过一系列案例&#xff0c;让你领略…

Latex中Reference的卷号加粗的问题

找到模板中的.bst文件&#xff0c;查找volume&#xff0c;修改如下 添加bold&#xff0c;卷号会加粗&#xff0c;去掉则正常

parted 磁盘分区

目录 磁盘格式磁盘分区文件系统挂载使用扩展 - parted、fdisk、gdisk 区别 磁盘格式 parted /dev/vdcmklabel gpt # 设置磁盘格式为GPT p # 打印磁盘信息此时磁盘格式设置完成&#xff01; 磁盘分区 开始分区&#xff1a; mkpart data_mysql # 分区名&…

基于Transformer的路径规划 - 第五篇 GPT生成策略_解码方法优化

上一篇&#xff1a;基于Transformer的路径规划 - 第四篇 GPT模型优化 在上一篇中&#xff0c;我尝试优化GPT路径生成模型&#xff0c;但没有成功。在随机生成的测试集上&#xff0c;路径规划成功率只有99%左右。而使用传统的路径规划算法&#xff0c;例如A*&#xff0c;路径规划…

【ROS的TF系统】

系列文章目录 TF系统简介 前面的章节实现了SLAM节点的建图功能&#xff1a; 激光雷达节点—> /scan话题 —>hector_mapping节点—> 地图数据话题/map 本期来实现SLAM节点的定位功能&#xff1a; TF&#xff08;TransForm&#xff09;主要描述的是两个坐标系的空间关…

Java中的线程安全问题(如果想知道Java中有关线程安全问题的基本知识,那么只看这一篇就足够了!)

前言&#xff1a;多线程编程已经广泛开始使用&#xff0c;其可以充分利用系统资源来提升效率&#xff0c;但是线程安全问题也随之出现&#xff0c;它直接影响了程序的正确性和稳定性&#xff0c;需要对其进行深入的理解与解决。 ✨✨✨这里是秋刀鱼不做梦的BLOG ✨✨✨想要了解…

奥数与C++小学四年级(第十八题 小球重量)

参考程序代码&#xff1a; #include <iostream> #include <vector>int main() {// 小球的重量std::vector<int> weights {1, 2, 3, 4, 5};// 用来存储可能的结果int a, b, c, d, e, x;// 穷举所有可能的 a, b, c, d, e 的组合for (int i 0; i < weight…

ESP32/ESP8266开发板单向一对多ESP-NOW无线通信

ESP32/ESP8266开发板单向一对多ESP-NOW无线通信 简介读取ESP32/ESP8266接收方Receiver的MAC地址ESP32/ESP8266发送方Sender程序ESP32/ESP8266接收方Receiver程序ESP-NOW通信验证总结 简介 本实验通过ESP-NOW无线通信协议实现多个ESP32/ESP 8266开发板向ESP32开发板发送数据。例…

Unity XR Interaction Toolkit 开发教程(2):导入 SDK【3.0 以上版本】

文章目录 &#x1f4d5;课程总结&#x1f4d5;安装 Unity 编辑器与打包模块&#x1f4d5;导入 OpenXR&#x1f4d5;导入 XR Interaction Toolkit&#x1f4d5;打包发布 获取完整课程以及答疑&#xff0c;工程文件下载&#xff1a; https://www.spatialxr.tech/ 视频试看链接&a…

直流电机在液压泵领域的应用

随着工业自动化的不断发展&#xff0c;液压技术已经成为现代工程中不可或缺的一部分。液压泵作为液压系统的核心部件&#xff0c;其性能直接关系到整个系统的效率和可靠性。近年来&#xff0c;直流电机因其独特的优势而逐渐应用于液压泵领域&#xff0c;为液压系统的提升与改进…

2024-10-29 商业分析-盗取他人游戏MOD牟利-记录

摘要&#xff1a; 2024-10-29 商业分析-盗取他人游戏MOD牟利-记录 事件&#xff1a; 【实锤】《真英雄》盗用本人《风林火山》mod地图售卖牟利&#xff01;_ryan_knight_12吧_百度贴吧 真英雄&#xff1f;&#xff1f;我从未见过如此厚颜无耻之人【ryan_knight_12吧】_百度贴吧…

利用钉钉与金蝶云星空进行付款单自动化集成

钉钉数据集成到金蝶云星空&#xff1a;付款申请单下推生成付款单的技术实现 在企业日常运营中&#xff0c;数据的高效流转和准确处理是业务顺利进行的关键。本文将分享一个具体的系统对接集成案例&#xff1a;如何将钉钉平台上的付款申请单&#xff0c;通过轻易云数据集成平台…

vscode 创建 vue 项目时,配置文件为什么收缩到一起展示了?

一、前言 今天用 vue 官方脚手架创建工程&#xff0c;然后通过 vscode 打开项目发现&#xff0c;配置文件都被收缩在一起了。就像下面这样 这有点反直觉&#xff0c;他们应该是在同一层级下的&#xff0c;怎么会这样&#xff0c;有点好奇&#xff0c;但是打开资源管理查看&…

001-Kotlin界面开发之Jetpack Compose Desktop学习路径

Compose Desktop学习之路 学习过程 理解Kotlin的基本语法 Compose Desktop采用Kotlin构建&#xff0c;因此对Kotlin的基本语法有很好的理解是必不可少的。你可以从官方的Kotlin文档开始。 用一句话概括&#xff0c;Kotlin是一种现代的、静态类型的编程语言&#xff0c;它结合…

Vue 组件基础(五)

一、Vue 组件的基础概念 组件(Component)是Vue最强大的功能之一。组件可以扩展HTML元素&#xff0c;封装可重用的代码。在较高层面上&#xff0c;组件是自定义元素&#xff0c;Vue的编译器为它添加特殊功能。每个组件负责一部分特定的任务&#xff0c;比如&#xff1a;显示一个…