02_organisation.md 12 KB

Le document s'organise de la façon suivante

  • Architecture de VLC
  • Présentation des modèles et abstractions utilisés dans la sandbox
  • Implémentation Linux des abstractions
  • Implémentation Windows des abstractions

===

Le principal de l'architecture actuelle de VLC est organisé autour de la notion de module. D'un côté, le core contrôle le chargement des fonctionnalités et joue un rôle d'orchestrateur. De l'autre côté chaque fonctionnalité est apportée par un module qui peut être chargé dynamiquement ou lié statiquement à l'application.

Les modules sont manipulés à travers les vlc_plugin_t qui eux-même manipulent les module_t contenant la représentation interne du module.

typedef struct vlc_plugin_t
{
    struct vlc_plugin_t *next;
    module_t *module;
    unsigned modules_count;

    const char *textdomain; /**< gettext domain (or NULL) */

    /* Variables set by the module to store its config options */
    struct
    {
        module_config_t *items; /**< Table of configuration parameters */
        size_t size; /**< Size of items table */
        size_t count; /**< Number of configuration items */
        size_t booleans; /**< Number of booleal config items */
    } conf;

#ifdef HAVE_DYNAMIC_PLUGINS
    bool unloadable; /**< Whether the plug-in can be unloaded safely */
    atomic_uintptr_t handle; /**< Run-time linker handle (or nul) */
    char *abspath; /**< Absolute path */

    char *path; /**< Relative path (within plug-in directory) */
    int64_t mtime; /**< Last modification time */
    uint64_t size; /**< File size */
#endif
} vlc_plugin_t;

vlc_plugin_t contient ainsi les informations pour charger et décharger les modules.

struct module_t
{
    vlc_plugin_t *plugin; /**< Plug-in/library containing the module */
    module_t   *next;

    /** Shortcuts to the module */
    unsigned    i_shortcuts;
    const char **pp_shortcuts;

    /*
     * Variables set by the module to identify itself
     */
    const char *psz_shortname;                              /**< Module name */
    const char *psz_longname;                   /**< Module descriptive name */
    const char *psz_help;        /**< Long help string for "special" modules */

    const char *psz_capability;                              /**< Capability */
    int      i_score;                          /**< Score for the capability */

    /* Callbacks */
    const char *activate_name;
    const char *deactivate_name;
    void *pf_activate;
    void *pf_deactivate;
};

De l'autre côté, module_t est réellement la description d'un module.

Chaque module dispose d'une priorité, qui correspond au score i_score dans module_t.

Ces modules sont chargés dans l'application depuis deux fonctions du core :

  • module_need permet de charger un module correspondant au nom ou à la fonction i_capability du module, en utilisant le premier module de plus forte priorité qui est capable de se charger sans erreur.
  • module_need_var qui dans les faits appelle module_need en lui donnant le contenu d'une variable au lieu d'une recherche hardcodée.
    /* from src/misc/xml.c */
    p_xml = vlc_custom_create( p_this, sizeof( *p_xml ), "xml" );
    p_xml->p_module = module_need( p_xml, "xml", NULL, false );

    reader = vlc_custom_create(obj, sizeof(*reader), "xml reader");
    reader->p_module = module_need(reader, "xml reader", NULL, false);

Par exemple, nous avons ici la création d'un objet représentant un module d'analyse syntaxique XML. Le module est ensuite chargé dans l'objet. Puis un autre couple d'objet et module est chargé, cette fois-ci pour une i_capability de type xml_reader.

TODO: décrire module_need en détail et comment on peut vouloir la patcher pour prendre en compte la sandbox.

  • parler de libvlc et vlc_object
  • parler des variables VLC
module_t *vlc_module_load(vlc_object_t *obj, const char *capability,
                          const char *name, bool strict,
                          vlc_activate_t probe, ...);

module_t *module_need(vlc_object_t *obj, const char *cap, const char *name,
                      bool strict)

Les détails de ces fonctions sont décrits ici car il s'agit de l'interface exposée par VLC pour charger un nouveau module. C'est donc un point d'entrée potentiel pour l'injection des abstractions de la sandbox.

Abstractions apportées par la sandbox

Architecture : broker vs non broker

Le modèle broker

Dans ce modèle, on considère un processus privilégié qu'on appelle broker, qui va contrôler tous les échanges entre les différents processus qu'on appelle workers, mais également leur cycle de vie et leurs permissions et accès.

Le broker est donc en charge de faire le routage des différents messages, de transmettre les ressources et faire la vérification d'accès. Il est également utile pour vérifier que les processus workers sont encore en vie et signaler les erreurs.

Modèle par capabilities et orchestrateur

Meilleures performances. On garde un «orchestrateur» pour initialiser mais les modules peuvent fonctionner sans après.

Communication inter-processus

On a besoin d'un moyen de créer une communication inter-processus et de pouvoir transéfer la capacité de communiquer dessus à travers les processus.

On introduit donc le type vlc_process_ipc_t qui est capable de stocker ce bus de communication et on construira les fonctions de communication dessus.

On a également besoin d'un moyen de sérialiser et envoyer des messages pour construire les appels de fonctions déportés (RPC, pour Remote Procedure Call). Cela va notamment nous servir à calquer l'architecture actuelle de VLC pour la faire évoluer progressivement vers un modèle plus découplé et moins contraint.

Initialisation des liens entre modules

On imagine que la situation est la même peu importe que l'on soit dans une architecture broker ou une architecture orchestrateur. C'est-à-dire qu'on peut de toute façon facilement passer d'une architecture non broker à une architecture broker avec la construction suivante :

On représente chaque lien entre processus par une IPC du point de vue des workers. Il s'agit du cas avec orchestrateur. Si on passe en sandbox avec modèle broker, chaque lien est cassé en deux avec une paire worker-broker et une autre paire broker-worker, et le broker s'assure ainsi de faire le routage et vérifier les accès. On a donc découplé le fonctionnement du worker de la politique d'accès choisie. Cette propriété est très importante pour la suite, car elle permet de construire le modèle puis d'adapter l'implémentation à ce qui se rapproche le plus du fonctionnement du système sous-jacent.

Gestion de la boucle d'événement

On peut donc commencer à construire chaque worker. On imagine que chacun est capable de communiquer à l'extérieur vers la bonne partie du core à partir du moment où l'initialisation a fait partager une paire d'IPC pour les relier.

On va donc devoir gérer les messages envoyés d'un processus à l'autre. Exactement, il va falloir raisonner en parlant des messages envoyés des processus vers le processus qu'on étudie en particulier. Pour s'intégrer plus facilement dans l'application, on ne va pas intégrer la gestion des événements directement dans le flot d'exécution de chaque partie du core. En effet, cela deviendrait difficile de raisonner en terme de zone de privilège étant donné que l'application elle-même (et non plus seulement la sandbox) devra savoir router les messages d'un module supérieur vers un module inférieur.

On va donc créer un thread par processus qui sera chargé de recevoir et exécuter les messages puis exécuter les RPC : il s'agit donc d'une boucle événementielle assez classique, mais qui doit prendre en compte le modèle threadé de l'application sous-jacente. Une attention particulière doit être mise sur la gestion des deadlocks.

Une IPC qui déclencherait une fonction d'un morceau de l'application qui déclencherait une nouvelle RPC dans un autre processus et attendrait le retour de fonction amènerait à une situation de deadlock, la boucle événementielle étant bloquée dans l'exécution de la première RPC.

Pire encore, une RPC créant le verrouillage de'certains mutexes qui seraient également verrouillés par le résultat de la RPC aboutirait également au blocage complet de ce bout de l'application.

Une fois ces problématiques en tête, il faut prendre en compte les mécanismes des systèmes d'exploitations pour la programmation asynchrone et écrire une abstraction qui permet de récupérer un message depuis l'une des IPC disponibles. C'est ainsi qu'apparaît vlc_msg_poller_t et l'API qui est liée.

// TODO: mettre structure et API

Gestion des blocs mémoires

Lorsque l'on traite des données en streaming, on récupère la plupart du temps des tronçons de données. Dans VLC, ces tronçons sont annotés de données multimédia : le dts et le pts par exemple. On verra son fonctionnement en détails dans la partie encodage matériel pour Android.

Ce type de données est particulièrement présent dans le pipeline multimédia de VLC. On obtient des block_t après une lecture depuis un fichier ou le réseau, qui est transmis au demux_t qui va analyser et extraire différents flux audio, vidéo et sous-titre. Il crée un block_t pour chacune des unités correspondant au format de ces flux, on peut par exemple penser à une image pour un flux MJPEG. Ces blocs sont ensuite transmis au décodeur correspondant qui va créer la picture_t pour la vidéo, ou subpicture_t pour les sous-titres, ou block_t pour l'audio et la passer dans la chaîne de filtre pour l'envoyer à la sortie correspondante.

Il faut désormais repenser à la situation de la sandbox et se souvenir que ces données-là vont devoir transiter d'un processus à l'autre de façon efficace. On va donc essayer de s'intéresser aux allocations effectuées par les modules et intégrer cette gestion en utilisant les mécanismes du système d'exploitation. Dans tous les cas, le système d'exploitation dispose d'un mécanisme de mémoire partagée pour dresser la correspondance de pages mémoires dans des processus différents. On peut donc essayer d'introduire des block_t alloués comme des mémoires partagées que l'on transmet de processus en processus.

Avec cette idée-là, la gestion de l'accès des processsus aux ressources devient cruciale.

Gestion des objets libvlccore

Armé des différentes abstractions mises en place pour la communication inter-processus des données de contrôle et des données applicatives, on va pouvoir commencer à intégrer réellement les morceaux de l'application dans la sandbox. On va donc devoir définir comment sera découpé et cloisonné l'application et quels seront les points d'entrées pour la sandbox.

Dans cette partie, nous allons majoritairement prendre en considération le cas du découpage du décodeur, entraînant la refactorisation d'autres dépendences de cet objet.

Découpage en module

Une autre méthode de cloisonnement qui n'a pas beaucoup été explorée est de cloisonner l'application par module et non par objet de libvlccore.

Cette méthode permet d'arriver plus rapidement à une situation où l'ensemble des modules de VLC est cloisonné, mais libvlccore ne profite pas du tout de ce cloisonnement et il nous reste alors environ 100 000 lignes de code à auditer pour s'approcher de loin à la méthode précédente. La technique reste intéressante à mentionner comme l'injection montre de façon plus simple comment le pattern fonctionne.

Implémentation sous Linux

Passage de descripteur de fichiers entre processus

Gestion asynchrone des messages reçus

Modèle par readiness

Routage des messages

Socket unix