- O compilador Zig, que oferece compilação de código C e compilação cruzada nativamente, é a linguagem mais surpreendente que o autor encontrou em 45 anos de experiência
- Com recursos únicos como execução em tempo de compilação, variáveis com tamanho arbitrário em bits e ambiente de blocos de teste, ele vai muito além de um simples substituto de C/C++ e oferece uma forma totalmente nova de programar
- Com sintaxe concisa e clara, como declaração de variáveis por inferência de tipos, structs anônimas e label break, é possível aprender rapidamente
- Com testes independentes de módulos por meio de blocos de teste e a função embutida @breakpoint, oferece suporte à depuração de código otimizado
- Com suporte à programação de baixo nível usando bit fields e operações de bits, alcança ao mesmo tempo eficiência e robustez, integrando a uma linguagem compilada as vantagens de linguagens interpretadas
Prefácio
- Em 45 anos de carreira, o autor nunca encontrou uma linguagem tão surpreendente quanto Zig
- Zig não é apenas uma nova linguagem, mas uma ferramenta que muda fundamentalmente a forma de programar
- Vê-la apenas como um substituto de C ou C++ é uma grande subestimação
- O objetivo deste texto é apresentar recursos simples, mas atraentes de Zig e ajudar programadores a começar rapidamente
- Existem muitos outros recursos que influenciam a adoção de Zig na indústria
O compilador Zig
- Oferece compilação de código C e compilação cruzada por padrão, sem configuração adicional, o que tem grande impacto na indústria
- A instalação é feita baixando o compilador para o processador/SO correspondente na página de downloads do Ziglang, descompactando-o e copiando-o para o diretório desejado
- No Windows 10, recomenda-se copiar o arquivo zip x86_64 para
Program Files e renomear o diretório raiz para zig-windows-x86_64, evitando a necessidade de alterar a variável de ambiente Path a cada atualização de versão
- Depois de adicionar o caminho do diretório raiz à variável de ambiente Path, o compilador pode ser usado no modo CLI
- Para compilar o programa "Hello World!", recomenda-se consultar a seção "Getting Started" do site oficial
Conceitos e comandos principais
Declaração de variáveis
- A declaração de variável é composta por uma primeira parte com acessibilidade (
pub ou omitido), var/const e nome da variável, uma segunda parte com a declaração de tipo e uma terceira parte com a inicialização
- Apenas a primeira e a terceira partes são obrigatórias, e o tipo pode ser inferido a partir do valor de inicialização
- Exemplo:
var sum : usize = 0;
- Variáveis declaradas sem
pub só podem ser acessadas dentro do módulo (semelhante a variáveis static em C)
- Não é recomendado declarar variáveis
pub, e é recomendado minimizar funções pub para reduzir acoplamento e aumentar coesão
Structs, structs anônimas e blocos de teste
- Literais de struct anônima envolvidos por
.{ e } são usados para inicializar elementos de outra struct ou criar uma nova struct com elementos inicializados
.{ } é um literal de struct anônima vazia
- A forma
struct { } é uma declaração de struct
- Blocos de teste permitem compilar e executar testes sem um executável
Bit fields
- Bit fields são declarados como campos de tipos com tamanho específico dentro de uma packed struct
- Ponteiros podem apontar para um bit field específico
Laço for
- A sintaxe de Zig é mais clara que a de C, mas usa intervalo aberto
[0..9) em vez de [0..8]
- A declaração de tipo, inicialização, teste e incremento da variável de laço
i são tratados automaticamente
Arrays
[_] define um array com tamanho desconhecido, seguido pelo tipo dos elementos e pela inicialização
- Exemplo:
var grid = [_]u8{0} ** 81; inicializa 81 elementos u8 com 0
- O tamanho do array é inferido a partir do argumento de repetição da inicialização
- Em ambiente de teste, é possível percorrer os elementos do array e somá-los
- A variável declarada entre
| no laço for é automaticamente assumida como do mesmo tipo dos elementos do array
usize é o inteiro sem sinal natural da plataforma (u64 em 64 bits, u32 em 32 bits)
Ponteiros multi-item
- Para que um ponteiro para array use aritmética de ponteiros, ele precisa ser explicitamente declarado como ponteiro multi-item, como
[*]const i32
- Mesmo que o array seja
const, o ponteiro pode ser declarado como var
Desreferenciação de ponteiros
- Um ponteiro ao qual foi atribuído o endereço de uma posição individual de array não pode ser atualizado com aritmética de ponteiros
- A desreferenciação de ponteiro usa
ptr.*
Label break
- É possível realizar diversas tarefas em tempo de compilação, como inicializar arrays
- No label break, acrescenta-se
: após o nome do bloco, e break retorna um valor a partir do bloco
0.. é um intervalo infinito começando em 0
- No laço
for, as variáveis são inicializadas e incrementadas automaticamente, e o laço termina após processar a última posição do array
- Um array pode não ser inicializado explicitamente com
undefined
Funções em Zig
- Funções são declaradas com
fn e, por padrão, são static (usadas apenas dentro do arquivo)
- Quando declaradas como
pub fn, podem ser importadas por outros arquivos
- Funções podem ser "inlined"
- Ponteiros para função vêm com
const na frente e o protótipo da função em seguida
Programação orientada a objetos em Zig
- Structs podem ter funções
- No exemplo da pilha, é possível armazenar até 81 elementos (do tipo
StkNode)
- Os operadores
++ e -- não existem em Zig; usam-se += e -=
- O ponteiro da pilha é um inteiro usado como índice do array
stk
- O ponteiro
self não é passado explicitamente como parâmetro; ele é implicitamente assumido como ponteiro para a instância da pilha na qual a função é chamada
- Em uma chamada como
stack.pop(), self é um ponteiro para stack (semelhante a this em Java/C++)
- A função
init() é o construtor da pilha
- As funções
pop e push são "inlined"
Compilando e executando programas em Zig
Gerando um executável
- Para gerar um executável, é necessária uma função
main que represente o ponto de entrada do programa
- Em programas simples, a função
main pode ficar no mesmo arquivo
- Para depuração independente de módulo, é possível inserir uma função
main no final do arquivo e comentá-la após concluir a depuração
- Comando de compilação:
zig build-exe -O ReleaseFast program.zig
Executando blocos de teste de um módulo
- Um dos melhores recursos de Zig, usado para testes e prototipagem
- Um bloco de teste começa com
test "message" { e termina com }
- "message" é a string exibida durante a execução do teste
- Blocos de teste são executados independentemente do executável, e o executável final não executa os testes
- Comando de teste:
zig test module.zig
- O bloco de teste de
example.zig testa as funções set e print; set recebe uma string decimal como parâmetro, e print exibe o cabeçalho "Input Grid" antes de imprimir o grid
Saída em Zig
- A instrução
std.debug.print chama a função print presente em debug.zig da biblioteca padrão std de Zig
- O primeiro parâmetro é a string de formato, e o segundo é uma struct anônima contendo a lista de variáveis a exibir
- Quando não há formato, a struct fica vazia
- Por padrão, a saída vai para
stderr
- Ao contrário do
printf em C, Zig pode processar strings literais e listas de variáveis em tempo de compilação
Depurando executáveis
- Usar depurador não é simples fora de IDEs com depurador integrado (Eclipse, IntelliJ IDEA) ou kits de desenvolvimento integrados (
w64devkit)
- A integração de símbolos incha o código e exige compilação no modo Debug, gerando código executável muito menos eficiente
- Zig oferece uma solução prática para evitar esses problemas
Função embutida @breakpoint
- Ao inserir
@breakpoint(); no código-fonte, o programa para nesse ponto quando executado em um depurador
- É um recurso útil para depurar código Zig otimizado sem símbolos
- Logo antes de
@breakpoint();, é possível usar std.debug.print para exibir as variáveis que se deseja rastrear e verificar seus valores naquele momento
- No exemplo
debug_example.zig, são inseridos dentro da função set o código para imprimir o grid e as variáveis, além de @breakpoint();
- Comando de build:
zig build-exe debug_example.zig
- Depois, chama-se
debug_example.exe com um depurador como gdb e executa-se o programa com o comando r
- Use o comando
c para continuar e acompanhar o conteúdo do grid e o rastreamento das variáveis
- Repetindo Enter para continuar, é possível confirmar que os valores do
grid coincidem com o bloco de teste de example.zig
Programação de baixo nível em Zig
Representação da matriz
- Os dígitos decimais são armazenados na matriz como inteiros padrão
u8
- O
grid de entrada está em formato de string, mas os caracteres ASCII são convertidos internamente para inteiros u8
- O armazenamento dos números é feito linearmente, linha por linha, no array
grid de 81 posições: var grid = [_]u8{0} ** 81;
- Para verificar a correção do
grid, é preciso acessar os elementos por linha e coluna
- Cria-se um array de 9 ponteiros, cada um apontando para o início de uma linha
- Usa-se label break para retornar um valor de um bloco de código:
break :fill9x9 m; inicializa a matrix com m
- Notação de acesso a elementos:
element = matrix[i][j]
Representando dígitos decimais com bits
- A ideia central é substituir o dígito decimal inteiro
i pelo inteiro code
i ∈ [1,9] → code = 2ⁱ⁻¹
i = 0 → code = 0
- A posição em que o único bit de
code é definido como 1 é i-1 (quando i está entre 1 e 9); caso contrário, todos os bits são 0
- É fornecida uma tabela com os valores de
code para cada número (1→1, 2→2, 3→4, ..., 9→256)
Calculando code em Zig
- O valor de
code é calculado com o operador de deslocamento à esquerda apenas quando c não é 0: code = @as(u9,1) << (c-1);
- Em Zig, constantes precisam ter o tamanho apropriado para que a operação seja compilada e o resultado seja atribuído à variável
code é declarado com o tipo u9 (o valor máximo 256 exige no mínimo 9 bits)
- Zig pode ter variáveis com tamanho arbitrário em bits
- A função embutida
@as faz cast da constante 1 para o tipo u9
Representação do grid com bit fields
grid com bit fields por linha
- O array
lines espelha todo o grid, representando cada linha como um inteiro de 9 bits: var lines = [_]u9{0} ** 9;
- Ao acessar o array pela linha
i, verifica-se com uma operação bitwise AND (&) se um determinado número já está naquela linha: lines[i] & code
- Se o resultado for 0, o número ainda não está na linha
i; caso contrário, é duplicado
grid com bit fields por coluna
- O array
columns espelha todo o grid, representando cada coluna como um inteiro de 9 bits: var columns = [_]u9{0} ** 9;
- Ao acessar o array pela coluna
j, verifica-se com uma operação bitwise AND se um determinado número já está naquela coluna: columns[j] & code
- Se o resultado for 0, o número ainda não está na coluna
j; caso contrário, é duplicado
Regras do Sudoku
- Ao inserir um novo número em um
grid de Sudoku vazio, ele não pode já existir na linha, coluna e célula correspondentes ao novo elemento
- Uma célula é cada um dos 9 grids 3x3 separados por linhas grossas
- Cada elemento específico do
grid 9x9 pertence a uma linha, coluna e célula únicas que o contêm
- No
grid de exemplo, a primeira célula contém 3, 5, 6, 8 e 9, e faltam 1, 2, 4 e 7
- Os arrays
lines e columns lidam com a verificação de duplicatas em linhas e colunas
- É necessário um novo array para verificar duplicatas nas células
grid com bit fields por célula
- O array
cells espelha todo o grid, representando cada célula como um inteiro de 9 bits: var cells = [_]u9{0} ** 9;
- Fica mais fácil acessar
cells como uma matriz 3x3
- O array
cell é preenchido de maneira semelhante ao que foi feito com a matriz 9x9
- É preciso determinar a linha e a coluna da matriz
cell a partir da linha e da coluna do elemento no grid 9x9 original
- Como divisão inteira é muito lenta, usa-se o array
cindx = [_]usize{ 0,0,0, 1,1,1, 2,2,2 }; para fornecer o resultado da divisão
- Ao acessar a matriz pela linha
i e coluna j do elemento do grid 9x9, verifica-se com uma operação bitwise AND se um determinado número já existe na célula do elemento: cell[cindx[i]][cindx[j]] & code
- Se o resultado for 0, o número ainda não está na célula; caso contrário, é duplicado
Teste de duplicidade de elementos
- A verificação de duplicidade de um elemento é concluída combinando com bitwise OR (
|) todos os elementos anteriores da mesma linha, coluna e célula e depois aplicando bitwise AND com o code do elemento
if (((lines[i]|columns[j]|cell[cindx[i]][cindx[j]])&code) != 0) {
unreachable;
}
- Se o resultado for 0, o elemento ainda não existe na linha, coluna e célula
- Se o resultado não for 0, o programa para ao executar o comando
unreachable
- É a forma mais simples em Zig de indicar explicitamente um erro de execução
- O código real também exibe detalhes sobre a posição em que ocorreu o erro
- Exemplo: se o
0 logo após o primeiro 8 na string de entrada for substituído por 5, ocorre erro porque já existe um 5 na linha 3 da coluna 1
Atualização das estruturas de dados
- Na função
set, um laço for duplo percorre as linhas e copia para o grid cada novo elemento da string de entrada s
- A variável
k mantém o índice do novo caractere de entrada na string s
- O caractere é convertido para
u4 (variável c) subtraindo '0'
- Se o novo elemento a inserir no
grid não for 0 (c != 0), o code calculado com a instrução de deslocamento à esquerda é copiado para cada grid espelho
- Isso é feito com bitwise OR (
|=) no grid espelho correspondente:
lines[i] |= code;
columns[j] |= code;
cell[cindx[i]][cindx[j]] |= code;
- Não é necessário testar explicitamente se
c está entre 1 e 9, porque a operação de shift causará overflow ao ser executada
- Exemplo: se o
0 logo após o primeiro 8 na string de entrada for substituído por :, ocorre erro de execução
- O mesmo acontece se esse mesmo
0 for substituído por /
- O programa só funciona quando os valores estão entre 1 e 9, ou seja, quando o
grid de entrada contém apenas dígitos decimais
- Muitos
grids de Sudoku na web usam . em vez de 0, por isso a função set contém a linha if (s[k] == '.') c = 0;
- Isso contorna convenientemente a operação de shift, já que o valor de
c é 0
Prototipagem e robustez
- Os erros forçados nas duas seções acima demonstram um recurso importante de Zig
- Um deles é a robustez de Zig — no caso da operação de shift, comportamento incorreto não é permitido e é capturado em tempo de execução
- Embora todo o esforço pareça voltado à eficiência, é um caso típico em que desempenho é trocado por robustez
- Em C, se a operação de shift perde bits, isso é problema do programador, e isso se converte em melhor desempenho de certas instruções de assembly
- Outro recurso é a possibilidade de usar blocos de teste para prototipagem
- As aplicações possíveis são inúmeras, e a mostrada aqui é apenas depurar uma situação específica quando ocorre um erro
- Só esses recursos já oferecem capacidades surpreendentes, muito raras em linguagens de programação, especialmente em linguagens compiladas
Conclusão
- Zig é composto por três elementos centrais: compatibilidade com C, compilação cruzada e instalação simples
- Essas características mostram o potencial de se tornar um novo padrão para linguagens de programação de sistemas
- Muitas vantagens antes encontradas apenas em linguagens interpretadas estão gradualmente migrando para linguagens compiladas para oferecer melhor desempenho
- Em Zig, a semelhança com linguagens interpretadas é especialmente marcante por causa do conceito de execução em tempo de compilação
- Isso é ao mesmo tempo o que torna Zig especialmente diferente e poderoso, e também o que pode torná-lo difícil de entender
1 comentários
Opiniões do Hacker News
Este texto começa afirmando que “Zig não é apenas uma linguagem simples, mas uma forma completamente nova de programar”, mas na prática quase não aborda recursos realmente próprios do Zig
Inferência de tipos, structs anônimas,
labeled breake afins já existem em outras linguagens há muito tempoO que de fato é único é o comptime, mas isso nem chega a ser mencionado
Não é um conceito totalmente novo como macros de Lisp, mas a forma como o Zig usa isso no lugar de genéricos é interessante
Ainda assim, a tese do texto soa bastante exagerada
Em Rust, dá para expressar claramente o momento em que o código é executado, e a ideia de um projeto tipo motor de consultas que explora todo o espaço do código é marcante
Veja a documentação do D
Se for uma
const-expression, a execução acontece automaticamenteSão linguagens tão diferentes quanto Java e Scala
comptimenão é uma invenção mágica, mas uma versão moderna de metaprogramaçãoO Zig é mais limpo que templates de C++, mas parece mais uma alternativa prática do que algo revolucionário
Pessoalmente, não entendo esse entusiasmo excessivo, como aconteceu com Rust
Li toda a documentação do Zig e mesmo assim não encontrei nada realmente surpreendente, o que me deixou confuso
O maior problema do Zig é que não dá para anexar dados aos erros
Erros são passados apenas por um canal auxiliar, o que dificulta o debugging e acaba levando desenvolvedores a omitirem dados de erro
Veja a issue relacionada
Só com um código simples como
AccessDeniedfica difícil saber a causaNa prática, mesmo usando objetos
Errorcomplexos, muitas vezes ainda é preciso um canal de diagnóstico separadoPor causa do overhead de desempenho ou de problemas no estado do sistema, em alguns casos é mais seguro tratar isso com binding tardio
O Zig adota uma filosofia que prioriza essa precisão e determinismo
Veja a issue relacionada
Mas o que realmente faz falta é logging estruturado e rastreamento de contexto com base na pilha de chamadas
std.zoné citado como um bom exemplo, e há um movimento na comunidade para reunir vários padrões de tratamento de erro e refletir isso no padrãoIsso evita que desenvolvedores preguiçosos simplesmente saiam grudando dados em tudo
Concordo com a afirmação de que o próprio modo de desenvolvimento do Zig é uma nova forma de desenvolver linguagens
O processo lento de evolução, com análise cuidadosa dos recursos e remoção do que é desnecessário, é marcante
Eu gostaria de ouvir de forma mais concreta o que exatamente há de único no Zig
Gosto do fato de que dá para instalar o Zig pelo PyPI
Se você instalar o pacote ziglang com
pip install ziglang, já pode usar na horaTambém dá para compilar código C usando
uvxÉ uma pena ver recursos que já existiam em linguagens como Ada, Object Pascal e Modula-2 sendo apresentados como “inovação” do Zig
É interessante como, ao serem reembaladas com sintaxe no estilo C, ideias de 40 anos atrás passam a parecer novas
A introdução do texto era boa, mas depois ele acaba virando apenas uma enumeração de recursos do Zig
A sintaxe intuitiva e o fluxo de controle explícito do Zig (
deferetc.) são atraentesGraças ao
comptime, também não é preciso aprender uma sintaxe separada de macrosTudo se encaixa naturalmente, a ponto de, mesmo no primeiro uso, parecer uma ferramenta usada há muito tempo
A sintaxe
for (0..9)do Zig é intuitiva, mas por ser um intervalo aberto, às vezes confundeComo no
range(0, 9)do Python, é fácil esquecer se o último valor está incluído ou não0..9e0..=9deixa isso claroO tamanho do intervalo vira simplesmente a diferença, e percorrer em ordem reversa também fica mais simples
0..<5(aberto) e0...5(fechado)Não gosto das regras de identificadores do Zig
A mistura de
snake_caseecamelCaseparece estranhaAinda assim, o sistema de build, o alocador de memória e a experiência de compilação são excelentes
Eu uso Rust principalmente, mas continuo curioso sobre o Zig
A regra de prefixos em bibliotecas C também me incomoda do mesmo jeito
O apelo do Zig não está em um único recurso, mas no acúmulo de decisões práticas
Escolhas que no começo pareciam radicais vão fazendo sentido conforme o entendimento se aprofunda
O Zig é uma linguagem que recompensa desenvolvedores curiosos
Um dos motivos pelos quais o Zig é bom é que ele reconhece a realidade do código de sistema de baixo nível
Muitas linguagens ignoram isso por razões estéticas, mas o Zig não faz isso
Veja a documentação do page_allocator