NdlM : discussion initialement ouverte depuis ce fil.
Je suis ce fil de discution, car le sujet m'intéresse. Il m'intéresse parce que pour projet "perso", je suis en train de définir un nouveau langage et un compilateur pour ce langage.
Je précise de suite que je n'ai pas d'ambition particulière pour ce langage, il ne vise pas a remplacer telle ou telle langage, et ne sortira certainement jamais de chez moi. C'est plus par plaisir. J'ai toujours voulu "écrire un compilateur", mais le faire pour un langage existant ne m'interesse pas du tout. D'où la création d'un langage.
Je précise aussi, par honnêteté, que j'ai abandonner le C++ il y a plus de 15 ans, car sa "syntaxe" devenait de plus en plus imbuvale. C'est un avis personnel. Je n'ai pas abandonné le C++ pour les raisons qui sont discutées ici (la sécurisation de la mémoire).
Je voulait que mon langage reste simple a lire et a comprendre. Je pourrai développer pourquoi, mais ce n'est pas le sujet.
Je ne pratique pas Rust non plus, car je n'ai pas eu l'occasion d'y regarder de près, car dans mon domaine, c'est le C et rien d'autre.
Ces petites précisions étant faites, je vais aborder le débat tel qu'il est posé ici. C'est à dire, faut-il passer du C++ à Rust.
Je n'ai pas d'avis tranché sur la question, car si Rust apporte une sécurité de la gestion mémoire, j'ai du mal a comprendre pourquoi l'équipe de Rust a fait certains choix. Je pense que ces choix ont été fais car Rust ne voulais trop s'éloigner de la syntaxe "de base" du C++, afin d'attirer à lui ces derniers. C'est certainement un choix plus "marketing" que "technique". Mais je ne veux pas rentrer dans ce débât.
Bref, je suis conscient que Rust apporte un plus au niveau sécurité, mais je pense qu'il aurait pu faire cela plus "simplement" (même si la solution Rust est tout à fait correcte, pertinante, utilisable et qu'on peut faire avec).
Je vais tenter d'expliquer mon point de vue. Si je commet une erreur dans mon raisonnement, je suis tout à fait capable d'entendre les arguments contre "ma petit réfexion".
Allez, je me lance. Le premier principe (pour la sécurisation de la mémoire) est l'Ownership.
1./ Que le scope où est fait une allocation soit "propriétaire ET responsable" de cette allocation, je trouve cela tout à fait normal.
2./ Que l'on désalloue la mémoire à la fin de ce scope, est tout aussi normal.
Là où je pense que Rust a commis une erreur, c'est dans la 3ème règle, qui "transfert" la propriété au scope de la fonction appelée.
Il "transfert" la propriété, mais si "on ne la lui rend pas" (ce qui est "possible", puisque non imposé), il ne vas pas permettre de compiler le code, car cela pourrait, si le scope la fonction appelée étant maintenant "propritétaire" déssalouait la mémoire à la sortie de son scope, provoquer un after-free-bug au niveau de l'appelant.
Donc, pour réssoudre se problème, intervient le second principe de Rust, le "Borrowing". Borrowing signifie "emprunt", et "dans ma petite tête", emprunter ne veut pas dire donner. Cette notion de "Borrowing" utilise des "références partagés", mais non "mutable", ou une référence "mutable". Logiquement, on ne peut pas avoir les 2 en même temps, on peut utiliser soit une seule référence mutable, soit plusieurs référence "immutable".
Mon "analyse" est-elle correcte jusqu'ici ? Il me semble que oui. Il est dit que cela n'est pas gênant, car tout les scénarios possibles peuvent fonctionner en utilisant ce "système". Je veux bien le croire, même si je n'ai pas vérifié que cette affirmation était vraiment valable.
Mais pour vérifier tout cela, Rust nécessite un "Borrow Checker", qui va "surveiller" que ces règles sont bien respectées. Et refuser la compilation si ce n'est pas le cas.
Suis-je toujours dans le vrai ? Il me semble que oui.
Mais la cause, la racine de toute cette infrastucture, est la notion de "tranfert", et le fait que de fil en aiguille, il faut un "BorrowChecker" pour que l'ensemble fonctionne.
L'équipe qui a développé Rust est très certainement d'un niveau supérieur au mien, je n'en doute pas. Je ne suis pas un théoricien et certainement pas un spécialiste des compilateurs (c'est le premier compilateur que j'écris, entièrement à la main) mais je suis un pragmatique il me semble que j'ai trouvé une "solution" plus simple et pratique pour le même résultat, que je suis en train d'implémenter dans mon petit langage perso. Je suis comme ça, j'aime simplifier.
Pour que cette "solution plus simple" puisse être utilisée, il faut 1 condition.
3./ Puisse qu'emprunté signifie "rendre à un moment donné" ce qui a été emprunté, il faut que le compilateur puisse obliger une fonction qu'on appel et qui a "emprunté" la "propriété" de la rende au "propriétaire d'origine". Ce qui n'est pas possible en Rust, me semble-t-il. Ce n'est pas possible, selon moi, mais dites-moi si je me trompe, parce que Rust ne fait pas la distinction entre ce que je nomme des paramètres d'entrées (qui sont contant par défaut), et des paramètres de sortie (variable par défaut, ce qui est somme toute logique) mais qui sont "obligés", par la syntaxe même d'une fonction d'être retournés à l'appelant, ce dernier ne pouvant pas ignorer ces dernier, ce qui est toujours le cas dans le type de fonction que j'ai implémenter dans mon langage.
Voici comment on déclare une fonction dans mon langage :
a, et b sont des entiers 8 bits constants, intialisé lors de l'appel de la fonction. Ce sont des "paramètres d'entrées".
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4 fct addition(int8 a, b) <- int16 result: result := a + b end
result, placé après le symbole <- est lui un "paramètre de sortie" variable.
Cette disctintion est un des points clef de ma méthode.
Voici comment "on doit utiliser" cette fonction :
3./ add est un entier 16 bits. Lors de l'appel de la fonction, il est donné en argument par le compilateur comme étant un troisiéme paramètre, utilisé sous le nom de "result" dans le corps de la fonction.
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3 int16 add add := addition(10, 30)
4./ On ne peut pas, de la sorte, ignorer la valeur de retour, puisque cette valeur de retour est la variable qui est assignée. Le troisième paramètre est la valeur de retour.
Si on applique le même principe lorsque l'on "prête" un "pointeur", cette méthode "assure par la syntaxe" que ce "pointeur" qui est emprunté est obligatoirement rendu à l'appelant.
Conclusion:
Il n'y pas besoin de "tranfert", il y a juste besoin "un prêt", dont on est assuré qu'il soit "rendu", et donc, pas besoin de "BorrowChecker" pour surveiller cela, puisqu'il est impossible, "par la syntaxe d'une fonction", d'ignorer que ce qui est rendu.
On peut écrire:
Il n'y a même pas besoin de parler d'Onwnership, de Borrowing et de BorrowChecker pour utiliser correctement, et sans erreur possible une fonction.
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7 fct allocation(int8 size) <- 'ptr' int8 result: result := alloc(size) end 'ptr' int8 mem mem := allocation(250)
Le compilateur sait que result est un paramètre de sortie, et qu'il ne DOIT PAS le désallouer en fin de scope.
Si l'allocation est 'interne' à la fonction, il le sait aussi, et DOIT le désallouer en fin de scope.
Voilà voilà, j'attend vos critiques et/ou rélexions sur le sujet.
BàV et "Peace & Love".
Partager