Rust es un lenguaje de programación de Mozilla que llega hasta un nivel bajo, es decir, puede estar embebido en microcontroladores o ser usado para programación de sistemas, aspectos que normalmente han estado domininados por C y C++, con una velocidad de respuesta semejante a estos lenguajes, de allí su faceta para construir una CLI (Interfaz de Línea de Comandos), incluso juegos. Además de estar disponible en varios sistemas (para Windows, macOS, Linux), facilita la programación para la web con WebAssembly, interactuando con Javascript usando memoria compartida. También puede ser interesante usar Rust como complemento con otros lenguajes (interoperabilidad), logrando así un tiempo de respuesta interesante, lo que hace pensar que aplicaciones móviles hibridas (con componentes web o “WebView”) que usen WebAssembly con Rust (siendo uno de sus enfoques) estarían a otro nivel y no tendrían que envidiar a las aplicaciones nativas si se logra una diferencia insignificante.
Esta es una referencia ágil para quién tenga nociones de programación o codifique con algún otro lenguaje, también a modo de repaso y uso frecuente. Siendo así, tener esta información como memoria te resultará simple de acceder a los fundamentos, incluso como prueba de concepto sobre el lenguaje.
Para quien tenga habilidades, experticia en programación y/o requiera agilidad en conceptos técnicos, pueden resumirse los siguientes tips esenciales del lenguaje:
i16
, u16
, f32
, bool
, str
, struct
. Las variables se definen con let mut
o let
(si no cambia o no muta) y se puede asignar el valor directamente sin necesidad de especificar el tipo de datos. Si requiere especificarse el tipo de datos se usa :
y luego el tipo.fn
, luego los parámetros van entre paréntesis (...)
continuando con el nombre del parámetro, el caracter :
que separa el tipo de datos posteriormente (y separando los parámetros con coma ,
). El tipo de datos a retornar va también después de ->
. Además, se usa return
para retornar un valor, el cual puede omitir si se trata de la última línea (expresando simplemente la variable). La función principal de un programa se denomina main
.{}
. Las sentencias deben terminar siempre con punto y coma ;
.C
, Java
, Javascript
, Kotlin
, es decir que se cuenta con una anatomía cercana para el uso de if
, for
, while
(distinto en el caso de try
, dado que tiene otro modo de controlar errores).new
y la clase se puede definer con la palabra reservada struct
y el nombre que debe iniciar en mayúscula. También existe un bloque de implementación de funciones con el nombre de la clase, anteponiendo la palabra reservada impl
. En realidad el lenguaje presenta una manera de homologar clases siendo en principio funcional.?.
.fn main() {
println!("Hi there!");
}
Revisa la instalación del lenguaje y el proyecto de inicio rápido avanzando el documento
Si tienes conocimiento en Javascript
u otro lenguaje, puede ser útil un sencillo escenario de comparación que nos da una referencia inmediata de Rust
.
Concepto | Javascript | Rust |
---|---|---|
Variable | let variable = ‘Ana’ | let mut variable = “Ana”; |
Función | function pow(x) { } | fn pow(x:i32) -> i32 { } |
Condición | if (i == 1) { } | if i == 1 { } |
Ciclo For | for (let i = 0; i < 10; i++) { } | for i in 0…9 { } |
En
Rust
no existen excepciones propiamente pero se pueden gestionar los errores de otro modo.
Se pueden declarar variables (mutables) con las palabra reservada let mut
con el tipo de datos posterior al nombre (serando con :
), o simplemente asignando el valor. Si se usa sólo let
el lenguaje asumirá que no es mutable, por ejemplo, con objetos o vectores (que reservan la misma posición de memoria). Estas variables inmutables pueden asemejarse a constantes aunque se usa la palabra reservada const
para constantes con una orientación global y cuyo nombre debe estar en mayúsculas.
Ejemplo:
let a = [1, 2, 3];
let mut name = "Ana";
let mut n = 0;
const N: i32 = 5;
static X: i32 = 0;
static se usa para variables globales fijadas en memoria, quizás de menor uso que
const
.
fn plus1(a: i8, b: i8) -> i8 { a + b }
fn plus2(a: i8, b: i8) -> i8 {
return a + b;
}
Puede omitirse la palabra reservada
return
si se trata de la última línea (o única) de la función.
Los parámetros de funciones que corresponden a cadenas de caracteres deben recibirse anteponiendo el caracter&
(ampersand) indicando así su referencia en la memoria (esto mismo ocurrirá con objetos, por ejemplo,&self
o&mut self
).
Se puede indicar que una función es pública, por ejemplo para librerías, anteponiendopub
(es decir, declarándolapub fn
)
if i = 1 {
println!("one");
}
else if i = 2 {
println!("two");
}
else {
println!("aha");
};
También puede usarse
if
en asignaciones para determinar si un valor se asigna de un modo o de otro.
for i in 0..5 {
println!(i);
}
let a = [1, 2, 3];
for i in a.iter() {
println!(i);
}
En este último caso se usa el método
iter()
que viene incorporado para ser usado con arreglos. También existelen()
para determinar la longitud y usarlo en rangos.
let mut i = 0;
while i < 10 {
i += 1;
}
let mut i = 0;
loop {
i++;
if i < 10 { break; }
}
También existe un ciclo infinito con la palabra reservada
loop
. Por ejemplo:loop { println!("Again!"); }
let i = 1;
match i {
1 => println!("one"),
2 => println!("two"),
_ => println!("aha"),
}
En realidad no aplica el concepto de excepciones de otros lenguajes. Básicamente, si una función puede producir un error se devuelve Result
con dos tipos de datos, uno para el tipo natural que retorna la función y otro String
para el error. Si Result
tiene respuesta exitosa se usa Ok
, sino Err
. Veamos:
fn tryError() -> Result<u8, String> {
return Err("Oops!".to_string());
Ok(240);
}
El lenguaje Rust
usa esencialmente la programación funcional (funciones) pero es posible aplicar una programación orientada a objetos (clases) combinando algunos conceptos y palabras reservadas. Para comprender esto, podrían plantearse cuestiones como ¿sobre qué cosa o características se trata algo? (struct
), ¿Qué podría hacerse con eso? (trait
), ¿Cómo lo haces? (impl
). Se usa struct
para definir la estructura de datos (lo que se conoce como propiedades). Se usa trait
para declarar o indicar las funciones que se deben implementar en relación a la estructura (lo que se conoce como interfaces en otros lenguajes). Y finalmente, se usa impl
para incorporar las funciones con su lógica (los métodos de la clase) reportando el mismo nombre de la estructura (struct
), y se usa como referencia de la misma clase la palabra reservada self
(también &self
o &mut self
en parámetros de funciones). Veamos un ejemplo:
struct Shape {
x: f64,
y: f64,
}
trait Area {
fn area(&self) -> f64;
}
impl Area for Shape {
fn area(&self) -> f64 {
self.x * self.y;
}
}
Se usa el operador
&
para recibir una referencia a un objeto. Semejante a las referencias de punteros enC
pero enRust
es básicamente lo único que se hace al respecto (no se definen punteros).
Nótese que el lenguaje establece el concepto de clase basándose en el nombre según las partes correspondientes (dados los criterios separados). Se puede omitir el uso detrait
(que se asemeja a las interfaces deJava
) y usar tan solostruct
para la estructura conimpl
para la lógica funcional (indicando el mismo nombre asignado astruct
), sin embargo,trait
proporciona un mecanismo para implementar los métodos y su herencia, incluso se pueden usar variosimpl
por cadatrait
asociado.
Para instalar Rust
en Windows se requiere descargar y ejecutar rustup-init.exe
. Si se cuenta con Subsistema Linux para Windows (WSL) o se tiene instalado Git Bash
(herramienta incorporada con la instalación de Git) se puede ejecutar lo siguiente desde la línea de comandos:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
Si no es posible instalarlo con el escenario anterior, una alternativa es intentar usar un gestor de paquetes como Scoop o Chocolatey (si lo tienes instalado, o instalándolo). De ese modo, se abre PowerShell
con privilegios de administrador y se inicia la instalación del paquete en Scoop con:
scoop install rust
Si se usa Chocolatey el comando sería:
choco install rustup.install
rustc -V
nos puede mostrar la versión instalada
Es probable que se instale en la carpeta %USERPROFILE%//.cargo/bin
(dónde %USERPROFILE%
es la carpeta de inicio del usuario que lo instala), por lo tanto, debe verificarse que se encuentre incorporada en el PATH
de las variables de entorno del sistema.
Dado que Rust
se configura o instala con la herramienta rustup
, para descargarla y lanzarla sólo tienes que ejecutar la siguiente instrucción:
curl https://sh.rustup.rs -sSf | sh
Puedes usar la instalación por defecto cuando pregunte la opción (1), por lo que simplemente tecleas otro
enter
.
Al finalizar solicita que ejecutes:source $HOME/.cargo/env
.
rustc -V
nos puede mostrar la versión instalada
Para iniciar un proyecto se usa la herramienta llamada cargo
(que es un gestor de paquetes), teniendo presente que debemos ubicarnos en una carpeta dónde se piensa crear la subcarpeta del proyecto, puesto que cargo
inicializará la subcarpeta. Para inicializar ejecutamos:
cargo init project
project
correspondería al nombre asignado a la carpeta del proyecto.
Al ejecutar el comando también se crea un archivo denominadoCargo.toml
que es un archivo de configuración de paquetes o dependencias externas (semejante apackage.json
usado enNode.js
), la extensión.toml
tiene similitudes a un archivo.ini
por lo que podría lucir familiar.
Para tener clara la estructura del proyecto veamos el siguiente esquema:
Teniendo ahora nuestra subcarpeta creada con el comando usado, nos ubicamos dentro del subdirectorio src
dónde encontramos el archivo main.rs
con el siguiente contenido:
fn main() {
println!("Hi there!");
}
Estando ubicados dentro de la carpeta inicializada para el proyecto (cd first
), lo siguiente será compilarlo con el siguiente comando:
cargo build
Esto nos debe haber generado un ejecutable que bien podemos buscarlo e invocarlo, sin embargo, al encontrarse en modo de desarrollo se recomienda ejecutar el proyecto con el siguiente comando:
cargo run
El comando cargo run
también evalúa si se requiere compilar de nuevo (build
). Ahora usaremos como dependencia clap
que nos ayudará a validar los argumentos en una CLI
. Para esto buscamos el archivo Cargo.toml
e incorporamos en la sección dependencies
lo siguiente:
clap = "~3.1.5"
La descarga del paquete se hace ejecutando de nuevo cargo build
. Ahora reemplazamos todo el contenido de nuestro programa main.rs
con el siguiente:
use clap::Parser;
#[derive(Parser, Debug)]
#[clap(author, version, about, long_about = None)]
struct Args {
#[clap(short, long)]
name: String
}
fn main() {
let args = Args::parse();
println!("Hi {}!", args.name);
}
Y ejecutamos nuestra actualización simplemente usando cargo run
, sin embargo, para efectos de la invocación con parámetros este comando varía colocándose --
(adicional) como en el siguiente ejemplo:
cargo run -- --name John
Es importante recordar que en este caso, cargo run --
lo usamos en tiempo de desarrollo puesto que con el ejecutable final se invocaría project John
como un verdadero CLI
. Podemos probar la ayuda que nos ha generado ejecutando cargo run -- -h
o consultar la versión asignada ejecutando cargo run -- -V
.
Basados en una CLI
, como en el ejercicio anterior, esta vez recibiremos como parametro un archivo para leerlo y mostrar su contenido. Veamos el código del ejemplo:
use std::fs::File;
use std::io::{self, BufRead};
use std::path::Path;
use clap::Parser;
#[derive(Parser, Debug)]
#[clap(author, version, about, long_about = None)]
struct Args {
#[clap(short, long)]
file: String
}
fn get_lines<P>(filename: P) -> io::Result<io::Lines<io::BufReader<File>>>
where P: AsRef<Path>, {
let file = File::open(filename)?;
Ok(io::BufReader::new(file).lines())
}
fn main() {
let args = Args::parse();
if Path::new(&args.file).exists() {
println!("File: {}", args.file);
if let Ok(lines) = get_lines(args.file) {
for line in lines {
if let Ok(text) = line {
println!("{}", text);
}
}
}
} else {
println!("Not found: {}", args.file);
}
}
Básicamente, la función get_lines
abre el archivo usando File.open(...)
y obtiene un “buffer” de líneas. En la función main
se recibe el parámetro file
y se invoca get_lines
para procesar cada línea y mostrarla en pantalla. Para correr el programa ejecutamos por ejemplo:
cargo run -- --file myfile.txt
A continuación veremos un ejemplo de un servidor web usando el lenguaje Rust con la librería axum
, la cual a su vez usa hyper
, y esta a tokio
, es decir, se basa en otras librerías altamente eficientes (de bajo nivel).
use axum::{ Router, routing::get };
async fn hi() -> &'static str {
"Hi there!"
}
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/", get(hi));
axum::Server::bind(&"0.0.0.0:3000".parse().unwrap())
.serve(app.into_make_service())
.await
.unwrap();
}
Para dar un ejemplo inicial sobre una base datos SQL (Structured Query Language) gestionada a través de Rust, podemos considerar DuckDB con la siguiente lógica:
use duckdb::{params, Connection, Result};
#[derive(Debug)]
struct Person {
id: i32,
name: String,
age: i8,
}
fn set_data_sql() -> Result<()> {
let conn = Connection::open_in_memory()?;
conn.execute_batch(
r"CREATE TABLE person(
id integer primary key,
name text not null,
age integer
);")?;
let person = Person {
id: 1,
name: "Andrey".to_string(),
age: 25,
};
conn.execute(
"INSERT INTO person (id,name,age) VALUES (?,?,?)",
params![person.id, person.name, person.age],
)?;
let mut stmt = conn.prepare("SELECT id, name, age FROM person")?;
let row_iter = stmt.query_map([], |row| {
Ok(Person {
id: row.get(0)?,
name: row.get(1)?,
age: row.get(2)?,
})
})?;
for person in row_iter {
println!("Found person {:?}", person.unwrap());
}
Ok(())
}
fn main() {
set_data_sql();
}
Básicamente, la función set_data
utiliza una conexón en memoria (Connection::open_in_memory()?
), puede ejecutar sentencias en bloque (execute_batch(...)
), sentencias simples de actualización (execute(...)
), así como sentencias de consulta (prepare
) que pueden mapearse (query_map()
) y luego iterarse (for ... in
). Para que funcione el programa anterior, se requiere configurar el archivo Cargo.toml
incluyendo la dependencia siguiente:
[dependencies.duckdb]
version = "0.1"
features = ["bundled"]
Si pensamos en una base de datos de clave/valor (NoSQL) como RocksDB podríamos plantear el siguiente ejemplo:
fn set_data_rocks() {
use rocksdb::{DB, Options};
let path = "/tmp/testdb";
{
let db = DB::open_default(path).unwrap();
db.put(b"1", b"Andrey").unwrap();
match db.get(b"1") {
Ok(Some(value)) => println!("retrieved value {}", String::from_utf8(value).unwrap()),
Ok(None) => println!("key not found"),
Err(e) => println!("problem encountered: {}", e),
}
}
// You can use: DB::destroy(&Options::default(), path);
}
fn main() {
set_data_rocks();
}
Para que funcione el programa anterior, se requiere configurar el archivo Cargo.toml
incluyendo la dependencia siguiente:
[dependencies]
rocksdb = 0.18.0
También logras esto ejecutando:
cargo add rocksdb
La idea de una web binaria o ensamblada ha tenido acogida con la tecnología WebAssembly
que proporciona un formato reconocido por los navegadores web modernos denominado wasm
. Rust
es quizás el lenguaje más interesante actualmente para trabajar con wasm
y lograr un puente fluido con el lenguaje Javascript
del navegador, comprendiendo que es posible generar wasm
con otros lenguajes.
Para establecer un ambiente actualizado en cuanto a Rust
, procedemos a ejecutar lo siguiente:
rustup update
cargo install wasm-pack
La línea que instala
wasm-pack
se refiere a una utilidad que facilita la generarión dewasm
y el flujo de trabajo conRust
.
Iniciaremos un proyecto nuevo (por ejemplo rustwasm
), recordando estar ubicados en una carpeta contenedora para los proyectos, pero esta vez usaremos en lugar de cargo init
el comando cargo new
indicando que se trata de una librería. Ejecutamos la siguiente instrucción:
cargo new --lib rustwasm
También es posible utilizar
cargo install --git https://github.com/repo
para crear un proyecto basado en un repositorio enGibHub
.
Nuestro proyecto tendrá la siguiente estructura:
Encontramos el programa principal como src/lib.rs
. Reemplazamos todo su contenido por el siguiente:
use wasm_bindgen::prelude::*;
pub fn greeting(name: &str) -> String {
format!("Hi from Rust with: {}!", name)
}
Luego, en nuestro archivo Cargo.toml
se requiere incorporar las siguientes líneas:
[package]
name = "rustwasm"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
wasm-bindgen = "0.2"
Estando ubicados en la subcarpeta de nuestro proyecto, en lugar de usar cargo build
ejecutamos la siguiente instrucción:
wasm-pack build --target web
Esto nos ha generado una carpeta pkg
con nuestros archivos de distribución wasm
y un package.json
para usarlo como módulo npm
. Además nos ahorra ejecutar los pasos directos para usar wasm-bindgen
, es decir, como alternativa se ejecutaría:
Ahora probaremos nuestro archivo index.html
con un contenido como el siguiente:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>rustwasm...</title>
</head>
<body>
<main></main>
<script type="module">
import init, { greeting } from "./pkg/rustwasm.js";
init().then(() => {
document.getElementsByTagName("MAIN")[0].innerHTML = greeting("WebAssembly");
});
</script>
</body>
</html>
Para ver el resultado, este archivo se puede exponer con un servidor web, por ejemplo usando Python:
python3 -m http.server
Para interactuar con Node.js
creamos primero nuestro proyecto Rust
, por ejemplo.
cargo new --lib rustffi
Modificamos el archivo Cargo.toml
para incluir lo siguiente:
[lib]
crate-type = ["cdylib"]
Además, nuestro archivo src\lib.rs
debe quedar con el siguiente código:
#[no_mangle]
pub extern fn hi() -> *const u8 {
r#"
Hi there, this is Rust!
"#.as_ptr()
}
Para que nuestra librería quede liberada y sea usada por Node.js
, ejecutamos:
cargo build --release
Ahora en un proyecto Node.js
(iniciado con npm init
o editando el archivo package.json
) instalamos el módulo ffi
, así:
npm install ffi
Nuestro archivo principal debe contener el siguiente código:
const ffi = require('ffi');
const libPath = './rustffi/target/release/rustffi';
const libWeb = ffi.Library(libPath, {
'hi': [ 'string', [] ]
});
const hi = libWeb.hi();
console.log(hi);
Y lanzamos este programa, por ejemplo, npm start
Primero veamos como usar Rust desde Lua, con el ejemplo tomado de la documentación oficial:
ffi = require("ffi")
ffi.cdef[[
int is_prime(unsigned int n);
]]
rust_lib = ffi.load("./libprime.so")
n = 1234567
if rust_lib.is_prime(n) then
print(n, "is prime")
else
print(n, "is not prime")
end
En el sentido contrario, tendríamos el siguiente ejemplo:
use rlua::Lua;
fn main() {
let lua = Lua::new();
_ = lua.context(|lua_context| {
lua_context.load(r#"
print("Hi there, Lua says!")
"#).exec()
});
}
Modificamos el archivo Cargo.toml
para incluir la depedencia siguiente:
[dependencies]
rlua = "0.19.1"
Si pensamos en lenguajes embebidos, QuickJS nos brinda un mecanismo de ejecución de código Javascript mediante Rust. Veamos un ejercicio sencillo iniciando con los siguientes comandos:
cargo init rustjs
cd rustjs
A continuación agremos en el archivo Cargo.toml
la siguiente dependencia:
[dependencies]
quick-js = "0.4.1"
Y nuestro archivo src/main.rs
tendrá el siguiente contenido:
use quick_js::{Context, JsValue};
fn main() {
let context = Context::new().unwrap();
let value = context.eval_as::<String>("let x = 'QuickJS'; x").unwrap();
let value2 = context.eval_as::<JsValue>("0 + 2").unwrap();
println!("Hello world with: {:?}", value);
println!("Sum: {:?}", value2);
}
eval
oeval_as
procesan el código Javascript para un contexto.
Y para compilar y revisar nuestro programa ejecutamos:
cargo build
cargo run
Para garantizar la ejecución, debe usarse Linux o un sistema compatible, por ejemplo macOS, o en caso de tener una máquina con Windows se usaría WSL (Windows Subsystem for Linux)