Conférence GStreamer 2017 : Oxydation de GStreamer

Posté par (page perso) . Édité par tankey, Davy Defaud, Nÿco, Benoît Sibaud et palm123. Modéré par Pierre Jarillon. Licence CC by-sa
Tags :
35
7
nov.
2017
Audiovisuel

Rustifiez votre multimédia !
Voici une traduction de la présentation Oxidising GStreamer — Rust out your multimedia!, qui a eu lieu le 22 octobre 2017 à Prague dans le cadre de la Conférence GStreamer 2017.

Merci à Sebastian Dröge pour cette présentation et pour son autorisation de traduction.

 Logo de GStreamer Logo de Rust

Sommaire


Résumé

Dans la suite logique de ma présentation de l’année dernière, je vais vous tenir au courant de ce qu’il s’est passé depuis autour de GStreamer pour le développement d’applications et d’extensions en Rust.

C’est maintenant le bon moment pour utiliser Rust dans le cadre de vos développements GStreamer au lieu du C ou du C++ et même à la place de Python ou de C#. Vous bénéficierez ainsi de plus de sûreté, d’une meilleure productivité et pourquoi pas d’un certain plaisir à développer, tout en conservant un haut niveau de performance et un faible surcoût qui ne se rencontrent généralement qu’en utilisant le C ou le C++ et avec en plus la possibilité d’exécuter votre application sur des équipements embarqués.

Alors que l’apprentissage d’un nouveau langage peut sembler superflu et qu’il y a trop de langages de nos jours, je vais vous montrer pourquoi vous devriez vous en préoccuper et pourquoi le langage en question semble être un candidat parfait pour les applications multimédias et pour plein d’autres cas d’utilisations sur environnements embarqués. Je vais vous indiquer comment vous mettre le pied à l’étrier et vous présenter quelques courts exemples de code.

Sebastian Dröge est un développeur de logiciels libres et l’un des mainteneurs et développeurs principaux de GStreamer. Il est impliqué dans le projet depuis plus de dix ans. Il contribue également à plusieurs autres projets tels que Debian, GNOME et WebKit. Après son _master en sciences des Systèmes d’Information à l’Université de Paderborn en Allemagne, il commence à travailler en tant qu’entrepreneur autour de GStreamer et des technologies connexes. Sebastian est l’un des fondateurs de Centricular, une société proposant des services de conseil. Depuis son nouveau domicile en Grèce, il travaille à l’amélioration de GStreamer et de l’écosystème du logiciel libre en général._

En plus des sujets relatifs au multimédia, Sebastian s’intéresse au traitement numérique du signal, aux langages de programmation, à l’apprentissage automatique, aux protocoles réseau et aux systèmes distribués.

Introduction, Qui ?

Aujourd’hui, nous allons parler de l’oxydation de GStreamer ou comment « rustifier » votre multimédia. Je suis Sebastian Dröge, je participe au développement de GStreamer depuis 2006 environ. Actuellement, je suis en charge de la préparation des versions. J’ai touché à pratiquement tout dans GStreamer. Je suis aussi l’un des cofondateurs de Centricular. Nous proposons du conseil autour de GStreamer et des logiciels libres de cette catégorie. C’est tout ce que j’ai à dire à mon sujet, passons au contenu.

Quoi ?

Je vais vous parler de Rust et de GStreamer. Je suppose que certains d’entre vous ne connaissent pas Rust, je vais donc commencer par une courte introduction. Si vous souhaitez approfondir, rendez‐vous sur le site Web, la documentation est de très bonne qualité. Je ne vais pas vous expliquer ce qu’est GStreamer, je pense que vous le savez déjà.

J’ai déjà eu l’occasion de vous parler de ce sujet l’année dernière, je vais donc évoquer ce qu’il s’est passé depuis, puis entrer un peu plus dans les détails sur la manière de développer des applications et des extensions GStreamer avec Rust. Je vous montrerai aussi quelques exemples de code. Enfin, je vous parlerai de ce à quoi on peut s’attendre pour l’avenir.

Qu’est‐ce que Rust ?

Définition

C’est « un langage de programmation système qui vise à garantir un typage sûr et une gestion sûre de la mémoire ». Les développements ont commencé chez Mozilla. Tout est open source. De nos jours, le langage bénéficie d’une large communauté et une bonne proportion des développeurs n’est plus employée par Mozilla. C’est une communauté assez diversifiée. La conception du langage suit les pratiques du développement open source. Ils utilisent un processus d’appel à commentaires (Request For Comments) pour toutes les modifications du langage. Chaque membre de la communauté peut apporter des éléments à la discussion, c’est très transparent… ça me plaît bien !

Qu’est‐ce que ces mots signifient au juste et plus particulièrement « gestion sûre de la mémoire » ? En fait, c’est un peu l’argument de vente de Rust. On sait tous ce qui peut arriver en C, si on déréférence le mauvais pointeur ou si l’on dépasse les limites d’un tableau : le truc part à veau‐l’eau. Rust empêche cela. Le typage est aussi beaucoup plus sûr qu’en C. Le C apporte un peu de sécurité pour le typage, mais bon… passons !

Selon moi, la caractéristique la plus importante est qu’il s’agit d’un langage de programmation système. Il travaille vraiment près du matériel, il n’y a pas de gros environnement d’exécution. Tout ce que vous écrivez tourne vraiment sur le processeur. Toutes les abstractions que le langage propose sont sans surcoût, du moins c’est la manière dont ils en font la pub, et je le crois aussi. Mais je dirais que c’est à vous de le vérifier par vous‐même.

Une autre caractéristique importante est l’existence d’un système très puissant permettant l’interopérabilité avec des bibliothèques écrites en C. Quel que soit l’existant écrit en C dont vous disposez, vous pouvez l’utiliser depuis votre code écrit en Rust.

De la programmation bas niveau avec un air de programmation haut niveau

Si vous regardez le code, cela ressemble parfois à ce que vous écririez en JavaScript ou en Python ou avec un autre langage d’orchestration (scripting). Ça peut sembler de plus haut niveau que ce que vous obtiendriez en C++. Néanmoins, on parle bien de programmation bas niveau : vous conservez le contrôle sur tout si vous le souhaitez. Vous pouvez décider d’où allouer la mémoire — doit‐on utiliser le tas ou la pile — de quand allouer, de quand désallouer. Mais l’important, c’est que vous n’avez pas à vous en soucier. Vous pouvez le faire… vous pouvez aussi, dans une certaine mesure, l’ignorer. Ça n’implique pas une gestion manuelle de la mémoire, c’est le compilateur qui s’assure que vous n’êtes pas en train de faire quelque chose de stupide.

Comme je l’ai dit, Rust ressemble un peu à Python ou à d’autres langages d’orchestration. L’une de ses caractéristiques importantes est l’inférence de type : vous n’êtes pas obligés de préciser systématiquement le type des variables. Généralement, le compilateur se débrouille tout seul, tout en vous garantissant un typage sûr. C’est quelque chose dont le C++ dispose aussi aujourd’hui. Vous avez également d’autres caractéristiques que l’on trouve plutôt dans les langages d’orchestration ou les langages fonctionnels, mais qui sont disponibles pour de la programmation système.

Domaines

Rust est actuellement utilisé pour :

  • du développement d’applications Web. Qui essaierait de développer des applications Web en C aujourd’hui ? Personne ? En revanche, vous pouvez en écrire en Rust ;
  • on l’utilise aussi pour le développement de jeux, la programmation de serveurs, etc ; en gros, tous les domaines pour lesquels vous utiliseriez le C++ ;
  • des gens écrivent aussi des systèmes d’exploitation en Rust, il y en a actuellement deux ou trois en cours d’écriture ;
  • certains l’utilisent pour des développements vraiment très proches du matériel, comme de la programmation de micro‐contrôleurs.

Rust couvre tout le spectre des cas d’utilisation pour lesquels vous utiliseriez un langage de programmation système.

Par ailleurs, Rust est désormais utilisé et poussé par des acteurs de l’industrie. Vous pouvez consulter cette page pour découvrir les sociétés qui déclarent publiquement utiliser Rust. Il y a des noms tels que Dropbox, npm, Atlassian, Coursera, Canonical et bien d’autres.

Rust est ce que le C++ aurait dû être

Selon moi, Rust est ce que le C++ aurait dû être. Bon, un certain nombre d’années se sont écoulées, les gens ont appris de l’histoire. Aujourd’hui, je l’utiliserais pour n’importe quel projet pour lequel j’aurais utilisé le C++ ou même le C auparavant.

Pourquoi Rust ?

De la difficulté d’écrire du code sûr en C et en C++

Il est difficile d’écrire du code sûr en C et en C++. Toute personne pouvant dire qu’elle n’a pas subi d’erreur de segmentation durant l’année écoulée n’a probablement programmé ni en C, ni en C++. Vous pouvez facilement faire quelque chose qui fera planter votre système et permettra à un attaquant de prendre le contrôle de la machine de l’utilisateur, sans même vous rendre compte que ce que vous avez écrit n’est pas sûr.

La gestion des erreurs est plutôt compliquée avec ces deux langages. En C, on s’arrange généralement avec les valeurs de retour, vous pouvez facilement les ignorer et personne ne s’en offusquera ! Ça va juste échouer à l’exécution avec des symptômes bizarres. En C++, vous avez bien les exceptions, qui bénéficient de leur propre lot de problèmes. En général, ça entraîne une certaine verbosité qui finit par masquer ce que le programme fait vraiment et rend la tâche encore plus difficile pour faire en sorte que le code soit sûr.

Laissez le compilateur vous aider à écrire du code correct et rapide

Il y a un inconvénient à cela : le compilateur risque de beaucoup se plaindre ! Il vous dira des choses comme : « non, tu ne peux pas faire ça, ce n’est pas sûr ». Ça va vous prendre plus de temps avant de pouvoir exécuter votre code. Mais une fois qu’il s’exécute, vous savez qu’un gros paquet de problèmes ne vont pas se présenter parce que le compilateur vous aura déjà prévenu : « ici, ici et là, tu ne peux pas faire ça, tu devrais faire ça autrement ». Avec le C ou le C++, en général, on fait cela à l’exécution et on sait tous combien il est pénible de déboguer des trucs à l’exécution.

Rust utilise un modèle très puissant de possession et de mutabilité. Il identifie en permanence le possesseur d’une variable et trace si celle‐ci est accessible en lecture seule ou en lecture et en écriture. Par défaut, toute variable est accessible en lecture seule. C’est le contraire de ce que font les C et C++. Avec ces langages, vous pouvez préciser qu’une variable est constante, mais par défaut elle est visible en lecture et en écriture. Rien qu’avec cette approche (chaque variable en lecture seule par défaut) le code est déjà plus sûr et il est plus difficile d’introduire des bogues.

Par défaut avec Rust, le code doit respecter les contraintes de sûreté. Ce qui veut dire que si vous ne sortez pas du bac à sable, votre programme ne plantera pas… Du moins, il ne produira pas d’erreur de segmentation. Vous pouvez toujours introduire des erreurs de logique qui feront que votre programme ne fera pas ce qu’il était censé faire, mais au moins il ne plantera pas.

On bénéficie aussi d’une certaine sécurité pour les fils d’exécution. Vous indiquez si votre type de donnée est sûr pour le partage entre fils d’exécution et/ou s’il peut être transféré d’un fil d’exécution à l’autre. Le compilateur se chargera ensuite de vérifier que ces contraintes sont bien respectées.

Comme je le disais dans la section relative aux erreurs, Rust vérifie la bonne prise en compte des erreurs. Vous ne pouvez pas ignorer les erreurs, vous devez les gérer. Mais la mécanique qui permet de le faire est telle que ça en devient sympa. Ça n’a rien à voir avec ce que l’on doit faire en C : « si ceci échoue, nettoie ceci et sors, si ceci échoue, nettoie cela et sors »… Dans le cas de Rust, c’est vraiment pratique à utiliser.

L’issue de secours : unsafe

Il y a une issue de secours : le mot clé unsafe. Dès qu’il introduit une section de code, vous pouvez faire ce que vous voulez dans cette section, comme vous le feriez en C. Vous pouvez déréférencer des pointeurs, vous pouvez créer des pointeurs à partir de nombres, tout ce qui vous passe par la tête. L’idée principale est que c’est déclaratif. Vous savez alors que vous avez une petite partie de votre code qui n’est pas sûre. Si ensuite votre programme plante, ça viendra de là ! Et si vous voulez relire votre code, vous saurez que ce sera par là qu’il faudra commencer.

Un langage de haut niveau qui n’est pas juste une glorification de l’assembleur

… comme c’est le cas pour le C.

Une autre différence notable par rapport au C est que Rust dispose d’une bonne bibliothèque standard. Elle contient tout un tas de structures de données. On sait tous qu’en C, la bibliothèque standard ne comporte aucune structure de données. Chacun développe ses propres listes chaînées, ses propres tableaux associatifs. La bibliothèque standard de Rust fournit des implémentations très efficaces de toutes les structures de données, d’algorithmes, etc.

Avec tous les langages d’orchestration récents, on trouve des petits outils pour récupérer les dépendances. Pour Nodes.js vous avez npm, pour Python il y a pip. Rust propose la même chose, vous pouvez facilement dire : « je veux telle et telle bibliothèques » et l’outil ira chercher les versions dont vous avez besoin, les compilera et les inclura lors de la construction de votre propre logiciel. Cet outil facilite vraiment l’utilisation des dépendances externes. Ça ne tourne pas au cauchemar comme en C ou en C++ où chaque chose fonctionne à sa manière et où il est très difficile de dépendre de quoi que ce soit. En fin de comptes, ça a poussé pas mal de bibliothèques en C et en C++ à réimplémenter plein d’algorithmes qui existaient déjà partout ailleurs… Mais bon, c’est tellement compliqué de tirer ces dépendances que l’on réimplémente les fonctions nous‐mêmes.

Pourquoi devrait‐on s’en préoccuper ?

L’analyse syntaxique de formats de médias

… venant de sources non sûres.

Je passerai assez vite sur le sujet, nous sommes déjà tous au courant des problèmes. L’analyse syntaxique (parsing) des formats multimédias n’est pas chose facile, particulièrement en C ou en C++. Par ailleurs, les données en entrée viennent rarement de sources de confiance. Certaines sources peuvent même proposer des médias qui contiennent intentionnellement quelque chose qui peut nuire à votre machine en exploitant des problèmes dans les logiciels que vous utilisez. C’est pour cela que je pense que, pour nous, Rust serait un bon langage. Il permet d’éviter beaucoup de problèmes de sécurité courants. Tous les problèmes de sécurité que nous avons rencontrés dans GStreamer les années passées auraient pu être évités en utilisant Rust. Je pense que c’est déjà assez éloquent.

Et donc, l’analyse syntaxique est compliquée en C. Il n’y a pas beaucoup de moyens d’abstraction, alors qu’avec Rust on dispose de tout un tas de possibilités pour représenter des abstractions. Il est même possible d’écrire des grammaires formelles. Du moins, quelque chose qui ressemble à de la grammaire formelle. Le code correspondant est ensuite généré automatiquement, ce qui rend la compréhension de ce que fait ce code vraiment aisée en comparaison à du code écrit à la main.

La maîtrise des fils d’exécution concurrents est difficile

… surtout en C !

Rust dispose de tout un tas de fonctionnalités pour nous aider lorsque l’on développe dans un environnement à plusieurs fils d’exécution. GStreamer exploite massivement les fils d’exécution concurrents, ça pourrait donc être une bonne idée…

Programmer comme si on était en 2017

Et par‐dessus tout, nous programmerions comme si nous étions en 2017 et non dans les années 60 du siècle dernier :

  • les langages modernes offrent toute une variété de fonctionnalités et d’outils ;
  • nous ne sommes plus obligés de réinventer des ustensiles de base comme GObject ;
  • nous pourrions attirer de nouveaux développeurs. Plus personne n’a vraiment envie d’apprendre le C de nos jours et encore moins des choses comme GObject, qui entraîne plein de copier‐coller partout. C’est plutôt difficile à prendre en main pour les nouveaux contributeurs.

Cependant, ce n’est pas la panacée

Tout code non trivial comporte des bogues. Mais au moins, une importante classe de bogues peut être évitée.

État, il y a un an

Les bindings GStreamer

L’année dernière, nous disposions d’un ensemble de bindings Rust pour GStreamer. Cet ensemble était écrit à la main et ne s’intégrait pas très bien avec le reste du code Rust. Par ailleurs, il imposait aux utilisateurs d’utiliser des sections avec le mot clé unsafe, ce qui est tout simplement rédhibitoire. Il divergeait aussi vis‐à‐vis de certains concepts de GStreamer, ce qui le rendait difficile à appréhender et il était incomplet. Pas mal de gens utilisaient ces bindings, mais ce n’était vraiment pas idéal.

Nous avions aussi ce dont je vous parlais la dernière fois : une manière expérimentale d’écrire des extensions GStreamer. Ce projet était également écrit à la main, il était très incomplet et difficile à faire évoluer. Oublions tout cela, nous sommes en 2017 maintenant !

Écrire des applications GStreamer avec Rust

Les nouveaux bindings GStreamer

Comparaison par rapport aux anciens bindings

Les nouveaux bindings Rust pour GStreamer sont en grande partie générés à partir des informations d’introspection de GObject. Malheureusement, certaines choses ne peuvent pas être générées automatiquement. Mais, globalement, ça facilite bien l’évolutivité. Ils remplacent complètement les anciens bindings. Ils ne sont pas compatibles au niveau interface de programmation, mais ça ne devrait pas être difficile de modifier un projet basé sur les anciens bindings pour qu’il utilise les nouveaux.

Une évolution importante du point de vue des applications est qu’il n’est plus nécessaire de passer par du code marqué unsafe… Sauf si le développeur le veut, bien sûr, ou s’il veut s’intégrer avec X11, qui n’est pas sûr par conception, mais c’est un autre problème !

Les nouveaux bindings couvrent pratiquement tous les composants du cœur de GStreamer, ainsi que ceux des autres bibliothèques. Ils s’intègrent bien avec les bibliothèques des infrastructures GLib et GTK, ce qui les rend sympas à utiliser ensemble.

Un style idiomatique rustien (en grande partie)

Les bindings sont écrits dans un style idiomatique rustien. Ils devraient être faciles à prendre en main par des développeurs habitués à Rust. Ils auront l’impression qu’il s’agit d’une vraie interface de programmation Rust. Et à la fois, les concepts des bindings correspondent pratiquement trait pour trait aux concepts de GStreamer. Ce qui signifie que vous, en tant que développeurs GStreamer, aurez seulement à apprendre un nouveau langage. En revanche, tous les concepts de GStreamer, c’est‐à‐dire comment l’interface de programmation fonctionne, restent les mêmes.

Les objets

Regardons rapidement quelques‐uns des points d’entrée des bindings. Dans GStreamer, nous avons tout un tas d’objets : les Elements, les Pads, le Pipeline, l’objet Clock, etc. La manière dont ils sont représentés dans les bindings est telle que vous retrouvez précisément les mêmes objets. Ils disposent d’une sorte de mécanisme semi‐automatique et sûr de comptage de références. Vous contrôlez toujours quand le compteur d’une référence est incrémenté, mais le compilateur se charge de vérifier que vous l’incrémentez bien quand c’est nécessaire. Ceci vous permet d’être alertés si vous faites des choses bizarres que vous n’auriez pas voulu faire. Donc, ce n’est pas complètement automatique, mais d’un autre côté, vous ne pouvez pas oublier de déréférencer quoi que ce soit. Quand un objet n’est plus dans la portée courante, quand il n’est plus nécessaire, il est automatiquement déréférencé et détruit au moment voulu.

Dans GStreamer, nous utilisons l’héritage. Rust ne gère pas l’héritage : il n’y a pas de classes, ni de vraie notion d’héritage. À la place, on peut utiliser des traits. Cela peut sembler un peu compliqué, un peu bizarre, mais si vous utilisez l’interface Rust, vous allez vous rendre compte qu’elle fonctionne de la même façon que si vous utilisiez, par exemple, les bindings C++ de GStreamer. Ça donne une sensation de quelque chose de naturel.

Comme je l’ai déjà dit, il y a une sorte de sûreté des fils d’exécution (thread safety) et elle est contrôlée à la compilation. Par exemple, on sait tous que les Elements de GStreamer sont censés être thread safe, on peut donc les échanger entre fils d’exécution et on peut les partager entre plusieurs fils d’exécution. Le compilateur n’ira pas se plaindre de tels agissements. En revanche, quelque chose comme GstAdapter n’est pas thread safe. Donc, si vous tentez de l’utiliser dans d’autres fils d’exécution, vous allez devoir le migrer, car vous ne pouvez pas en disposer depuis deux fils d’exécution différents. Si vous faites cela, le compilateur vous dira : « non, tu ne peux pas faire ça ». C’est une caractéristique plutôt sympa.

Vous disposez également de toute l’interface standard GObject de GStreamer. Vous retrouverez : les propriétés, les signaux, toutes les méthodes… Tout ce dont vous avez besoin.

J’aurais dû mentionner ceci auparavant : tout est garanti contre l’utilisation inappropriée de pointeurs NULL. Il n’y a aucun pointeur NULL caché. Vous devez gérer explicitement tout ce qui peut être NULL, vous ne pouvez pas ignorer ce genre de problèmes. Donc, dans toute l’interface, lorsque quelque chose renvoie un Element, il y aura bien un Element derrière. Il est impossible qu’il ne soit pas disponible. Ça ne peut pas planter ensuite, vous ne pouvez pas vous retrouver face à un pointeur NULL. Et, bien sûr, si l’appel est susceptible d’échouer, vous devez gérer ce cas. Le compilateur ne vous laissera pas utiliser la valeur retournée sans avoir vérifié au préalable qu’elle est bien valide.

Les MiniObjects

Les MiniObjects sont probablement le recoin le plus obscur de GStreamer. Ils sont générés automatiquement dans la plupart des autres bindings, ce qui les rend un peu inhabituels vis‐à‐vis du langage cible. Avec Rust c’est différent : la plupart des concepts autour des MiniObjects correspondent à ce que l’on fait déjà en Rust. Par exemple, le concept de mutabilité des MiniObjects est tel que vous ne pouvez modifier un MiniObject que s’il n’est utilisé qu’à un seul endroit à un instant donné, sinon vous devez le copier. C’est quelque chose qui trouve un équivalent direct en Rust. Le compilateur vous empêchera de modifier des MiniObjects qui ne sont pas en mode écriture. À titre de comparaison, en C on a pas mal de code qui modifie des MiniObjects alors qu’ils ne sont pas en mode écriture. L’approche de Rust me semble présenter un avantage significatif.

Les types basés sur les MiniObjects paraissent aussi typiquement rustiens. Ils comprennent les Caps, les Structures, même quelque chose comme GstFraction, qui semblait un peu bizarre à l’utilisation en Python par le passé, ressemble vraiment à quelque chose que vous auriez utilisé en Rust.

Qu’est‐ce qui manque ?

J’ai dit que la plupart du cœur de GStreamer était déjà disponible. Il manque encore :

  • GstMemory, GstAllocator, GstMeta et GstCapsFeatures, non pas parce qu’ils présentent une difficulté, mais plutôt parce que je n’en ai pas encore eu l’utilité et apparemment personne n’en a eu besoin pour le moment ; donc, si quelqu’un rencontre un problème pour lequel le besoin se fait sentir, criez, on pourra les ajouter à ce moment‐là ;
  • il manque aussi des trucs mineurs comme les TypeFinders ; ils sont du même acabit : faciles à ajouter en cas de besoin ;
  • les objets dans la catégorie des GstControlBindings ;
  • à part pour celles du cœur, les bibliothèques des autres modules ne sont pas entièrement couvertes. Mais, par exemple, pour la bibliothèque Audio, GstAudioInfo est disponible, pour la bibliothèque Video, vous avez déjà VideoInfo et VideoFrame. Il y aussi GstAdapter, AppSrc et AppSink.

Tout ce dont vous avez habituellement besoin est donc disponible et, pour le reste, criez et on l’ajoutera !

Est‐ce utilisable ? Oui !

Selon moi, tout est déjà très utilisable. Je l’utilise moi‐même pour pas mal d’applications de test afin de m’assurer que tout fonctionne correctement ou pour écrire des cas de test quand quelque chose ne marche pas. De part mon expérience, je peux dire que c’est facile et généralement plus rapide à écrire que l’équivalent en C. C’est facile à déboguer, ou du moins aussi facile à déboguer que le C. Vous pouvez toujours utiliser gdb, obtenir la pile d’appels, scruter les variables, etc. Ça marche de la même manière qu’avec du C. Et généralement, ça donne plus ou moins le même code machine que ce que vous obtiendriez avec du C. Si vous lisez ce que le compilateur produit, il est assez facile de savoir ce qui se passe, comment les correspondances avec le C se concrétisent. Donc c’est prêt à être utilisé !

Quelques exemples de code

Création d’Elements

Voici un exemple de création d’Elements. Ça ne devrait pas être trop difficile à lire :

let pipeline = gst::Pipeline::new(None);                  // 1

let src = gst::ElementFactory::make("filesrc", None)
    .ok_or(MyError::ElementNotFound("filesrc"))?;         // 2
let dbin = gst::ElementFactory::make("decodebin", None)
    .ok_or(MyError::ElementNotFound("decodebin"))?;
  • En 1, on génère un nouveau Pipeline. Ici, on ne lui donne pas de nom particulier. En C, on passerait NULL, en Rust on indique None. On récupère toujours un Pipeline. Si vous regardez l’interface en C, vous verrez que ça ne renvoie jamais NULL, donc on récupère vraiment un Pipeline.
  • En 2, on veut construire une variable src. On utilise donc gst::ElementFactory::make, ce qui est très similaire à ce que vous feriez en C. L’objectif est de créer une source sur un fichier avec l’extension filesrc. Bien sûr, il peut arriver que l’extension filesrc ne soit pas disponible sur votre machine, cette fonction est donc susceptible d’échouer en renvoyant None. Vous pouvez indiquer comment gérer les différents cas avec la fonction ok_or : « si tout est ok, alors renvoie ce que la fonction gst::ElementFactory::make a généré, sinon renvoie un type d’erreur dédié ». Pour cela, vous pouvez créer une sorte d’énumération d’erreurs et préciser les différents cas tels que ElementNotFound, à quoi vous pouvez ajouter une variable. Dans notre cas, on a indiqué le nom de l’Element. À la fin de la ligne, on trouve un point d’interrogation. Il est là pour indiquer ceci : « en cas d’échec, sors de la fonction et renvoie l’erreur en question ». L’appelant peut donc gérer l’erreur et prendre les mesures appropriées, mais dans la fonction en question on s’arrêtera là en cas d’erreur. Bien sûr, il n’y aura pas de fuite mémoire, tout ce qui doit être déréférencé et détruit le sera automatiquement.

Création de Caps

Voici maintenant un exemple de création d’un objet Caps :

let caps = gst::Caps::new_simple(                   // 1
    "video/x-raw",                                  // 2
    &[                                              // 3
        ("format", "BGRA"),                         // 4
        ("width", &(1080i32),                       // 5
        ("height", &(720i32)),                      // 6
        ("framerate", &gst::Fraction::new(30, 1)),  // 7
    ],
);
  • en 1, vous utilisez gst::Caps::new_simple exactement comme en C ;
  • en 2, vous indiquez le nom, rien de spécial à signaler ici ;
  • en 3, vous fournissez un tableau de propriétés qui sont en fait des tuples ;
  • en 5, je pense que ce qu’il faut retenir, c’est que vous n’indiquez pas seulement la valeur pour la propriété, vous devez aussi préciser son type. Assez souvent, c’est quelque chose qui tourne mal en C : les gens fournissent un entier non signé ou un entier sur 64 bits pour une propriété donnée et ça ne fonctionne pas comme prévu ensuite. Ici, vous devez être explicite. En interne, ça ne va pas créer d’objet GValue, si vous lisez le code généré, il sera très similaire à ce que vous auriez obtenu avec du C.

Question d’un membre du public :

— L’esperluette est nécessaire dans ce cas ?
— En fait, l’esperluette n’est pas indispensable ici. L’esperluette crée une référence, c’est comme un pointeur dont la valeur ne peut pas être NULL.
— Et donc, elle indique si ta constante est statique ou non ?
— En fait… Bon, parlons‐en après, cela nécessiterait d’entrer un peu plus dans les détails. En bref, l’esperluette introduit une référence et les références ne peuvent jamais être NULL. Le compilateur s’assure que cette valeur est toujours valide au moment où tu l’utilises. Donc, tu ne peux pas libérer ce qui en dépend, sinon ça ne sera pas accepté à la compilation. Mais, la raison exacte pour laquelle nous avons besoin d’une esperluette ici nécessiterait une explication dans laquelle je ne souhaite pas m’engager maintenant. Ça a un rapport avec le fait que chacun des éléments présente un type différent : en 4 il s’agit d’une chaîne de caractères, en 5 et 6 ce sont des entiers signés, en 7 on trouve une fraction.

N. D. L. T. : À ce stade de la présentation, Sebastian commence à être à court de temps. Il décide donc de passer quelques exemples. Vous pouvez les retrouver dans les planches au format PDF.

Le signal pad-added

Le signal pad-added fait partie des choses utilisées fréquemment. Voici un court exemple utilisant ce signal :

let pipeline = _;                                           //  1
decodebin.connect_pad_added(move |dbin, src_pad| {          //  2
    let sink = gst::ElementFactory::make(                   //  3
        "fakesink",                                         //  4
        None                                                //  5
    ).unwrap();                                             //  6
    pipeline.add(&sink);                                    //  7

    let sink_pad = sink.get_static_pad("sink").unwrap();    //  8
    src_pad.link(&sink_pad);                                //  9

    sink.sync_state_with_parent();                          // 10
});
  • En 2, on utilise un decodebin qui a été créé auparavant. On le connecte au signal pad-added en lui passant une fermeture (closure). En C, vous auriez à définir une nouvelle fonction, sa déclaration, tout cette verbosité… Ici, vous pouvez écrire la fonction directement avec la connexion au signal. Tout ce qui est disponible en amont peut être capturé dans la fermeture. Par exemple, le pipeline défini en ligne 1 est utilisé à l’intérieur de la fermeture à la ligne 7. Rust s’assure que tout ce qui est capturé est utilisé en respectant les règles de sûreté. Vous pouvez être amenés à devoir copier les objets pour les utiliser à l’intérieur d’une fermeture parce qu’il peut y avoir des cas pour lesquels l’utilisation du même objet ne serait pas sûre. Mais, généralement, c’est très pratique à utiliser et vous pouvez simplement écrire vos gestionnaires de signaux directement sans code superflu. Vous vous demandez peut‐être à quoi correspond ce truc avec les | en ligne 2. C’est la façon dont on définit une fermeture : vous retrouvez les arguments decodebin et src_pad qui correspondent à ceux passés lors de l’invocation du signal pad-added.
  • À l’intérieur de la fermeture, on crée un fakesink (lignes 3 à 6),
  • que l’on ajoute au pipeline (ligne 7).
  • On récupère ensuite le pad « sink » (ligne 8), que l’on relie au pad « src » du decodebin (ligne 9).
  • Finalement en 10, on lance la synchronisation.

Toute personne ayant déjà utilisé GStreamer devrait être en mesure de lire cet exemple et de comprendre exactement tout ce qu’il s’y passe.

Réaction d’un membre du public :

— inaudible
— Ce n’est pas ton cas ?
— Est‐ce que Rust permet à gst_element_sync_state_with_parent de devenir thread safe ? (rires)
— Bien sûr que non, parce que l’implémentation est en C et que cette fonction est défectueuse par conception. Il faut que l’on trouve une meilleure solution à cela. La fermeture que tu passes ici est marquée comme devant respecter des contraintes de sûreté pour l’exécution en environnement à plusieurs fils d’exécution. Donc, tu ne peux pas simplement utiliser une autre référence vers un GstAdapter là‐dedans par exemple, parce que GstAdapter ne respecte pas ces contraintes. Tu ne peux rien utiliser qui ne soit pas thread safe dans cette fermeture, à moins de migrer la variable à l’intérieur, ou dit autrement : à moins que l’unique référence restante sur cet objet se trouve à l’intérieur de la fermeture au final.

Mapping de Buffer

En Python le mapping de tampon est un peu étrange. En Rust, on peut le faire de façon assez élégante :

let mut buffer = gst::Buffer::with_size(320 * 240 * 4).unwrap();    // 1
{                                                                   // 2
    let buffer = buffer.get_mut().unwrap();                         // 3
    let mut data = buffer.map_writable().unwrap();                  // 4

    for p in data.as_mut_slice().chunks_mut(4) {                    // 5
        p[0] = b; p[1] = g;                                         // 6
        p[2] = r; p[3] = 0;                                         // 7
    }                                                               // 8
}                                                                   // 9
  • En 1, on crée un nouveau Buffer. On a besoin d’un Buffer mutable pour pouvoir écrire dedans.
  • Ensuite de 2 à 9, on est obligé d’écrire un truc un peu moche. Il s’agit d’un nouveau bloc qui limite la validité des variables qu’il contient. Elles seront libérées à la fin du bloc et le destructeur sera exécuté.
  • En 4, on mappe le Buffer en écriture. À la fin du bloc, la fonction unmap est exécutée automatiquement.
  • On peut aussi mentionner que le Buffer doit être récupéré sous une forme mutable (ligne 3) avant de pouvoir en extraire une map en écriture. Toutes ces fonctions peuvent échouer, c’est pour cela qu’on utilise unwrap. En cas d’échec, cette fonction termine l’application. Bien sûr, il y a des façons plus élégantes de gérer cela. Je ne les ai pas indiquées parce que ça surchargerait la planche. Il faudrait un peu de code pour gérer le cas d’erreur.
  • De 5 à 8, on itère sur la zone mémoire et on renseigne les valeurs des composantes R, G et B. On peut simplement utiliser les itérateurs de Rust pour parcourir le Buffer par tronçons de 4 octets, c’est ce que l’on peut lire en ligne 5.

Une fois compilé, ça ressemble pratiquement à ce que vous auriez obtenu si vous l’aviez écrit en C.

Quelques liens

  • Les bindings sont actuellement sur mon dépôt GitHub. J’ai l’intention de les déplacer vers freedesktop à un moment donné, mais j’aimerais qu’on migre d’abord vers GitHub_. Je n’ai pas envie de les déplacer deux fois, donc pour le moment ils restent sur GitHub.
  • Vous pouvez trouver plein d’exemples de code ici.
  • La plupart des tutoriels de GStreamer ont aussi été portés ici. Je n’en ai écrit qu’un, les autres ont été portés par des contributeurs. Donc, grâce à eux, vous disposez des tutoriels.

Écrire des extensions GStreamer en Rust

Je vais vous parler rapidement de la manière d’écrire des extensions GStreamer en Rust. Comme je l’ai dit plus haut, l’année dernière nous disposions d’une infrastructure écrite à la main. La plupart de l’infrastructure pour les extensions est toujours écrite à la main, mais ce qui est important c’est qu’elle s’appuie désormais sur les nouveaux bindings Rust de GStreamer. Toute l’infrastructure hors sous‐classes est générée automatiquement et prise en charge par les bindings. Évidemment, toute l’infrastructure pour les sous‐classes, la surcharge des méthodes virtuelles et l’installation des propriétés sont encore écrites à la main.

La différence par rapport à l’année dernière, c’est que vous pouvez maintenant utiliser des méthodes virtuelles et vous pouvez définir des propriétés. Par ailleurs, les gens qui implémentent des Elements ne sont plus obligés d’écrire du code Rust non sûr (déclaré unsafe), ils peuvent le faire, mais ils n’y sont plus contraints.

Le but de tout cela est de pouvoir écrire des extensions GStreamer en implémentant simplement des traits Rust, sans utiliser quoi que ce soit qui ne fasse pas partie du langage, de façon à ce que cela soit très facile pour des gens connaissant Rust d’écrire des extensions GStreamer.

Comme je l’ai dit, une partie du code est encore écrite à la main. Tout cela va être amélioré. Les gens de GNOME travaillent actuellement sur des solutions à base de macros qui font en gros la même chose que les GST_DEFINE_…s, mais d’une manière plus sympa. À l’avenir vous pourrez écrire du code qui ressemblera à du Vala ou du C#, et les macros convertiront cela en quelque chose de similaire à ce que j’ai écrit à la main. Ceci dit, c’est pour plus tard, cela va prendre un peu de temps avant d’en arriver là.

Classes de base disponibles

Actuellement, des classes de base sont disponibles pour :

  • les Elements ;
  • BaseSrc, BasSink et BaseTransform ;
  • Thibault, qui est peut‐être parmi nous, travaille actuellement sur le binding VideoDecoder et il a aussi l’intention d’écrire un décodeur d’images GIF ;
  • il y a une chose que je voudrais souligner ici. Chaque fois que vous faites quelque chose que vous n’êtes pas autorisé à faire, par exemple ce que je faisais tout à l’heure avec unwrap (la création d’un Element pouvant échouer, car il peut ne pas être disponible), et donc si vous utilisez unwrap, en cas d’erreur, Rust va paniquer. Ce que l’on entend par là, c’est que la pile va être déroulée et…

N. D. L. T. : l’organisation informe Sebastian qu’il ne lui reste plus beaucoup de temps.

Elements disponibles

Nous disposons des Elements suivants :

  • démultiplexeur FLV ;
  • source HTTP ;
  • source et « dissipateur » (sink) depuis et vers un fichier ;
  • source et « dissipateur » (sink) depuis et vers Amazon S3 ;
  • il y a quelques jours, j’ai réécrit en Rust l’Element AudioEcho qui existait déjà en C ;
  • et bientôt, si tout va bien, on aura aussi un décodeur d’images GIF (animées).

Une anecdote intéressante : lorsque j’ai écrit l’Element AudioEcho en Rust, il était environ 1,7 fois plus rapide que l’implémentation en C. Ce n’est pas parce que Rust est plus rapide, mais plutôt parce qu’il était plus difficile de faire en sorte que l’équivalent en C soit rapide. J’ai trouvé l’expérience intéressante. Ce n’était pas exactement l’objectif initial lorsque j’ai entrepris la réécriture de cet Element, mais ça montre bien que l’on n’a pas affaire à un langage de haut niveau qui ralentirait les choses, c’est vraiment un langage que l’on peut utiliser pour ce genre de développements.

État actuel

  • les bindings sont encore assez jeunes ;
  • ils sont utilisables pour une implémentation nécessitant les classes de base que je mentionnais plus haut ;
  • les fonctionnalités manquantes seront ajoutées le moment venu.

Si vous voulez écrire des extensions GStreamer en Rust maintenant, lancez‐vous, dites‐moi s’il manque des choses et je les ajouterai ou je vous aiderai à les ajouter. Jusqu’à présent, j’ai pris beaucoup de plaisir à écrire des extensions GStreamer en Rust, j’ai beaucoup plus apprécié cela que d’en écrire en C. Donc, essayez et voyez si c’est le cas pour vous aussi !

N. D. L. T. : Sebastian passe les exemples de code par manque de temps. Vous pouvez les retrouver dans les planches au format PDF.

Quelques liens

Si vous voulez voir à quoi cela ressemble, c’est sur mon dépôt GitHub. Les extensions sont également dans le dépôt.

À venir…

Encore quelques mots à propos de l’avenir : mon objectif est que l’on écrive de plus en plus de code en Rust plutôt qu’en C. Commençons d’abord par les extensions externes. Plus tard, on pourra aussi envisager de remplacer le code du cœur. Mais, ça, c’est pour l’avenir. Il n’y a pas de raison de se faire du souci pour le moment. Nous pouvons faire tout cela de façon itérative en remplaçant des petites portions de code grâce au système FFI de Rust qui rend les choses vraiment faciles pour s’intégrer dans du code C existant. Vous pouvez remplacer un simple module ou même une simple fonction si vous le souhaitez.

Globalement, je voudrais faire en sorte que les bindings soient mieux finis. Que l’infrastructure pour écrire des extensions soit mieux finie aussi, plus complète, mais bon, je suppose que c’est assez évident.

Et le plus important, c’est que j’aimerais faire en sorte que plus de personnes s’intéressent et s’impliquent dans l’utilisation de Rust, ce qui était le but principal de cette présentation. J’espère avoir atteint mon objectif. Jetez‐y un œil, essayez‐le, j’espère que vous ne le regretterez pas. Moi, je ne le regrette pas ! Plus généralement, je dirais : n’écrivez plus de projets en C, c’est une mauvaise idée. Regardez tous les CVE des projets à base de C… Ce n’est pas une bonne idée !

Merci / Questions ?

On n’a plus le temps pour des questions, donc passez me voir après. Voici encore quelques liens. Il y a pas mal d’informations sur le site Web de Rust, la documentation est de très bonne qualité, vous y trouverez probablement toutes les réponses aux questions que vous vous posez :

Merci !

  • # Merci pour ce témoignage

    Posté par . Évalué à 5 (+5/-0).

    J'ai moi-même sauté le pas il y a un an (je ne veux plus utiliser C/C++ pour de nouveaux projets) ; mais lire le témoignage concret et détaillé d'un développeur expérimenté sur les avantages du Rust est très intéressant.

    Le langage devient suffisement mature (des choses manquantes étaient vraiment gênantes auparavent, je pense notamment au fait de ne pas pouvoir utiliser d'autres paramètres génériques que des types). Je pense vraiment que de plus en plus d'entreprises vont l'utiliser. D'ailleurs, si quelqu'un a du travail à proposer, je veux bien quitter mon boulot où je fais du C# toute la journée :p

  • # Pas encore de gestion des erreurs d'allocation mémoire

    Posté par (page perso) . Évalué à 8 (+9/-3). Dernière modification le 09/11/17 à 09:32.

    Soit un mini programme :

        use std::io;
        use std::io::Read;
        use std::fs::File;
    
        fn readfile() -> Result<String, io::Error> {
            let mut f = File::open("hello.txt")?;
            let mut s = String::new();
            f.read_to_string(&mut s)?;
            Ok(s)
        }
    
        fn main() {
            let bla = readfile();
            match bla {
                Ok(_) => println!("success"),
                Err(e) => println!("failure {}", e),
            }
        }

    Le fichier hello.txt fait 10 Mo, et je limite la mémoire du programme à 20 Mo avec "ulimit -v 20000". Résultat : le runtime Rust affiche "fatal runtime error: allocator memory exhausted", puis le programme Rust plante avec le signal SIGILL (Illegal instruction).

    Sur IRC, on m'apprend que Rust n'a pas encore implémenté la gestion d'erreur sur les allocations mémoires. Ah. C'est dommage ça.

    De là à appuyer tout l'argumentaire de Rust sur "les programmes écrits en C et C++ peuvent planter, un code écrit en Rust ne peut pas planter", je trouve que ça méritait quelques précisions :-) (Par planter je veux dire : signal fatal genre SIGSEGV… ou SIGILL.)

    • [^] # Re: Pas encore de gestion des erreurs d'allocation mémoire

      Posté par (page perso) . Évalué à 4 (+3/-0).

      Par planter je veux dire : signal fatal genre SIGSEGV… ou SIGILL.

      Note bien que SIGSEGV c'est le cas idéal du code qui fait n'importe quoi avec la mémoire : ça veut dire que ton OS s'en est rendu compte. Il y a bien plus insidieux, c'est quand ton programme corrompt la mémoire silencieusement dans ton dos, et là t'es dans la mouise. Le système de types de Rust ne dit rien à propos d'un signal fatal, et plus généralement que ton programme est correct par rapport à une spécification qui n'existe que dans la tête du développeur (quand elle existe). Par contre, il te garantit le même genre de sûreté que les langages à GC, sans avoir de GC lui-même, ce qui n'a rien à voir avec 'planter'.

    • [^] # Re: Pas encore de gestion des erreurs d'allocation mémoire

      Posté par (page perso) . Évalué à 7 (+5/-0).

      Ton commentaire est intéressant, mais tu présentes les choses de manière un peu provocante dans cette phrase:

      De là à appuyer tout l'argumentaire de Rust sur "les programmes écrits en C et C++ peuvent planter, un code écrit en Rust ne peut pas planter", je trouve que ça méritait quelques précisions

      L'interview parle plutôt de bugs étranges et de failles de sécurité, plutôt du fait que ça ne plante jamais. Par exemple à propos de C/C++:

      Vous pouvez facilement faire quelque chose qui fera planter votre système et permettra à un attaquant de prendre le contrôle de la machine de l’utilisateur, sans même vous rendre compte que ce que vous avez écrit n’est pas sûr.

      Ça va juste échouer à l’exécution avec des symptômes bizarres.

      Et dans ton cas il n'y a ni de faille de sécurité (enfin jusqu'à preuve du contraire), ni de symptôme bizarre (un message nous indique clairement le problème—en revanche oui le SIGILL est pour le moins déroutant j'avoue).

      Et quand le texte dit qu'un programme Rust ne plante pas, il se ravise aussitôt:

      Ce qui veut dire que si vous ne sortez pas du bac à sable, votre programme ne plantera pas… Du moins, il ne produira pas d’erreur de segmentation.

      (et on n'a pas une erreur de segmentation :) )

      J'ajouterai qu'il s'agit d'un problème dont les créateurs de Rust sont au courant (tu nous donnes justement les liens qui vont bien) qui devrait être réglé à terme, et pas d'une limite intrinsèque et insoluble du langage (comme l'obligation d'avoir des pauses dans un langage basé sur un GC par exemple).

      Mais comme tu le soulignes le terme "sûr" qu'utilise Rust n'est pas sans ambiguïté ; il s'agit de cohérence des données (pas de pointeur qui écrit au pif dans la mémoire, ou de data-race entre 2 threads par exemple), pas du fait qu'un programme ne va pas terminer prématurément. Par exemple, il est souvent dit qu'il n'y pas de fuite mémoire avec Rust ; en fait c'est faux. La gestion mémoire est certes très bonne, mais par exemple il est possible lorsqu'on utilise du comptage de référence (on en a besoin dans certains cas) de "perdre" de la mémoire si on a des cycle de références. C'est bien dommage, mais ça ne rentre pas en contradiction avec la définition de sûreté du langage (le programme pourra finir par être à court de mémoire comme dans ton cas, mais ça n'introduira pas de faille de sécurité par exemple).

      • [^] # Re: Pas encore de gestion des erreurs d'allocation mémoire

        Posté par (page perso) . Évalué à 1 (+0/-1).

        (le programme pourra finir par être à court de mémoire comme dans ton cas, mais ça n'introduira pas de faille de sécurité par exemple).

        Affirmation qui ne demande qu'à être démontrée.

        * Ils vendront Usenet^W les boites noires quand on aura fini de les remplir.

        • [^] # Re: Pas encore de gestion des erreurs d'allocation mémoire

          Posté par (page perso) . Évalué à 3 (+1/-0).

          Tu as tout à fait raison.

          J'aurais tendance à dire que si l'instruction illégale est toujours la même et ne correspond pas aux données du fichier (comme dans un buffer overflow), ça me parait difficilement exploitable.

          Après comme je ne vais pas aller vérifier, et que je n'ai pas les connaissances suffisantes pour affirmer que ça suffit à l'absence de faille, je vais botter en touche en disant que la sécurité est un point auquel l'équipe de Rust a toujours fait très attention (et qu'en l'occurrence le problème est connu).

          • [^] # Re: Pas encore de gestion des erreurs d'allocation mémoire

            Posté par (page perso) . Évalué à 2 (+0/-0).

            J'aurais tendance à dire que si l'instruction illégale est toujours la même et ne correspond pas aux données du fichier (comme dans un buffer overflow), ça me parait difficilement exploitable.

            Je suppose quand même qu'on trouver un scenario. Le premier qui m'est venu à l'esprit, c'es le sigill qui déclenche l'écriture d'un coredump, celui-ci pouvant contenir des informations sensibles qui deviennent alors lisibles de l'extérieur.

            * Ils vendront Usenet^W les boites noires quand on aura fini de les remplir.

            • [^] # Re: Pas encore de gestion des erreurs d'allocation mémoire

              Posté par (page perso) . Évalué à 4 (+2/-0).

              Le premier qui m'est venu à l'esprit, c'es le sigill qui déclenche l'écriture d'un coredump, celui-ci pouvant contenir des informations sensibles qui deviennent alors lisibles de l'extérieur.

              Ça voudrait dire que l'environnement est configuré pour générer des core dumps (depuis bien longtemps sous Debian/Ubuntu ce n'est pas le cas dans la configuration de base, vu que c'est plutôt pour les développeurs), et que ces dumps sont lisibles de l'extérieur. Ça ne veut pas dire qu'il n'existe pas de scénario (ça serait assez pédant vu la complexité du problème), mais pour le coup celui-là ne me parait pas hyper pertinent (parce que ça indique plutôt une faille qui n'a pas grand chose à voir avec le logiciel en lui-même).

Envoyer un commentaire

Suivre le flux des commentaires

Note : les commentaires appartiennent à ceux qui les ont postés. Nous n'en sommes pas responsables.