Retour au blog
Jonathan Serra
30/10/2026
14 min

Ce qu'il faut savoir avant de choisir Rust

Rust n'est pas devenu un choix par défaut parce qu'un LLM le fait compiler. Bun l'a tenté à l'échelle d'un million de lignes. Le code qui passe le compilateur montre ce que vous achetez vraiment.

RustLLMBunDenoSécuritéChoix technique

Choisir Rust en 2026 part souvent de deux phrases. La première : le langage est sûr par construction. La seconde : les LLM ont retiré son défaut historique, la difficulté. Les deux phrases circulent ensemble. Elles ne décrivent pas la même chose.

Rust empêche, dans sa partie sûre, des bugs que C et C++ laissent passer : use-after-free, double-free, débordement de tampon, data race. Ce gain est mesuré là où le code nouveau remplaçait du C ou du C++. Il ne rend pas un CRUD plus sûr qu'une application déjà écrite en TypeScript, en Go ou en Python. Et un modèle qui fait compiler du Rust n'a pas, pour autant, choisi le bon propriétaire pour vos données.

Le projet qui permet de voir l'écart en grand s'appelle Bun. Le contre-exemple, plus ancien, s'appelle Deno.

Ce que les chiffres de sûreté mesurent

En 2019, le Microsoft Security Response Center écrivait qu'environ 70 % des vulnérabilités auxquelles il attribue un CVE viennent de la sûreté mémoire, malgré les revues et l'analyse statique. Le même texte dit de ne payer la complexité de la gestion mémoire que lorsque c'est nécessaire, et de préférer sinon un langage à garbage collector.

Android a suivi cette logique sur du code système. La part des failles de sûreté mémoire y est passée de 76 % en 2019 à 24 % en 2024, puis sous 20 % en 2025, en écrivant le code nouveau dans des langages sûrs, Rust à la place de C et C++. Google estime la densité de ces failles dans son Rust plus de 1 000 fois plus basse que dans son C/C++ historique.

Le rapport de la Maison-Blanche de février 2024 va dans le même sens, et cite Rust comme langage sûr adapté au système. Il cite aussi, via la NSA, C#, Go, Java, Ruby et Swift comme langages déjà memory-safe.

Un CRUD dont le goulot est la base, le réseau et les droits n'achète pas cette classe de failles en passant à Rust. Il achète des temps de compilation, un écosystème plus mince, et un modèle d'ownership que quelqu'un doit comprendre. Ce quelqu'un était, historiquement, le frein. C'est lui que le discours actuel déclare disparu.

Bun a choisi Zig parce que Rust ne rendait pas productif

Bun est un runtime JavaScript. En 2021, Jarred Sumner l'écrit en Zig, seul, en un an, avant les LLM. Il avait essayé Rust. Il le dit ensuite clairement : il n'arrivait pas à être productif, Zig s'apprend vite, et chaque allocation est visible.

Zig a rendu le projet possible. Il a aussi laissé passer la classe de bugs que le mélange « mémoire manuelle + garbage collector de JavaScriptCore » produit sans cesse. La v1.3.14 corrigeait encore des use-after-free dans node:zlib, node:http2, UDPSocket.send(), des double-free dans le parseur CSS, des fuites dans crypto.scrypt et TLS. Le détail est dans le billet du 8 juillet 2026.

Rust est ici un vrai outil. Drop exécute le nettoyage quand la valeur meurt, au lieu d'un defer à écrire à chaque site d'appel. Sur 2 000 appels Bun.build() dans le même processus, le billet mesure 6 745 Mo retenus en v1.3.14, et 609 Mo une fois la transition de Zig vers Rust en place. Bun v1.4.4, publiée le 22 octobre 2026, est cette base.

Le moyen utilisé pour y arriver est l'autre moitié de l'histoire.

On a demandé au modèle de produire du Rust sans en faire

La transition est assumée comme mécanique. 535 496 lignes de Zig, hors commentaires. Environ 6 500 commits hors fusions, du 3 au 14 mai 2026. Le diff posé fait plus d'un million de lignes. Jusqu'à 64 sessions Claude en parallèle, 5,9 milliards de tokens d'entrée non cachés, environ 165 000 $ d'API. Bun avait été racheté par Anthropic en décembre 2025. Jarred Sumner le écrit dans le même billet : une préversion de Claude a écrit l'essentiel de la transition, et un humain surveillait les boucles.

Le guide donné au modèle fixe la méthode :

  • le fichier Rust doit suivre la structure du Zig ;
  • unsafe est acceptable quand le Zig l'était déjà, avec un commentaire SAFETY ;
  • en phase A, on ne met pas de paramètre de lifetime sur une structure ;
  • il ne faut pas sortir un pointeur brut seulement pour faire taire le borrow checker.

La sûreté promise est celle du borrow checker. La consigne d'écriture est de coller au Zig, langage sans borrow checker. Le compilateur devient une file de tickets. Le billet raconte la suite : environ 16 000 erreurs, traitées caisse par caisse, jusqu'à ce que cargo check passe.

Le premier réflexe du modèle, toujours selon le billet, a été de vider les fonctions qui ne compilaient pas. Des stubs. Puis de longs commentaires pour expliquer pourquoi le contournement était acceptable. La règle ajoutée aux revues tient en une phrase : si le contournement a besoin d'un paragraphe, le code est faux.

C'est l'absurdité centrale. Le modèle ne contourne pas le compilateur. Il déforme le programme jusqu'à ce que le compilateur se taise. Vert, dans ce régime, veut dire « plus d'erreur ». Cela ne veut pas dire « la durée de vie est la bonne ».

Au moment du billet, environ 4 % du Rust de Bun est dans un bloc unsafe : de l'ordre de 13 000 mots-clés unsafe, sur environ 27 000 lignes, pour environ 780 000 lignes de Rust. 78 % de ces blocs tiennent sur une seule ligne, un pointeur venu de C++ ou un appel à une bibliothèque C. Une partie de ce unsafe est le prix normal d'un runtime collé à JavaScriptCore. Une autre partie est le Zig traduit tel quel, avec le mot unsafe rendu visible. Le rewrite vendu pour supprimer les use-after-free en contient des milliers, en attendant un refactor que l'auteur situe après la v1.4.

Quatre programmes qui compilaient

Les exemples qui suivent viennent du dépôt et du billet sur la transition. Chacun compilait.

Le correctif d'une libération, c'est une fuite

La première version du binding Windows de spawn détruisait un Box à la fin du match. uv_close est asynchrone. Libuv garde le pointeur jusqu'au tick suivant, puis le callback libère. Résultat : use-after-free, puis double-free. Le code était propre pour le compilateur. Le correctif, toujours sur main, consiste à fuir le Box pour qu'il survive au scope :

spawn::WindowsStdioResult::Buffer(pipe) => {    // uv_close est asynchrone. Libuv garde le pointeur, puis    // on_pipe_close récupère l'allocation via heap::take.    // Détruire le Box ici serait un use-after-free, puis un double-free.    Box::leak(pipe).close(Subprocess::on_pipe_close)}

Le dépôt ajoute même un raccourci, heap::release, documenté comme « Box::leak sous un autre nom », pour réserver la fuite nue aux données qui vivent vraiment toute la vie du processus.

Le geste est juste une fois qu'un humain a nommé le propriétaire. Le chemin pour y arriver est absurde : le modèle écrit un Box que Rust détruira trop tôt, le compilateur accepte, et la porte de sortie du langage est de déclarer que la mémoire ne sera pas rendue. Sans le commentaire, un lecteur voit une fuite. Avec le commentaire, il voit un contrat avec libuv. Le modèle avait produit le premier. La revue a produit le second.

L'assertion qui protège disparaît dans le binaire livré

ZStr::from_buf est une fonction publique sûre. À l'intérieur, un unsafe fabrique une chaîne terminée par un octet nul. Le contrôle buf[len] == 0 est un debug_assert!. En release, cette macro est effacée. Le commentaire dit que l'appelant garantit la précondition.

pub fn from_buf(buf: &[u8], len: usize) -> &ZStr {    debug_assert!(len < buf.len(), "ZStr::from_buf: NUL must lie within buf");    debug_assert_eq!(buf[len], 0, "ZStr::from_buf: missing NUL at buf[len]");    unsafe { Self::from_raw(buf.as_ptr(), len) }}

C'est l'état du code au 30 octobre 2026. Une correction encore ouverte explique que le Zig livré faisait ce contrôle en ReleaseSafe, et que la transition l'a rétrogradé en assertion de debug. Un len faux part ensuite vers open, stat ou CreateFileW.

Le même décalage a déjà cassé un comportement visible. En Zig, assert est une fonction : l'argument s'exécute dans tous les builds. En Rust, debug_assert! est une macro : en release, l'expression entière disparaît, y compris l'appel qu'elle contient. insert_stale, qui enregistre un fichier dans le graphe de hot reload, a cessé de tourner. Le hot reload cassait. Les builds debug passaient. Deux langages, une ligne qui se ressemble, une sémantique opposée. La transition mécanique copie la ressemblance.

Le pourcentage par défaut panic avant d'être ignoré

Dans le parseur CSS, un color-mix() peut omettre un pourcentage. L'autre prend le complément. La version issue de la transition écrivait ceci :

let p1 = first.percentage.unwrap_or(1.0 - second.percentage.unwrap());

unwrap_or calcule son argument tout de suite. second.percentage.unwrap() s'exécute même quand first.percentage est déjà là. Sur color-mix(in srgb, red 40%, blue), le second pourcentage est absent, et le processus panic dans l'argument que le unwrap_or s'apprêtait à jeter. Le correctif est unwrap_or_else, qui n'évalue la fermeture qu'en cas d'absence. Le billet cite le commit 90111846a14. Le parseur porte aujourd'hui la forme paresseuse.

Le compilateur n'a rien à dire. unwrap sur un Option est du Rust légal. L'absurdité est dans l'évaluation : le code de secours explose dans le cas qu'il est censé traiter.

Une API sûre, un pointeur mort

PathString::slice se présentait comme du Rust sûr. Le commentaire SAFETY affirmait que l'appelant gardait le buffer en vie. Rien dans le type ne l'imposait. Le 15 mai 2026, un rapport montrait le programme suivant, et la sortie de Miri :

let test = Box::new(*b"Hello World");let init = PathString::init(&*test);drop(test);println!("{:?}", init.slice());

error: Undefined Behavior: constructing invalid value of type &[u8]:encountered a dangling reference

Le bot du projet a reproduit le cas. Le type a été supprimé le 2 juin 2026. Le compilateur n'a pas refusé cette API au moment où le modèle l'a écrite. Un outil extérieur, lancé par quelqu'un qui lisait le unsafe caché dans une fonction sûre, l'a fait.

Le billet ajoute 19 régressions connues, corrigées depuis, venues de ce décalage syntaxe / sémantique. Parmi elles, un plafond d'internement de noms de fichiers abaissé par une constante laissée en placeholder (64 à la place de la taille Zig), jusqu'à rendre accessible un accès hors limite que le Zig release ne vérifiait pas de la même façon. Rust a panic. C'est mieux qu'une écriture sauvage. Cette limite trop basse vient de la transition, et la suite de tests a fini par l'atteindre.

Deno montre ce que choisir Rust veut dire

Deno est l'autre runtime. Il a commencé en Go en 2018. Fin 2019, l'équipe le réécrit en Rust. Ryan Dahl l'explique : le garbage collector de Go et celui de V8 se gênaient. La réécriture, fusionnée en janvier 2020, remplace l'ancien binding par Rusty V8.

Là, le système de types sert à quelque chose de précis. Un handle V8 Local est attaché à un scope. Le rendre hors de ce scope ne compile pas. L'exemple en tête du binding est cette imbrication : isolate, HandleScope, contexte, script. Personne n'a demandé à un modèle de coller à un autre langage en interdisant les lifetimes. Les lifetimes sont le sujet.

Même ce choix-là ne rend pas le produit sûr par magie. Deno.ffi construit un buffer JavaScript sur un pointeur venu de l'appelant. Le contrôle qui précède est une permission. Le bloc d'après fait confiance au pointeur :

permissions.check_ffi_partial_no_path()?;// SAFETY: Trust the user to have provided a real pointer, offset,// and a valid matching size to it.let backing_store = unsafe {    v8::ArrayBuffer::new_backing_store_from_ptr(        ptr.offset(offset),        len,        noop_deleter_callback,        ptr::null_mut(),    )};

La phrase « secure by default » désigne le bac à sable, --allow-read, --allow-net, --allow-ffi. C'est une décision d'architecture. Le borrow checker ne la prend pas.

Ce que vous pariez vraiment

Bun est la démonstration publique qu'un LLM peut faire la transition d'un très gros programme vers Rust. Elle tient parce que l'auteur du système était dans la boucle, que la suite de tests compte plus d'un million d'assertions, et que la transition a eu le droit de rester proche du Zig. Elle ne dit pas qu'une équipe qui ne lit pas un lifetime peut démarrer son produit en Rust « puisque Claude compile ».

Sans cette lecture, le modèle fera ce que le guide de Bun a dû lui interdire, puis rattraper :

  • il clone jusqu'à ce que l'emprunt disparaisse ;
  • il pose unwrap() au milieu d'une chaîne qui avait l'air de gérer les erreurs ;
  • il enferme l'état dans Rc<RefCell<T>> ou Arc<Mutex<T>>, et le panic ou le deadlock arrive au runtime ;
  • il pose 'static ou Box::leak pour tuer un lifetime ;
  • il marque unsafe une fonction sûre, avec un commentaire qui répète la doc sans tenir l'invariant.

Ces réflexes sont décrits, hors Bun, comme des échecs typiques d'agents. Le clone pour faire taire le borrow checker est un anti-pattern documenté par la communauté. Bun montre les mêmes familles à l'échelle d'un runtime : fuite pour satisfaire une durée de vie, assertion effacée dans le binaire, API sûre dont le contrat est un commentaire.

Le code qui en sort attire ensuite un profil précis. Quelqu'un qui sait dire si un Box::leak a un propriétaire, si un debug_assert! existe encore en production, si un SAFETY est une preuve ou une phrase. Ce profil était le coût que le langage imposait avant les LLM. Le modèle ne l'a pas supprimé. Il l'a déplacé vers la revue, au moment où le dépôt est déjà gros.

Pour un CRUD, ce déplacement n'achète pas la baisse de failles mesurée chez Android. Il achète un compilateur strict et une dette d'ownership. Les failles qui restent, droits, injection, logique métier, sont celles que le langage ne voit pas. Nous les détaillons pour le code généré dans les plus grosses failles du vibe coding et les coûts cachés.

Rust reste le bon choix quand le problème est celui de Deno ou celui qui a poussé Bun à quitter Zig : une frontière avec C ou C++, des durées de vie que le type peut exprimer, un budget mémoire ou de latence que le garbage collector ne tient pas. Dans ce cas, l'équipe qui écrit doit savoir lire ce que le modèle propose. Le compilateur est alors un allié. Utilisé comme un juge à qui l'on présente des stubs jusqu'à ce qu'il acquitte, il produit exactement les programmes ci-dessus.

Cette complexité a un autre coût. Lifetimes, emprunts, clones et blocs unsafe occupent l'attention, celle du lecteur comme celle du modèle. Une règle métier fausse, un droit trop large, un montant calculé de travers, se retrouve noyée dans le code écrit pour faire taire le compilateur. Le modèle qui a produit ce code est mal placé pour la voir : son effort est passé à obtenir un programme qui compile, pas à vérifier que la règle est la bonne.

Le choix du langage vient du bug que vous voulez rendre impossible. Le LLM vient après, pour accélérer une équipe qui sait encore relire le résultat.

Si votre dépôt compile, fuit, et ne s'explique que par des commentaires SAFETY, le sujet n'est plus le prompt. C'est la reprise du code. AI2H fait ce travail de remise en état sur les bases générées qui ont perdu leur forme.

Prêt à faire performer votre technologie ?

Échangeons sur vos enjeux business. Nos experts vous guident vers des résultats concrets, sans jargon.