Pular para o conteúdo principal

4 postagens marcadas com "c"

Ver todas os Marcadores

Thiago Lages de Alencar

Imagem mostrando camada do App, OS e Hardwares

Quando você quer ler o arquivo, você pede ao sistema operacional pelo conteúdo do arquivo.

Quando você quer escrever no arquivo, você pede ao sistema operacional para inserir o conteúdo no arquivo.

É importante saber que o sistema operacional toma diversos cuidados para que desenvolvedores não acessem diretamente o hardware, ou seja, por baixo dos panos você está pedindo para o sistema operacional ler/escrever.

  • C
    • fgets()
    • fwrite()
  • Python
    • file.read()
    • file.write()
  • Rust
    • file.read_to_string()
    • file.write_all()
  • Go
    • os.ReadFile()
    • os.WriteFile()

Veremos como garantir a segurança de um arquivo quando se tem múltiplos processos querendo altera-lo.

Process

A função utilizada para se criar processos é fork(), está função faz com que o atual processo crie um processo filho quase idêntico e executando o mesmo código que o pai.

Olhe este código que printa duas vezes "Hi":

#include <stdio.h>
#include <unistd.h>

int main() {
fork();
printf("Hi\n");
}

Se você executa-lo irá notar que o filho é tão igual ao pai que ele continua exatamente do mesmo local que o pai se encontrava (logo após fork() retornar um valor). Se tivessemos variáveis, poderiamos ver que até o valor delas são idênticos ao do pai.

No entanto, precisamos de uma maneira de reconhecer quem é o pai e filho, caso contrário este código executária exatamente a mesma coisa para ambos (não seria nada produtivo). Acontece que a função fork() retorna um valor e este valor é utilizado para sabermos se estamos no pai ou no filho.

#include <stdio.h>
#include <unistd.h>

int main() {
int pid = fork();

if (pid == -1) {
printf("Failed to create child process\n");
} else if (pid == 0) {
printf("I'm the child process\n");
} else {
printf("I'm the parent process\n");
}
}

A função fork() vai retornar ao pai o PID do filho (ou -1 em caso de error).
A função fork() vai retornar ao filho zero.

tip

Normalmente o código do pai e filho são inseridos em funções em vez de deixar tudo dentro de um if/else.

if(pid == 0) {
child_code();
} else {
parent_code();
}

Ou se utiliza funções exec para transformar completamente o código executado naquele processo.

Problem

Imagem ilustrativa do app 1 lendo do arquivo, app 2 lendo do arquivo, app 1 escrevendo no arquivo, app 2 escrevendo no arquivo

Quando dois processos interagem com o mesmo arquivo, pode acontecer da informação ser preenchida incorretamente? Afinal, precisamos primeiramente descobrir se isso é possível ou não de acontecer.

Como o escalonamento pode ser imprevisivel, uma maneira de testar se durante a interação com um arquivo houve troca de processo é repetindo a ação diversas vezes e ver se pelo menos uma vez ocorreu.

O seguinte código irá ser executado para o processo pai e filho:

int count = 0;
FILE* file = fopen("example.txt", "w+");

while(count < 10000) {
int i;

fseek(file, 0, SEEK_SET);
fscanf(file, "%d", &i);
fseek(file, 0, SEEK_SET);
fprintf(file, "%d ", ++i);

count++;
}

fclose(file);

O código irá garantir que o cursor está no início do arquivo, ler o atual número do arquivo, mover o cursor para o início do arquivo e sobreescrever o número.

note
fprintf(file, "%d     ", ++i);

Por que inserir espaço após o número? Foi uma maneira de evitar que o número de ambos processos se misturem.

Por exemplo: Processo 1 escreve 5000 e processo 2 escreve 9, o arquivo irá conter "9000" pois o 9 foi escrito em cima do 5.

Agora só precisamos adicionar a lógica de criar processo vista anteriormente:

#include <stdio.h>
#include <unistd.h>

void code() {
int count = 0;
FILE* file = fopen("example.txt", "w+");

while(count < 10000) {
int i;

fseek(file, 0, SEEK_SET);
fscanf(file, "%d", &i);
fseek(file, 0, SEEK_SET);
fprintf(file, "%d ", ++i);

count++;
}

fclose(file);
}

int main() {
FILE* file = fopen("example.txt", "w");
fputc('0', file);
fclose(file);

int pid = fork();

if (pid == -1) {
printf("Failed to create child process\n");
} else if (pid == 0) {
code();
printf("Child finished\n");
} else {
code();
printf("Parent finished\n");
}
}

Quando executei este código para 10 iterações, o valor final do arquivo foi 20.
Quando executei este código para 1000 iterações, o valor final do arquivo foi 1000.
Quando executei este código para 10000 iterações, o valor final do arquivo foi 10015.

O que somos capaz de deduzir com isto?

  • O resultado é imprevisível pois não temos controle de quando o escalonador vai trocar os processos
  • Dependendo do volume de iterações e da máquina do usuário, um processo pode ou não conseguir fazer a tarefa antes do escalonador trocar o processo
  • Se houver troca durante uma tarefa, pode corromper o resultado do arquivo

Quais as chances disto acontecer? Depende do software, pois existem arquivos que a chance de dois softwares interagirem ao mesmo tempo é 0%.

Locks

Acontece que existe mais de uma maneira de aplicar locks no Linux.

Uma pessoa bem surpresa

  • flock
    • file lock
    • Origem do sistema operacional BSD
  • lockf
    • lock file
    • POSIX
    • É uma versão simplificada do fcntl "Advisory record locking"
  • fcntl "Advisory record locking"
    • file control
    • POSIX
    • Uma função capaz de fazer diversas operações sobre file descriptors
      • Uma delas é utilizar "Advisory record locking"
  • fcntl "Open file description locks (non-POSIX)"
    • file control
    • Linux specific
      • Existem propostas para ser adicionado ao padrões POSIX
    • Uma função capaz de fazer diversas operações sobre file descriptors
      • Uma delas é utilizar "Open file description locks (non-POSIX)"

Se quiser saber mais sobre cada um, é bom ler o post no blog: https://gavv.net/articles/file-locks/
Incrivel como um blog de 8 anos atrás me ajudou mais do que pesquisas no Google.

flock

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/file.h>
#include <unistd.h>

#define BUFFER_SIZE 256

void code() {
int count = 0;
char *buffer = malloc(sizeof(char) * BUFFER_SIZE);
int fd = open("example.txt", O_RDWR);

while (count < 10000) {
int i;

flock(fd, LOCK_EX);
lseek(fd, 0, SEEK_SET);
read(fd, buffer, BUFFER_SIZE);
i = atoi(buffer) + 1;
sprintf(buffer, "%d ", i);
lseek(fd, 0, SEEK_SET);
write(fd, buffer, strlen(buffer));
flock(fd, LOCK_UN);

count++;
}

close(fd);
}

int main() {
FILE *file = fopen("example.txt", "w");
fputc('0', file);
fclose(file);

int pid = fork();

if (pid == -1) {
printf("Failed to create child process\n");
} else if (pid == 0) {
code();
printf("Child finished\n");
} else {
code();
printf("Parent finished\n");
}
}

lockf

#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#define BUFFER_SIZE 256

void code() {
int count = 0;
char *buffer = malloc(sizeof(char) * BUFFER_SIZE);
int fd = open("example.txt", O_RDWR);

while (count < 10000) {
int i;

lockf(fd, F_LOCK, 0);
lseek(fd, 0, SEEK_SET);
read(fd, buffer, BUFFER_SIZE);
i = atoi(buffer) + 1;
sprintf(buffer, "%d ", i);
lseek(fd, 0, SEEK_SET);
write(fd, buffer, strlen(buffer));
lockf(fd, F_ULOCK, 0);

count++;
}

close(fd);
}

int main() {
FILE *file = fopen("example.txt", "w");
fputc('0', file);
fclose(file);

int pid = fork();

if (pid == -1) {
printf("Failed to create child process\n");
} else if (pid == 0) {
code();
printf("Child finished\n");
} else {
code();
printf("Parent finished\n");
}
}

fcntl - "Advisory record locking"

#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#define BUFFER_SIZE 256

void code() {
int count = 0;
struct flock fl;
char *buffer = malloc(sizeof(char) * BUFFER_SIZE);
int fd = open("example.txt", O_RDWR);

fl.l_whence = SEEK_SET;
fl.l_start = 0;
fl.l_len = 0;

while (count < 10000) {
int i;

fl.l_type = F_WRLCK;
fcntl(fd, F_SETLKW, &fl);
lseek(fd, 0, SEEK_SET);
read(fd, buffer, BUFFER_SIZE);
i = atoi(buffer) + 1;
sprintf(buffer, "%d ", i);
lseek(fd, 0, SEEK_SET);
write(fd, buffer, strlen(buffer));
fl.l_type = F_UNLCK;
fcntl(fd, F_SETLKW, &fl);

count++;
}

close(fd);
}

int main() {
FILE *file = fopen("example.txt", "w");
fputc('0', file);
fclose(file);

int pid = fork();

if (pid == -1) {
printf("Failed to create child process\n");
} else if (pid == 0) {
code();
printf("Child finished\n");
} else {
code();
printf("Parent finished\n");
}
}

References

Extra

Por diversão, eu experimentei replicar o mesmo problema em outras linguagens.

package main

import (
"fmt"
"os"
"os/exec"
)

func code() {
var count int = 0
file, _ := os.OpenFile("example.txt", os.O_RDWR, 0666)

for count < 10000 {
var i int

file.Seek(0, 0)
fmt.Fscanf(file, "%d", &i)
file.Seek(0, 0)
fmt.Fprintf(file, "%d ", i+1)

count++
}

file.Close()
}

func main() {
if len(os.Args) >= 2 {
code()
fmt.Println("Child finished")
} else {
os.WriteFile("example.txt", []byte{'0'}, 0600)

command := exec.Command("/usr/bin/go", "run", "main.go", "fork")
command.Start()
code()
command.Wait()

fmt.Println("Parent finished")
}
}

Thiago Lages de Alencar

Durante o post C/C++ Libs escrevi sobre usar bibliotecas no Linux e nunca imaginaria que teria novamente a dor de cabeça de ver o assunto quando fosse fazer o mesmo no Windows.

Personagem do desenho The Owl House chorando

warning

Importante avisar que eu irei escrever este post contando que você leu o post passado sobre Linux.

Irei ser breve e não irei estudar detalhes que nem fiz no do linux (pois estou cansado do assunto).

.lib (library)

Basicamente idêntico ao .a do Linux, uma biblioteca estática (static library).

note

Descobri que estes arquivos podem ser abertos tranquilamente com programas de zip tipo: Winrar, 7zip, PeaZip.

O que faz total sentido dado que eles programas para lidar com "File archive and extractor" e no Linux usando o formato .a de "archive".

Seria o próximo do .so do Linux, servindo o mesmo propósito de uma biblioteca compartilhada (shared library) porém o Windows possui uma implementação própria deles (dynamic-link library).

A grande diferença que precisamos saber é que durante a criação de bibliotecas compartilhadas, três formatos de arquivos são criados: .dll, .lib e .exp (vamos ignorar este último).

Onde o arquivo .lib NÃO é o mesmo que o gerado durante a biblioteca estática! Porém ainda é necessário para a utilização da biblioteca compartilhada.

Project from Zero

Seguiremos a mesma ideia do post no Linux. Apenas tendo o nosso código:

project/
└── src/
├── main.c
└── ...
cl src\main.c

Agora com um header:

project/
├── include/
│ └── header.h
└── src/
├── main.c
└── ...
cl src/main.c /Iinclude

.lib

project/
├── include/
│ └── header.h
├── lib/
│ └── name.lib
└── src/
├── main.c
└── ...
cl src/main.c /Iinclude lib/name.lib
note

Windows não tem o padrão de botar lib na frente das bibliotecas então não precisa nem pensar nisso.

.dll

Dessa vez vamos separar em duas etapas: compilar o código para .obj e depois linkar (gerar o .exe), isso torna mais fácil adicionar explicações no meio.

project/
├── include/
│ └── header.h
├── lib/
│ └── name.lib
└── src/
│ ├── main.c
│ └── ...
└── name.dll

Lembre que bibliotecas dinâmicas no Windows utilizam um arquivo .dll e um .lib (por isso temos os dois no projeto).
Windows busca os arquivos .dll na mesma pasta do executável, por isso ele não está na pasta lib.

cl src/main.c /c /Iinclude /Dexample

/c é justamente para pausar antes de linkar.
/Dexample é uma maneira de adicionar uma definição no início do código, equivalente a

#define example

Por que precisamos definir? Em bibliotecas do Windows, muitas vezes pode se encontrar código como o seguinte:

#if defined(EXPORT_DLL)
#define LIB_API __declspec(dllexport)
#elif defined(IMPORT_DLL)
#define LIB_API __declspec(dllimport)
#else
#define LIB_API
#endif

Onde LIB_API é substituido por:

  • __declspec(dllexport) quando criando uma biblioteca dinânmica
  • __declspec(dllimport) quando importando uma biblioteca dinâmica
  • nada quando é uma biblioteca estática

Não entendo bem do assunto então não pretendo entrar no assunto, mas é a maneira do windows lidar com bibliotecas dinâmicas.

Biblitoecas geralmente requerem que você passe essa definição para que ela adicione o contexto certo ao código durante a criação do objeto (para fazer name mangling corretamente?).


link /LIBPATH:lib name.lib main.obj

É durante a etapa de linkar que o arquivo .lib é finalmente utilizado!

Lembrando novamente que o executável vai buscar o .dll na pasta do executável, então bote ambos juntos.

Thiago Lages de Alencar

Não bastava meu sofrimento com utilização de bibliotecas, agora tive mais dor de cabeça por elas virem do package manager (apt).

Meu sofrimento foi enquanto tentava usar a biblioteca GTK em C.

Rosto sorrindo com os olhos de forma idiota (e com um chápeu de mágico)

Installing

Duas opções:

  • Instalar o pacote pronto da minha distribuição
  • Baixar e compilar o código fonte

Como eu ainda tenho algum amor por mim mesmo, fui pela primeira opção:

$ sudo apt install libgtk-4-1 libgtk-4-dev

E podemos descobrir os arquivos que estes pacotes trouxeram com:

$ dpkg -L libgtk-4-1 libgtk-4-dev

O que nos mostra que o pacote responsável por desenvolvimento (libgtk-4-dev) trouxe muitos headers files e uma biblioteca compartilhada (/usr/lib/x86_64-linux-gnu/libgtk-4.so).

Alguns nomes de headers

Code

Qual será o grande código utilizado durante este post???

#include <gtk/gtk.h>

int main() { return 0; }

Exatamente! Estou cagando para o código, apenas quero acessar a biblioteca!

¿Donde esta la biblioteca?

Rosto do Deadpool

Includes

Talvez seja meio óbvio mas o código não vai ser executado com um simples

$ clang -o my_project main.c

Pois o compilador não irá encontrar a biblioteca.

$ clang -o my_project main.c
main.c:1:10: fatal error: 'gtk/gtk.h' file not found
#include <gtk/gtk.h>
^~~~~~~~~~~
1 error generated.

Existe duas maneiras de adicionar headers ao seu código:

  • #include "biblioteca.h"
    • Dentro do seu diretório atual, busque o arquivo biblioteca.h.
  • #include <biblioteca.h>
    • Dentro dos diretório padrões, busque o arquivo biblioteca.h.
info

Ambos os tipos aceitam caminhos para o arquivo. Exemplo:
#include "dir1/dir2/biblioteca.h"
#include <dir1/dir2/biblioteca.h>

E aspas também podem ser usadas para buscar em diretórios padrões...
Mas se você faz isso, você é um criminoso.

Qual o diretório padrão para bibliotecas? /usr/include

Lembra quando vimos a lista dos arquivos que o pacote trouxe? Muitos headers foram justamente para o diretório padrão.

...
/usr/include/gtk-4.0/gtk/gtkshortcutmanager.h
/usr/include/gtk-4.0/gtk/gtkshortcutsgroup.h
/usr/include/gtk-4.0/gtk/gtkshortcutssection.h
/usr/include/gtk-4.0/gtk/gtkshortcutsshortcut.h
/usr/include/gtk-4.0/gtk/gtkshortcutswindow.h
...

Perfeito então, podemos alterar nosso código para usar #include <gtk-4.0/gtk/gtk.h>

#include <gtk-4.0/gtk/gtk.h>

int main() { return 0; }

E vai dar tudo cert.... ei...

$ clang -o my_project main.c
In file included from main.c:1:
/usr/include/gtk-4.0/gtk/gtk.h:29:10: fatal error: 'gtk/css/gtkcss.h' file not found
#include <gtk/css/gtkcss.h>
^~~~~~~~~~~~~~~~~~
1 error generated.

Nope!

emoticon :^)

Packages

Está é a organização do desenvolvedor da biblioteca:

project/
├── gdk
├── gsk
├── gtk
└── unix-print

Para eles realmente é #include <gtk/gtk.h> por isso outras partes do código deles utiliza assim!
E não é como se eles fossem ficar alterando em todos os arquivos do projeto para referênciar o caminho com versão mais recente do projeto.

Queremos é dizer ao compilador os diretórios a procurar os arquivos...
Pera, nós já fizemos isso no post anterior, usando a flag -I.

#include <gtk/gtk.h>

int main() { return 0; }

Agora basta executar utilizando a flag e sucess... ei...

$ clang -o my_project main.c -I/usr/include/gtk-4.0
In file included from main.c:1:
In file included from /usr/include/gtk-4.0/gtk/gtk.h:29:
/usr/include/gtk-4.0/gtk/css/gtkcss.h:29:10: fatal error: 'glib.h' file not found
#include <glib.h>
^~~~~~~~
1 error generated.

Você esqueceu que está biblioteca pode usar outras bibliotecas e precisamos adicionar o diretório delas também!

emoticon :^) feito de emoticons :^) menores

Dependecies

Existe um programa justamente para ajudar a descobrir as depêndencias de um módulo.

$ pkg-config --cflags gtk4
-I/usr/include/gtk-4.0 -I/usr/include/pango-1.0 -I/usr/include/glib-2.0 -I/usr/lib/x86_64-linux-gnu/glib-2.0/include -I/usr/include/harfbuzz -I/usr/include/freetype2 -I/usr/include/libpng16 -I/usr/include/libmount -I/usr/include/blkid -I/usr/include/fribidi -I/usr/include/cairo -I/usr/include/pixman-1 -I/usr/include/gdk-pixbuf-2.0 -I/usr/include/x86_64-linux-gnu -I/usr/include/graphene-1.0 -I/usr/lib/x86_64-linux-gnu/graphene-1.0/include -mfpmath=sse -msse -msse2 -pthread

$ pkg-config --libs gtk4
-lgtk-4 -lpangocairo-1.0 -lpango-1.0 -lharfbuzz -lgdk_pixbuf-2.0 -lcairo-gobject -lcairo -lgraphene-1.0 -lgio-2.0 -lgobject-2.0 -lglib-2.0

Mas como se descobre qual o nome do módulo da minha biblioteca? Não sei, se você souber, me conte!

tip

Utilizando o pkg-config --list-all você consegue uma lista de todos os módulos mas nada me disse explicitamente que o módulo de libgtk-4-dev é gtk4.

Talvez seja o Source: gtk4 quando utilizando apt show libgtk-4-1...
Mas não sei ¯\_(ツ)_/¯

Graças a este programa podemos gerar facilmente as flags e finalmente executar o código!

$ clang `pkg-config --cflags gtk4` -o my_project main.c `pkg-config --libs gtk4`

headers entrando numa caixa que representa o binário

Language Server

Sabe aquela ferramenta responsável por completar o código, avisar de errors, te levar à definições...
Bem, ela está reclamando e não queremos deixar ela assim né?.

#include <gtk/gtk.h> // 'gtk/gtk.h' file not found

int main() { return 0; }

Estamos passando diversas informações para nosso compilador (clang) sobre diretórios para utilizar mas não estamos passando nada para o language server (clangd). Podemos fazer um teste rápido para ver o que clangd acha do nosso arquivo com:

$ clangd --check=main.c
...
E[01:35:13.895] [pp_file_not_found] Line 1: 'gtk/gtk.h' file not found
...

Pessoa representando Clang com informações e outra pessoa representando Clangd pedindo também

Project Dependencies

Poderiamos pesquisar e descobrir os argumentos a se passar ao clangd mas isso é algo que varia de projeto a projeto e última coisa que queremos é ficar configurando no Visual Studio os argumentos para cada projeto.

Por isso que clangd por padrão sempre procura configurações do projeto em certos arquivos do projeto como compile_commands.json! Poderiamos escrever este arquivo na mão... mas não queremos então vamos utilizar um programinha amigo chamado bear!

Ele recebe o comando que você está utilizando para criar o executável e cria o compile_commands.json:

$ sudo apt install bear
$ bear -- clang `pkg-config --cflags gtk4` -o my_project main.c `pkg-config --libs gtk4`

Pronto, conseguimos o arquivo de configuração para o clangd.

my_project/
├── compile_commands.json
├── main.c
└── my_project

Basta dar um tempo (ou reniciar o editor de texto) e seu language server deve perceber que está tudo certo!

Fim!
Código funciona!
Language server funciona!
Você está pronto para desenvolver com GTK!
Não se sente uma nova pessoa com todo esse conhecimento?

Rosto cansado

Bem, você não é a primeira pessoa a achar tudo isso desnecessariamente complicado para começar a programar... Outras pessoas criaram ferramentas para ajudar nisso!

Então continue lendo se quiser jogar tudo que viu até aqui no lixo e descobrir uma maneira mais fácil!

Rosto cansado e puto

Meson!

GTK adora falar do Meson e como já estou sofrendo mesmo... Por que não parar pra ver?
Instalando!

$ sudo apt install meson ninja-build

Claro que se vamos testar outra ferramenta, precisamos testar do zero! Apenas com o nosso querido arquivo main.c:

my_project/
└── main.c

Setuping

$ meson init

Isso cria o arquivo de configuração chamado meson.build, nele tem diversas informações mais sofisticadas sobre o projeto:

project('my_project', 'c',
version : '0.1',
default_options : ['warning_level=3'])

executable('my_project',
'main.c',
install : true)

Viu?
Ele não é apenas "estou rodando um código".
Ele é "estou criando um ⭐projeto⭐".

Como ele é um projeto sério, ele vai guardar todas as informações dele em um pasta separada para não sujar o seu projeto ❤️!

Então diga para ele onde botar os arquivos dele (eu escolhi builddir):

$ meson setup builddir

Seu projeto deve acabar com a estrutura:

my_project/
├── builddir/
├── main.c
└── meson.build

Project Dependencies

Sim! Em toda ferramenta é necessário que você a biblioteca que quer usar!
Você não quer que ela ande por todos os diretórios do seu computador procurando a sua biblioteca, né?
Imagina se pega a errada por acidente!

Fazemos isso pelo meson.build!

project('my_project', 'c',
version : '0.1',
default_options : ['warning_level=3'])

executable('my_project',
'main.c',
install : true,
dependencies: dependency('gtk4'))
note

Eu ainda não sei o como se descobre que o nome do módulo é gtk4!

info

Caso você tenha mais que uma dependência, o parâmetro dependencies aceita lista:

executable('my_project',
'main.c',
install : true,
dependencies: [dependency('gtk4')])

Compiling

Pronto! Você está pronto para ter seu projeto compilado sem erro! Talvez warnings coff coff...

Entre no diretório de configuração do Meson (no meu caso builddir) e execute o comando de compilação:

$ cd builddir
$ meson compile
note

Por que entrar no diretório antes? Meson permite que você tenha diversos setups.
Se você estiver fora do diretório é possível rodar o comando se adicionar o argumento para -C:

$ meson compile -C builddir

"clangd está reclamando novamente de 'gtk/gtk.h' file not found!"
Não se desespere pois se você olhar os arquivos criados após compilação, um deles é bem útil.

my_project/
├── builddir/
│ ├── ...
│ ├── compile_commands.json
│ └── my_project
├── main.c
└── meson.build

Olha lá arquivo que o clangd quer!
E o seu executável mas isso era de se esperar...

Copie ele para a raiz do seu projeto e pronto, seu clangd deve parar de chorar erro!

my_project/
├── builddir/
│ ├── ...
│ ├── compile_commands.json
│ └── my_project
├── compile_commands.json
├── main.c
└── meson.build

Conclusion

Meson possui uma etapa de configuração, mas tirando isso os comandos depois ficam bem mais sensatos!

Compilando com clang e atualizando compile_commands.json:

$ clang `pkg-config --cflags gtk4` -o my_project main.c `pkg-config --libs gtk4`
$ bear -- clang `pkg-config --cflags gtk4` -o my_project main.c `pkg-config --libs gtk4`

Compilando com meson e atualizando compile_commands.json:

$ meson compile
$ cp compile_commands.json ../compile_commands.json

Se eu descobrir uma maneira de após compilação já copiar o arquivo compile_commands.json, tudo vai ficar perfeito!

References

Thiago Lages de Alencar
  • Durante a faculdade eu aprendi C

    • Abri Visual Studio
    • Escrevi código em C
    • Executei
  • Recentemente eu usei C++ no projeto Godot

    • Segui instrução do Godot para configurar o VSCode
    • Escrevi código em C++
    • Executei

Em ambos os casos eu nunca aprendi direito sobre bibliotecas... Notei claramente quando segui as instruções de projeto para Build from Source e não sabia mais o que fazer com os arquivos .a e .so gerados.

Desenho de um rosto rindo que nem idiota

Eu sabia que poderia compilar C/C++ com GCC ou Clang, mas meu conhecimento se resumia a compilar projetos 100% meus.

gcc main.c -o main

Bem, vamos entender cada um dois formatos primeiro.

.a (archive)

Se trata de uma biblioteca estática (static library), ou seja, podemos ver como um conjunto de funções/variáveis que seram anexadas ao seu executável durante a etapa de compilação (na etapa do linker).

Ótimo quando você quer que seu programa tenha toda a lógica.

.so (shared object)

Se trata de uma biblioteca compartilhada (shared library), ou seja, podemos ver como um executável que será utilizado por qualquer programa que precise dele (ainda é preciso avisar ao linker da existencia da biblioteca).

Ótimo pois ocupa menos espaço do computador da pessoa com a mesma lógica.

GCC 4 Steps

Eu já mencionei linker duas vezes, para entender o que ele é precisamos olhar para cada etapa do GCC.

4 etapas do GCC

Um resumo seria:

  • Preprocessor
    • Responsável por fazer pré processamentos, o mais popular é a substituição de #include pelo código dentro dos headers (.h).
    • A saída dessa etapa é conhecida como translation unit (ainda é código C).
  • Compiler
    • Responsável por converter o código C para código assembly.
  • Assembler
    • Responsável por converter o código assembly para um object code, ele é formado de código de máquina (código especifico para rodar naquela máquina) e "referências" a serem encontradas.
      • Por exemplo, você pode referênciar a função void do_it() mas ela não estar neste arquivo .c.
  • Linker
    • Responsável por encontrar as referências de um arquivo e linkar elas, a saída é justamente o executável final.

Project from Zero

Começamos com uma estrutura bem vazia de projeto:

project/
└── src/
├── main.c
└── ...

Apens possuimos código nosso, então tudo que precisamos fazer para criar o executável é

gcc src/main.c -o main

Algum momento do nosso projeto decidimos usar bibliotecas de terceiro e não queremos misturar ela com o nosso código então resolvemos sempre separar os arquivos dela em outros diretórios.

O primeiro tipo de arquivo que bibliotecas trazem junto são os headers, utilizados justamente para o compilador conseguir determinar o que exatamente tem que buscar nas bibliotecas.

project/
├── include/
│ └── header.h
└── src/
├── main.c
└── ...

Podemos adicionar um diretório onde se buscar headers com o argumento -I seguido pelo diretório (não é separado por espaço):

gcc src/main.c -o main -Iinclude

Agora podemos adicionar #include <header.h> sem o compilador reclamar que header.h não foi encontrado.

Se tentarmos chamar uma função que está no header.h ainda teremos erro pois não temos a função, apenas a assinatura dela. Nós precisamos do código da função, que pode estar em um .so ou .a.

warning
  • Saiba se sua biblioteca é C++ ou C para saber se deve usar gcc ou g++.
  • Se ambos tipos de bibliotecas existirem o linker talvez priorize o .so.

.a

Novamente não vamos querer misturar este arquivo com os do nosso projeto, então vamos deixar a biblioteca na pasta lib.

project/
├── include/
│ └── header.h
├── lib/
│ └── libname.a
└── src/
├── main.c
└── ...

Podemos passar ao compilador diretório onde as nossas bibliotecas se encontram com o argumento -L seguido pelo diretório (não é separado por espaço):

gcc src/main.c -o main -Iinclude -Llib

Precisamos especificar as que desajamos usar e isso é feito com o argumento -l seguido pelo nome base da biblitoeca (não é separado por espaço):

gcc src/main.c -o main -Iinclude -Llib -lname
note

-lname vai buscar pela biblioteca libname.a, este é um atalho para referênciar bibliotecas.
-l:libname.so pode ser usado em casos que o nome da biblioteca não segue estes padrões.


.so

Segue basicamente a mesma lógica do .a, deixar a biblioteca no diretório lib.

project/
├── include/
│ └── header.h
├── lib/
│ └── libname.so
└── src/
├── main.c
└── ...

Passar ao compilador diretório onde as nossas bibliotecas se encontram com o argumento -L seguido pelo diretório (não é separado por espaço):

gcc src/main.c -o main -Iinclude -Llib

No caso de bibliotecas compartilhadas este diretório pode possuir centenas de bibliotecas, então faz sentido você ter que especificar as que você quer usar. Novamente usamos o argumento -l seguido pelo nome base da biblitoeca (não é separado por espaço):

gcc src/main.c -o main -Iinclude -Llib -lname
note

-lname vai buscar pela biblioteca libname.so, este é um atalho para referênciar bibliotecas.
-l:libname.so pode ser usado em casos que o nome da biblioteca não segue estes padrões.

Durante a execução do código o sistema operacional vai buscar a biblioteca em lugares predefinidos (linux busca em lugares como /usr/lib).

Mas se nossa biblioteca não for estar em um destes lugares predefinidos? Ainda é possível adicionar lugares onde se buscar durante a execução.

Podemos passar argumentos ao linker com -Wl seguido pelos argumentos que ele deve receber, no caso -Rlib (argumentos separados por virgula):

gcc src/main.c -o main -Iinclude -Llib -lname -Wl,-Rlib

Nosso executável agora vai sempre tentar buscar a biblioteca na pasta lib que estiver no mesmo diretório que ele.

References