Rust et Python, ensemble écrivons une librairie pour générer un Geohash

Le but de cet article est d’expliquer comment créer une librairie Python à partir d’une implémentation écrite en Rust. Nous allons implémenter un système de codage et décodage de Geohash. Geohash est une représentation en chaîne de caractère en base32 d’un point sur la terre. En fonction de la longueur de la chaîne de caractère, nous améliorons la précision. Il devient alors plus simple de partager une position dans une URL ou un message.

Python & Rust

Présentation 

Commençons par Python, qui est un langage interprété devenu très populaire grâce à une syntaxe accessible pour tous et un écosystème très riche et varié. Cependant ce langage a un souci de performance dans certains cas. Il est alors usuel d’utiliser une librairie en C pour accélérer l’exécution du code, notamment avec FFI (Foreign Function Interface). Dans les faits, FFI est moins facile d’accès que PyO3. Cependant l’article ne détaillera pas l’utilisation de FFI.

Ensuite, nous avons Rust, né chez Mozilla qui cherchait un remplaçant à C++, liant sécurité et performance. C’est un langage de plus en plus populaire, on le retrouve dans de nombreuses entreprises comme AWS (https://github.com/firecracker-microvm/firecracker et rocket OS) ou Discord qui l’utilise au quotidien, même Microsoft réfléchit sérieusement à l’intégrer dans son système d’exploitation. Rust s’appuie sur llvm, ce qui permet d’avoir de bonnes performances à l’utilisation.

Enfin, PyO3 qui est un langage proposant de pouvoir utiliser du code Rust avec Python et inversement. C’est un pont entre les 2 langages de programmation et non un simple binding. Cependant, la libraire peut être utilisée soit du Rust vers Python ou inversement et ne sera traitée uniquement de l’écriture d’un binding Rust vers Python.

Les pré-requis : 

  • installation de python3 et de ses dépendances
  • installation de Rust
  • connaître Python, virtualenv et pip
  • avoir des bases en Rust

Dans un premier temps, nous allons créer un projet Rust avec cargo : 

cargo new geohashrs
cd geohashrs

Puis ensuite préparer le fichier cargo.toml : 

[package]
name = "geohashrs"
version = "0.1.4"
authors = ["nabil@eml.cc"]
edition = "2018"
license = "Apache-2.0"
repository = "https://github.com/blackrez/geohashrs"
homepage = "https://github.com/blackrez/geohashrs"
readme = "README.md"


[lib]
name = "geohashrs"
crate-type = ["cdylib", "rlib"]


[package.metadata.maturin]
maintainer = "Nabil Servais"
maintainer-email = "nabil@eml.cc"
requires-python = ">=3.5"
classifier = ["Programming Language :: Python"]


[dependencies]
geohash = "0.9.0"


[dependencies.pyo3]
version = "0.11.1"
features = ["extension-module"]

Il existe une librairie en Rust qui permet de générer du geohash, son utilisation se fait de la façon suivante :

Pour encoder :

fn geohash_decode(hash: &str) -> geohash::Cordinates {
  let (coord, _, _) = decode(&hash).expect("Invalid hash string");
  Ok(coord)
}

let decoded = geohash_decode(geohash_str).expect("Invalid hash string");

assert_eq!
 decoded,   (      geohash::Coordinate {          x: 5.44971227645874,          y: 43.527982234954834,        },         0.02197265625,         0.02197265625,
 ), );

Pour encoder  des coordonnées :

fn geohash_encode(x: f64, y: f64, level: usize) -> <String> {
 let c = Coordinate { x: x, y: y };
 let geohash_string = geohash::encode(c, level).expect("Invalid coordinate");
Ok(geohash_string)
}
let coord = geohash_encode(-120.6623, 35.3003, 5);

let geohash_string = geohash::encode(coord, 5).expect("Invalid coordinate");

assert_eq!(geohash_string, "9q60y");

L’API  propose de retourner une chaîne de caractère pour un encodage et une Structure Cordinate pour un décodage. 

Afin de faciliter l’utilisation de la librairie, l’API retournera une liste Python.  Il faut adapter le code Rust afin de retourner un vector.

let coord = geohash::Coordinate { x: -120.6623, y: 35.3003 };
let vec = vec![coord.x, coord.y];

PyO3 propose de générer un wrapper autour de ces fonctions. En ajoutant les macros dans le code cela permet de créer directement les fonctions en Python.

Pour l’utilisation en Python, cela se présentera de la façon suivante dans un shell Python :

>>> from geohashrs import geohash_encode, geohash_decode
'spezsh02g'
>>> geohash_encode(5.4497, 43.528, level=9)
[5.44971227645874, 43.527982234954834]

Notre API en rust retourne donc une liste Python et une chaîne de caractère. C’est là que PyO3 permet de simplifier l’utilisation de Rust avec Python.

En se basant sur le tableau de la documentation, il suffit de changer le type retourné par les fonctions Rust et notre code est prêt à être compilé.

#[pyfunction]fn geohash_encode(x: f64, y: f64, level: usize) -> PyResult<String> {
 let c = Coordinate { x: x, y: y };
let geohash_string = geohash::encode(c, level).expect("Invalid coordinate");
Ok(geohash_string)
}
#[pyfunction]fn geohash_decode(hash: &str) -> PyResult<Vec<f64>> {
let (c, _, _) = decode(&hash).expect("Invalid hash string");
 let vec = vec![c.x, c.y];
Ok(vec)
}

Le résultat est visible sur github ici

Compilation et publication

Désormais la libraire est prête, il faut la compiler et puis si besoin la publier sur une instance pypi (l’index de référence des librairies Python).

Il est toujours possible d’utiliser cargo pour compiler la librairie. Cependant, il faut encore changer le nom, préparer le paquet, ajouter les métadonnées, uploader le paquet…

Heureusement, Maturin s’occupe de ça. D’ailleurs Maturin est un projet python et Rust.

Maturin s’installe comme une librairie python ou s’utilise via une image Docker.

 

$ pip install maturin

Ou avec docker

$ docker run --rm -v $(pwd):/io konstin2/maturin publish

L’avantage de l’image docker est de fournir le support de multiplateforme Linux, grâce au support des Wheels .

Ne pas hésiter à consulter les autres projets utilisant Maturin pour s’en inspirer : Used by 71 (NDLA: cela m’a été très utile).

Maturin s’occupe aussi de la release sur une instance pypi (publique ou privée) assez simplement. Il suffit de lancer la commande :

$ maturin publish

La page README du projet détaille l’ensemble des options.

Désormais le projet est prêt à être utilisé et est visible https://pypi.org/project/geohashrs/.

$ virtualenv venv
$ source venv/bin/activate
$ pip install geohashrs
Python 3.7.3 (default, Dec 20 2019, 18:57:59)
Type 'copyright', 'credits' or 'license' for more information
>>> from geohashrs import geohash_decode, geohash_encode
>>> geohash_encode(5.4497, 43.528, level=9)
... 'spezsh02g'
>>> geohash_decode('spezsh02g')
... [5.44971227645874, 43.527982234954834]

Pour récapitulatif, dans cet article ont été évoqués :

  • une présentation de rust et python,
  • une explication de PyO3,
  • un exemple d’utilisation avec une librairie Rust avec PyO3,
  • l’utilisation du Maturin pour compiler,
  • la publication sur Pypi de la librairie.

Cet article a été fortement inspiré par l’article (EN) medium.com/@nabil.servais/, normal l’auteur est le même 🙂.

 

Merci d’avoir pris le temps de le lire, n’hésitez pas à nous contacter ou commenter cet article pour échanger sur le sujet ! 

Nabil Servais, DevOps/FinOps chez Think