Partilhar via


Noções básicas sobre codificação

Sugerimos que o código da aplicação cumpra um padrão de qualidade mínimo, conforme definido neste tópico. Através das nossas parcerias com clientes que procuram melhorar as aplicações implementadas na produção, encontrámos alguns problemas comuns que, quando corrigidos, melhoram o desempenho das aplicações.

Problemas comuns

  • Ao definir o conjunto de API de destino, recomendamos que utilize as ferramentas mais recentes do CMake e do Azure Sphere e, em última análise, compile os binários de Versão finais ao definir AZURE_SPHERE_TARGET_API_SET="latest-lts". Para obter mais detalhes, veja Codificação para segurança renovável.

Nota

Ao criar especificamente pacotes de imagens para serem sideloadados num processo de fabrico, defina AZURE_SPHERE_TARGET_API_SET para a versão adequada do SO do Azure Sphere com a qual o dispositivo foi obtido ou recuperado. Se não o fizer, o SO do Azure Sphere rejeitará o pacote de imagem.

  • Quando estiver pronto para implementar uma aplicação para produção, confirme que compila os pacotes de imagens finais no Modo de lançamento.
  • É comum ver aplicações implementadas na produção, apesar dos avisos do compilador. A imposição de uma política de avisos zero para compilações completas garante que todos os avisos do compilador são intencionalmente abordados. Seguem-se os tipos de aviso mais frequentes, que recomendamos vivamente que aborde:
    • Avisos implícitos relacionados com a conversão: Os erros são muitas vezes introduzidos devido a conversões implícitas resultantes de implementações rápidas iniciais que permaneceram não supervisionadas. 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 a análise intencional e a conversão de fundição e não apenas a conversão.
    • Evite alterar os tipos de parâmetro esperados: Ao chamar APIs, se não for explicitamente a conversão, as conversões implícitas podem causar problemas; por exemplo, ultrapassar uma memória intermédia quando é utilizado um tipo numérico assinado em vez de um tipo numérico não assinado.
    • Avisos de eliminação de erros: Quando uma função requer um tipo de parâmetro const, substitui-lo pode levar a erros e comportamento imprevisível. O motivo do aviso é garantir que o parâmetro const permanece intacto e tem em conta as restrições ao conceber uma determinada API ou função.
    • Avisos de parâmetros ou ponteiro incompatíveis: Ignorar este aviso pode, muitas vezes, ocultar erros que serão difíceis de controlar mais tarde. Eliminar estes avisos pode ajudar a reduzir o tempo de diagnóstico de outros problemas da aplicação.
  • A configuração de um pipeline CI/CD consistente é fundamental para uma gestão de aplicações sustentável a longo prazo, uma vez que permite facilmente recriar binários e os respetivos símbolos correspondentes para depurar versões de aplicações mais antigas. Uma estratégia de ramificação adequada também é essencial para controlar as versões e evita um espaço em disco dispendioso no armazenamento de dados binários.
  • Sempre que possível, defina todas as cadeias fixas comuns como global const char* em vez de as codificar (por exemplo, nos printf comandos) para que possam ser utilizadas como ponteiros de dados em toda a base de código, mantendo o código mais mantêvel. Nas aplicações do mundo real, recolher texto comum de registos ou manipulações de cadeias (como OK, Succeededou nomes de propriedades JSON) e globalizá-lo para constantes resultou frequentemente em poupanças na secção de memória de dados só de leitura (também conhecida como .rodata), que se traduz em poupanças na memória flash que podem ser utilizadas noutras secções (como .text para obter mais código). Este cenário é muitas vezes ignorado, mas pode gerar poupanças significativas na memória flash.

Nota

As opções acima também podem ser obtidas simplesmente através da ativação de otimizações do compilador (por -fmerge-constants exemplo, em gcc). Se escolher esta abordagem, inspecione também a saída do compilador e verifique se as otimizações pretendidas foram aplicadas, uma vez que estas podem variar em diferentes versões do compilador.

  • Para estruturas de dados globais, sempre que possível, considere dar comprimentos fixos a membros de matriz razoavelmente pequenos em vez de utilizar ponteiros para alocar dinamicamente a memória. 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 devolvam um ponteiro para uma memória intermédia e considere convertê-las em funções que devolvem um ponteiro de memória intermédia referenciado e o respetivo tamanho relacionado para os autores da chamada. A razão para tal é que devolver apenas um ponteiro a uma memória intermédia tem muitas vezes causado problemas com o código de chamada, uma vez que o tamanho da memória intermédia devolvida não é confirmado à força e, portanto, pode colocar em risco a consistência da área dinâmica para dados. 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..])

Contentores dinâmicos e memórias intermédias

Os contentores, como listas e vetores, também são frequentemente utilizados em aplicações C incorporadas, com a ressalva de que, devido às limitações de memória na utilização de bibliotecas padrão, normalmente precisam de ser explicitamente codificados ou ligados como bibliotecas. Estas implementações de biblioteca podem acionar uma utilização intensiva da memória se não forem cuidadosamente concebidas.

Além das matrizes típicas alocadas 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 (N+1)th queue push, a fila aumenta por um X fixo objetos pré-alocados adicionais (N=N+X), que permanecerão alocados dinamicamente até que outra adição à fila exceda a sua capacidade atual e incremente a alocação de memória por X objetos pré-alocados adicionais. Pode eventualmente implementar uma nova função de compactação para chamar com moderação (uma vez que seria demasiado caro chamar regularmente) para recuperar a memória não utilizada.

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

Esta abordagem elimina a "conversa" gerada pela alocação contínua de memória e desalocação em implementações de filas tradicionais. Para obter detalhes, veja Gestão e utilização da memória. Pode implementar abordagens semelhantes para estruturas como listas, matrizes, etc.