Integrar Rust a tus proyectos Python
Tags: python, rust
Published: 2019-04-01 06:45:40

Uno de los aspectos que siempre me ha parecido interesante de **[Rust](https://rust-lang.org/)**, además de las ventajas y características por las que desarrolladores y empresas están apostando por este, es lo bien que se integra con otros lenguajes y tecnologías, como con **[Python](https://python.org/)** usando **[PyO3](https://github.com/PyO3/PyO3)** o **[Node.js](https://nodejs.org/en/)** gracias a **[Neon](https://neon-bindings.com/)**.

Empecé a aprender Rust a principios de 2016, en aquella época ya desarrollaba con Python y enseñaba programación en una universidad local. Desde entonces tenía en mi lista de pendientes aprender sobre su integración con otras tecnologías.

Hace una semana estuve trabajando en una nueva interfaz de usuario (UI) y realizando correcciones y mejoras al código de una aplicación que estoy desarrollando con Rust y **[Rocket](https://rocket.rs/)**, cuya primera versión estaba escrita en Python y de la cual hablé en un **[artículo](https://dev.to/mattdark/a-gallery-for-your-revealjs-presentations-293d)** anterior.

Con la idea de retomar un proyecto en el que estuve trabajando a finales de 2017, una aplicación desarrollada con Python y **[Flask](http://flask.pocoo.org/)**, empecé a redactar el plan para hacer la migración a Rust, sin embargo, existen algunas bibliotecas para las que no existe una crate que la reemplace.

La idea era migrar la aplicación a Rust y reescribir todo el código, pero al encontrar este problema, vi la oportunidad de aprender a integrar ambos lenguajes usando PyO3, y asi mantener parte del código en Python y reescribir el resto del código con Rust.

En este breve artículo contaré lo que aprendí el fin de semana sobre como usar PyO3 para integrar Rust con tus proyectos Python.

## PyO3
**[PyO3](https://github.com/PyO3/PyO3)** es un crate que permite la integración entre Python y Rust. Puede usarse para escribir módulos de Python, asi como para ejecutar e interactuar con código de Python desde Rust. PyO3 funciona con la versión Nightly de Rust.

### Uso
De acuerdo al repositorio de GitHub, PyO3 soporta Python 3.5 en adelante. La versión mínima de Rust requerida es la 1.34.0 (Nightly) del 6 de Febrero.

Se puede escribir un módulo nativo de Python con Rust o usar Python desde un binario de Rust.

En algunos sistemas operativos se necesita instalar algunos paquetes adicionales, como en Ubuntu que se instalan los siguientes:

```bash
$ sudo apt install python3-dev python-dev
```

### Configuración

- Instalar Rust Nightly con **rustup** si no se tiene instalado.

```bash
$ rustup install nightly
```

- Crear un nuevo proyecto con **Cargo**.

```bash
$ cargo new hello_python
```

- Asignar Nightly al proyecto creado.

```bash
$ cd hello_python
$ rustup override set nightly
```

### Usando Rust desde Python

PyO3 puede usarse para generar un módulo nativo de Python. El siguiente ejemplo corresponde a una función que dará formato a la suma de dos números como cadena.

Editar el archivo **Cargo.toml**, agregar PyO3 como dependencia e indicarle a **Cargo** que se está creando una biblioteca. 

#### Cargo.toml

```toml
[package]
name = "string-sum"
version = "0.1.0"
edition = "2018"

[lib]
name = "string_sum"
crate-type = ["cdylib"]

[dependencies.pyo3]
version = "0.6.0-alpha.4"
features = ["extension-module"]
```

#### src/lib.rs

Crear el archivo **lib.rs** y colocar el código correspondiente.

```rust
// Not required when using Rust 2018
extern crate pyo3;

use pyo3::prelude::*;
use pyo3::wrap_pyfunction;

#[pyfunction]
/// Formats the sum of two numbers as string
fn sum_as_string(a: usize, b: usize) -> PyResult<String> {
    Ok((a + b).to_string())
}

/// This module is a python module implemented in Rust.
#[pymodule]
fn string_sum(py: Python, m: &PyModule) -> PyResult<()> {
    m.add_wrapped(wrap_pyfunction!(sum_as_string))?;

    Ok(())
}
```

Se importa el crate de PyO3 mediante `extern crate pyo3;`. Esta línea es opcional si se está usando la edición 2018 de Rust.

Se indican las funciones y macros que se usarán.

Se declara la función `sum_as_string` que realizará la suma de dos números y convertirá a cadena el resultado.

Se declara la función `string_sum` que corresponde al módulo de Python y dentro de esta se indica que la función `sum_as_string` podrá ser llamada una vez que el módulo sea importado dentro del script de Python.


En Windows y GNU/Linux se puede compilar normalmente con `cargo build --release`. En MacOS se deben indicar argumentos adicionales. Una opción es compilar con `cargo rustc --release -- -C link-arg=-undefined -C link-arg=dynamic_lookup`, la otra es crear el archivo `.cargo/config` con el siguiente contenido:

```
[target.x86_64-apple-darwin]
rustflags = [
  "-C", "link-arg=-undefined",
  "-C", "link-arg=dynamic_lookup",
]
```

Una vez que termina la compilación, se copia y renombra la biblioteca compartida desde el directorio `target`. En MacOS, renombrar `libstring_sum.dylib` a `string_sum.so`, en Windows `libstring_sum.dll` a `string_sum.pyd` y en GNU/Linux `libstring_sum.so` a `string_sum.so`.

En el directorio donde se ha copiado la biblioteca, se puede ejecutar una shell interactiva de Python e importar el módulo con `import string_sum`.

```bash
>>> import string_sum
>>> a = 15
>>> b = 17
>>> s = string_sum.sum_as_string(a, b)
>>> print(s)
32
```

Para compilar, probar y publicar el crate como módulo de Python, se puede usar [pyo3-pack](https://github.com/PyO3/pyo3-pack) o [setuptools-rust](https://github.com/PyO3/setuptools-rust).

#### sum.py
El módulo también puede usarse desde un script de Python.

```python
import string_sum
a = 15
b = 17
s = string_sum.sum_as_string(a, b)
print(s)
```

```bash
$ python sum.py
32
```

### Usando Python desde Rust
El siguiente ejemplo muestra el valor de `sys.version`.

Editar el archivo **Cargo.toml** y agregar PyO3 como dependencia

#### Cargo.toml

```toml
...

[dependencies]
pyo3 = "0.6.0-alpha.4"
```

#### src/main.rs

Editar el archivo **main.rs** y colocar el código correspondiente que mostrará el valor de `sys.version`.

```rust
// Not required when using Rust 2018
extern crate pyo3;

use pyo3::prelude::*;
use pyo3::types::IntoPyDict;

fn main() -> PyResult<()> {
    let gil = Python::acquire_gil();
    let py = gil.python();
    let sys = py.import("sys")?;
    let version: String = sys.get("version")?.extract()?;
    let locals = [("os", py.import("os")?)].into_py_dict(py);
    let code = "os.getenv('USER') or os.getenv('USERNAME') or 'Unknown'";
    let user: String = py.eval(code, None, Some(&locals))?.extract()?;
    println!("Hello {}, I'm Python {}", user, version);
    Ok(())
}
```

Ejecutar la aplicación con `cargo run`.

#### Exportar SVG a PDF usando CairoSVG
Para el proyecto en el que estoy trabajando, debo exportar una imagen en formato SVG a PDF, usando [CairoSVG](https://cairosvg.org/), que solo está disponible para Python.

El siguiente código muestra como usar CairoSVG desde Rust.

```rust
use pyo3::prelude::*;
use pyo3::types::IntoPyDict;

fn main() -> PyResult<()> {
    let gil = Python::acquire_gil();
    let py = gil.python();

    let svg_file = "img.svg";
    let pdf_file = "doc.pdf";

    let locals = [("cairosvg", py.import("cairosvg")?)].into_py_dict(py);
    let code = "cairosvg.svg2pdf(url='".to_owned()+svg_file+"', write_to='"+pdf_file+"')";
    py.eval(&code, None, Some(&locals))?;

    Ok(())
}
```

Espero esta breve introducción sea de utilidad. Contaré en un próximo artículo cual ha sido mi experiencia usando PyO3 en la aplicación que estoy desarrollando.