一给 发表于 2024-9-2 07:41:56

HarmonyOS Next开发学习手册——C/C++尺度库机制

概述

HarmonyOS NDK提供业界尺度库 libc尺度库 、 C++尺度库,本文用于介绍C/C++尺度库在HarmonyOS中的机制,开发者相识这些机制有助于在NDK开发过程中避免相干题目。
1. C++兼容性

在HarmonyOS系统中,系统库与应用Native库都在使用C++尺度库(参考 libc++版本),系统库依赖的C++尺度库随镜像版本升级,而应用Native库依赖的C++尺度库随编译使用的SDK版本升级,两部分依赖的C++底子库会跨多个大版本,产生ABI兼容性题目。为相识决此题目,HarmonyOS上把两部分依赖的C++尺度库举行了区分。


[*]系统库:使用libc++.so, 随系统镜像发布。
[*]应用Native库:使用libc++_shared.so,随应用发布。
两个库使用的C++定名空间不一样,libc++.so使用__h作为C++符号的定名空间,libc++_shared.so使用__n1作为C++符号的定名空间。
注意:系统和应用使用的C++尺度库不能举行混用,Native API接口当前只能是C接口,可以通过这个接口隔离两边的C++运行情况。因此在使用共享库HAR包构建应用时,如果HAR包含的libc++_shared.so差别于应用使用的libc++_shared.so版本,那么只有其中一个版本会安装到应用里,可能会导致不兼容题目,可以使用相同的SDK版本更新HAR包解决此题目。
已知C++兼容性题目:应用启动或者dlopen时hilog报错symbol not found, s=__emutls_get_address,原因是API9及之前版本SDK中的libc++_shared.so无此符号,而API11之后版本SDK的libc++_shared.so是有此符号的。解决此题目必要更新应用或者共享库HAR包的SDK版本。
2. musl libc动态链接器

动态库加载定名空隔断离

动态库加载定名空间(namespace,下面统称为ns)是动态链接器计划的一个概念(区别于C++语言中的定名空间),其计划的主要目的是为了在历程中做native库资源访问的管控,以达到安全隔离的目的。比方系统native库答应加载系统目录(/system/lib64;/vendor/lib64等)下的native库,但是平凡应用native库仅答应加载平凡应用native库和ndk库,而不答应直接加载系统native库。
动态链接器无论是在加载编译依赖(DT_NEEDED)中指定的共享库,照旧调用dlopen加载指定的共享库,都必要关联到详细的ns。
HarmonyOS中动态库加载namespace设置的情况


[*] default ns:动态链接器启动时默认创建的ns,它可以搜索/system/lib{abi};/vendor/lib{abi}等系统目录路径下的so。
[*] ndk ns:动态链接器启动时默认创建的ns,它可以搜索/system/lib{abi}/ndk目录下的so,主要是袒露了NDK接口的so。
[*] app ns: 应用启动时创建的ns,它的搜索路径一般是应用的安装路径(可能为沙箱路径),即可加载应用的so。
当前这一套定名空间机制主要限定了应用native库和系统native库之间的调用,如图所示,详细规则为

[*]default ns和ndk ns可以互相访问全部so,不能访问app ns的so。
[*]app ns能访问ndk ns的全部so,不能访问default ns的so。
https://img-blog.csdnimg.cn/img_convert/afdcf4426a96dbb020cf2a1f8beb3a62.webp?x-oss-process=image/format,png
rpath机制

rpath(run-time path)是在运行时指定共享库搜索路径的机制。该机制答应在可实行文件或共享库中嵌入一个用于在运行时指定库的搜索路径的信息。
由于上文介绍的定名空隔断离机制,应用仅答应加载对应安装目录拼接native库路径下(比方arm64平台上为libs/arm64)的应用native库,当应用步调涉及加载较多的native库,盼望创建多个native库加载路径方便管理,但是会导致无法加载新创建目录下的native库,这种情况可以通过rpath机制编译时指定搜索路径。
比方,应用安装目录lib/arm64下的libhello.so依赖新创建路径lib/arm64/module下的libworld.so,那么在应用的CMakeList.txt里设置上rpath编译选项后编译,使用readelf查看libhello.so的rpath设置如图所示,$ORIGIN为libhello.so地点路径,运行时即可正常加载module目录下的libworld.so。
SET(CMAKE_BUILD_WITH_INSTALL_RPATH TRUE)
SET(CMAKE_INSTALL_RPATH "\${ORIGIN}/module")
https://img-blog.csdnimg.cn/img_convert/b858893e9dc936c575ee56860faefb91.webp?x-oss-process=image/format,png
支持dlclose

支持使用dlclose真实卸载动态库的本领。
支持symbol-version机制

symbol-version是libc在动态链接-符号重定位阶段的符号检索机制,支持差别版本的符号重定位,也可以帮助解决重复符号的题目。可参考 LD Version Scripts (GNU Gnulib)
网络接口select支持fd fortify检测

宏定义FD_SET/FD_CLR新增fd有用值查抄,当传入的fd不在区间[0, 1024)中会触发abort crash。
宏定义FD_ISSET新增fd有用值查抄,当传入的fd不在区间[0, 1024)中会返回false。
举世化支持

自API12起,newlocale及setlocale接口支持将locale设置C、C.UTF-8、en_US、en_US.UTF-8、zh_CN及zh_CN.UTF-8。新增在zh_CN及zh_CN.UTF-8的locale设置下对strtod_l、wcstod_l和localeconv的支持。注意strtod_l及wcstod_l不支持对十六进制及十六进制小数的转换。
fdsan功能

fdsan功能可以帮助检测文件的重复关闭和关闭后使用题目。
fdsan使用指南

1. 功能介绍

fdsan针对的操尴尬刁难象是文件描述符,主要用于检测差别使用者对相同文件描述符的错误操作,包罗多次关闭(double-close)和关闭后使用(use-after-close)。这些文件描述符可以是操作系统中的文件、目录、网络套接字和其他I/O设备等,在步调中,打开文件或套接字会生成一个文件描述符,如果此文件描述符在使用后出现反复关闭、或者关闭后使用等场景,就会造成内存泄露、文件句柄泄露等安全隐患题目。该类题目非常潜伏,且难以排查,为了更好地检测此类题目,因此引入了此种针对文件描述符错误操作的检测工具fdsan。
2. 实现原理

计划思路:当打开已有文件或创建一个新文件的时候,在得到返回fd后,设置一个关联的tag,来标志fd的属主信息;关闭文件前,检测fd关联的tag,判断是否符合预期(属主信息一致),符合就继续走正常文件关闭流程;如果不符合就是检测到非常,根据设置,调用对应的非常处理。
tag由两部分组成,最高位的8-bit构成type,后面的56-bit构成value。
type,标识fd通过何种封装情势举行管理,比方 FDSAN_OWNER_TYPE_FILE就表示fd通过平凡文件举行管理,type类型在 fdsan_owner_type举行定义。
value,则用于标识实际的owner tag。
tag构成图示
https://img-blog.csdnimg.cn/img_convert/7bb9e48a42d1b5bf4143f0b3fe6be0c4.webp?x-oss-process=image/format,png
3. 接口阐明

fdsan_set_error_level

enum fdsan_error_level fdsan_set_error_level(enum fdsan_error_level new_level);
描述: 可以通过fdsan_set_error_level设定error_level,error_level用于控制检测到非常后的处理行为。默认error_level为FDSAN_ERROR_LEVEL_WARN_ALWAYS。
参数: fdsan_error_level
名称阐明FDSAN_ERROR_LEVEL_DISABLEDdisabled ,此level代表什么都不处理。FDSAN_ERROR_LEVEL_WARN_ONCEwarn-once,第一次出现错误时在hilog中发出告诫,然后将级别低落为disabled(FDSAN_ERROR_LEVEL_DISABLED)FDSAN_ERROR_LEVEL_WARN_ALWAYSwarn-always,每次出现错误时都在hilog中发出告诫FDSAN_ERROR_LEVEL_FATALfatal ,出现错误时调用abort非常退出. 返回值: 返回旧的error_level。
fdsan_get_error_level

enum fdsan_error_level fdsan_get_error_level();
描述: 可以通过fdsan_get_error_level获取error level。
返回值: 当前的error_level。
fdsan_create_owner_tag

uint64_t fdsan_create_owner_tag(enum fdsan_owner_type type, uint64_t tag);
描述: 通过传入的type和tag字段,拼接成一个有用的文件描述符的关闭tag。
参数: fdsan_owner_type
名称阐明FDSAN_OWNER_TYPE_GENERIC_00默认未使用fd对应的type值FDSAN_OWNER_TYPE_GENERIC_FF默认非法fd对应的type值FDSAN_OWNER_TYPE_FILE默认平凡文件对应的type值,使用fopen或fdopen打开的文件具有该类型FDSAN_OWNER_TYPE_DIRECTORY默认文件夹对应的type值,使用opendir或fdopendir打开的文件具有该类型FDSAN_OWNER_TYPE_UNIQUE_FD默认unique_fd对应的type值,生存暂未使用FDSAN_OWNER_TYPE_ZIPARCHIVE默认zip压缩文件对应的type值,生存暂未使用 返回值: 返回创建的tag,可以用于fdsan_exchange_owner_tag函数的输入。
fdsan_exchange_owner_tag

void fdsan_exchange_owner_tag(int fd, uint64_t expected_tag, uint64_t new_tag);
描述: 修改文件描述符的关闭tag。
通过fd所以找到对应的FdEntry,判断close_tag值与expected_tag是否一致,一致阐明符合预期,可以用new_tag值重新设定对应的FdEntry。
如果不符合,则阐明检测到了非常,后续则举行对应的非常处理。
参数:
名称类型阐明fdintfd句柄,作为FdEntry的索引expected_taguint64_t盼望的ownership tag值new_taguint64_t设置新的ownership tag值 fdsan_close_with_tag

int fdsan_close_with_tag(int fd, uint64_t tag);
描述: 根据tag描述符关闭文件描述符。
通过fd找到匹配的FdEntry。如果close_tag与tag相同,则符合预期,可以继续实行文件描述符关闭流程,否则意味着检测到非常。
参数:
名称类型阐明fdint待关闭的fd句柄taguint64_t盼望的ownership tag 返回值: 0或者-1,0表示close乐成,-1表示close失败。
fdsan_get_owner_tag

uint64_t fdsan_get_owner_tag(int fd);
描述: 根据文件描述符获取tag信息。
通过fd找到匹配的FdEntry,并获取其对应的close_tag。
参数:
名称类型阐明taguint64_townership tag 返回值: 返回对应fd的tag。
fdsan_get_tag_type

const char* fdsan_get_tag_type(uint64_t tag);
描述: 根据tag计算出对应的type类型。
通过获取到的tag信息,通过计算获取对应tag中的type信息。
参数:
名称类型阐明taguint64_townership tag 返回值: 返回对应tag的type。
fdsan_get_tag_value

uint64_t fdsan_get_tag_value(uint64_t tag);
描述: 根据tag计算出对应的owner value。
通过获取到的tag信息,通过偏移计算获取对应tag中的value信息。
参数:
名称类型阐明taguint64_townership tag 返回值: 返回对应tag的value。
4. 使用示例

如何使用fdsan?这是一个简朴的double-close题目:
void good_write()
{
    sleep(1);
    int fd = open(DEV_NULL_FILE, O_RDONLY);
    sleep(3);
    ssize_t ret = write(fd, "fdsan test\n", 11);
    if (ret == -1) {
      OH_LOG_ERROR(LOG_APP, "good write but failed?!");
    }
    close(fd);
}

void bad_close()
{
    int fd = open(DEV_NULL_FILE, O_RDONLY);
    close(fd);
    sleep(2);
    // This close expected to be detect by fdsan
    close(fd);
}

void functional_test()
{
    std::vector<std::thread> threads;
    for (auto function : { good_write, bad_close }) {
      threads.emplace_back(function);
    }
    for (auto& thread : threads) {
      thread.join();
    }
}

int main()
{
    functional_test();
    return 0;
}
上述代码中的goog_write函数会打开一个文件并写入一些字符串而bad_close函数中也会打开一个文件同时包含double-close题目,这两个线程同时运行那么步调的实行情况会是如许的:
https://img-blog.csdnimg.cn/img_convert/0ca115ebb140ffe9fbd927cc4c6fcccc.webp?x-oss-process=image/format,png
由于每次open返回的fd是顺序分配的,在进入主函数后第一个可用的fd是43,bad_close函数中第一次open返回的fd是43,在关闭之后,43就酿成了可用的fd,在good_write函数中open返回了第一个可用的fd,即43,但是由于bad_close函数中存在double-close题目,因此错误的关闭了另一个线程中打开的文件,导致写入失败
在fdsan引入之后,有两种方法可以检测这类题目:使用尺度库接口或实现具有fdsan的函数接口
使用尺度库接口

尺度库接口中fopen,fdopen,opendir,fdopendir都已经集成了fdsan,使用前述接口而非直接使用open可以帮助检测题目。在前述案例中可以使用fopen替换open:
void good_write()
{
    sleep(1);
    // fopen is protected by fdsan, replace open with fopen
    // int fd = open(DEV_NULL_FILE, O_RDONLY);
    FILE *f = fopen(DEV_NULL_FILE, O_RDONLY);
    sleep(3);
    ssize_t ret = write(fileno(f), "fdsan test\n", 11);
    if (ret == -1) {
      OH_LOG_ERROR(LOG_APP, "good write but failed?!");
    }
    close(fileno(f));
}
使用fopen打开的每个文件描述符都必要有一个与之对应的 tag 。fdsan 在 close 时会查抄关闭的 fd 是否与 tag 匹配,不匹配就会默认提示相干日记信息。下面是上述代码的日记信息:
# hilog | grep MUSL-FDSAN
04-30 15:03:41.760 109331624 E C03f00/MUSL-FDSAN: attempted to close file descriptor 43,                           expected to be unowned, actually owned by FILE* 0x00000000f7b90aa2
从这里的错误信息中可以看出FILE接口体的文件被其他人错误的关闭了,FILE接口体的地点可以帮助进一步定位。
别的,可以在代码中使用fdsan_set_error_level设置错误等级error_level,设置为Fatal之后如果fdsan检测到错误会提示日记信息同时crash生成堆栈信息用于定位。下面是error_level设置为Fatal之后生成的crash堆栈信息:
Reason:Signal:SIGABRT(SI_TKILL)@0x0000076e from:1902:20010043
Fault thread info:
Tid:15312, Name:e.myapplication
#00 pc 000e65bc /system/lib/ld-musl-arm.so.1(raise+176)(3de40c79448a2bbced06997e583ef614)
#01 pc 0009c3bc /system/lib/ld-musl-arm.so.1(abort+16)(3de40c79448a2bbced06997e583ef614)
#02 pc 0009de4c /system/lib/ld-musl-arm.so.1(fdsan_error+116)(3de40c79448a2bbced06997e583ef614)
#03 pc 0009e2e8 /system/lib/ld-musl-arm.so.1(fdsan_close_with_tag+836)(3de40c79448a2bbced06997e583ef614)
#04 pc 0009e56c /system/lib/ld-musl-arm.so.1(close+20)(3de40c79448a2bbced06997e583ef614)
#05 pc 000055d8 /data/storage/el1/bundle/libs/arm/libentry.so(bad_close()+96)(f3339aac824c099f449153e92718e1b56f80b2ba)
#06 pc 00006cf4 /data/storage/el1/bundle/libs/arm/libentry.so(decltype(std::declval<void (*)()>()()) std::__n1::__invoke<void (*)()>(void (*&&)())+24)(f3339aac824c099f449153e92718e1b56f80b2ba)
#07 pc 00006c94 /data/storage/el1/bundle/libs/arm/libentry.so(f3339aac824c099f449153e92718e1b56f80b2ba)
#08 pc 000067b8 /data/storage/el1/bundle/libs/arm/libentry.so(void* std::__n1::__thread_proxy<std::__n1::tuple<std::__n1::unique_ptr<std::__n1::__thread_struct, std::__n1::default_delete<std::__n1::__thread_struct>>, void (*)()>>(void*)+100)(f3339aac824c099f449153e92718e1b56f80b2ba)
#09 pc 00105a6c /system/lib/ld-musl-arm.so.1(start+248)(3de40c79448a2bbced06997e583ef614)
#10 pc 000700b0 /system/lib/ld-musl-arm.so.1(3de40c79448a2bbced06997e583ef614)
此时,从crash信息中可以看到是bad_close中存在题目,同时crash中也包含了全部打开的文件,帮助举行定位,提拔服从。
OpenFiles:
0->/dev/null native object of unknown type 0
1->/dev/null native object of unknown type 0
2->/dev/null native object of unknown type 0
3->socket: native object of unknown type 0
4->socket: native object of unknown type 0
5->anon_inode: native object of unknown type 0
6->/sys/kernel/debug/tracing/trace_marker native object of unknown type 0
7->anon_inode: native object of unknown type 0
8->anon_inode: native object of unknown type 0
9->/dev/console native object of unknown type 0
10->pipe: native object of unknown type 0
11->pipe: native object of unknown type 0
12->socket: native object of unknown type 0
13->pipe: native object of unknown type 0
14->socket: native object of unknown type 0
15->pipe: native object of unknown type 0
16->anon_inode: native object of unknown type 0
17->/dev/binder native object of unknown type 0
18->/data/storage/el1/bundle/entry.hap native object of unknown type 0
19->anon_inode: native object of unknown type 0
20->anon_inode: native object of unknown type 0
21->socket: native object of unknown type 0
22->anon_inode: native object of unknown type 0
23->anon_inode: native object of unknown type 0
24->anon_inode: native object of unknown type 0
25->anon_inode: native object of unknown type 0
26->anon_inode: native object of unknown type 0
27->anon_inode: native object of unknown type 0
28->anon_inode: native object of unknown type 0
29->anon_inode: native object of unknown type 0
30->anon_inode: native object of unknown type 0
31->anon_inode: native object of unknown type 0
32->anon_inode: native object of unknown type 0
33->anon_inode: native object of unknown type 0
34->anon_inode: native object of unknown type 0
35->socket: native object of unknown type 0
36->socket: native object of unknown type 0
38->socket: native object of unknown type 0
40->/data/storage/el1/bundle/entry_test.hap native object of unknown type 0
41->socket: native object of unknown type 0
42->/sys/kernel/debug/tracing/trace_marker native object of unknown type 0
43->/dev/null FILE* 4155724704
44->socket: native object of unknown type 0
45->pipe: native object of unknown type 0
46->pipe: native object of unknown type 0
47->pipe: native object of unknown type 0
49->pipe: native object of unknown type 0
50->pipe: native object of unknown type 0
实现具有fdsan的函数接口

除了直接使用具有fdsan功能的尺度库函数之外,还可以实现具有fdsan的函数接口。fdsan机制主要通过两个接口实现:fdsan_exchange_owner_tag和fdsan_close_with_tag,fdsan_exchange_owner_tag可以设置对应fd的tag,而fdsan_close_with_tag可以在关闭文件时查抄对应的tag是否准确。
下面是一个具有fdsan的函数接口实现实例:
struct fdsan_fd {
    fdsan_fd() = default;

    explicit fdsan_fd(int fd)
    {
      reset(fd);
    }

    fdsan_fd(const fdsan_fd& copy) = delete;
    fdsan_fd(fdsan_fd&& move)
    {
      *this = std::move(move);
    }

    ~fdsan_fd()
    {
      reset();
    }

    fdsan_fd& operator=(const fdsan_fd& copy) = delete;
    fdsan_fd& operator=(fdsan_fd&& move)
    {
      if (this == &move) {
            return *this;
      }
      reset();
      if (move.fd_ != -1) {
            fd_ = move.fd_;
            move.fd_ = -1;
            // Acquire ownership from the moved-from object.
            exchange_tag(fd_, move.tag(), tag());
      }
      return *this;
    }

    int get()
    {
      return fd_;
    }

    void reset(int new_fd = -1)
    {
      if (fd_ != -1) {
            close(fd_, tag());
            fd_ = -1;
      }
      if (new_fd != -1) {
            fd_ = new_fd;
            // Acquire ownership of the presumably unowned fd.
            exchange_tag(fd_, 0, tag());
      }
    }

private:
    int fd_ = -1;

    // Use the address of object as the file tag
    uint64_t tag()
    {
      return reinterpret_cast<uint64_t>(this);
    }

    static void exchange_tag(int fd, uint64_t old_tag, uint64_t new_tag)
    {
      if (&fdsan_exchange_owner_tag) {
            fdsan_exchange_owner_tag(fd, old_tag, new_tag);
      }
    }

    static int close(int fd, uint64_t tag)
    {
      if (&fdsan_close_with_tag) {
            return fdsan_close_with_tag(fd, tag);
      }
    }
};
这里的实现中使用fdsan_exchange_owner_tag在开始时将fd与布局体对象地点绑定,然后在关闭文件时使用fdsan_close_with_tag举行检测,预期tag是布局体对象地点。
在实现了具有fdsan的函数接口之后,可以使用该接口包装fd:
void good_write()
{
    sleep(1);
    // int fd = open(DEV_NULL_FILE, O_RDONLY);
    fdsan_fd fd(open(DEV_NULL_FILE, O_RDONLY));
    sleep(3);
    ssize_t ret = write(fd.get(), "fdsan test\n", 11);
    if (ret == -1) {
      OH_LOG_ERROR(LOG_APP, "good write but failed?!");
    }
    close(fd.get());
}
此时运行该步调可以检测到另一个线程的double-close题目,详细信息可以参考3.2节。同样也可以设置error_level为fatal,如许可以使fdsan在检测到crash之后自动crash以获取更多信息。
鸿蒙全栈开发全新学习指南

之前总有很多小伙伴向我反馈说,不知道学习哪些鸿蒙开发技术?不知道必要重点掌握哪些鸿蒙应用开发知识点?而且学习时频仍踩坑,终极浪费大量时间。所以这里为各人准备了一份实用的鸿蒙(HarmonyOS NEXT)学习门路与学习文档用来跟着学习是非常有必要的。
针对一些列因素,整理了一套纯血版鸿蒙(HarmonyOS Next)全栈开发技术的学习门路,包含了鸿蒙开发必掌握的核心知识要点,内容有(ArkTS、ArkUI开发组件、Stage模型、多端摆设、分布式应用开发、WebGL、元服务、OpenHarmony多媒体技术、Napi组件、OpenHarmony内核、OpenHarmony驱动开发、系统定制移植等等)鸿蒙(HarmonyOS NEXT)技术知识点。
本门路共分为四个阶段:

第一阶段:鸿蒙初中级开发必备技能

https://img-blog.csdnimg.cn/direct/50513a90f3bf4d169cc892232794263c.png#pic_center
第二阶段:鸿蒙南北双向高工技能底子:gitee.com/MNxiaona/733GH

https://img-blog.csdnimg.cn/direct/350e757aac894a3cb655cf334e1df47b.png
第三阶段:应用开发中高级就业技术

https://img-blog.csdnimg.cn/direct/55709e9664824aebb329325124ad7dd2.png
第四阶段:全网首发-工业级南向设备开发就业技术:gitee.com/MNxiaona/733GH

https://img-blog.csdnimg.cn/direct/9655f4c1cf1249559ba98372e17c6a17.png
《鸿蒙 (Harmony OS)开发学习手册》(共计892页)

如何快速入门?

1.根本概念
2.构建第一个ArkTS应用
3.……
https://img-blog.csdnimg.cn/direct/41127b7024bf4c2aacf98f6e5d8aaf4e.png
开发底子知识:gitee.com/MNxiaona/733GH

1.应用底子知识
2.设置文件
3.应用数据管理
4.应用安全管理
5.应用隐私保护
6.三方应用调用管控机制
7.资源分类与访问
8.学习ArkTS语言
9.……
https://img-blog.csdnimg.cn/direct/47ff5221520f4fd29b4b344040b6c910.png
基于ArkTS 开发

1.Ability开发
2.UI开发
3.公共事件与通知
4.窗口管理
5.媒体
6.安全
7.网络与链接
8.电话服务
9.数据管理
10.后台使命(Background Task)管理
11.设备管理
12.设备使用信息统计
13.DFX
14.国际化开发
15.折叠屏系列
16.……
https://img-blog.csdnimg.cn/direct/dde1b78b86224ce5ae8dbc84fea54eba.png
鸿蒙开发面试真题(含参考答案):gitee.com/MNxiaona/733GH

https://img-blog.csdnimg.cn/direct/6ecd0695d29b44b3acd452cb350131f1.png
鸿蒙入门讲授视频:

https://img-blog.csdnimg.cn/direct/6c4a4627851541cabe0f7614bcb2c0e8.png
美团APP实战开发讲授:gitee.com/MNxiaona/733GH

https://img-blog.csdnimg.cn/direct/84ad2d4eaec84d16acfeab591834605d.png
写在最后



[*]如果你觉得这篇内容对你还蛮有帮助,我想邀请你帮我三个小忙:
[*]点赞,转发,有你们的 『点赞和评论』,才是我创造的动力。
[*]关注小编,同时可以期待后续文章ing
页: [1]
查看完整版本: HarmonyOS Next开发学习手册——C/C++尺度库机制