Sommaire
nb : dans cet article, je n'évoque que l'interpréteur officiel, CPython (3.4+) et l'usage de modules standard à l'interpréteur (donc pas d'exemples de cffi, quel qu’en soient les qualités par ailleurs !).
Introduction
Ce week-end, j'ai fait une énième recherche sur une bricole pour la communication Python et Rust via cTypes. Sur ces "détails" qu'on oublie aussi vite qu'on se casse les dents dessus lorsqu'on ne pratique pas.
Comme ma mémoire est encore plus limitée que le nombre d'onglets et de marques-page sur Firefox, je me suis dit que la prochaine fois autant tomber directement sur un article francophone qui fait un résumé global. Et que ça peut en aider d'autres.
Bien sûr il existe des bibliothèques toutes prêtes pour faciliter la communication entre ces deux mondes :
- soit pour interagir avec l'interpréteur Python,
- soit "en ramenant" Python au sein d'un applicatif.
Aujourd'hui la bibliothèque Pyo3 est probablement la plus aboutie et la mieux supportée ; j'aime bien en faire la pub car je la trouve géniale - et par extension à deux langages que j'apprécie.
Lorsque j'évoque la possibilité de "ramener" Python, c'est avoir non seulement la possibilité d'évaluer une expression mais aussi l'ensemble d'un script. Comme pour Lua sur les consoles de jeux vidéos, cela ouvre des possibilités infinies (même si parfois complexes) pour étendre votre applicatif actuel à de nouvelles sources de données ou possibilité de programmation.
Bref : c'est bien, c'est bon, mangez-en. [fin de la minute pub]
Du reste si cette possibilité semble la plus intéressante, elle s'avère complexe à mettre en œuvre si votre interpréteur Python est dans un ensemble plus large, avec beaucoup de code C ou C++ par exemple, et qu'il s'agit de rentrer dans les pas de versions précédentes ou du code partagé entre plusieurs applicatifs (dont tous ne seraient pas du Python) ; n'est-ce pas le principe des .so
après tout ? La bascule de C/C++ à Rust pourrait déclencher des problématiques si vous restez sur le seul usage de Pyo3.
Mais rien n'est insurmontable, surtout pour le serpent : cTypes est votre amie.
Présente par défaut depuis les temps immémoriaux dans l'interpréteur standard, cette bibliothèque vous permet de consommer des .so
(assez) facilement avec - c'est assez rare pour être souligné -, un respect strict du typage lors de l'appel de fonction. En effet, vous quittez alors le monde merveilleux (et lâche) de l'interprété, pour le terrible et cruel (et implacable) monde du C, où tout doit être connu (si possible à l'avance).
Voici un pense-bête rédigé pour s'en sortir.
Préparer l'environnement
Dans votre console préférée, créez un projet de bibliothèque :
julien@julien-Vostro-7580:~/Developpement/$ cargo new --lib rust-python
Created library `rust-python` package
Puis éditez comme suit votre fichier Cargo.toml
:
[package]
name = "rust-python"
version = "0.1.0"
edition = "2021"
[lib]
name = "rust_python"
crate-type = ["cdylib"]
[dependencies]
A la racine de votre projet, créez également le fichier test-ffi.py
avec le contenu suivant :
#!/usr/bin/env python3
from ctypes import *
malib = cdll.LoadLibrary("target/release/librust_python.so")
A partir de cet instant, malib
correspond à un objet permettant d'accéder au contenu de la bibliothèque. Lors d'un appel de fonction à celle-ci, vous pouvez indiquer les types de paramètres et du retour directement dans le code Python.
Par exemple le code suivant précise pour la fonction test_string
les types attendus :
malib.test_string.argtypes = [c_char_p,] # arguments d'appel attendus, ici un seul
malib.test_string.restype = c_char_p # type du retour
resultat = malib.test_string( message.encode() ) # appel de la fonction partagée, avec la récupération du résultat
Enfin ajoutez le nécessaire dans les sources Rust (lib.rs
) :
use std::ffi::CStr;
use std::ffi::CString;
use std::os::raw::c_char;
use std::os::raw::c_int;
Notez que les chemins sont relatifs à votre compilation Rust :
-
target/debug
pourcargo build
; -
target/release
pourcargo build --release
.
A partir de là, vous pourrez ajouter les blocs de code les uns à la suite des autres et les tester avec :
-
cargo build && ./test-ffi.py
(compilation plus rapide, message d'erreur plus complet mais moins efficace à l'usage) -
cargo build --release && ./test-ffi.py
(compilation moins rapide mais plus efficace à l'usage)
Morceaux choisis
nb : il existe déjà des tutoriaux sur les types simples, tels que les entiers. Je ne les mets pas directement en exemple ici. De même il y a de nombreux autres cas généraux que je l'indique pas ; cependant les exemples fournis ici me semble-t-il, permettent de s'en sortir !
Partie 1 - Les chaînes de caractères
Quelques ressources pour approfondir :
Côté Rust :
#[no_mangle]
pub unsafe extern "C" fn test_string(ptr_source: *mut c_char) -> *const c_char {
// je récupère en argument un pointer vers un type 'c_char'
// je dois d'abord en faire un CStr et grâce à "to_string_lossy"
// toutes les valeurs non-conformes UTF-8, seront éliminées (remplacées précisément)
// puis j'assigne à 'v' le résultat (une chaîne de caractère)
let v = CStr::from_ptr( ptr_source )
.to_string_lossy()
.to_string();
println!("[RUST] -( 1 )-> {:?}", ptr_source);
println!("[RUST] -( 2 )-> {:?}", v);
// pour renvoyer ma chaîne, je dois modifier son type pour être conforme
// (par exemple : ajouter "\0" à la fin, car on perd la taille fixée en c_char)
// ainsi que d'obtenir le pointeur associée
let s = CString::new( v ).unwrap();
let p = s.as_ptr();
// au regard de Rust, 's' est ma chaîne c_char et 'p' est le pointeur
// si "je n'oublie pas" 's' avant de quitter la fonction, Rust va désallouer la mémoire
// le pointeur 'p' renvoyé serait donc invalide
// 'std::mem::forget' nous permet de forcer cet "oubli de désallocation" lors de la compilation
std::mem::forget(s);
p
}
Côté Python :
print( "--------------------" )
print( "--- partie 1 - chaînes de caractère seules (UTF8)" )
print( "--------------------" )
message = "&é\"'(-è_çà)"
print( "--- partie 1.1 - sans précision sur le type d'argument" )
malib.test_string.restype = c_char_p
resultat = malib.test_string(
# n'ayant pas indiqué le type d'argument attendu, je dois faire une transformation
# moi-même. Attention cependant, il est toujours préférable d'indiquer le bon type
c_char_p( bytes( message, "utf-8" ) )
)
print( "--- partie 1.2 - avec précision sur le type d'argument" )
malib.test_string.argtypes = [c_char_p,] # ici la précision du type
malib.test_string.restype = c_char_p
resultat = malib.test_string( message.encode() ) # par défaut, ".encode()" est en UTF-8
print( "[PYTHON] ===>", resultat.decode() )
Résultat :
julien@julien-Vostro-7580:~/Developpement/rust-python$ cargo build && ./test-ffi.py
Finished dev [unoptimized + debuginfo] target(s) in 0.00s
--------------------
--- partie 1 - chaînes de caractère seules (UTF8)
--------------------
--- partie 1.1 - sans précision sur le type d'argument
[RUST] -( 1 )-> 0x7f67723d3e90
[RUST] -( 2 )-> "&é\"'(-è_çà)"
--- partie 1.2 - avec précision sur le type d'argument
[RUST] -( 1 )-> 0x7f67723d3e90
[RUST] -( 2 )-> "&é\"'(-è_çà)"
[PYTHON] ===> &é"'(-è_çà)
Facile.
Partie 2 - Les structures
Côté Rust :
#[repr(C)]
#[derive(Debug)]
pub struct MonObjet {
pub a: c_int
}
#[no_mangle]
pub unsafe extern "C" fn test_structure(mut monobj: MonObjet) {
// du fait que l'aligment se fait sur le C, on peut directement récupérer l'objet
println!("[RUST] -(3)-> {:?}", monobj);
// et comme on l'a déclaré "mut(able)" alors on peut agir dessus ; voir ci-après (1)
monobj.a += 1;
println!("[RUST] -(4)-> {:?}", monobj);
}
#[no_mangle]
pub unsafe extern "C" fn test_structure_ref(ptr_monobj: *mut MonObjet) {
// le format '&mut *' semble étrange mais est parfaitement valide. On déréférence d'abord le pointeur, puis on créé un emprunt (mutable) au format Rust pour agir dessus ; voir ci-après (2)
let monobj = &mut *ptr_monobj;
println!("[RUST] -(3)-> {:?}", monobj);
monobj.a = 3;
println!("[RUST] -(4)-> {:?}", monobj);
}
nb (1) : attention à la déclaration de l'objet en argument dans test_structure
. Si mut
n'était pas déclaré, il serait impossible d'agir sur l'objet, conformément aux règles de la Rouille…
error[E0594]: cannot assign to `monobj.a`, as `monobj` is not declared as mutable
--> src/lib.rs:69:5
|
67 | pub unsafe extern "C" fn test_structure(monobj: MonObjet) {
| ------ help: consider changing this to be mutable: `mut monobj`
68 | println!("[RUST] -(3)-> {:?}", monobj);
69 | monobj.a += 1;
| ^^^^^^^^^^^^^ cannot assign
For more information about this error, try `rustc --explain E0594`.
error: could not compile `analyse-terme` due to previous error
nb (2) : dans le cas que je présente, l'emprunt est nécessaire car la structure MonObjet n'implémente pas le trait de copie, comme le signale très bien le compilateur…
error[E0507]: cannot move out of `*ptr_monobj` which is behind a raw pointer
--> src/lib.rs:75:22
|
75 | let mut monobj = *ptr_monobj;
| ^^^^^^^^^^^
| |
| move occurs because `*ptr_monobj` has type `MonObjet`, which does not implement the `Copy` trait
| help: consider borrowing here: `&*ptr_monobj`
For more information about this error, try `rustc --explain E0507`.
error: could not compile `analyse-terme` due to previous error
Côté Python :
print( "--------------------" )
print( "--- partie 2 - structures et passage par référence" )
print( "--------------------" )
print( "--- partie 2.1 - envoi par valeur (l'objet initial n'est pas modifié)" )
# il s'agit d'une classe un peu particulière, qui prend l'héritage de "Structure" :
# Structure permet via un attribut lui-aussi particulier "_fields_", d'indiquer à cType
# ce qui est nécessaire d'envoyer à la fonction C partagée
class MyStruct(Structure):
_fields_ = [
("a", c_int)
]
monobjet = MyStruct()
monobjet.a = 2
# monobjet.b = 3 --> vous pouvez essayer sans problème, mais l'attribut n'étant pas déclaré dans _fields_, le champ ne sera pas transmis
# notez que je n'ai pas déclaré le type d'arguments attendus
resultat = malib.test_structure(
monobjet # j'envoi l'objet via un pointeur
)
print( "[PYTHON] ===>", monobjet.a ) # pas de modification sur l'objet initial, a = 2
print( "--- partie 2.2 - envoi par référence (l'objet initial est modifié)" )
resultat = malib.test_structure_ref(
byref( monobjet ) # j'envoi une référence à l'objet via un pointeur
)
print( "[PYTHON] ===>", monobjet.a ) # modification sur l'objet initial, a = 3
Résultat :
julien@julien-Vostro-7580:~/Developpement/rust-python$ cargo build && ./test-ffi.py
Finished dev [unoptimized + debuginfo] target(s) in 0.00s
(...)
--------------------
--- partie 2 - structures et passage par référence
--------------------
--- partie 2.1 - envoi par valeur
[RUST] -(3)-> MonObjet { a: 2 }
[RUST] -(4)-> MonObjet { a: 3 }
[PYTHON] ===> 2
--- partie 2.2 - envoi par référence (l'objet initial est modifié)
[RUST] -(3)-> MonObjet { a: 2 }
[RUST] -(4)-> MonObjet { a: 3 }
[PYTHON] ===> 3
Simple non ?
Partie 3 - Les tableaux
Le cas de transfert des tableaux est un peu plus délicat. Il n'est pas comparable à une chaîne de caractère. En effet une chaîne de caractères représente, comme son nom l'indique, un tableau contigu de caractères (peu importe leur taille individuelle). Cependant pour le tableau d'entiers par exemple, ce procédé ne fonctionne pas… car la valeur "0" est une valeur légitime.
Il faut donc passer deux éléments :
- le tableau en lui-même, ici sous la forme d'un pointeur,
- la taille réelle du tableau derrière le pointeur.
De la même façon, cela me permet d'introduire l'usage de Box
au lieu de l'oubli par mem::forget()
comme vu jusque là pour la gestion des pointeurs de Rust vers C.
Côté Rust :
#[repr(C)]
#[derive(Debug)]
pub struct ValeurRetour {
pub a: c_int,
pub contenu: Vec<c_int>
}
#[no_mangle]
#[allow(improper_ctypes_definitions)]
pub unsafe extern "C" fn test_array( nbre: c_int, ptr_tab: *mut [c_int] ) {
let tab = &mut *ptr_tab;
println!("[RUST] -(5)-> {:?}", nbre);
for i in 0..(nbre as usize) {
println!("[RUST] -(6)-> [{:?}] {:?}", i, tab[i]);
}
}
#[no_mangle]
#[allow(improper_ctypes_definitions)]
pub unsafe extern "C" fn test_array_retour_simple( nbre: c_int, ptr_tab: *mut [c_int] ) -> *mut c_int {
let tab = &mut *ptr_tab;
println!("[RUST] -(7)-> {:?}", nbre);
let nbre = nbre as usize; // une version courte (mais sale) du casting : attention aux valeurs max admissibles par le système pour 'usize' dans un tel cas
for i in 0..nbre {
println!("[RUST] -(8)-> [{:?}] {:?}", i, tab[i]);
tab[i] += 1;
println!("[RUST] -(8')-> [{:?}] {:?}", i, tab[i]);
}
let mut nouveau_vecteur: Vec<c_int> = vec![42; nbre+1];
// pas propre mais fonctionnel
let p = nouveau_vecteur.as_mut_ptr();
std::mem::forget(nouveau_vecteur);
p
}
#[no_mangle]
pub unsafe extern "C" fn test_array_retour_complexe( nbre: c_int ) -> *mut ValeurRetour {
// notre 'nbre' reçu depuis le monde Python via un c_int, devra changer pour être utilisé dans deux contexte différent
println!("[RUST] -(9)-> {:?}", nbre);
let nbre_c_int: c_int = (nbre+1).try_into().unwrap();
let nbre_usize: usize = (nbre+1).try_into().unwrap();
let vecteur_retour = Box::new(
ValeurRetour {
a: nbre_c_int, // ici un entier au format c_int
contenu: vec![42; nbre_usize] // ici un usize pour définir la taille finale du vecteur, même si ce dernier aurait pu être conçu "à la volée" en ajoutant progressivement des valeurs - ici c'est juste plus efficient
}
);
println!("[RUST] -(10)-> {:?}", vecteur_retour);
// plus propre que 'mem::forget()'
Box::into_raw( vecteur_retour )
}
Côté Python :
Peut-être une expression va vous choquer : (c_int * len(tab_valeurs))(*tab_valeurs)
. Elle est pourtant tout à fait correcte ! Une fois décomposée, elle est très simple à comprendre :
tab_valeurs = [1, 2, 3, 4]
taille_tab_valeurs = len(tab_valeurs)
taille_fixe_tableau_c_int = c_int * taille_tab_valeurs
… La dernière partie est l'assignation des valeurs contenues dans tab_valeurs
vers taille_fixe_tableau_c_int
, comme si l'on faisait une boucle for
. Attention une telle boucle n'est pas réellement possible (d'où l'appel de fonction) :
julien@julien-Vostro-7580:~/Developpement/rust-python$ python3
Python 3.10.4 (main, Jun 29 2022, 12:14:53) [GCC 11.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from ctypes import *
>>> tab_valeurs = [1, 2, 3, 4]
>>> taille_tab_valeurs = len(tab_valeurs)
>>> taille_fixe_tableau_c_int = c_int * taille_tab_valeurs
>>> taille_fixe_tableau_c_int
<class '__main__.c_int_Array_4'>
>>> for i, v in enumerate(tab_valeurs):
... taille_fixe_tableau_c_int[i] = v
...
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
TypeError: '_ctypes.PyCArrayType' object does not support item assignment
Vous aurez noté au passage le type : Class __main__.c_int_Array_4
(avec 4 qui correspond à la taille) ; notre multiplication est en réalité la construction d'un objet.
Pour l'étoile, c'est exactement le même principe que l'argument du reste pour une fonction ou lors de la construction des listes ou dictionnaires:
julien@julien-Vostro-7580:~/Developpement/rust-python$ python3
Python 3.10.4 (main, Jun 29 2022, 12:14:53) [GCC 11.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> a = [0,1,2,3]
>>> b = [*a,]
>>> b
[0, 1, 2, 3]
>>> c = {"ok":1}
>>> d = {**c,}
>>> d
{'ok': 1}
>>> d = {**c,'ko':0}
>>> d
{'ok': 1, 'ko': 0}
… plus de mystère !
print( "--------------------" )
print( "--- partie 3 - tableaux et passage par référence" )
print( "--------------------" )
print( "--- partie 3.1 - envoi par référence (l'objet initial est modifié)" )
tab_valeurs = [1, 2, 3, 4]
tableau = (c_int * len(tab_valeurs))(*tab_valeurs)
malib.test_array(
len(tableau),
byref( tableau )
)
print( "[PYTHON] ===>", len(tableau), list( tableau ) )
print( "--- partie 3.2 - envoi par référence x2 et acquisition pré-connue retours" )
malib.test_array_retour_simple.restype = POINTER(c_int)
r1 = malib.test_array_retour_simple(
len(tableau),
byref( tableau )
)
print( "[PYTHON] =( r1 )=>", len(tableau), list( tableau ) )
print( "[PYTHON] =( r1 )=>", len(tableau)+1, [ r1[i] for i in range(0,len(tableau)+1) ] )
r2 = malib.test_array_retour_simple(
len(tableau),
byref( tableau )
)
print( "[PYTHON] =( r2 )=>", len(tableau), list( tableau ) )
print( "[PYTHON] =( r2 )=>", len(tableau)+1, [ r1[i] for i in range(0,len(tableau)+1) ] )
print( "--- partie 3.2 - création d'un objet de retour de taille indéterminée à l'appel" )
class ValeurRetour(Structure):
_fields_ = [
( "a", c_int ),
( "contenu", POINTER(c_int) )
]
malib.test_array_retour_complexe.restype = POINTER( ValeurRetour )
r3 = malib.test_array_retour_complexe(
len(tableau)
)
a = r3.contents.a
v = r3.contents.contenu
print( "[PYTHON] ===>", a, [ v[i] for i in range(0,a) ] )
Parfait.
Conclusion
Nous avons fait un tour rapide mais j'espère assez complet du propriétaire. En fonction de vos retours (coquilles ou ajouts majeurs), je demanderais peut-être à un admin de pouvoir passer un dernier coup de polish sur l'article…
En attendant, bon code à tous ! :)
# Merci
Posté par xoddark . Évalué à 3. Dernière modification le 07 novembre 2022 à 23:30.
Je n'ai pas encore tout lu, mais cette article / partage m'intéresse.
Je suis programmeur C++ expérimenté et programmeur Rust en apprentissage, ça pourrais m'intéresser si j'ai besoin de faire interagir Rust avec du Python.
Pas que j'aime le Python ou que j'ai envie d'en faire, mais s'il y avait besoin d'intérragir avec du code existant et/ou fait par quelqu'un d'autres.
Pour moi c'est le monde de l'interprété qui est cruel.
Le compilateur (et outils de check associé) permette de détecter tellement d'erreurs de programmation et autre bugs alors qu'ils ne sont détecté qu'à l'exécution dans le cas de langage interprété.
Et c'est encore plus flagrant en Rust, j'ai tellement plus confiance en mon programme une fois qu'il compile (sa qualité, son nombres réduit de bugs).
C'est plus reposant pour l'esprit, et ça permet de plus ce concentrer sur les problématique à résoudre, et moins sur la technique pour les résoudre.
[^] # Re: Merci
Posté par JulienG . Évalué à 3. Dernière modification le 06 novembre 2022 à 19:17.
Je suis entièrement d'accord avec toi. Rust a vraiment des qualités intrinsèques tant la définition du langage comme grammaire que du fonctionnement de son compilateur (et ses outils associés), que c'est un plaisir. Avec la garantie de la mémoire comme une sublime cerise. Il nous oblige à faire correctement.
Bien sûr j'ai rajouté un peu d'humour sur la différence entre interprété et compilé. Du reste j'ai plaisir à Python pour la beauté de résoudre certains aspects métier, en faisant une (forte) abstraction du sous-jacent technique.
Pour les projets perso, je ne me vois pas commencer un projet ou un développement un peu conséquent, si d'abord passer par la case "script pythonique" pour tenter d'abord de bien saisir ce que j'ai à faire et les ramifications. Après le passage à Rust est juste l'outil le plus correct pour tout mettre en œuvre ou simplement "accélérer" un traitement comme ce week-end.
Au passage, merci pour ton commentaire sur l'intérêt de l'article.
[^] # Re: Merci
Posté par xoddark . Évalué à 2.
Oui j'avais compris l'humour mais j'avais envie d'en profiter ^
En effet, les langages interprété sont souvent présenté comme plus facile, plus simple d'accès. Hors je trouve que dans les faits c'est vraiment pas forcement le cas.
La simplicité de façade se traduit souvent par des inconvénients (notamment ceux que je citais précédemment.
Alors autant C/C++ avait quand même des subtilités qui pouvais compliquer leur utilisations par des néophyte ou programmeur occasionnel.
Autant Rust, avec son aspect explicite et tous les outils qui viennent avec rend les choses vraiment plus accessible. Les outils en deviennent de bon guide pour l'apprentissage.
[^] # Re: Merci
Posté par xoddark . Évalué à 1.
Ooops, j'ai raté ma citation, un modérateur peux corriger ?
Il faut arrêter la citation a partir de "Pour moi c'est le monde de l'interprété qui est cruel."
[^] # Re: Merci
Posté par Benoît Sibaud (site web personnel) . Évalué à 4.
Corrigé, merci.
# memforget et fuite mémoire
Posté par GaMa (site web personnel) . Évalué à 3.
N'y a-t-il pas une fuite mémoire là ?
Certes tu es obligé de dire à rust de ne pas desalouer la variable pour la passer à python, mais il faut bien la libérer à un moment. Sur un cas de test comme ça c'est pas bien grave, mais comment tu gères ça en vrai sur un programme réel ?
Matthieu Gautier|irc:starmad
[^] # Re: memforget et fuite mémoire
Posté par JulienG . Évalué à 3.
Si j'en crois la doc, Python récupère le pointeur et créé via cTypes, tout le nécessaire pour gérer son cycle de vie. Donc non, il n'y a pas de risque.
En somme je garde la mémoire allouée pour qu'elle passe sous la supervision Python via un pointeur, sous la forme d'une objet référençable interne : l'objet cible (ou la référence elle-même) qui l'utilisera formera sa première référence incrémentée.
Si l'objet cible n'est plus utilisé, il sera supprimé par le GC qui décrémentera le compteur interne de l'objet référençable, qui tombera à 0 et sera donc lui aussi supprimé.
De ce que j'en ai compris - à confirmer par un connaisseur -, c'est le principe de Numpy.
[^] # Re: memforget et fuite mémoire
Posté par GaMa (site web personnel) . Évalué à 4. Dernière modification le 07 novembre 2022 à 14:39.
C'est quelle doc ?
Python n'a aucun moyen de savoir comment il doit désallouer la mémoire, ni même si il doit la désallouer.
Si tu wrap du code c++ et que tu retournes un pointeur sur un class, il faut appeler le
destructeur. Python ne le connais pas. Pareil au niveau de Rust, il y a peut-être des
drop
à appeler.Voir même, il ne faut pas détruire la donnée. Image que ta box est dans un stockage de ta lib rust et que tu veux juste retourner un pointer sur cette valeur tout en gardant sa gestion mémoire dans rust : tu peux en récupérer un pointer en "coerçant" la référence (https://doc.rust-lang.org/std/primitive.pointer.html#common-ways-to-create-raw-pointers)
mais python ne doit pas supprimer la mémoire. Donc Python ne doit pas le faire par défaut.
Si tu veux que le code python supprime la mémoire, il faut repasser le pointer à rust pour qu'il le supprime comme il faut, probablement avec une fonction de ce type (si tu as utiliser
Box::into_raw
, à adapter aux cas d'usages) :Tu peux lancer ton programme avec valgrind pour voir si il y a des fuites mémoire.
Matthieu Gautier|irc:starmad
[^] # Re: memforget et fuite mémoire
Posté par JulienG . Évalué à 3. Dernière modification le 07 novembre 2022 à 19:53.
Mea maxima culpa.
https://doc.sagemath.org/html/en/thematic_tutorials/numerical_sage/ctypes_examples.html
La doc est pas claire justement, car elle indique que le pointeur donné est transformé en une référence interne, elle-même utilisée ailleurs si nécessaire. En reprenant la stratégie globale d'allocation des variables Python, ça ne m'a pas choqué. Compte tenu que tu déclares le type, j'ai bêtement pensé qu'il connaissait donc sa taille calculable et par conséquent, faisait le nécessaire.
C'est moche. Je regarde pour la fuite mémoire et j'écris cette semaine une édition de cet article.
Merci !
Edit : Wrapper "__del__" pour appel à Box comme tu m'indiques.
[^] # Re: memforget et fuite mémoire
Posté par JulienG . Évalué à 2.
@Maga : voici une proposition de complément de l'article. Si tu l'as l'occasion / la possibilité d'y jeter un coup d’œil… (peur d'une coquille supplémentaire)
Mea maxima culpa
Errare humanum est, perseverare diabolicum : j'ai fait une erreur. Pris dans mon élan je n'ai pas évoqué la question de la libération de la mémoire. En réalité, j'avais tort sur ma vision de la gestion de la mémoire par cTypes.
Mon idée était la suivante : cTypes n'évoque à aucun moment la question de la libération de la mémoire. J'ai supposé que c'est la stratégie de gestion de la mémoire de Python qui s'applique, et donc qu'il était en capacité de prendre en charge pour nous.
Las, le commentaire de GaMa, tape juste : quoiqu'il arrive ça pêche sur l'usage, car un pointeur renvoie vers une zone qui peut avoir elle-même d'autres pointeurs (voir ici et là). Il faut gérer côté Python cet aspect, en demandant côté Rust dans notre cas, la libération de la mémoire. Et il ne peut pas connaître du reste, toutes les tailles à désallouer (typiquement le tableau mais pour d'autres éléments, il connaît la taille).
Dans notre bibliothèque Rust, rajoutons donc le nécessaire :
Côté Python, c'est plus compliqué. On doit gérer désormais la durée de vie de la mémoire derrière le pointeur, mais l'appeler au bon moment pour ne pas désallouer côté Rust et utiliser côté Python.
Pour cela, le mieux que j'ai trouvé est d'avoir un objet intermédiaire dont on aura le signal de destruction. ("__del__"). C'est lui qui permettra de faire appel à la désallocation :
Si la comparaison d'attributs vous est importante, gardez à l'esprit que cTypes créée un nouvel objet interne à chaque appel à l'objet pointé, comme l'illustre l'exemple de la documentation :
Cependant si vous vous ne souciez pas de ce point, pensez au décorateur "property" pour envelopper plus aisément l'appel à l'objet lié par le pointeur, dans l'objet d'enveloppe :
Enfin vous pouvez retrouver très facilement le jeu de poupées imbriquées qui sert à la mécanique interne ("tout est objet en Python") :
Au premier niveau, on retrouve bien un objet "pointeur" à la sauce Python :
<__main__.LP_ValeurRetour object at 0x7fdd12f7f940>
Qui permet d'accéder via ".contents" à l'objet "reconstitué" via la classe du même nom,
<__main__.ValeurRetour object at 0x7fdd12f7c6c0>
Qui permet lui-même d'accéder à mon tableau au travers d'un autre pointeur :
<__main__.LP_c_int object at 0x7fdd12f7c3c0>
Qui, devinez quoi ?, me permet d'accéder à ma valeur (la première case qui est à 42, puis de pouvoir d'itérer sur le tableau en mémoire).
[^] # Re: memforget et fuite mémoire
Posté par GaMa (site web personnel) . Évalué à 4.
Ça me semble mieux :)
J'émets cependant une réserve sur le
__del__
. Ça peut poser quelques soucis avec le garbage collecteur de python (https://stackoverflow.com/questions/1481488/what-is-the-del-method-and-how-do-i-call-it)En l’occurrence, ça peut bloquer le gc pour chercher des cycles à détruire. Et en fait, tu n'as aucune garantie que
__del__
soit appelé par python (il y a de fortes chances que oui, mais c'est pas garantie).Si je devais faire la même chose, je partirais plus sur un wrapper (EnveloppeValeurRetourLibDistante) écrite en c++ avec un destructeur en c++ et qui serait wrappé en python (et tout ça probablement avec cython). Mais on sort un peu du sujet d'origine (utiliser ctypes). [*]
Et une autre réserve. Ça marche parce que tu copies les valeurs du tableau rust dans un tableau python. (D’ailleurs, tu pourrais libérer la mémoire juste après ça). Mais dans un cas plus complexe (par exemple une structure qui à des pointers sur d'autres zone mémoire) il faut faire attention à ne pas garder un pointer vers une de ces zones et libérer la structure en même temps. Sinon, rust risque de libérer la mémoire alors que python continue de pointer dessus.
En fait, si je devais faire la même chose, je ne le ferais probablement pas et j'utiliserais des libs déjà existantes faites pour ça. La gestion mémoire est un sujet complexe (c'est pas pour rien qu'on a inventer des langages haut niveau qui la gère pour nous) et la gérer correctement dans une couche glue entre deux langages qui n'ont pas les mêmes logiques de gestion mémoire n'est pas une mince affaire.
[*] Peut être qu'un jour je m'y amuserais, vous aurez droit à un journal si c'est le cas.
Matthieu Gautier|irc:starmad
[^] # Re: memforget et fuite mémoire
Posté par JulienG . Évalué à 2.
Merci de ton retour et de tes alertes !
Sur la partie
__del__
, j'ai bien conscience qu'il existe des cas limites. Comme tu le soulignes, rien n'empêche d'exploiter par le développeur en appelantmalib.liberer_valeurretour( obj_ptr )
"au bon moment" (celui qui serait attendu).Sur le deuxième point c'est effectivement un corollaire à ton commentaire initial : ne connaissant pas tous les pointeurs vers un objet en mémoire, on ne connaît pas l'usage de tous ces pointeurs potentiels.
Par contre je m'interroge sur le caractère automatique de la désallocation de la stratégie d'emprunt et de suppression de Rust. Peut-être auras-tu une réponse ?
Premier cas :
… c'est le cas de mon exemple. La mémoire est libéré par Rust, car il a la possession. Si un pointeur existe vers cette valeur en mémoire, c'est embêtant mais pas pour l'objet source : pour celui-ci, pas de problème.
Si par contre j'avais eu un partage mutable ou non :
… Rust n'aurait pas libéré la mémoire, car il n'a pas de possession mais d'emprunt. La désallocation dans de tel cas, invaliderait gravement le fonctionnement des durées de vie.
Dans l'exemple, ma compréhension du phénomène est la suivante : le passage dans
liberer_valeurretour
supprime le tableau car j'ai déclaré côté Rust, la pleine possession de celui-ci.Ce journal à au moins cet avantage de (bien) mettre en lumière qu'on peut se prendre facilement les pieds dans le tapis, oui. Mais sinon on ne progresse pas :)
PS : hâte de lire ça si tu fais un journal sur le sujet.
[^] # Re: memforget et fuite mémoire
Posté par GaMa (site web personnel) . Évalué à 3.
MonObjet
est propriétaire dechamp
. Donc à la destruction deMonObject
,champ sera détruit aussi
.Et rust garantie qui rien de pointera dessus parce qu'il n'autorise pas de références qui vivent plus longtemps que
MonObjet
. Sauf si tu fais duunsafe
(ce qui est le cas ici), rust ne sait pas. C'est à toi de savoir comment python et rust fonctionne pour rien casserPour
Déjà, c'est faux. C'est :
Et ça veut dire que
MonObjet
, qui emprunte (a une référence vers)UnAutreObjet
, ne peut pas vivre plus longtemps queUnAutreObjet
. (Même règle que plus haut, mais dans l'autre sens).Si python garde un pointeur sur
UnAutreObjet
, le problème n'arrivera pas quandMonObjet
est détruit mais quandUnAutreObjet
(ou son propriétaire) sera détruit.Tout à fait.
Tu déclare une structure qui est propriétaire du vecteur qui est propriétaire des entiers.
Avec
let valeur_retour = Box::new(ValeurRetour{...});
tu crées uneValeurRetour
dans une box etvaleur_retour
est propriétaire de la box (et doncValeurRetour
)Avec
Box::into_raw( valeur_retour )
, tu quittes la propriété (au profit de personne), ta box est vidé et tu récupère le pointeur survaleur_retour
.Avec
Box::from_raw(ptr)
, tu reprends la propriété. À la destruction de la box, le valeur est détruite (ainsi que lecontenu
).Entre temps, niveau python tu as deux options :
Matthieu Gautier|irc:starmad
# Edition de l'article ?
Posté par JulienG . Évalué à 1.
Si un administrateur ou un habitué du site connaît le meilleur moyen pour demander l'édition de ce journal (à qui je l'envoi… ? comment ?) car je sèche un peu sur l'aide du site…
[^] # Re: Edition de l'article ?
Posté par Gil Cot ✔ (site web personnel, Mastodon) . Évalué à 3.
Tu dois malheureusement faire tes demandes en commentaires sous l'article et attendre que la modération passe par là.
“It is seldom that liberty of any kind is lost all at once.” ― David Hume
Suivre le flux des commentaires
Note : les commentaires appartiennent à celles et ceux qui les ont postés. Nous n’en sommes pas responsables.