Fundamentos de codificação
Importante
Esta é a documentação do Azure Sphere (herdado). O Azure Sphere (herdado) 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 aplicativo atenda a um padrão mínimo de qualidade, conforme definido neste tópico. Por meio 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, por fim, compilar os binários de versão final definindo
AZURE_SPHERE_TARGET_API_SET="latest-lts"
. Para obter mais detalhes, consulte Codificação para segurança renovável.
Observação
Ao criar especificamente pacotes de imagem a serem carregados em um processo de fabricação, defina AZURE_SPHERE_TARGET_API_SET
como a versão apropriada do sistema operacional do Azure Sphere com a qual o dispositivo foi originado ou recuperado; não fazer isso fará com que o sistema operacional do 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 final no modo de versão.
- É comum ver aplicativos implantados na produção, apesar dos avisos do compilador. Impor uma política de avisos zero para builds completos garante que todos os avisos do compilador sejam abordados intencionalmente. A seguir estão os tipos de aviso mais frequentes, que recomendamos que você aborde:
- 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 crítica de precisão 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, não apenas a conversão.
- Evite alterar os tipos de parâmetro esperados: ao chamar APIs, se não for explicitamente convertido, as conversões implícitas podem causar problemas; por exemplo, ultrapassar um buffer quando um tipo numérico assinado é usado em vez de um tipo numérico não assinado.
- avisos const-discarding: quando uma função requer um tipo const como parâmetro, substituí-lo pode levar a bugs e comportamento imprevisível. O motivo do aviso é garantir que o parâmetro const permaneça intacto e leve em consideração as restrições ao projetar uma determinada API ou função.
- Avisos de ponteiro ou parâmetro incompatíveis: ignorar esse aviso geralmente pode ocultar bugs que serão difíceis de rastrear posteriormente. A eliminação desses avisos pode ajudar a reduzir o tempo de diagnóstico de outros problemas de aplicação.
- A configuração de um pipeline consistente de CI/CD é fundamental para o gerenciamento sustentável de aplicativos de longo prazo, pois permite facilmente a recriação de binários e seus símbolos correspondentes para depurar versões de aplicativos mais antigas. Uma estratégia de ramificação adequada também é essencial para rastrear versões e evita espaço em disco dispendioso no armazenamento de dados binários.
Problemas relacionados a memória
- Quando possível, defina todas as cadeias de caracteres fixas comuns em
global const char*
vez de codificá-las (por exemplo, dentroprintf
de comandos) para que possam ser usadas como ponteiros de dados em toda a base de código, mantendo o código mais sustentável. Em aplicativos do mundo real, coletar texto comum de logs ou manipulações de cadeia de caracteres (comoOK
,Succeeded
, ou nomes de propriedade JSON) e globalizá-lo para constantes geralmente resulta 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 pode ser usada em outras seções (como .text para mais código). Esse cenário é frequentemente negligenciado, mas pode gerar economias significativas na memória flash.
Observação
O acima também pode ser obtido simplesmente ativando otimizações do compilador (como -fmerge-constants
no gcc). Se você escolher essa abordagem, inspecione também 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 fornecer comprimentos fixos para membros de matriz 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 dinâmica de memória sempre que possível, especialmente em funções chamadas com frequência.
- 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. O motivo para fazer isso é que retornar apenas um ponteiro para um buffer geralmente leva a problemas com o código de chamada, já 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 e buffers dinâmicos
Contêineres, como listas e vetores, também são frequentemente usados em aplicativos C incorporados, com a ressalva de que, devido às 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 disparar o uso intensivo de memória se não forem cuidadosamente projetadas.
Além das matrizes alocadas estaticamente típicas 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 da fila (N+1)th, a fila cresce por X objetos pré-alocados adicionais fixos (N=N+X), que permanecerão alocados dinamicamente até que outra adição à fila estoure sua capacidade atual e incremente a alocação de memória em X objetos pré-alocados adicionais. Você pode eventualmente implementar uma nova função de compactação para chamar com moderação (já que 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 "vibração" gerada pela alocação e desalocação contínuas 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.