Site menu Molhando os pés na linguagem Rust
e-mail icon
Site menu

Molhando os pés na linguagem Rust

e-mail icon

2015.11.11

Este artigo expressa a opinião do autor na época da sua redação. Não há qualquer garantia de exatidão, ineditismo ou atualidade nos conteúdos. É proibida a cópia na íntegra. A citação de trechos é permitida mediante referência ao autor e este sítio de origem.

No artigo anterior mencionei a linguagem Rust, mais por entusiasmo que por conhecimento de causa. Aproveitando esse entusiasmo e procurando quitar o "débito técnico" contraído no artigo, procurei desenvolver um projeto pequeno, porém completo, e adequado a uma linguagem de baixo nível, como é o Rust. Mesmo para um projeto de brinquedo, não faz sentido escolher um objetivo que Python ou Javascript cumpriria em 15 linhas de código.

Também juntei força de vontade para livrar-me do PHP de uma vez por todas. A vida é muito curta para usar linguagens de programação ruins. Aceitando uma sugestão de anos atrás, implementei um gerador de HTML's estáticos, em Python. Bem simples, indigno de virar um projeto no GitHub, mas suficiente para mim. Qualquer necessidade futura de dinamismo do lado servidor pode ser coberta pelo servidor Node.js, que já cuida dos comentários.

Voltando ao Rust, o projeto escolhido foi um parser EXIF, para extrair meta-dados de imagens (data, câmera, informações técnicas da fotografia, etc.). O EXIF é oficialmente parte do padrão TIFF. Uma imagem JPEG com meta-dados embute um pequeno TIFF, que por sua vez contém os dados EXIF.

O resultado do esforço é o projeto RExif. Procurei fazer o ciclo completo de um pacote (crate), com site GitHub, página de documentação gerada automaticamente com base no código e publicação no crates.io para instalação fácil via Cargo.

Certamente o RExif não pode competir com projetos do tipo rexiv2 já que a biblioteca exiv2 é extremamente completa; ela faz as vezes de documentação para as extensões proprietárias EXIF que os fabricantes de câmeras tipo Nikon, Canon, etc. ficam inventando. Eu queria aprender Rust, e escrever bindings para uma biblioteca C não seria um bom primeiro projeto para isso. Então o pacote RExif é puro Rust, e tem este pequeno diferencial positivo frente à concorrência :)

Também deve ser óbvio que meu código vai ser ruim, no sentido de "pouco idiomático". A API de nível mais alto eu acho que está ok, então é uma questão de ir melhorando as camadas mais baixas.

Comentários sobre a linguagem

Novamente, considere que são comentários de um neófito em Rust. FWIW, lá vai:

Rust é realmente de baixo nível

Não há risco de alguém dizer que Rust é o substituto rápido do Python — aliás, é engraçado como tanta gente se apressa em declarar que Python é passé, basta aparecer uma nova linguagem, de qualquer tipo. Escrever um sistema Web usando Rust seria bobagem, a meu ver. Escrever o próprio servidor Web em Rust seria mais justificável.

Rust é rápido, verboso e lembra C++ em muitos aspectos. É para desenvolvedores profissionais, que conhecem conceitos de base e.g. ponteiros.

A checagem estática de ponteiros merece a(s) fama(s)

O ponto alto da linguagem é realmente prático e leva a uma implementação de alta performance sem os riscos usuais do C/C++. Mas demora um pouco para acostumar-se com ela. É muito chato quando você tem absoluta certeza que uma operação é segura, mas o compilador discorda.

Uma vez que se acostuma, é agradável porque não é preciso documentar convenções típicas do C/C++, do tipo "quem é dono deste ponteiro?". Por exemplo, a função

pub fn read_urational_array(le: bool, count: u32, raw: &[u8])
	-> Vec<URational>
{
	...
}

Nós sabemos que o vetor Vec retornado pela função possui a sua própria memória, então ele "pertence" ao chamador. Também sabemos que os objetos URational dentro do vetor não contém referências à matriz raw. Se tivessem, então raw precisaria ter um tempo de vida igual ou superior ao vetor, e isto estaria sinalizado usando a sintaxe 'lifetime (um recurso que felizmente não precisei usar no RExif). E se não estivesse sinalizado, não compilaria.

Entender empréstimo, mutabilidade, etc. de um tipo simples é tranqüilo. A coisa complica quando há vários níveis. Por exemplo, um vetor de objetos. Quem é mutável: o vetor, os objetos contidos nele, ou as propriedades de cada objeto? Tipicamente, a mutabilidade dos elementos segue a da coleção:

	let exif_entries_copy = exif_entries.clone();

	for entry in &mut exif_entries {
                // a funcao abaixo modifica entry
		exif_postprocessing(entry, &exif_entries_copy);
	}

No exemplo acima, exif_entries tem de ser mutável, para que o iterador seja mutável e entry seja modificável. Para obter objetos modificáveis num vetor imutável, teriamos de encapsulá-los numa referência, algo como Vec<RefCell<Tipo>>.

Outro aspecto é que tivemos de copiar a coleção para exif_entries_copy, pois precisamos passá-la a exif_postprocessing(), mas a coleção original está sendo modificada dentro do loop e não pode ser usada de outra forma até que o loop acabe. Neste caso eu "tenho certeza" (como a Ofélia do Zorra Total) que não haveria problema, mas esta simples regra evita uma legião de bugs.

Por conta dessas interações entre mutabilidade e posse, não adianta pensar em termos de const do C++. Passar uma referência "const" no Rust garante que o próprio objeto é "const" enquanto essa referência existir, o que é bem diferente do C++.

Algo relacionado a isto é a dicotomia entre tipos String, strings literais e referências &str, bem como a relação entre vetor Vec<Tipo>, matriz literal [<Tipo>; n] e "fatia" (slice) [<Tipo>]. A diferença básica é que String, strings literais, Vec e matrizes literais possuem a memória que ocupam. Já &str e fatias são meras referências aos primeiros.

O Rust faz uso massivo de "interfaces" e "templates"

Oops, estes conceitos são chamados de traits e generics no Rust.

Muito do que se faz usando hierarquias de classes, métodos virtuais, classes abstratas e interfaces em C++ e Java, ou mesmo protocolos do Objective-C, em Rust faz-se com traits.

Comportamentos default de uma classe C++, como cópia, comparação, etc. são explicitamente habilitados para uma classe Rust por meio de traits. Exemplo:

#[derive(Clone)]
pub struct ExifEntry {
    ...
}

Com o trait Clone, um objeto ExifEntry pode ser livremente copiado (duplicado). E há efeitos secundários: se uma classe é clonável, um vetor de objetos dessa classe também é clonável.

No caso de ExifEntry a implementação padrão de Clone (cópia membro por membro, que também é o default do C++) foi suficiente. Quando a implementação do trait é especificada, a sintaxe é outra. O exemplo abaixo implementa o trait Error para outra classe:

impl Error for ExifError {
	fn description(&self) -> &str {
		self.readable()
	}
}

Os templates, digo, generics, também são amplamente utilizados no Rust. Embora o suporte do compilador seja muito mais amigável que os templates do C++, ainda pode desagradar quem tem pavor de templates. Existe uma enorme discussão sobre a conveniência da programação genérica, é um dos pontos da discussão Rust × Go.

Enumerações são a pedra-de-canto do Rust

As enumerações são um canivete suíço no Rust. Seu uso mais trivial, como constante tipada, é análoga ao C/C++:

pub enum ExifTag {
	UnknownToMe = 0x0000ffff,
	ImageDescription = 0x0000010e,
	Make = 0x0000010f,
	Model = 0x00000110,
	...
}

Mas uma enumeração Rust também pode ser análoga a uma union, porém segura. No exemplo abaixo, a enumeração TagValue é uma espécie de tipo "variant", que pode assumir o subtipo U8, Ascii, etc:

pub enum TagValue {
	U8(Vec<u8>),
	Ascii(String),
	U16(Vec<u16>),
	U32(Vec<u32>),
	...

A única forma de acessar a variante de uma enumeração desse estilo, é usando o comando match. No exemplo abaixo, a função "sabe" que só pode receber a variante U16, mas o compilador obriga que todas as possíveis variantes sejam tratadas, mesmo que com uma opção "pega-tudo" (sublinhado):

let s = match e {
      &TagValue::U16(ref v) => {
               let n = v[0];
               match n {
                       1 => "Straight",
                       3 => "Upside down",
                       ...
                       _ => return "Unknown",
               }
       },
       _ => panic!(err),
};

No exemplo acima, encerrar o programa com panic!() é simples e também correto, porque se esta função recebesse uma variante diferente de U16, seria devido a um bug no próprio RExif (uma imagem corrompida não poderia provocar esta condição).

Usar enumerações não é uma opção. Elas estão por toda parte no Rust, e substituem inclusive o tratamento de exceções:

let mut f = match File::open(fname) {
        Ok(f) => f,
        Err(_) => return Err(ExifError{
                        kind: ExifErrorKind::FileOpenError,
                        extra: fname.to_string()}),
};

Como Result contém o resultado da operação na variante Ok, é preciso usar match para chegar a ele, mas então estamos obrigados a tratar a outra variante que é Err. Subterfúgios como .unwrap() e try!(...) elidem o tratamento de Err mas servem apenas para simplificar programas triviais; não são para uso "sério".

e-mail icon