编码基础知识

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

常见问题

  • 在设置目标 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 个附加的预分配对象递增内存分配。 最终可以实现新的压缩函数来谨慎调用 (,因为定期调用) 回收未使用的内存的成本太高。

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

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