Rust

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.

Tips esenciales del 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:

  1. Tipos de datos básicos: 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.
  2. Las funciones se definen con 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.
  3. Para el bloque de la función o el flujo de control se usan llaves {}. Las sentencias deben terminar siempre con punto y coma ;.
  4. El flujo de control es semejante a lenguajes como 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).
  5. El constructor de una clase lleva el nombre 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.
  6. Actualmente, es posible usar operador de nulo seguro (null-safe): ?..

Ejemplo esencial

fn main() {
  println!("Hi there!");
}

Revisa la instalación del lenguaje y el proyecto de inicio rápido avanzando el documento

Una breve comparación con Javascript

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.

Tipos de datos básicos

Declaración de variables

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.

Definición de funciones

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, anteponiendo pub (es decir, declarándola pub fn)

Condicionamiento if / else (if)

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.

El Ciclo For

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 existe len() para determinar la longitud y usarlo en rangos.

El Ciclo While

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!"); }

Evaluador Match

let i = 1;
match i {
    1 => println!("one"),
    2 => println!("two"),
    _ => println!("aha"),
}

Excepciones

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);
}

Clases

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 en C pero en Rust 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 de trait (que se asemeja a las interfaces de Java) y usar tan solo struct para la estructura con impl para la lógica funcional (indicando el mismo nombre asignado a struct), sin embargo, trait proporciona un mecanismo para implementar los métodos y su herencia, incluso se pueden usar varios impl por cada trait asociado.

Instalación

Windows 8+

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.

macOS & Linux

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

Proyecto de inicio rápido

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 denominado Cargo.toml que es un archivo de configuración de paquetes o dependencias externas (semejante a package.json usado en Node.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.

Siguiente ejemplo

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

Servidor Web Simple

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();
}

Acceso a base datos con Rust

DuckDB

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"]

RocksDB

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

Rust & WebAssembly

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 de wasm y el flujo de trabajo con Rust.

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 en GibHub.

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

Rust & Node.js (FFI)

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

Rust & Lua

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"

Rust & QuickJS

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 o eval_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)