Cargo
Rust viene con una herramienta por defecto para gestionar dependencias,
crear proyectos y realizar otras tareas comunes, esta herramienta se llama
Cargo.
Esta herramienta ofrece algo similar a lo que sería pip y virtualenv en
python, pero además realiza otras tareas como los test y la generación de
documentación.
Por lo tanto Cargo sirve para:
- Crear nuevos proyectos Rust a partir de templates (new, init)
- Compilar el proyecto actual (run, build, install)
- Gestionar dependencias del proyecto (search, update)
- Publicar el proyecto en crates.io (publish)
- Generar la documentación del proyecto (doc)
- Ejecutar los tests (test, bench)
Es una herramienta genial que engloba casi todas las utilidades para llevar
un proyecto de forma sencilla. Cuando empecé a trastear con Rust es una de
las cosas que más me gustó, porque te simplifica la vida a la hora de usar
el lenguaje y en crates.io cada vez hay más módulos, lo que enriquece a
Rust, ya que puedes encontrar fácilmente bibliotecas para casi todo.
Creando un proyecto
Con Cargo se pueden crear por defecto dos tipos de proyectos, un ejecutable
o una biblioteca. La diferencia básica es el punto de entrada, por defecto
el fichero src/main.rs es el punto de entrada, lo que se ejecuta, para los
proyectos ejecutables, y el fichero src/lib.rs es el punto de entrada para
las bibliotecas.
$ cargo new --bin ejemplo
Created binary (application) `ejemplo` project
Esto nos crea un directorio ejemplo con los ficheros básicos:
$ tree ejemplo
ejemplo/
├── Cargo.toml
└── src
└── main.rs
El comando además de estos ficheros también nos crea, por defecto, un
repositorio git, por lo tanto existen los ficheros ocultos .git y
.gitignore. Este comportamiento se puede modificar con la configuración
--vcs, que puede ser: git, hg o none.
$ ls -la ejemplo/
total 8
drwxr-xr-x. 4 danigm danigm 120 feb 11 08:59 .
drwxr-xr-x. 3 danigm danigm 60 feb 11 08:59 ..
-rw-r--r--. 1 danigm danigm 117 feb 11 08:59 Cargo.toml
drwxr-xr-x. 6 danigm danigm 180 feb 11 08:59 .git
-rw-r--r--. 1 danigm danigm 7 feb 11 08:59 .gitignore
drwxr-xr-x. 2 danigm danigm 60 feb 11 08:59 src
Aparte del vcs, se crean dos ficheros, el fichero Cargo.toml, que es el
fichero de configuración, y el fichero src/main.rs, que es donde va el
código, el punto de entrada que se usará para generar el binario.
$ cat ejemplo/Cargo.toml
[package]
name = "ejemplo"
version = "0.1.0"
authors = ["Daniel García Moreno <danigm@wadobo.com>"]
[dependencies]
Como vemos en el fichero de configuración por defecto van una serie de
variables básicas con el nombre, la versión y los autores, aunque hay
muchas más cosas que se pueden especificar aquí.
También está aquí el listado de dependencias, que por defecto está vacío,
pero que iremos rellenando según vayamos necesitando módulos externos y
Cargo se encargará de descargar esas dependencias y compilarlas por
nosotros.
Primera ejecución
Una vez creado el proyecto, ejecutarlo es tan fácil como:
$ cd ejemplo
$ cargo run
Compiling ejemplo v0.1.0 (file:///tmp/cargo/ejemplo)
Finished debug [unoptimized + debuginfo] target(s) in 0.38 secs
Running `target/debug/ejemplo`
Hello, world!
Esto compila el proyecto y lo ejecuta, si no ha cambiado, simplemente lo
ejecuta, no se pierde el tiempo compilando.
Como se puede ver, lo que ejecuta es lo que hay dentro del fichero
src/main.rs:
fn main() {
println!("Hello, world!");
}
El comando run compila en modo debug, esto quiere decir que no está
optimizado el ejecutable que se genera. Para generar un ejecutable final se
puede usar el comando:
$ cargo build --release
Compiling ejemplo v0.1.0 (file:///tmp/cargo/ejemplo)
Finished release [optimized] target(s) in 0.15 secs
Los binarios se generan en el directorio target, en su correspondiente
carpeta según sean de debug o release, target/debug/ejemplo y
target/release/ejemplo.
Usando dependencias externas
Como hemos visto antes, el listado de dependencias está vacío por defecto.
Vamos a añadir alguna:
# fichero Cargo.toml
...
[dependencies]
time = "*"
regex = "0.2"
Es muy fácil añadir dependencias, que se pueden buscar con el comando
cargo search o directamente en la web crates.io, y además, cargo
soporta una sintaxis rica para especificar números de versión, además
de poder especificar otras fuentes para descargar las dependencias, por si
son privadas o no están en crates.io.
Si ejecutamos ahora cargo build, se descargarán las dependencias y se
compilarán.
$ cargo build
Updating registry `https://github.com/rust-lang/crates.io-index`
Downloading aho-corasick v0.6.2
Compiling libc v0.2.20
Compiling utf8-ranges v1.0.0
Compiling void v1.0.2
Compiling regex-syntax v0.4.0
Compiling unreachable v0.1.1
Compiling memchr v1.0.1
Compiling time v0.1.36
Compiling thread-id v3.0.0
Compiling thread_local v0.3.2
Compiling aho-corasick v0.6.2
Compiling regex v0.2.1
Compiling ejemplo v0.1.0 (file:///tmp/cargo/ejemplo)
Finished debug [unoptimized + debuginfo] target(s) in 11.35 secs
El fichero Cargo.lock contiene la información de todas las dependencias
descargadas y su versión, permitiendo así compilaciones exactamente
iguales.
Hemos hablado de dependencias y crates y hemos visto cómo cargo
gestiona e instala las dependencias de forma sencilla.
Crate es como se llaman a las bibliotecas en Rust. Traducido
literalmente significa Cajón, y se podría decir que un crate es un
proyecto Rust completo, un fichero Cargo.toml que especifica su nombre,
versión, etc y una serie de ficheros en src donde se definen los
diferentes módulos.
Para usar un crate en nuestro proyecto se usa extern crate XXX, así
podemos usar las dependencias que hemos añadido anteriormente, sería algo
similar al import de python o al include de C o C++.
extern crate time;
fn main() {
if let Ok(now) = time::strftime("%c", &time::now()) {
println!("Hello, world! {}", now);
}
}
En el momento en el que declaramos el extern crate time ya es posible
utilizar el módulo time.
Además del uso de extern crate, en Rust existe otra palabra clave para
importar módulos o funciones externas en el ámbito local, algo similar a
lo que en python se hace con from X import Y. En Rust la sintaxis
utilizada es use, por ejemplo en nuestro caso podríamos tener:
extern crate time;
use time::{now, strftime};
fn main() {
if let Ok(n) = strftime("%c", &now()) {
println!("Hello, world! {}", n);
}
}
Hemos usado aquí una sintaxis compacta, pero se puede definir uno por línea
o incluso todo a la vez:
use time::now;
use time::strftime as format_date;
use time::*;
Módulos
Como hemos visto, en Rust las bibliotecas se llaman crates y en estas
bibliotecas puede haber diferentes agrupaciones que se llaman modules, o
módulos en español. Así que tenemos un cajón que tiene uno o más módulos
dentro.
El módulo por defecto se define en el fichero src/lib.rs, donde se pueden
definir directamente funciones, estructuras, etc. o submódulos.
Por defecto en Rust todo es privado, por lo que si queremos que desde fuera
de un módulo se pueda usar algo definido dentro, debemos usar la palabra
clave pub.
Podemos definir un par de funciones en un nuevo fichero src/lib.rs, para
usar después en src/main.rs:
extern crate time;
pub fn fecha() -> String {
let mut fecha = String::from("");
if let Ok(n) = time::strftime("%d/%m/%Y", &time::now()) {
fecha = n.clone();
}
fecha
}
pub fn hora() -> String {
let mut hora = String::from("");
if let Ok(n) = time::strftime("%H:%M", &time::now()) {
hora = n.clone();
}
hora
}
En nuestro proyecto de ejemplo, tenemos el crate, que es el proyecto en
sí, y el módulo por defecto, que es src/lib.rs, donde hemos definido dos
funciones públicas, fecha y hora. Esto lo podemos utilizar en nuestro
ejecutable, exactamente igual que usamos una dependencia externa, con el
uso de extern crate.
extern crate ejemplo;
fn main() {
println!("fecha: {}", ejemplo::fecha());
println!("hora: {}", ejemplo::hora());
}
Submódulos
En el ejemplo anterior hemos visto cómo se define el módulo por defecto de
un crate, que no es más que el fichero src/lib.rs. Pero en cualquier
proyecto que no sea un ejemplo habrá que definir más de un módulo, no puede
estar todo junto en el mismo.
Para definir submódulos en Rust se puede usar mod, que sirve
exactamente para definir submódulos:
// fichero src/lib.rs
pub mod fecha {
extern crate time;
pub fn ahora() -> String {
//...
}
}
pub mod hora {
extern crate time;
pub fn ahora() -> String {
//...
}
}
En este ejemplo se definen dos módulos, fecha y hora. La única
diferencia con el caso anterior es que ahora podemos agrupar en módulos las
diferentes funciones que definamos. También hay que tener en cuenta que en
cada módulo en ámbito es diferente, de ahí la necesidad de hacer el
extern crate time en los dos módulos, aunque estén en el mismo fichero.
El uso en el main.rs sería similar:
extern crate ejemplo;
fn main() {
println!("fecha: {}", ejemplo::fecha::ahora());
println!("hora: {}", ejemplo::hora::ahora());
}
Módulos en ficheros separados
Pero este uso tiene poca utilidad real, normalmente los módulos han de ir
en ficheros separados o incluso en carpetas, creando una jerarquía de
ficheros que simboliza exactamente la jerarquía de los módulos.
En nuestro ejemplo, podemos crear un nuevo fichero con el contenido del
módulo fecha, un fichero llamado src/fecha.rs.
// fecha.rs
extern crate time;
pub fn ahora() -> String {
//...
}
Se puede observar que ya no es necesario el uso de mod, puesto que al
estar en un fichero separado, es implícito.
Y en el fichero módulo por defecto sólo tendremos que declarar este módulo
como público para que sea accesible desde main.rs.
// lib.rs
pub mod fecha;
pub mod hora;
De esta forma ya tenemos nuestros módulos en ficheros diferentes y son
accesibles desde fuera.
Además de ficheros con el nombre del módulo, en Rust se pueden declarar
carpetas, y dentro de esa carpeta debe existir el fichero mod.rs, sería
algo similar a los ficheros init.py en los módulos python.
Podríamos tener una estructura de ficheros tal que así:
ejemplo/
├── Cargo.toml
└── src
├── main.rs
├── lib.rs
├── fecha.rs
└── hora
├── mod.rs
├── local.rs
└── utc.rs
- main.rs es el ejecutable, si estamos hablando de una biblioteca, no
existiría este fichero.
- lib.rs define el módulo principal, este fichero puede no existir si
estamos hablando de un crate ejecutable, sin módulos propios.
- fecha.rs un módulo más del proyecto, que para ser accesible debe
debe estar declarado en el módulo por defecto.
- hora es una carpeta que contiene otro módulo, que al igual que
fecha.rs debe estar definido en lib.rs para ser accesible.
- mod.rs definición del módulo hora.
- local.rs y utc.rs submódulos de hora, que deben estar definidos en
mod.rs para ser accesibles.
Además del uso de módulos desde fuera, por ejemplo en el main.rs o si lo
usamos como dependencia en otro proyecto, pueden existir módulos internos
en el proyecto, que no exportemos como públicos, y que sólo se usen de
manera interna. Por defecto las importaciones son relativas al módulo por
defecto:
use fecha; // -> importación relativa al módulo actual
use ::fecha; // -> importación absoluta, llevaría a src/fecha.rs
use self::fecha; // -> relativo al módulo actual, por ejemplo no funcionaría
// -> si se usara esto dentro de *hora/mod.rs*, aunque sí
// -> en otro módulo al nivel de fecha.rs
use super::fecha; // -> con super se puede acceder al módulo padre
Con estas declaraciones podemos hacer uso de unos módulos en otros, variará
lo que tengamos que usar según nuestra estructura de ficheros.
También se puede cambiar el nombre de un módulo en una declaración use,
para evitar conflictos de nombres, con la palabra reservada as:
extern crate ejemplo;
use ejemplo::fecha::ahora as fecha;
use ejemplo::hora::ahora as hora;
fn main() {
println!("{} - {}", fecha(), hora());
}
La verdad es que el sistema de módulos de Rust es muy parecido al de
python, podríamos decir que self sería el from . import de python y el
super podría ser from .. import.
There are comments.