Partilhar via


Fundamentos de codificação

Importante

Esta é a documentação do Azure Sphere (Legado). O Azure Sphere (Legado) será desativado em 27 de setembro de 2027 e os usuários devem migrar para o Azure Sphere (Integrado) até esse momento. Use o seletor de versão localizado acima do sumário para exibir a documentação do Azure Sphere (Integrado).

Sugerimos que o código do seu aplicativo atenda a um padrão mínimo de qualidade, conforme definido neste tópico. Através de nossas parcerias com clientes que buscam melhorar seus aplicativos implantados em produção, encontramos alguns problemas comuns que, quando corrigidos, melhoram o desempenho dos aplicativos.

Problemas comuns

  • Ao definir o conjunto de APIs de destino, recomendamos usar as ferramentas mais recentes do CMake e do Azure Sphere e, finalmente, compilar os binários finais da versão definindo AZURE_SPHERE_TARGET_API_SET="latest-lts". Para obter mais detalhes, consulte Codificação para segurança renovável.

Nota

Ao criar especificamente pacotes de imagem para serem sideloaded dentro de um processo de fabricação, defina AZURE_SPHERE_TARGET_API_SET para a versão apropriada do sistema operacional Azure Sphere com a qual o dispositivo foi originado ou recuperado, não fazer isso fará com que o sistema operacional Azure Sphere rejeite o pacote de imagem.

  • Quando estiver pronto para implantar um aplicativo na produção, certifique-se de compilar os pacotes de imagem finais no modo de versão.
  • É comum ver aplicativos implantados em produção apesar dos avisos do compilador. A imposição de uma política de avisos zero para compilações completas garante que cada aviso do compilador seja intencionalmente abordado. A seguir estão os tipos de aviso mais frequentes, que recomendamos que você resolva:
    • Avisos implícitos relacionados à conversão: os bugs geralmente são introduzidos devido a conversões implícitas resultantes de implementações iniciais e rápidas que permaneceram sem revisão. Por exemplo, o código que tem muitas conversões numéricas implícitas entre diferentes tipos numéricos pode resultar em perda de precisão crítica ou até mesmo erros de cálculo ou ramificação. Para ajustar corretamente todos os tipos numéricos, recomenda-se tanto a análise intencional quanto o casting, não apenas o casting.
    • Evite alterar os tipos de parâmetros esperados: ao chamar APIs, se não forem explicitamente lançadas, as conversões implícitas podem causar problemas, por exemplo, a sobrecarga de um buffer quando um tipo numérico assinado é usado em vez de um tipo numérico não assinado.
    • const-discarding warnings: Quando uma função requer um tipo const como parâmetro, substituí-la pode levar a bugs e comportamento imprevisível. A razão para o aviso é garantir que o parâmetro const permaneça intacto e leve em conta as restrições ao projetar uma determinada API ou função.
    • Avisos de ponteiro ou parâmetro incompatíveis: ignorar esse aviso muitas vezes pode ocultar bugs que serão difíceis de rastrear mais tarde. A eliminação desses avisos pode ajudar a reduzir o tempo de diagnóstico de outros problemas do aplicativo.
  • Configurar um pipeline de CI/CD consistente é fundamental para o gerenciamento sustentável de aplicativos a longo prazo, pois permite facilmente a reconstrução de binários e seus símbolos correspondentes para depurar versões de aplicativos mais antigos. Uma estratégia de ramificação adequada também é essencial para rastrear lançamentos e evita espaço em disco dispendioso no armazenamento de dados binários.
  • Quando possível, defina todas as cadeias de caracteres fixas comuns como global const char* em vez de codificá-las (por exemplo, dentro printf de comandos) para que possam ser usadas como ponteiros de dados em toda a base de código, mantendo o código mais fácil de manter. Em aplicativos do mundo real, coletar texto comum de logs ou manipulações de cadeia de caracteres (como OKnomes de propriedade , Succeeded, ou JSON) e globalizá-lo em constantes geralmente resultou em economia na seção de memória de dados somente leitura (também conhecida como .rodata), o que se traduz em economia de memória flash que poderia ser usada em outras seções (como .text para obter mais código). Este cenário é muitas vezes ignorado, mas pode gerar economias significativas na memória flash.

Nota

O acima também pode ser alcançado simplesmente ativando otimizações do compilador (como -fmerge-constants no gcc). Se você escolher essa abordagem, também inspecione a saída do compilador e verifique se as otimizações desejadas foram aplicadas, pois elas podem variar entre diferentes versões do compilador.

  • Para estruturas de dados globais, sempre que possível, considere dar comprimentos fixos a membros de array razoavelmente pequenos em vez de usar ponteiros para memória alocada dinamicamente. Por exemplo:
    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;
  • Evite a alocação de memória dinâmica sempre que possível, especialmente em funções frequentemente chamadas.
  • Em C, procure funções que retornem um ponteiro para um buffer de memória e considere convertê-las em funções que retornem um ponteiro de buffer referenciado e seu tamanho relacionado aos chamadores. A razão para fazer isso é que retornar apenas um ponteiro para um buffer muitas vezes levou a problemas com o código de chamada, uma vez que o tamanho do buffer retornado não é reconhecido à força e, portanto, pode colocar em risco a consistência do heap. Por exemplo:
    // 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..])

Contêineres dinâmicos e buffers

Contêineres como listas e vetores também são frequentemente usados em aplicativos C incorporados, com a ressalva de que, devido a limitações de memória no uso de bibliotecas padrão, eles normalmente precisam ser explicitamente codificados ou vinculados como bibliotecas. Essas implementações de biblioteca podem desencadear o uso intensivo de memória se não forem cuidadosamente projetadas.

Além dos típicos arrays alocados estaticamente ou implementações altamente dinâmicas de memória, recomendamos uma abordagem de alocação incremental. Por exemplo, comece com uma implementação de fila vazia de N objetos pré-alocados; no push de fila (N+1), a fila cresce por um X fixo de objetos pré-alocados adicionais (N=N+X), que permanecerão alocados dinamicamente até que outra adição à fila transborde sua capacidade atual e incremente a alocação de memória por X objetos pré-alocados adicionais. Você pode, eventualmente, implementar uma nova função de compactação para chamar com moderação (pois seria muito caro chamar regularmente) para recuperar a memória não utilizada.

Um índice dedicado preservará dinamicamente a contagem de objetos ativos para a fila, que pode ser limitada a um valor máximo para proteção adicional contra estouro.

Essa abordagem elimina a "tagarelice" gerada pela alocação e desalocação contínua de memória em implementações de fila tradicionais. Para obter detalhes, consulte Gerenciamento e uso de memória. Você pode implementar abordagens semelhantes para estruturas como listas, matrizes e assim por diante.