DEV Community

Matheus de Gondra
Matheus de Gondra

Posted on

C23, CMake e vcpkg: Como estruturar um projeto C moderno

A linguagem C evolui ao longo do tempo atualizando sua versão, até a atual C23. As ferramentas do ambiente C também evoluíram, e isso que quero mostrar.

Criando projeto

Não vamos começar com algo complexo de início. Vamos apenas criar um código C simples com um src/main.c e ir incrementando.

#include <stdio.h>
#include <stdlib.h>

int main() {
  printf("Iniciando projeto!\n");

  return EXIT_SUCCESS;
}
Enter fullscreen mode Exit fullscreen mode

Agora vamos compilar usando o GCC e executar

gcc src/main.c -o main
./main
Enter fullscreen mode Exit fullscreen mode

Agora vamos criar um struct para representar um usuário. Essa struct terá os campos id, nome, email e created_at. Vamos criar também funções para criar a struct, printar os valores dela e para liberar a memória.

#include <stdint.h>
#include <inttypes.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>

typedef struct {
    uint64_t id;
    char *name;
    char *email;
    time_t created_at;
} User;

User *User_new(uint64_t id, const char *name, const char *email) {
    User *user = malloc(sizeof(User));
    if (!user) {
        fprintf(stderr, "Falha ao alocar memória para o usuário\n");
        exit(EXIT_FAILURE);
    }

    bool is_email_valid = strstr(email, "@") != NULL;
    if (!is_email_valid) {
        fprintf(stderr, "Email inválido: %s\n", email);
        free(user);
        exit(EXIT_FAILURE);
    }

    user->id = id;
    user->name = strdup(name);
    user->email = strdup(email);
    user->created_at = time(NULL);

    return user;
}

void User_free(User *user) {
    if (user) {
        free(user->name);
        free(user->email);
        free(user);
    }
}

void User_print(const User *user) {
    struct tm *info_time = gmtime(&user->created_at);
    char timestamp_str[20];

    strftime(timestamp_str, sizeof(timestamp_str), "%Y-%m-%dT%H:%M:%S", info_time);

    printf(
    "User{id: %" PRIu64 ", name: %s, email: %s, created_at: %s}\n",
        user->id,
        user->name,
        user->email,
        timestamp_str
    );
}

int main() {
  printf("Iniciando projeto!\n");
  User *user = User_new(1, "Fulano", "fulano@mail.com");

  User_print(user);

  User_free(user);
  return EXIT_SUCCESS;
}
Enter fullscreen mode Exit fullscreen mode

Agora vamos compilar e vamos adicionar explicitamente para usar a versão C23 da linguagem C.

gcc src/main.c -o main -std=c23
./main
Enter fullscreen mode Exit fullscreen mode

Separando arquivos

Fazer tudo dentro do arquivo main.c deixa tudo muito poluído. Uma boa prática é separar o código em outros arquivos. Em C podemos criar arquivo de cabeçalho (.h) com as definições das funções e structs e um arquivo fonte (.c) para implementar o cabeçalho.

Vamos criar o include/user.h e mover a definições da estrutura e das funções para lá. Vamos usar o #pragma once no arquivo para impedir que o cabeçalho seja importado duas vezes.

// Garante que só importe o cabeçalho apenas uma vez,
// caso use o #include duas vezes ou mais
#pragma once

#include <stdint.h>
#include <time.h>

typedef struct {
    uint64_t id;
    char *name;
    char *email;
    time_t created_at;
} User;

User *User_new(uint64_t id, const char *name, const char *email);
void User_free(User *user);
void User_print(const User *user);
Enter fullscreen mode Exit fullscreen mode

E criaremos um arquivo fonte src/user.c para implementar essas funções.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <inttypes.h>
#include "user.h" // importa o cabeçalho

User *User_new(uint64_t id, const char *name, const char *email) {
    User *user = malloc(sizeof(User));
    if (!user) {
        fprintf(stderr, "Falha ao alocar memória para o usuário\n");
        exit(EXIT_FAILURE);
    }

    bool is_email_valid = strstr(email, "@") != NULL;
    if (!is_email_valid) {
        fprintf(stderr, "Email inválido: %s\n", email);
        free(user);
        exit(EXIT_FAILURE);
    }

    user->id = id;
    user->name = strdup(name);
    user->email = strdup(email);
    user->created_at = time(NULL);

    return user;
}

void User_free(User *user) {
    if (user) {
        free(user->name);
        free(user->email);
        free(user);
    }
}

void User_print(const User *user) {
    struct tm *info_time = gmtime(&user->created_at);
    char timestamp_str[20];

    strftime(timestamp_str, sizeof(timestamp_str), "%Y-%m-%dT%H:%M:%S", info_time);

    printf(
    "User{id: %" PRIu64 ", name: %s, email: %s, created_at: %s}\n",
        user->id,
        user->name,
        user->email,
        timestamp_str
    );
}
Enter fullscreen mode Exit fullscreen mode

E nosso src/main.c irá importar nosso arquivo de cabeçalho user.h e isso deixará nosso código na main mais limpo.

#include <stdio.h>
#include <stdlib.h>
#include "user.h"

int main() {
  printf("Iniciando projeto!\n");
  User *user = User_new(1, "Fulano", "fulano@mail.com");

  User_print(user);

  User_free(user);
  return EXIT_SUCCESS;
}
Enter fullscreen mode Exit fullscreen mode

Agora para compilar vamos ter que passar os dois arquivos fonte main.c e user.c.

Além disso como importamos o cabeçalho como #include "user.h" vamos precisar passar onde esse cabeçalho está usando a flag -I<path>. Poderiamos passar o caminho completo ou relativo do arquivo no include, eliminando a necessidade da flag, #include "../include/user.h, mas o padrão é passar a flag ao compilar.

Também vou adicionar outras flag para tornar o compilador mais rigoroso como -Wall e -Wextra para ativar alguns warnings e -Werror para transformar os warnings em erros.

gcc -o main -std=c23 -Wall -Wextra -Werror -Iinclude src/main.c src/user.c
./main
Enter fullscreen mode Exit fullscreen mode

Automatizando o build

À medida que o projeto cresce, novos arquivos de cabeçalho e fonte são criados. Com isso, o nosso comando de compilação vai crescendo e se torna cada vez mais difícil de lembrar. Você pode acabar esquecendo de incluir um arquivo ou um diretório e falhar na compilação.

Para resolver isso, temos ferramentas de build como o Make e o Ninja, que é uma opção mais rápida e moderna que iremos usar. Geralmente, o Ninja não é escrito diretamente à mão, ele costuma ser usado junto de outra ferramenta: o CMake. O CMake é um gerador de sistema de build. Ele lê um arquivo de configuração simples e gera a estrutura complexa necessária para o Ninja realizar o trabalho pesado da compilação.

Vamos criar um arquivo chamado CMakeLists.txt na raiz do projeto para descrever como nosso código deve ser compilado:

cmake_minimum_required(VERSION 3.20) # versão mínima do CMake necessária para o projeto
project(projectUsers LANGUAGES C) # Nome do projeto e a linguagem como C

set(CMAKE_C_STANDARD 23) # definindo a versão padrão do C como C23
set(CMAKE_C_STANDARD_REQUIRED ON) # garantindo que a versão especificada seja obrigatória
set(CMAKE_C_EXTENSIONS OFF) # desativando extensões específicas do compilador

# Nome do arquivo de saída e os arquivos fonte que serão compilados para gerar o executável
# Usarei o nome do projeto como nome do executável
add_executable(${PROJECT_NAME}
    src/main.c
    src/user.c
)

# Informa ao CMake onde procurar pelos arquivos de cabeçalho (.h)
# O equivalente à flag -Iinclude do GCC
target_include_directories(${PROJECT_NAME} PRIVATE include)

# Adiciona opções de compilação específicas para o alvo (target) do projeto
target_compile_options(${PROJECT_NAME} PRIVATE -Wall -Wextra -Werror)
Enter fullscreen mode Exit fullscreen mode

Agora podemos gerar o sistema de build, ninja, usando o CMake. Usaremos a flag -B <path> para dizer o direto de saída e -G <generator> para dizer qual o sistema de build será gerado.

cmake -B build -G Ninja
Enter fullscreen mode Exit fullscreen mode

Com isso o cmake criará um diretório build com o Ninja como sistema de build. Agora para compilar o código e gerar um executável usamos o seguinte comando:

cmake --build build
Enter fullscreen mode Exit fullscreen mode

Isso irá gerar o executável build/projectUsers que é o nome que demos no nosso CMakeLists.txt no add_executable().

./build/projectUsers

Iniciando projeto!
User{id: 1, name: Fulano, email: fulano@mail.com, created_at: 2026-10-02T12:10:42}
Enter fullscreen mode Exit fullscreen mode

Com isso se eu modificar algum arquivo já existente eu só preciso recompilar usando o cmake --build build.

void User_print(const User *user) {
    struct tm *info_time = gmtime(&user->created_at);
    char timestamp_str[20];

    strftime(timestamp_str, sizeof(timestamp_str), "%Y-%m-%dT%H:%M:%S", info_time);

    // Colocando as string entre aspas no print
    printf(
    "User{id: %" PRIu64 ", name: \"%s\", email: \"%s\", created_at: %s}\n",
        user->id,
        user->name,
        user->email,
        timestamp_str
    );
}
Enter fullscreen mode Exit fullscreen mode
cmake --build build
./build/projectUsers

Iniciando projeto!
User{id: 1, name: "Fulano", email: "fulano@mail.com", created_at: 2026-10-02T12:11:38}
Enter fullscreen mode Exit fullscreen mode

Mas se eu adicionar novos arquivos ao projeto eu tenho que listar esses arquivos dentro do CMakeLists.txt e recriar o sistema de build com o cmake -B build -G Ninja e depois compilar com cmake --build build.

Dependências Externas

Para usar bibliotecas externas no C é um pouco trabalhoso, pois você precisa manualmente baixar e compilar cada biblioteca. Em linguagens como Java, NodeJS, Python, temos um gerenciador de dependências. No C temos o vcpkg, um gerenciador de dependências da Microsoft.

O vcpkg pode ser integrado com o CMake para buscar as dependências externa ao criar o sistema de build.

Na raiz do projeto vamos usar o comando vcpkg new --aplication. Isso irá gerar os arquivos vcpkg-configuration.json e vcpkg.json.

No vcpkg.json será onde ficará listados as dependências do projeto. Vamos usar a biblioteca cJSON. Para adicionar ela ao projeto usamos o comando vcpkg add port cjson. Isso atualizará o vcpkg.json.

{
  "dependencies": [
    "cjson"
  ]
}
Enter fullscreen mode Exit fullscreen mode

Se quiser achar outra dependência pode olhar no site do vcpkg.

Integrando vcpkg e CMake

Agora no comando para gerar o sistema de build passamos a flag --toolchain <diretorio_vcpkg>/scripts/buildsystems/vcpkg.cmake para injetar o script do vcpkg no CMake que fica dentro do diretório de instalação do vcpkg.

rm -rf build
cmake -B build -G Ninja --toolchain $VCPKG_ROOT/scripts/buildsystems/vcpkg.cmake 
Enter fullscreen mode Exit fullscreen mode

Na saída do terminal você verá que mostra o que deve ser adicionado ao CMakeLists.txt para compilar com essa dependência. No caso do cJSON temos essa saída

cjson provides CMake targets:

  # this is heuristically generated, and may not be correct
  find_package(cJSON CONFIG REQUIRED)
  target_link_libraries(main PRIVATE cjson)
Enter fullscreen mode Exit fullscreen mode

O CMakeLists.txt ficou assim:

cmake_minimum_required(VERSION 3.20) 
project(projectUsers LANGUAGES C) 

set(CMAKE_C_STANDARD 23) 
set(CMAKE_C_STANDARD_REQUIRED ON) 
set(CMAKE_C_EXTENSIONS OFF)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON) # Para intelisense da IDE usando o clangd

find_package(cJSON CONFIG REQUIRED)

add_executable(${PROJECT_NAME}
    src/main.c
    src/user.c
)

target_link_libraries(${PROJECT_NAME} PRIVATE cjson)

target_include_directories(${PROJECT_NAME} PRIVATE include)

target_compile_options(${PROJECT_NAME} PRIVATE -Wall -Wextra -Werror)
Enter fullscreen mode Exit fullscreen mode
rf -rf build
cmake -B build -G Ninja --toolchain $VCPKG_ROOT/scripts/buildsystems/vcpkg.cmake
Enter fullscreen mode Exit fullscreen mode

Com isso temos o cJSON configurado para usar no projeto. Vamos criar um nova função no include/user.h para usar a biblioteca.

#pragma once

#include <stdint.h>
#include <time.h>

typedef struct {
    uint64_t id;
    char *name;
    char *email;
    time_t created_at;
} User;

User *User_new(uint64_t id, const char *name, const char *email);
void User_free(User *user);
void User_print(const User *user);

// Nova função para converter um User em JSON
char *User_to_json(const User *user);
Enter fullscreen mode Exit fullscreen mode

E vamos implementar no src/user.c a nova função usando o cJSON.

#include <cjson/cJSON.h>

// resto do código omitido

char *User_to_json(const User *user) {
    if (!user) {
        return nullptr;
    }

    cJSON *user_json = cJSON_CreateObject();

    struct tm *info_time = gmtime(&user->created_at);
    char timestamp_str[20];
    strftime(timestamp_str, sizeof(timestamp_str), "%Y-%m-%dT%H:%M:%S", info_time);

    cJSON_AddNumberToObject(user_json, "id", user->id);
    cJSON_AddStringToObject(user_json, "name", user->name);
    cJSON_AddStringToObject(user_json, "email", user->email);
    cJSON_AddStringToObject(user_json, "created_at", timestamp_str);

    char *json_string = cJSON_Print(user_json);
    cJSON_Delete(user_json);

    return json_string;
}
Enter fullscreen mode Exit fullscreen mode

E agora vamos usar a função no src/main.c

#include <stdio.h>
#include <stdlib.h>
#include "user.h"

int main() {
  printf("Iniciando projeto!\n");
  User *user = User_new(1, "Fulano", "fulano@mail.com");

  User_print(user);

  char *json = User_to_json(user);
  if (!json) {
    fprintf(stderr, "Falha ao converter o usuário para JSON\n");
    User_free(user);
    return EXIT_FAILURE;
  }

  printf("JSON: %s\n", json);

  User_free(user);
  free(json);
  return EXIT_SUCCESS;
}
Enter fullscreen mode Exit fullscreen mode
cmake --build build
./build/projectUsers

Iniciando projeto!
User{id: 1, name: "Fulano", email: "fulano@mail.com", created_at: 2026-10-02T16:06:28}
JSON: {
        "id":   1,
        "name": "Fulano",
        "email":        "fulano@mail.com",
        "created_at":   "2026-10-02T16:06:28"
}
Enter fullscreen mode Exit fullscreen mode

Simplificando comandos com CMake Presets

Se você reparou, nosso comando do CMake ainda está muito grande e pode ser que acabe sendo digitado de forma errada ou faltando algo.

cmake -B build -G Ninja --toolchain $VCPKG_ROOT/scripts/buildsystems/vcpkg.cmake
Enter fullscreen mode Exit fullscreen mode

E além disso, se rodar o mesmo comando no Windows irá falhar, pois a sintaxe para variáveis de ambiente é diferente.

Para resolver isso o CMake possui os Presets. Podemos criar um arquivo CMakePresets.json na raiz do projeto.

{
    "version": 3,
    "cmakeMinimumRequired": {
        "major": 3,
        "minor": 20,
        "patch": 0
    },
    "configurePresets": [
        {
            "name": "default",
            "displayName": "Configuração padrão (Ninja + vcpkg)",
            "generator": "Ninja",
            "binaryDir": "${sourceDir}/build",
            "cacheVariables": {
                "CMAKE_TOOLCHAIN_FILE": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake"
            }
        }
    ],
    "buildPresets": [
        {
            "name": "default",
            "configurePreset": "default"
        }
    ]
}
Enter fullscreen mode Exit fullscreen mode

Repare que transferimos a variável do vcpkg para o arquivo de preset, usando $env{VCPKG_ROOT} para ler a variável de ambiente do sistema operacional, seja ele Windows ou Linux.

Agora podemos gerar nosso sistema de build usando um comando mais simples

rm -rf build
cmake --preset default
Enter fullscreen mode Exit fullscreen mode

O CMake vai ler o CMakePresets.json para usar o preset default que criamos para gerar o sistema de build. Agora basta compilar o projeto e executar:

cmake --build build
./build/projectUsers

Iniciando projeto!
User{id: 1, name: Fulano, email: fulano@mail.com, created_at: 2026-10-02T21:38:56}
JSON: {
        "id":   1,
        "name": "Fulano",
        "email":        "fulano@mail.com",
        "created_at":   "2026-10-02T21:38:56"
}
Enter fullscreen mode Exit fullscreen mode

Conclusão

O C tem a fama de ser uma linguagem velha e intimidadora e ter um ecossistema difícil de configurar. No entanto, como vimos ao longo deste artigo, isso não é verdade hoje em dia.

A versão C23 trouxe muita modernidade e as ferramentas do ecossistema C está evoluindo.

Espero ter te ajudado a ver como algo simples com 1 arquivo e compilando usando o gcc pode evoluir para arquivos separados e um build automatizado com bibliotecas externas.

Tente fazer um projeto C e adicionar alguma biblioteca e deixe o link do repositório nos comentários e diga o que você achou da leitura.

Top comments (0)