Introduction
Cet article a pour objectif premier de comprendre comment les conversions fonctionnent, et non d’expliciter comment PyO3 marche en détail.
Utiliser un autre langage dans un code Python est commun. Le plus naturel est le C, car c’est le langage dans lequel a été développé Python, comme les bibliothèques numpy ou panda. Malgré tout, le Rust, par la sécurité que le langage procure, devient petit à petit une alternative au C pour écrire des bibliothèques Python comme polar, qui est une alternative à panda.
PyO3
PyO3 est une bibliothèque Rust permettant d’appeler du code Rust directement dans Python. Cela n’est pas évidant car le Rust est un langage compilé typé, tandis que le Python est générique et interprété. L’objectif de cet article est de comprendre comment les types sont gérés lors de cette conversion. Dans cet article, on se basera sur la release 0.25.1 de PyO3.
Rust : analyse de PyO3
Nous allons nous confronter à un exemple, mais cela n’est pas forcément une
généralité.
En effet, les types peuvent se gérer différemment et il faut le garder en tête.
Commençons par essayer de trouver la conversion générique, puis
focalisons nous sur l’exemple du type float
.
Comment PyO3 devine les types Python ?
Comme Python est un langage générique, on ne sait pas quels arguments vont être donnés. Cependant, Rust est typé, il doit donc vérifier que les arguments donnés soient bons. Pour commencer, il faut savoir que tous les objets Python sont représentés par un PyObject. Pour cela, il faut regarder du côté des fichiers suivants :
pyO3-ffi/src/object.rs
: contient la fonction Py_TYPE et la méthode Py_IS_TYPE.
|
|
pyO3-ffi/src/cPython/object.rs
: contient PyTypeObject. On remarque le PyObject vient de l’interface C. Ces 3 éléments permettent de deviner le type d’un object Python.
Voici un graphique, non exhaustif, qui permet de voir ce qui se passe.
En résumé, Py_TYPE utilise l’interface C pour son argument qui est le PyObject, Py_TYPE produit un PyTypeObject, et Py_IS_TYPE utilise un PyTypeObject et un PyObject. Grâce à ces éléments, PyO3 peut faire la vérification des types. Il y a bien sûr plus de subtilités au niveau des implémentations techniques, mais ce schéma donne déjà une bonne idée de la procédure.
Une autre méthode est de prendre notre objet PyObject et de le convertir en un autre object Python comme PyFloat.
La conversion Python-Rust
Pour commencer, on va se focaliser sur la conversion de Python vers Rust.
Regardons le fichier src/conversion.rs
:
src/conversion.rs
: permet de prendre un PyAny et retourne le bon type grâce à sa méthode extract. Le type PyAny se trouve danspyO3/src/types/any.rs
. Finalement, il faut chercher danspyO3-macros-backend/src/frompyobject.rs
etsrc/conversion.rs
.src/type_object.rs
: contient des méthodes comme is_type_of, is_exact_type_of ou type_object_raw.
Dans src/conversion.rs
, on trouve IntoPyObject et FromPyObject. Cependant,
la méthode de conversion peut différer en fonction du type.
Ainsi, il est préférable de prendre un exemple.
La conversion Python-Rust et les flottants
Dans src/types/float.rs
, on a
|
|
Dans le code d’origine, il y avait l’application de la méthode downcast_exact qui est là pour un problème de performance que nous n’allons pas développer. Pour en savoir plus, vous pouvez regarder la pull request qui l’implémente.
Il faut savoir que PyFloat_AsDouble est dans l’interface C. Cela permet d’expliquer pourquoi une méthode globale est compliquée à trouver pour la conversion. En effet, la conversion se fait au cas par cas car il utilise des fonctions de l’interface C qui ne sont pas les mêmes pour tous les types.
|
|
PyFloat_FromDouble fait partie de l’API C de Python. Donc, de même, la conversion se fait grâce à l’interface C.
Résumé
Les fichers et PyO3
Les fichiers ne sont pas utilisables avec PyO3, expliquons pourquoi.
Pour commencer, l’interface entre ce qui permet de convertir les fichiers Python
en file descriptor est bien implémentée dans pyO3-ffi/src/fileobject.rs
.
Ainsi, pyO3 a déjà la fonction de conversion de Python vers C implémentée, et on peut donc bien traiter les fichiers. Cependant, le type fichier n’est pas créé et implémenté dans PyO3 et nous allons en chercher la raison.
Dans la documentation, la seule fonction qui permet de convertir un fichier Python en fichier C est la fonction PyObject_AsFileDescriptor qui retourne un file descriptor. Une des raisons qui pourrait expliquer cette absence est le fait que Windows n’utilise pas de file descriptors mais une structure HANDLE pour ses fichiers. Cependant, il est toujours possible de convertir un file descriptor en HANDLE. Il faudrait plutôt regarder dans la logique d’implémentation :
- Qui doit fermer le fichier ?
- Si Python décide de fermer le fichier, comment doit-on gérer l’erreur ?
- Comment être sûr du type ? En effet, un file descriptor peut définir une pipe, un terminal ou un socket.
- Unix et Windows ont des comportements bien différents. Par exemple, il n’y a aucune API officielle pour connaître le mode d’ouverture d’un fichier dans Windows.
Les développeurs avaient sûrement des fonctionnalités plus importantes à implémenter ou n’ont pas eu le temps de répondre à ces questions pour implémenter ces fichiers. D’autant plus que l’objet PyFile n’a pas de structure. Il n’existe que des PyObject dans l’API C de Python.
Dans ce cas, il ne nous reste plus qu’à l’implémenter (seulement pour les systèmes Unix).
Ajouter le type file
pour PyO3
Il faudra d’abord rajouter nix = "0.26"
dans les dépendances de PyO3.
Il faut créer le fichier src/types/file.rs
où on mettra notre PyFile.
|
|
Python et Rust ferment tous les deux le fichier qu’ils ont par défaut.
Ainsi, dup
permet de créer un nouveau file descriptor indépendant de l’original
afin que les deux langages puissent fermer leur fichier sans problème.
Ensuite, il faut ajouter la méthode pour bind Python vers Rust.
|
|
Ensuite, il faut ajouter la méthode pour bind le Rust vers le Python.
|
|
Il ne reste plus qu’à ajouter pub(crate) mod file;
dans src/types/mod.rs
afin de rendre
accessible notre type.
Résumé
Implémenter l’environnement
Créer un nouveau projet avec la commande cargo init
.
Dans Cargo.toml
, il faudra mettre :
|
|
Il faut maintenant créer un simple programme test dans src/lib.rs
|
|
Il faut maintenant compiler le programme :
|
|
Ensuite, nous devons écrire du texte dans le fichier something.txt avec la
commande echo foo > something.txt
et on pourra alors lancer le programme
Python suivant.
|
|
Une comparaison avec le C
Comme Rust utilise les fonctions C pour communiquer avec Python, pourquoi utiliser pyO3 plutôt que le C directement comme prévu par la documentation Python?
Rust offre des nombreux avantages conséquents par rapport au C tout en gardant des performances très proches :
- la sécurité mémoire
- une concurrence sûre
- un écosystème moderne
- la simplicité d’écrire des fonctions Python en Rust grâce aux macros
Conclusion
PyO3 est une bibliothèque intéressante pour développer des bibliothèques Python grâce aux avantages du langage Rust. Cependant, elle est encore en développement et manque de fonctionnalités importantes, comme on l’a vu avec le type fichier. Cependant, ça ne saurait tarder.