Paulo Henrique Rodrigues Pinheiro

Um blog sobre programação para programadores!


Rust Pills - resolvendo nomes por DNS

Resolvendo nomes FQDN para endereço IP, na linguagem Rust.

Rust pills

Em artigo anterior, falei sobre minhas aventuras em ler da entrada padrão, como um filtro Unix. A estrutura básica desse tipo de programa é:


use std::io;
use std::io::prelude::*;

fn main() {
  let stdin = io::stdin();

  for l in stdin.lock().lines() {
    let line = l.unwrap();

    println!("{}", line);
  }

}

Se, por exemplo, eu compilar e executar, passando alguma coisa para a entrada padrão:


$ rustc resolver.rs
$ ls /|./resolver
bin
boot
dev
etc
home
lib
lib64
lost+found
media
mnt
opt
proc
root
run
sbin
srv
sys
tmp
usr
var
$

Se executar o programa sem passar alguma entrada, ele ficará à espera de algo digitado, até encontrar um "CONTROL-D".

Essa é uma estrutura bem básica para desenvolver inúmeros utilitários. O desse POST é um programa que lê uma lista de hosts (pode ser host ou ip), e caso seja host, descobre o(s) ip(s) e imprime.

Esse também será um programa a partir do qual faremos outras coisas em outros POSTS.

Preciso resolver nomes de hosts em endereços IPs. Poderia usar a função lookup_host do módulo std::net, mas ela está classificada como instável, e o rust que tenho instalado é do canal estável.

O lance é procurar algum módulo que funcione para um compilador estável. Encontrei muitos crates que trabalham com DNS, mas são todos para versões instáveis.

Lembrei que a libc tem funções para trabalhar com isso. E lembrei que o rust pode trabalhar com bibliotecas escritas em outras linguagens, inclusive C. Portanto, ficou fácil, basta escrever um programa em rust que use a função apropriada da libc.

Para isso, usa-se o Foreign Function Interface (também já escrevi sobre isso para Perl6, veja nesse artigo). Com ele, reescrevemos estruturas e enumerações, além dos protótipos de função, que estão em C, para uma forma que o rust entenda. Para não reescrever as estruturas que usarei, usei um crate que já tem isso, o, pasmem, crate libc.

Criei meu novo projeto com cargo new resolver --bin, que cria o novo (new) projeto resolver para ser um programa executável (--bin). Editei o arquivo Cargo.toml que ficou assim para usar o crate libc:


[package]
name = "resolver"
version = "0.1.0"
authors = ["Paulo Henrique Rodrigues Pinheiro <[email protected]>"]

[dependencies]
libc = "0.2.8"

Agora vamos ao programa, que parte já com a parte voltada a ler a entrada padrão.

Como será usado o crate libc, adicionei o seguinte:


extern crate libc;
use libc::{c_char, hostent};

Será usada a definição de c_char e a estrutura hostent.

Também será usada a CString, que é uma conveniência que nos facilita lidar com as strings em C que precisam acabar com o caractere \0:


use std::ffi::CString;

E, da libc nativa (não o crate), usaremos a função gethostbyname:


#[link(name="c")]
extern {
  fn gethostbyname(name: *mut c_char) -> *mut hostent;
}

Veja o name="c". Se fôssemos usar uma biblioteca chamada libfoobar, ali deveria ser informado name="foobar".

O protótipo em C da função é esse:


struct hostent *gethostbyname(const char *name);

Usamos *mut em nossa tradução, pois é assim que o rust define os raw pointers, necessários para trabalhar em baixo nível com a memória.

Os detalhes da minha implementação não interessam muito aqui, pois temos mais coisas importantes a observar.

Primeiro, para usar a função em C:


let gethost_result :*mut hostent = unsafe { gethostbyname(to_resolv) };

if gethost_result.is_null() {
    return None;
}

É preciso encapsular a chamada com o unsafe, que diz ao rust algo como eu sei o que estou fazendo. O rust tem muitas preocupações e verificações que só podem ser validadas no escopo da linguagem. Portanto, qualquer operação que esteja fora do controle do compilador rust, tem que ser marcado como inseguro.

Os tipos com assinatura *mut disponibilizam um método is_null para que se possa verificar se um ponteiro é NULL tal como entendido em C. Nesse caso, se o retorno for nulo, a reposta será vazia. Esse tratamento de erro do rust é algo que merece um estudo à parte, vou considerar que esse assunto é entendido pelo leitor. Caso não seja, leia com paciência a longa explicação no livro oficial.

Observe que em toda a parte que lidamos com ponteiros, usamos unsafe. Mais sobre ele aqui.

A estrutura hostent é um tanto embaçada. Isso é facilmente percebido pelo código que fiz.

Leia o manual sobre a chamada gethostbyname. Lá ele fala que, embora o retorno seja um ponteiro para a estrutura, é o ponteiro para dados estáticos, ou seja, não precisamos nos preocupar em liberar memória. Mas se você estiver trabalhando com algo que não seja estático, lembre-se sempre de dar um free, por exemplo, implementando o trait Drop. Aqui há um exemplo.

Aqui está o código para este artigo.

Se você copiar os arquivos em seu computador, pode testar dessa forma:


$ cargo build && echo -e "noxexist.noxexist\nwww.tor.com\ngoogle.com\nwww.terra.com\nwww.sysincloud.it"|target/debug/resolver
   Compiling libc v0.2.8
   Compiling resolver v0.1.0 (file:///home/paulohrpinheiro/Dropbox/projetos/artigos/rust_pills/dns-resolver/resolver)
"noxexist.noxexist" => None
"www.tor.com" => Some(["104.20.16.112", "104.20.15.112"])
"google.com" => Some(["216.58.202.14"])
"www.terra.com" => Some(["208.70.188.57"])
"www.sysincloud.it" => Some(["54.175.90.218"])

Espero logo poder usar a chamada nativa que já está disponível como instável: lookup_host.