编码基础知识

重要

这是 Azure Sphere(旧版)文档。 Azure Sphere(旧版)将于 2027 年 9 月 27 日停用,用户此时必须迁移到 Azure Sphere(集成)。 使用位于 TOC 上方的版本选择器查看 Azure Sphere(集成)文档。

建议应用程序代码符合本主题中定义的最低质量标准。 通过我们与寻求改进其生产部署的应用程序的客户的合作关系,我们发现了一些常见问题,这些问题在修复时提高了应用程序的性能。

常见问题

  • 在设置目标 API 集时,我们建议使用最新的 CMake 和 Azure Sphere 工具,最后通过设置 AZURE_SPHERE_TARGET_API_SET="latest-lts"编译最终的发布二进制文件。 有关更多详细信息,请参阅 “为可续订安全性编码”。

注意

在制造过程中专门创建要旁加载的映像包时,请设置为 AZURE_SPHERE_TARGET_API_SET 设备已源或恢复到的相应 Azure Sphere OS 版本;否则会导致 Azure Sphere OS 拒绝映像包。

  • 准备好将 应用程序 部署到生产环境时,请确保在发布模式下编译最终映像包。
  • 尽管编译器警告,但通常会看到部署到生产的应用程序。 强制实施完整版本的零警告策略可确保有意解决每个编译器警告。 下面是最常见的警告类型,强烈建议你解决这些警告类型:
    • 隐式转换相关的警告: 由于初始、快速实现保持未恢复的隐式转换,通常会引入 Bug。 例如,在不同数值类型之间具有许多隐式数值转换的代码可能会导致关键精度损失,甚至导致计算或分支错误。 为了正确调整所有数值类型,建议同时进行有意分析和强制转换,而不仅仅是强制转换。
    • 避免更改预期的参数类型: 调用 API 时,如果未显式强制转换,则隐式转换可能会导致问题;例如,在使用有符号数字类型而不是未签名数值类型时溢出缓冲区。
    • const-discarding 警告: 当函数需要 const 类型作为参数时,重写它可能会导致 bug 和不可预知的行为。 警告的原因是确保 const 参数保持不变,并在设计特定 API 或函数时考虑限制。
    • 不兼容的指针或参数警告: 忽略此警告通常会隐藏稍后难以跟踪的 bug。 消除这些警告有助于减少其他应用程序问题的诊断时间。
  • 设置一致的 CI/CD 管道是可持续长期应用程序管理的关键,因为它可以轻松重新生成二进制文件及其对应符号来调试较旧的应用程序版本。 适当的分支策略对于跟踪发布并避免在存储二进制数据时占用昂贵的磁盘空间也至关重要。
  • 如果可能,请将所有常见的固定字符串定义为 global const char* 而不是硬编码它们(例如,在命令中 printf ),以便它们可以用作整个代码库的数据指针,同时使代码保持更可维护性。 在实际应用程序中,从日志或字符串操作(例如 OKSucceeded或 JSON 属性名称)中获取通用文本,并将其全球化为常量通常会导致在只读数据内存部分(也称为 .rodata)中节省,这可以转换为可在其他部分使用的闪存(如用于更多代码的 .text)。 这种情况通常被忽略,但它可以节省大量闪存。

注意

还可以通过激活编译器优化(如 -fmerge-constants gcc 上的)来实现上述目的。 如果选择此方法,还检查编译器输出并验证是否应用了所需的优化,因为这些优化可能因不同的编译器版本而异。

  • 对于全局数据结构,尽可能考虑为相当小的数组成员提供固定长度,而不是使用指针来动态分配内存。 例如:
    typedef struct {
      int chID;
      ...
      char chName[SIZEOF_CHANNEL_NAME]; // This approach is preferable, and easier to use e.g. in a function stack.
      char *chName; // Unless this points to a constant, tracking a memory buffer introduces more complexity, to be weighed with the cost/benefit, especially when using multiple instances of the structure.
      ...
    } myConfig;
  • 尽可能避免动态内存分配,尤其是在经常调用的函数中。
  • 在 C 中,查找返回指向内存缓冲区的指针的函数,并考虑将它们转换为返回引用的缓冲区指针及其相关大小的函数。 这样做的原因是,仅返回指向缓冲区的指针通常会导致调用代码出现问题,因为返回的缓冲区的大小不会强行确认,因此可能会危及堆的一致性。 例如:
    // This approach is preferable:
    MY_RESULT_TYPE getBuffer(void **ptr, size_t &size, [...other parameters..])
    
    // This should be avoided, as it lacks tracking the size of the returned buffer and a dedicated result code:
    void *getBuffer([...other parameters..])

动态容器和缓冲区

嵌入的 C 应用程序中也经常使用列表和向量等容器,并指出由于使用标准库的内存限制,它们通常需要显式编码或链接为库。 如果不经过精心设计,这些库实现可能会触发大量内存使用量。

除了典型的静态分配数组或高度内存动态实现之外,我们建议采用增量分配方法。 例如,从 N 预分配对象的空队列实现开始;在(N+1)的队列推送上,队列由固定的 X 其他预分配对象(N=N+X)增长,该对象将保持动态分配,直到队列的另一个添加将溢出其当前容量,并将内存分配增加 X 个额外的预分配对象。 你最终可以实现一个新的压缩函数来谨慎调用(因为它太昂贵,无法定期调用),以回收未使用的内存。

专用索引将动态保留队列的活动对象计数,该计数可以限制为额外的溢出保护的最大值。

此方法消除了传统队列实现中连续内存分配和解除分配生成的“聊天”。 有关详细信息,请参阅 内存管理和使用情况。 可以为列表、数组等结构实现类似的方法。