Quanto do C23 já pode ser usado em sistemas embarcados?
Introdução
No artigo anterior, 25 anos de evolução da linguagem C: de C99 a C23, percorremos os recursos que a linguagem ganhou desde 1999 e terminamos com uma pergunta: quantas dessas funcionalidades já estão realmente disponíveis no ecossistema embarcado atual?
Comecemos por um exemplo concreto. Somar dois comprimentos recebidos pela rede sem verificar overflow é uma origem clássica de vulnerabilidades, e há anos o GCC oferece um builtin para isso:
uint32_t result;
if (__builtin_add_overflow(a, b, &result)) {
return -ERANGE;
}
O C23 padronizou a mesma operação, com o nome ckd_add, no cabeçalho stdckdint.h:
uint32_t result;
if (ckd_add(&result, a, b)) {
return -ERANGE;
}
O código fica portável e independente de compilador. Mas a pergunta imediata de quem escreve firmware é outra: será que o meu toolchain já suporta isso?
A resposta curta é: depende de duas coisas diferentes, e quase sempre só verificamos uma delas.
Para responder com dados, e não com impressões, montamos um conjunto de testes, um para cada recurso do C23, e o rodamos em 14 toolchains: quatro versões do GCC, cinco do Clang, três da Arm GNU Toolchain e o Zephyr SDK. Este artigo mostra o que encontramos, inclusive algumas surpresas, e termina com um caminho prático para usar o C23 hoje sem quebrar o build de ninguém.
Duas perguntas: o compilador e a biblioteca
Quando alguém pergunta “o meu toolchain suporta C23?”, na verdade está fazendo duas perguntas.
A primeira é sobre a linguagem: nullptr, constexpr, literais binários, enums com tipo fixo, atributos, #embed. Esses recursos são implementados pelo compilador. Se o GCC ou o Clang aceita a construção, ela funciona, independentemente do resto do sistema.
A segunda é sobre a biblioteca: stdbit.h, memset_explicit, o especificador %b do printf, as funções strdup e memccpy, que entraram no padrão no C23. Esses recursos vêm da biblioteca C (libc). E aqui está o ponto que costuma passar despercebido: em sistemas embarcados, a libc quase nunca é a glibc que usamos no Linux. É a newlib, que acompanha a Arm GNU Toolchain; a picolibc, adotada pelo Zephyr; ou uma biblioteca do fabricante do compilador.
Existe ainda um caso intermediário. Os cabeçalhos stdckdint.h e stddef.h (onde fica a macro unreachable()) fazem parte da biblioteca padrão, mas quem os fornece é o próprio compilador. Por isso, eles se comportam como recursos de linguagem: dependem só da versão do GCC ou do Clang.
A consequência prática é que a opção -std=c23 diz ao compilador qual linguagem aceitar, mas não garante nada sobre a libc. É perfeitamente possível ter um compilador que aceita todo o C23 ligado a uma biblioteca que ainda não implementou as novidades.
Como medimos
Cada recurso tem um arquivo de teste pequeno. Um exemplo:
/* tipo: biblioteca; verifica: linka; recurso: memset_explicit */
#include <string.h>
int main(void) {
char chave[16] = "segredo"; memset_explicit(chave, 0, sizeof chave);
return chave[0];
}
A primeira linha diz o tipo do recurso e o que verificar: se o código compila, se também linka (para recursos de biblioteca, a declaração no cabeçalho não basta, a função precisa existir) ou se roda e produz a saída esperada. Um script compila cada arquivo em cada toolchain com -std=c23 (ou -std=c2x, o nome provisório, quando o compilador não aceita c23), -pedantic-errors e -Werror. Essas duas últimas opções garantem que uma extensão do compilador não seja contada como suporte ao padrão.
Os toolchains de host foram testados em imagens Docker: gcc:13 a gcc:16, silkeh/clang:18 a 22 e fedora:44, a única com a glibc 2.43. Para os alvos ARM, usamos as Arm GNU Toolchains 13.3, 14.3 e 15.2, com a newlib, e o Zephyr SDK 1.0.1, com GCC 14.3 e picolibc 1.8.10, compilando para Cortex-M4. Nos alvos ARM os binários são linkados, mas não executados. As páginas de status do GCC e do Clang [10, 11] indicam em que versão cada recurso foi implementado; os testes confirmam isso na prática, no modo estrito e também nos alvos ARM.
Todos os testes, os scripts e a matriz completa, em uma planilha, estão no repositório rdws_examples_articles [1]. A matriz é um retrato de outubro de 2026, e qualquer pessoa pode rodá-la de novo para acompanhar a evolução das toolchains.
A Figura 1 resume os recursos em que os toolchains diferem; os demais funcionaram em todos. As próximas seções explicam essas diferenças.
A linguagem: quase tudo já chegou
A primeira boa notícia é que a maior parte do C23 como linguagem, já está disponível em qualquer GCC ou Clang recente. Doze dos recursos testados funcionaram em todos os 14 toolchains, inclusive no GCC 13 e no Clang 18: bool, true e false como palavras-chave, nullptr, os atributos padronizados, os separadores de dígitos, os enums com tipo subjacente, typeof, auto, static_assert sem mensagem, o inicializador vazio = {}, __VA_OPT__, #elifdef e unreachable().
As diferenças aparecem nos recursos restantes:
| Recurso | GCC 13 | GCC 14 | GCC 15 e 16 | Clang 18 | Clang 19 a 22 |
|---|---|---|---|---|---|
| Os 12 recursos acima | sim | sim | sim | sim | sim |
| Literais binários 0b | sim | sim | sim | não* | sim |
| constexpr | sim | sim | sim | não | sim |
| #embed | não | não | sim | não | sim |
| _BitInt(N) | não | sim** | sim** | sim | sim |
| stdckdint.h | não | sim | sim | sim | sim |
| __STDC_VERSION__ | 202000L | 202000L | 202311L | 202311L | 202311L |
* O Clang 18 aceita 0b, mas ainda o trata como extensão GNU mesmo em -std=c23, o que falha com -pedantic-errors.
** Apenas no x86-64. Nos alvos Cortex-M, nenhuma versão do GCC testada aceita _BitInt, como explicado logo abaixo.
Três observações merecem destaque.
#embed é o recurso mais recente. Só funciona a partir do GCC 15 e do Clang 19. Se o seu SDK ainda traz o GCC 14, como o Zephyr SDK 1.0.1, incorporar um certificado no firmware continua exigindo o velho xxd -i.
_BitInt não funciona em ARM de 32 bits. No x86-64, o GCC aceita _BitInt(N) desde a versão 14. Nos alvos Cortex-M, nenhuma das versões testadas, nem a 15.2, aceita o recurso, e a mensagem é direta: “_BitInt(12) is not supported on this target”. É um bom lembrete de que “o compilador suporta” pode depender do alvo para o qual ele gera código.
__STDC_VERSION__ engana. O C23 define essa macro como 202311L, mas o GCC 13 e o GCC 14 usam 202000L, o valor provisório, mesmo com -std=c23. Só o GCC 15 passou a usar o valor final. Um teste como #if __STDC_VERSION__ >= 202311L conclui que não há C23 no GCC 14, que aceita quase tudo. Voltaremos a isso na seção sobre como usar o C23 hoje.
A biblioteca: onde o embarcado ainda está atrás
Na biblioteca, a situação é bem diferente. Aqui a coluna que importa não é a do compilador, mas a da libc:
| Recurso | glibc 2.36 | glibc 2.41 | glibc 2.43 | newlib 4.4 e 4.5 | picolibc 1.8.10 |
|---|---|---|---|---|---|
| stdbit.h | não | sim | sim | não | não |
| memset_explicit | não | não | sim | não | sim |
| strdup e strndup | sim | sim | sim | só gnu | só gnu |
| memccpy | sim | sim | sim | só gnu | só gnu |
| timegm | sim | sim | sim | não | só gnu |
| printf(“%b”) | sim | sim | sim | não testado | não testado |
“Só gnu” significa que a função existe e linka, mas o cabeçalho só a declara quando o código é compilado com -std=gnu23, e não com -std=c23.
stdbit.h ainda não chegou às libcs de embarcados. Na glibc, o cabeçalho existe desde a versão 2.39 [2]. Nem a newlib 4.5 nem a picolibc 1.8.10 o oferecem. Quem precisa de stdc_count_ones ou stdc_leading_zeros em um Cortex-M continua usando os builtins do GCC, como __builtin_popcount e __builtin_clz.
Em memset_explicit, a picolibc está à frente da glibc. A função só entrou na glibc na versão 2.43, publicada no início de 2026 [2]. A newlib ainda não a tem. A picolibc 1.8.10, que acompanha o Zephyr SDK, já a declara e implementa. Para um firmware que guarda chaves de criptografia, como uma PSK de DTLS, essa é exatamente a função que interessa.
As funções “novas” que são velhas conhecidas. strdup, strndup e memccpy existem há décadas no POSIX e só no C23 entraram no padrão da linguagem. A newlib e a picolibc as implementam, mas os cabeçalhos ainda as escondem atrás das macros de visibilidade do POSIX. Em modo estrito, com -std=c23, a declaração não aparece e o código não compila. Com -std=gnu23, tudo funciona. Esse comportamento tem uma explicação: no modo estrito, o compilador define a macro __STRICT_ANSI__, e é ela que o cabeçalho sys/features.h da newlib consulta para decidir o que expor. timegm é um caso à parte: a newlib simplesmente não tem a função.
%b no printf não foi testado nos alvos ARM, porque os binários não foram executados. Além disso, em firmware, o printf costuma ser uma versão reduzida, como a nano da newlib ou as variantes de inteiros da picolibc, e o suporte a cada especificador depende da variante escolhida no build.
E nos SDKs do dia a dia?
Para quem desenvolve firmware, a pergunta prática não é qual é o GCC mais recente, mas qual GCC vem com o SDK que já está em uso. Alguns exemplos, em outubro de 2026:
- Zephyr. O Kconfig tem a opção CONFIG_STD_C23, mas o padrão continua sendo CONFIG_STD_C17, e C90, C99 e C11 aparecem como obsoletos [3]. Ao escolher C23, o sistema de build passa -std=c2x ao compilador, o nome provisório, aceito também por compiladores mais antigos. As extensões GNU vêm desligadas por padrão (CONFIG_GNU_C_EXTENSIONS), o que significa modo estrito: exatamente o cenário em que a picolibc esconde strdup e memccpy. O Zephyr SDK 1.0.1 traz o GCC 14.3, sem #embed e com __STDC_VERSION__ igual a 202000L.
- ESP-IDF. A Espressif anda à frente: segundo o arquivo tools/tools.json de cada versão [4], o ESP-IDF v5.5 usa o GCC 14.2, as versões v6.0 e v6.1 usam o GCC 15.2, e o ramo de desenvolvimento já está no GCC 16.1. Para quem está na v6.x, #embed e o valor final de __STDC_VERSION__ já estão disponíveis.
- STM32CubeIDE. A versão 2.2.0 traz as GNU Tools for STM32, baseadas na Arm GNU Toolchain 14.3, e também o STARM-Clang 21.1.1, a variante do Clang mantida pela ST [5]. Com o GCC, valem as limitações do GCC 14: sem #embed e com __STDC_VERSION__ igual a 202000L. Pela versão do LLVM, o STARM-Clang deve aceitar toda a parte de linguagem da tabela, mas não o testamos, e a biblioteca que o acompanha é outra questão.
- Arm GNU Toolchain. A versão 15.2 já está disponível, mas muitos projetos e IDEs ainda fixam a 13.x ou a 14.x.
A regra geral é que os SDKs de fabricantes ficam uma ou duas versões atrás do GCC mais recente. Na prática, a coluna da tabela de linguagem que vale para o seu projeto provavelmente é a do GCC 14, e não a do GCC 16.
E os compiladores comerciais? Eles não entraram nos testes, então os dados a seguir vêm da documentação de cada fabricante:
- IAR Embedded Workbench for Arm. A documentação da versão 10.1x informa que o compilador suporta o C18, que é o padrão default, e que recursos do C23 “são continuamente adicionados”. A lista atual se resume à sintaxe de atributos [[…]], a __has_c_attribute e aos atributos padronizados [[deprecated]], [[nodiscard]], [[maybe_unused]], [[fallthrough]] e [[noreturn]] [6].
- Arm Compiler. A Arm está substituindo o Arm Compiler 6 pela Arm Toolchain for Embedded (ATfE), baseada no LLVM. Como o compilador é o Clang, o suporte à linguagem tende a acompanhar a versão do LLVM em que cada release se baseia. A biblioteca é outra história: a ATfE começou com a picolibc e está migrando para a LLVM libc [7].
- Microchip MPLAB XC32. Aqui a resposta depende da arquitetura, mesmo dentro da versão 6, de julho de 2026. Para os PIC32C e SAM, com núcleos Arm, as release notes informam que o compilador passou a usar por padrão o C17 com extensões GNU, e que alguns recursos e funções da biblioteca que não fazem sentido em bare-metal ficam de fora; a base é o GCC 13, desde a versão 5.00 [8]. Para os PIC32M, com núcleo MIPS, o guia do usuário da mesma versão lista os padrões aceitos pela opção -std só até o C11 [9]. Nenhum dos dois documentos menciona o C23. Como a base Arm é o GCC 13, é provável que -std=c2x seja aceito, mas isso não está documentado e não o testamos.
Como usar o C23 hoje sem quebrar o build
Com esse cenário heterogêneo, a estratégia mais segura é testar cada recurso, e não a versão do padrão.
1. Não detecte o C23 por __STDC_VERSION__ >= 202311L. Como vimos, isso exclui o GCC 13 e o GCC 14. Se a ideia é saber se o compilador está em modo C23, compare com o valor do C17:
/* 201710L é o C17. GCC 13 e 14 definem 202000L com -std=c2x/c23, então não use
* __STDC_VERSION__ >= 202311L para detectar o C23. */
#if __STDC_VERSION__ > 201710L
#define COMPAT_C23 1
#else
#define COMPAT_C23 0
#endif
2. Para cabeçalhos, use __has_include. É o jeito de saber se stdckdint.h ou stdbit.h existem, independentemente do compilador e da libc. Para #embed, existe __has_embed; para atributos, __has_c_attribute.
3. Centralize os substitutos em um cabeçalho do projeto. Quando o recurso falta, quase sempre existe um equivalente: um builtin do GCC, uma macro, uma pequena função. Um trecho do cabeçalho que acompanha os exemplos:
#if defined(__has_include)
#if __has_include(<stdckdint.h>)
#include <stdckdint.h>
#define COMPAT_TEM_CKDINT 1
#endif
#endif
#ifndef COMPAT_TEM_CKDINT
#define ckd_add(r, a, b) __builtin_add_overflow((a), (b), (r))
#define ckd_sub(r, a, b) __builtin_sub_overflow((a), (b), (r))
#define ckd_mul(r, a, b) __builtin_mul_overflow((a), (b), (r))
#endif
O código do projeto usa ckd_add sempre; o cabeçalho decide se ela vem da biblioteca padrão ou do builtin do GCC, que tem a mesma semântica. O mesmo arquivo faz isso para nullptr, bool e static_assert em compiladores anteriores ao C23.
4. Para funções de biblioteca sem macro de teste, deixe o build decidir. Não existe uma forma, dentro do código, de saber se a libc tem memset_explicit. A solução é o sistema de build testar (compilando um programa de uma linha) e definir uma macro como HAVE_MEMSET_EXPLICIT. Na falta da função, o cabeçalho oferece um substituto que impede o compilador de remover a escrita:
#ifndef HAVE_MEMSET_EXPLICIT
static inline void *compat_memset_explicit(void *p, int c, size_t n)
{
memset(p, c, n);
__asm__ volatile("" : : "r"(p) : "memory");
return p;
}
#define memset_explicit compat_memset_explicit
#endif
A linha de __asm__ é uma barreira de compilador: diz ao GCC que a memória apontada por p pode ser lida por algo que ele não enxerga, e por isso o memset não pode ser eliminado. É uma extensão do GCC e do Clang, assunto do próximo artigo.
Testamos esse cabeçalho do -std=c11 ao -std=c23 no GCC 13, no GCC 15, no Clang 18, no Fedora 44 com o memset_explicit nativo e nos toolchains ARM.
5. Teste em mais de um toolchain. Um pipeline de CI que compila o firmware com o GCC do SDK e com uma versão mais nova do GCC ou do Clang pega cedo tanto o uso acidental de um recurso que o SDK ainda não tem quanto o código que deixou de compilar em um compilador mais recente. Chris Coleman, fundador da Memfault, defende essa prática no post Better Firmware with LLVM/Clang [13]: não é preciso trocar o compilador do produto para ganhar com um segundo, porque “dois conjuntos de avisos são melhores que um” e o código fica menos dependente dos recursos de um único compilador. Para montar esse pipeline, um bom ponto de partida é o post A Modern C Development Environment, de Martin Lampacher, publicado no blog Interrupt, da Memfault [12]: ele mostra como empacotar o ambiente de build em um contêiner Docker, usado tanto no editor quanto no CI do GitHub Actions, de modo que o desenvolvedor e o CI compilam exatamente no mesmo ambiente. Acrescentar uma segunda imagem, com outro compilador, é um passo natural a partir dali; foi com imagens Docker do GCC e do Clang que montamos a matriz deste artigo.
Placar final: o que fazer com o seu toolchain
Para sair deste artigo com uma decisão, um resumo por compilador, considerando o que a matriz mostrou para a linguagem:
- GCC 13 (use -std=c2x): pode usar bool e nullptr, atributos, literais binários, separadores de dígitos, enums com tipo fixo, constexpr, typeof, auto, static_assert sem mensagem, = {}, __VA_OPT__, #elifdef e unreachable(). Ainda não tem stdckdint.h nem #embed: continue com __builtin_add_overflow e com o xxd -i.
- GCC 14 (Zephyr SDK 1.0.1, STM32CubeIDE 2.2, ESP-IDF 5.5): tudo do GCC 13 e mais ckd_add. Ainda sem #embed. Atenção ao __STDC_VERSION__, que vale 202000L: para detectar o C23, compare com > 201710L.
- GCC 15 e 16 (ESP-IDF 6.x, Arm GNU Toolchain 15.2): toda a parte de linguagem testada, inclusive #embed, e __STDC_VERSION__ com o valor final. A exceção é _BitInt, que não funciona em ARM de 32 bits.
- Clang 18: quase toda a linguagem, mas sem constexpr, e com literais binários tratados como extensão. A partir do Clang 19, toda a parte de linguagem testada, inclusive #embed.
E para a biblioteca, que depende da libc e não do compilador:
- newlib (Arm GNU Toolchain): sem stdbit.h (use __builtin_popcount e __builtin_clz), sem memset_explicit (use um substituto como o do cabeçalho de compatibilidade) e sem timegm; strdup e memccpy só com -std=gnu23.
- picolibc (Zephyr SDK): memset_explicit disponível; sem stdbit.h; strdup, memccpy e timegm só com -std=gnu23.
- glibc: stdbit.h a partir da 2.39 e memset_explicit a partir da 2.43.
Conclusão
A resposta à pergunta do título tem três partes.
Já dá para usar, em qualquer GCC a partir do 13 ou Clang a partir do 19, a maior parte do C23 como linguagem: nullptr, bool sem cabeçalho, atributos, literais binários, separadores de dígitos, enums com tipo fixo, constexpr, typeof, auto, = {}, __VA_OPT__ e unreachable(). A partir do GCC 14, também stdckdint.h. Esses são os recursos que mais mudam o código de firmware do dia a dia.
Depende da libc: stdbit.h, memset_explicit e as funções que vieram do POSIX. Nas libcs de embarcados, o suporte é parcial e desigual: a picolibc tem memset_explicit antes da glibc, mas nenhuma das duas libcs de embarcados tem stdbit.h. Aqui, um cabeçalho de compatibilidade resolve a maioria dos casos.
Ainda é cedo para #embed em quem está preso ao GCC 14, para _BitInt em ARM de 32 bits e para tudo que dependa de compiladores comerciais, cujo suporte ao C23 ainda é limitado.
Enquanto o C23 não chega inteiro ao seu toolchain, boa parte do que ele trouxe já existia como extensão do GCC havia anos: typeof, literais binários, atributos, builtins para contar bits e detectar overflow. No próximo artigo, vamos ver as extensões do GCC que aparecem com frequência em projetos embarcados, o que o -std=gnu17 faz de verdade e como usar essas extensões sem amarrar o projeto a um único compilador.
Referências
- DIAS, Rafael. rdws_examples_articles: testes e matriz deste artigo. GitHub.
- GNU C Library. Arquivo NEWS da glibc 2.43, seções das versões 2.39 (stdbit.h) e 2.43 (memset_explicit).
- Zephyr Project. Kconfig: CONFIG_STD_C23 e CONFIG_GNU_C_EXTENSIONS.
- Espressif. tools/tools.json do ESP-IDF, nos ramos release/v5.5, release/v6.0, release/v6.1 e master.
- STMicroelectronics. RN0114: STM32CubeIDE release v2.2.0.
- IAR. C language overview, IAR Embedded Workbench for Arm 10.1x.
- Arm. Arm Toolchain for Embedded: migration from Picolibc to LLVM libc.
- Microchip. MPLAB XC32 v6.00 Release Notes (Arm).
- Microchip. MPLAB XC32 C/C++ Compiler User’s Guide for PIC32M MCUs (DS50002799), versão 6, tabela 5-9, “Acceptable Language Standards”.
- GCC. C23 support in GCC e notas das versões 14 e 15.
- LLVM. C support in Clang.
- LAMPACHER, Martin. A Modern C Development Environment. Interrupt (Memfault), 2023.
- COLEMAN, Chris. Better Firmware with LLVM/Clang. Interrupt (Memfault), 2020.
Esta obra está licenciada com uma Licença Creative Commons Atribuição-CompartilhaIgual 4.0 Internacional.
