Rust: Organizando fontes em arquivos e diretórios separados

Posted by Vanius on 5 November 2020

A documentação oficial do Rust é formidável, porém a explicação sobre a estruturação de arquivos e diretórios pode parecer confusa. Nesse artigo pretendo mostrar de maneira simples - sem se aprofundar em todas possibilidades e detalhes. Vamos conferir!

Em Rust os componentes fn, struct, enum, trait, etc. são chamados de itens. Os itens por suas vez podem ser agrupados em módulos mod. Os módulos também são considerados itens, então formam uma estrutura hierárquica.

Criando um outro fonte

Vamos criar um programa vazio, entrar no diretório e abrir o vs code:


    cargo new struct-dir
    cd struct-dir
    code .

Quando criamos um projeto novo temos a seguinte estrutura:

estrutura de diretórios e arquivos

No main.rs há a função de Hello World:


    fn main() {
        println!("Hello, world!");
    }

Legal, agora vamos criar uma nova função say_hello e chamá-la do main():


    fn main() {
        say_hello();
    }

    fn say_hello() {
        println!("Hello, world!");
    }

Suponha que queremos colocar o método say_hello em um arquivo chamado services.rs. A estrutura ficará assim:

estrutura de diretórios e arquivos

Ao compilarmos agora temos o erro:


    error[E0425]: cannot find function `say_hello` in this scope
    --> src/main.rs:2:5
    |
  2 |     say_hello();
    |     ^^^^^^^^^ not found in this scope

Para resolver isso temos que fazer mais algumas coisas. Primeiro, temos que tornar a função say_hello pública, ou seja, ser visível para funções fora do seu código fonte. Para isso basta informar o prefixo pub antes de fn:


    pub fn say_hello() {
        println!("Hello, world!");
    }

Segundo, temos que indicar que o arquivo services.rs será um módulo. Para isso temos que inserir no início do arquivo main.rs a linha mod services;

Agora podemos informar o nome do módulo no prefixo da função, usando o delimitador ::. O código ficará assim:


    mod services;

    fn main() {
        services::say_hello();
    }

Pronto, agora o projeto compila e executa corretamente.

Porém pode ficar muito extenso informar o nome do módulo antes da função. Para evitar isso é possível fazer o import desse módulo e função, onde inserimos no início do fonte a linha use services::say_hello;:


    mod services;
    use services::say_hello;

    fn main() {
        say_hello();
    }

O main.rs é um arquivo importante dentro do diretório src, no cabeçalho ele define os módulos que este diretório contém.

A seguir criaremos uma nova função dentro do services.rs:


    pub fn say_goodbye() {
        println!("Goodbye!");
    }

E vamos chamar no main:


    mod services;
    use services::say_goodbye;
    use services::say_hello;

    fn main() {
        say_hello();
        say_goodbye();
    }

Quando importamos funções de mesmo módulo podemos "agrupar" o módulo e distinguir as funções entre chaves {...}. Por exemplo:


    mod services;
    use services::{say_goodbye, say_hello};

    fn main() {
        say_hello();
        say_goodbye();
    }

Vamos seguir e criar um arquivo chamado config.rs, com o seguinte conteúdo:


    pub struct Config {
        pub user_name: String,
    } 

    pub fn get_config() -> Config {
        Config {
            user_name: "Silva".to_string(),
        }
    }

Perceba que struct Config e o campo user_name possuem o pub como prefixo, indicando eles podem ser acessível em outro fonte.

A estrutura ficará assim:

estrutura de diretórios e arquivos

Agora vamos acessar no main.rs:


    mod config;
    mod services;
    use config::get_config;
    use services::{say_goodbye, say_hello};

    fn main() {
        say_hello();
        let config = get_config();
        println!("{}", config.user_name);
        say_goodbye();
    }

Para poder acessar a função get_config foi necessário inserir as linhas mod config; e use config::get_config;.

Criando subdiretório

Agora vamos supor que queremos criar entidades de domínio, mas queremos organizar num diretório novo, chamado domain, dentro do src. Será criado o arquivo user.rs e product.rs:

estrutura de diretórios e arquivos

O arquivo user.rs terá o conteúdo:


    pub struct Product {
        pub product_name: String,
    }

E o arquivo product.rs terá:


    pub struct Product {
        pub product_name: String,
    }

Certo, agora temos que indicar que o diretório domain será um módulo, para isso voltamos a alterar o main.rs e no cabeçalho vamos inserir a linha mod domain;:


    mod config;
    mod services;
    mod domain;
    use config::get_config;
    use services::{say_goodbye, say_hello};

    // Resto do código

Porém ao tentar compilar temos um erro:


    --> src/main.rs:3:1
    |
  3 | mod domain;
    | ^^^^^^^^^^^
    |
    = help: to create the module `domain`, create file "src/domain.rs"

    error: aborting due to previous error

O compilador espera que domain seja um arquivo.

Para indicar que um diretório seja considerado um módulo precisamos criar um arquivo chamado mod.rs dentro do deste diretório:

estrutura de diretórios e arquivos

Depois disso deve compilar corretamente.

A mesma palavra mod serve para definir módulo, tanto se for um arquivo ou diretório.

Acessando um arquivo em um subdiretório

Legal, agora vamos tentar acessar a entidade User de dentro do main.rs, inserindo no cabeçalho: use domain::user::User;:


    mod config;
    mod services;
    mod domain;
    use config::get_config;
    use domain::user::User;
    use services::{say_goodbye, say_hello};

    fn main() {
        say_hello();
        let config = get_config();
        println!("{}", config.user_name);
        let user = User {
            user_name : config.user_name,
        };
        say_goodbye();
    }

Porém ao compilar temos o seguinte erro:


    --> src/main.rs:5:13
    |
  5 | use domain::user::User;
    |             ^^^^ could not find `user` in `domain`

    error: aborting due to previous error

    For more information about this error, try `rustc --explain E0432`.

O compilador entendeu que o diretório domain é um módulo mas não entendeu que o arquivo user também é um módulo.

Para isso precisamos indicar no mod.rs do subdiretório domain que user é um módulo, então nele vamos inserir a linha:


    pub mod user;

Pronto, agora compilou. O que fizemos no mod.rs foi semelhante ao que fizemos no main.rs.

Então perceba que, assim como main.rs definia os módulos (arquivos e diretórios que contém funções, objetos, etc), para os demais subdiretórios será o arquivo mod.rs que fará isso. Existe uma hierarquia, cada subdiretório poderá ter o seu mod.rs.

estrutura de diretórios e arquivos

No arquivo mod.rs também podemos definir funções, objetos, etc.

Acessando módulo em um nível anterior

Agora vamos supor que no arquivo user.rs queremos acessar o objeto Config. Vamos tentar inserir use config::get_config; e criar um método default que retorna o usuário a partir da função get_config:


    use config::get_config;

    pub struct User {
        pub user_name: String,
    }

    impl User {
        pub fn default() ->  User {
            User {
                user_name : get_config().user_name,
            }
        }
    }

Mas ao compilarmos temos o seguinte erro:


    error[E0432]: unresolved import `config`
    --> src/domain/user.rs:1:5
    |
  1 | use config::get_config;
    |     ^^^^^^ help: a similar path exists: `crate::config`
    |
    = note: `use` statements changed in Rust 2018; read more at <https://doc.rust-lang.org/edition-guide/rust-2018/module-system/path-clarity.html>

    error: aborting due to previous error

Vimos que podemos acessar outras funções e objetos declarados em outros fontes, através do use, acessando os módulos delimitados por ::. Porém essa via dos módulos que utilizamos até agora é relativa ao fonte atual. Se fazemos use config no user.rs o compilador tentará acessar esse módulo no diretório do user.rs. Observe que no erro acima o compilador já nos deu uma dica do que precisamos: crate::config. O prefixo crate significa a via "raiz" do nosso projeto. Se alterarmos a linha para use crate::config::get_config; então agora compila corretamente:


    use crate::config::get_config;

    pub struct User {
        pub user_name: String,
    }

    impl User {
        pub fn default() ->  User {
            User {
                user_name : get_config().user_name,
            }
        }
    }

Conclusão

Aqui vimos o funcionamento básico para criar módulos em Rust, distintos entre fontes no mesmo nível e em subdiretórios. Também foi mostrado como podemos acessar as funções de um módulo a outro.

Rust ainda permite abstrair a forma como os módulos são expostos, podendo ter os arquivos físicos numa estrutura diferente de como são publicados. Porém o objetivo deste artigo foi justamente mostrar uma forma simples que podemos organizar os fontes, que possivelmente será a maneira que utilizaremos na maioria das vezes.