5

Eu recriei a clássica função printf() do C do zero!

Olá, pessoal do TabNews! Sophia aqui com mais um post!

Há pouco tempo compartilhei aqui a minha experiência desenvolvendo a libft, minha primeira biblioteca própria em C na 42 São Paulo. Hoje estou de volta para contar como foi encarar o segundo grande desafio dessa jornada de Cadete: recriar do zero uma das funções mais utilizadas, famosas e misteriosas do início do aprendizado de baixo nível: a printf().

O projeto se chama ft_printf e, seguindo a filosofia da escola, a regra de ouro se mantém: não podemos utilizar funções prontas da biblioteca padrão (além da permissão estrita da write e da própria libft que criamos anteriormente). Tudo precisa ser construído na unha!

O que é a ft_printf?

O objetivo principal parece simples: analisar uma string de formatação (o primeiro argumento) e imprimir os caracteres na saída padrão (stdout). O desafio real começa quando encontramos o caractere %, que funciona como uma flag avisando que precisamos capturar um argumento variável e formatá-lo corretamente para exibir na tela.

Pelas regras do projeto, precisei implementar obrigatoriamente o suporte para as seguintes conversões:

  • %c para um único caractere
  • %s para strings de caracteres
  • %p para o endereço de memória de um ponteiro (em formato hexadecimal)
  • %d e %i para inteiros com sinal em base decimal
  • %u para inteiros sem sinal em base decimal
  • %x para números hexadecimais em letras minúsculas (base 16)
  • %X para números hexadecimais em letras maiúsculas (base 16)
  • %% para imprimir o próprio caractere de porcentagem

Além disso, há um detalhe crucial: a função precisa retornar exatamente o mesmo valor inteiro que a original, que representa a contagem precisa de todos os caracteres impressos. Se houver um desvio de um único byte sequer nos testes, a Moulinette (o sistema automático de correção) zera o projeto sem dó 😬.

Variadic Functions: o grande...

Quando bati o olho no protótipo da função pela primeira vez, confesso que deu um nó na cabeça:

int ft_printf(const char *format, ...);

O que diabos significam aquelas reticências no final? 🤯 kkkk

Foi aí que descobri e precisei dominar o conceito de Funções Variádicas. Em C, para criar uma função capaz de receber uma quantidade n-ésima e dinâmica de argumentos passados pelo usuário, precisamos recorrer à biblioteca <stdarg.h> e suas macros:

  1. va_list: um tipo de dado especial que serve para rastrear e armazenar os argumentos adicionais
  2. va_start: inicializa a lista de argumentos, apontando para o primeiro elemento logo após o argumento fixo (format)
  3. va_arg: resgata o próximo argumento da lista, exigindo que você informe especificamente o tipo esperado (ex.: int, char *)
  4. va_end: limpa a memória e finaliza o acesso à lista de argumentos

Entender como o computador navega pela memória empilhando esses argumentos sem que eles tenham um nome fixo foi um baita aprendizado de como as coisas funcionam por baixo dos panos.

Os maiores desafios da implementação

Se na libft o maior vilão foi entender o gerenciamento de memória bruta com ponteiros e alocações, na ft_printf a dor de cabeça foi com a arquitetura do código e a precisão do fluxo.

  • A saga dos Hexadecimais e Ponteiros (%p): Converter um número inteiro longo para hexadecimal exige uma lógica de divisões sucessivas pela base 16. No entanto, o tratamento do %p exige atenção redobrada: ele precisa injetar o prefixo 0x na frente e lidar corretamente com o caso de ponteiros nulos (NULL), exibindo a string (nil) de forma idêntica ao comportamento padrão do sistema operacional.

  • A Meticulosa Contagem de Caracteres: Como o retorno da função principal depende da soma exata de tudo o que foi escrito, cada pequena função auxiliar (seja imprimindo um número negativo enorme ou uma string vazia) precisa retornar a quantidade exata de caracteres que enviou para o terminal. Uma falha de lógica em um único if e o retorno final quebrava.
    Desta vez, o papel e a caneta voltaram a ser meus melhores amigos: desenhar o fluxo do parser (mapeando a leitura caractere por caractere e os desvios de fluxo ao encontrar o %) me poupou horas valiosas de depuração.

Jump Tables: apenas deixe isso simples

Eu tentei bancar a esperta e economizar nos if-else, então fui em busca de alternativas. Acabei encontrando sobre "Jump Tables", que é basicamente chamar funções em uma tabela de array. O problema foi essas linhas de código aqui:

while (i < 256)
{
    ctx->table[i] = NULL;
    i++;
}

Esse código serve para definir toda a minha tabela para NULL até 256, evitando lixos da memória. A questão é que, a cada chamada da minha ft_printf, esse while fará essa limpeza várias e várias vezes, sendo bem mal performatico. Eu fiz essa correção na minha "mega-libft", onde concentrei o ft_printf e a get_next_line, que irei falar sobre posteriormente. Lá apenas usei o básico e simples if-else.

O Projeto

Para quem tiver curiosidade em conferir como estruturei as funções, organizei os arquivos auxiliares e configurei o Makefile para gerar a biblioteca, o repositório completo está público no meu GitHub:
🔗 https://github.com/ciproterona/42_ft_printf

Vale destacar que eu não fiz o bônus, que incluí precisão de floats (%.2f). Pretendo fazê-la mais a frente apenas pelo aprendizado, pois não iremos usar essa funcionalidade do printf, então não vi necessidade imediata de implementá-la.

Nota sobre contribuições: Por se tratar de um projeto puramente acadêmico e individual da grade da 42, não aceito merges de Pull Requests para alterar o escopo principal. Porém, sinta-se totalmente livre para abrir uma Issue ou PR educacional para propor melhorias de performance, discutir conceitos ou apontar alguma otimização na lógica! 👐🏼

Superar o desafio da printf me trouxe uma confiança legal para continuar. Ver uma função tão clássica e robusta rodando perfeitamente a partir de linhas de código que eu mesma escrevi é uma sensação indescritível.

Adoraria saber de vocês: Como foi o primeiro contato de vocês lidando com funções variádicas ou manipulação de strings em baixo nível? Para quem já fez esse tipo de projeto, vocês costumam estruturar o parser com grandes blocos de if/else ou preferem seguir minha loucura e usar tabelas de ponteiros para funções (array of function pointers)?

Estarei trazendo atualizações aqui no TabNews para os próximos passos da jornada. Até mais! \(^-^)/

Carregando publicação patrocinada...
2

Parece bem complicado e bem interessante. Eu também gosto bastante da linguagem C e mantenho projetos com ela. Comecei a experimentar a mais de 20 anos atrás, na época do windowsXP. O software/ide/compiler que eu usava era modo ms-dos. O msdos rodava uma camada abaixo do windows. Hoje em dia o 'prompt de comando' (sucessor do ms-dos) roda como um software do windows. Era o inverso. Eu programo em C a mais de vinte anos e nunca rodei makefiles. Antes eu usava softwares/ides/compilers. Hoje uso/chamo o tcc Tiny C Compiler (que é incrível) pela linha de comando.

1

Nossa Rodrigo, que experiência legal!

Eu comecei no C agora, uns 4 meses na 42. Eu gosto muito da simplicidade do C. Espero poder trabalhar com ela no futuro kkkk.

Se você puder dar dicas, estou a disposição aqui para lê-las :) 20 anos de C não é pouca coisa, é literalmente meu tempo de

1

Olha,
Eu espero que você veja como a questão de 'enfileirar instruções' pode ser útil além da computação.

A dica é:
Considera que o bloco de código fundamental é instrução: chamada de função.
Existem ifs fors e whiles e declarações de funções e varáveis... mas em um outro nivel tudo isso é instrução do tipo 'leva esse byte daqui pra la'