WebAssembly con Rust y ReactJs

Josué Acevedo Maldonado
8 min readDec 16, 2021

Primeros pasos.

Durante mucho tiempo, era sabido que JavaScript no era un lenguaje enfocado en el rendimiento, y no existía un consenso entre los principales navegadores para solventar esta deficiencia; hasta que Google comenzó un proyecto, el cual permitiría que los programas escritos en C y C++ se ejecuten en un Sandbox dentro de Chrome, el Cliente Nativo Portátil.

Para construir la aplicación de ejemplo, será necesario que tenga Node v17.2.0+ y Rust V1.57.0+ instalados en la computadora. Alternativamente es posible usar el siguiente contenedor de Docker, el cual tiene ambas herramientas ya instaladas.

Por su parte, Mozilla y Microsoft optaron por un subconjunto de instrucciones de JavaScript de bajo nivel que ejecutara el navegador mediante una API, asm.js (un proyecto paralelo se enfocó en la conversión de código C y C++ a esta versión de JavaScript). En 2015 ambos esfuerzos se unificaron en WebAssembly que partió de la solución propuesta por asm.js.

En los desarrollos web actuales, el JavaScript del navegador traduce esas instrucciones a código máquina. Pero con WebAssembly, el programador hace mucho del trabajo anterior en el proceso, produciendo un programa que está entre los dos estados. Eso libera al navegador de mucho del trabajo duro de crear el código de la máquina, pero también cumple la promesa de la Web: ese software se ejecutará en cualquier dispositivo con un navegador sin importar los detalles de hardware subyacentes.

El código de WebAssembly se ejecuta dentro de una máquina virtual de bajo nivel que imita la funcionalidad de los muchos microprocesadores sobre los que se puede ejecutar. Ya sea a través de la compilación o la interpretación Just-In-Time (JIT), el motor de WebAssembly puede funcionar casi a la velocidad del código compilado para una plataforma nativa.

Compatibilidad de los navegadores con webAssembly.

Mozilla en 2019 anunció un proyecto llamado WASI (WebAssembly System Interface) para estandarizar la forma en que el código de WebAssembly interactúa con los sistemas operativos fuera del contexto de un navegador. Con la combinación del soporte de los navegadores para WebAssembly y WASI, los binarios compilados podrán ejecutarse tanto dentro como fuera de los navegadores, a través de diferentes dispositivos y sistemas operativos, a velocidades casi nativas.

Todo el código de este proyecto se encuentra disponible en GitHub.
https://github.com/neomatrixcode/webassembly-app

El código de WebAssembly puede grenerarse utilizando una amplia gama de lenguajes como C, Go, C#, Kotlin, etc. y ejecutarse en una gran variedad de sistemas operativos y tipos de procesador, en este tutorial utilizaremos Rust.

ReactJs

El primer paso es configurar una aplicación ReactJs, nuestra aplicacion de ejemplo se llamara Gines. Existen herramientas que realizan esta tarea, sin embargo, para personalizar nuestra solución emplearemos Webpack, inicialicemos nuestro package.json ejecutando el siguiente comando:

npm init -y

El comando anterior, nos dará un package.json predeterminado para instalar los paquetes que necesitaremos. Tomando en cuenta los cambios entre versiones de los paquetes de node, considere usar las mismas versiones de este tutorial. Comience instalando React, Babel y Webpack.

npm i react react-domnpm i -D webpack webpack-cli webpack-dev-server html-webpack-pluginnpm i -D babel-core babel-loader @babel/preset-env @babel/preset-react

Cree las carpetas src, public, build y dist. Agrege un elemento h1 de ejemplo en el archivo index.jsx de la carpeta src.

import React from "react";
import ReactDOM from "react-dom";

ReactDOM.render(<h1>Hello, world!</h1>, document.getElementById("root"));

Ahora, vamos a configurar babel creando el archivo .babelrc, el cual crearemos en la raiz de proyecto, y escribiendo lo siguiente.

{
"presets": ["@babel/preset-env", "@babel/preset-react"]
}

en la carpeta public, crearemos el html predeterminado.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Gines</title>
</head>
<body>
<div id="root"></div>
</body>
</html>

y en webpack configuraremos, la ruta del archivo index.jsx, el puerto de ejecucion de la aplicacion web, la carpeta de recursos staticos, y la direccion del archivo index.html en el webpack.config.js, el cual crearemos tambien en la raiz de proyecto.

const HtmlWebpackPlugin = require('html-webpack-plugin');
const path = require("path");
module.exports = {
entry: "./src/index.jsx",
output: {
path: path.resolve(__dirname, "dist"),
filename: "bundle.[hash].js"
},
devServer: {
compress: true,
port: 8080,
hot: true,
static: './dist',
historyApiFallback: true,
open: true
},
module: {
rules: [
{
test: /.(js|jsx)$/,
exclude: /node_modules/,
use: {
loader: "babel-loader"
}
}
]
},
plugins: [
new HtmlWebpackPlugin({
template: __dirname + "/public/index.html",
filename: "index.html"
}),
],
mode: "development",
devtool: 'inline-source-map',
};

Por ultimo, agregaremos al archivo package.json, el nombre del proyecto, la ubicacion del archivo index.js y el comando dev, el cual invocara a webpack para levantar la aplicacion.

{
"name": "Gines",
"version": "1.0.0",
"description": "A skeleton app showing how to use Rust to leverage Wasm in your React app.",
"main": "src/index.jsx",
"scripts": {
"dev": "webpack server"
},
"keywords": [],
"license": "MIT",
"dependencies": {
"react": "^17.0.2",
"react-dom": "^17.0.2",
"wasm-pack": "^0.10.1"
},
"devDependencies": {
"@babel/preset-env": "^7.16.4",
"@babel/preset-react": "^7.16.0",
"@wasm-tool/wasm-pack-plugin": "^1.6.0",
"autoprefixer": "^10.4.0",
"babel-core": "^6.26.3",
"babel-loader": "^8.2.3",
"html-webpack-plugin": "^5.5.0",
"postcss-cli": "^9.0.2",
"tailwindcss": "^2.2.19",
"webpack": "^5.64.2",
"webpack-cli": "^4.9.1",
"webpack-dev-server": "^4.5.0"
}
}

Con todo lo anterior echo de forma local, ejecutaremons el comando

npm run dev

y visualizar la aplicación ReactJs funcionando en http://localhost:8080/.

Rust

Primero cree la aplicación Rust ejecutando el comando.

cargo init --lib .

(El punto final indica que el proyecto se creará en esta misma carpeta)

Esto agregara los archivos Cargo.tom y src/lib.rs al proyecto, es necesario indicarle a Rust que el código generado será para WebAssembly para lo cual agregaremos el target wasm32, se puede agregar con el comando.

rustup target add wasm32-unknown-unknown

O en nuestro caso, lo indicaremos en el archivo rust-toolchain.toml, el cual crearemos en la raíz del proyecto.

[toolchain]
channel = "stable"
targets = ["wasm32-unknown-unknown"]

También es necesario, configurar el paquete como una biblioteca compartida que se pueda vincular con C y C++ (cdylib), para lo cual modificaremos el archivo Cargo.toml.

[package]
name = "Gines"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[build-dependencies]
wasm-bindgen = "0.2"
[target.'cfg(target_arch = "wasm32")'.build-dependencies]
wasm-bindgen = "0.2"

Por último, instalaremos el paquete wasm-bindgen, el cual será de utilidad para que la aplicación Rust pueda traducir el codigó Rust a Wasm, con el siguiente comando.

cargo install -f wasm-bindgen-cli

De esta forma, ya tendremos instalado y configurado lo necesario para crear código WebAssembly.

En el archivo src/lib.rs, agregaremos un par de funciones, uno suma un par de números enteros y el otro calcula el fibonacci de un determinado valor.

use wasm_bindgen::prelude::*;#[wasm_bindgen]
pub fn add_two_ints(a: u32, b: u32) -> u32 {
a + b
}
#[wasm_bindgen]
pub fn fib(n: u32) -> u32 {
if n == 0 || n == 1 {
return n;
}
fib(n - 1) + fib(n - 2)
}

Estas funciones las podremos utilizar posteriormente en la aplicación de ReactJs, gracias a un envoltorio que crearemos con los comandos.

cargo build --target wasm32-unknown-unknownwasm-bindgen target/wasm32-unknown-unknown/debug/Gines.wasm --out-dir build

El cual colocara el código Wasm en la carpeta build.

ReactJs + Wasm

Primero, vamos a automatizar los comandos anteriores, por lo que los agregaremos a la sección de scripts de nuestro package.json.

"scripts": {
"build:wasm": "cargo build --target wasm32-unknown-unknown",
"build:bindgen": "wasm-bindgen target/wasm32-unknown-unknown/debug/Gines.wasm --out-dir build",
"build": "npm run build:wasm && npm run build:bindgen && npx webpack",
"dev": "webpack server"
}

Esto nos permitirá ejecutar npm run build, y empaquetar los archivos de forma ordenada.

Existe un paquete de NPM que será de ayuda para crear la conexión con Wasm, así que lo agregaremos a nuestras dependencias de desarrollo.

npm i -D @wasm-tool/wasm-pack-plugin

Una vez instalado, actualizaremos el archivo webpack.config.js para aprovechar el nuevo paquete.

const HtmlWebpackPlugin = require('html-webpack-plugin');
const WasmPackPlugin = require("@wasm-tool/wasm-pack-plugin");
const path = require("path");
module.exports = {
entry: "./src/index.jsx",
output: {
path: path.resolve(__dirname, "dist"),
filename: "bundle.[hash].js"
},
devServer: {
compress: true,
port: 8080,
hot: true,
static: './dist',
historyApiFallback: true,
open: true
},
module: {
rules: [
{
test: /.(js|jsx)$/,
exclude: /node_modules/,
use: {
loader: "babel-loader"
}
}
]
},
plugins: [
new HtmlWebpackPlugin({
template: __dirname + "/public/index.html",
filename: "index.html"
}),
new WasmPackPlugin({
crateDirectory: path.resolve(__dirname, ".")
}),
],
mode: "development",
devtool: 'inline-source-map',
experiments: {
asyncWebAssembly: true,
},
};

Una vez hecho esto, ejecutaremos el comando

npm run build

En este punto, ya somos capaces de compilar el código de Rust a Wasm y permitiendo que Webpack lo cargue en nuestra aplicación de ReactJs. Ahora, lo único que falta es utilizarlo en el componente de ReactJs.

En el archivo src/index.jsx colocaremos el siguiente código, el cual realizara la importación y ejecución del código Wasm.

import React, { useState } from "react";
import ReactDOM from "react-dom";
const wasm = import("../build/Gines");wasm.then(m => {
const App = () => {
const [sum, setSum] = useState(0);
const [fib, setFib] = useState(0);
const handleSum = () => {
const sumResult = m.add_two_ints(10, 20);
setSum(sumResult);
}
const handleFib = () => {
const fibResult = m.fib(10);
setFib(fibResult);
}
return (
<>
<div>
<button onClick={handleSum}>Suma!</button>
<div>Sum Results: {sum}</div>
</div>
<div>
<button onClick={handleFib}>Fibonacci de 10!</button>
<div>Fib Results: {fib}</div>
</div>
</>
);
};
ReactDOM.render(<App />, document.getElementById("root"));
});

Pasamos a ejecutar los comandos.

npm run buildnpm run dev

Al dirigirnos en nuestro navegador, a la url http://localhost:8080/, podremos ver los dos botones que añadimos a nuestra aplicación, los cuales, al presionarlos ejecutaran nuestro código Wasm compilado desde Rust y nos mostrará el resultado en ReactJs.

Josue Acevedo Maldonado is a software engineer, currently working as a consultant.

Connect in LinkedIn.

Thank you for being part of the community!
You can find related content on the channel YouTube, Twitter, Twitch, Spotify, etc, besides the book Ensamblador X86.

Finally, if you have enjoyed this article and feel that you have learned something valuable, please share so others can learn from it as well.

Thanks for reading!

--

--

Josué Acevedo Maldonado

Amante de la tecnologia y con pasion en resolver problemas interesantes, consultor, y creador del canal de youtube NEOMATRIX. https://linktr.ee/neomatrix