Le document s'organise de la façon suivante
===
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.
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.
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.
Meilleures performances. On garde un «orchestrateur» pour initialiser mais les modules peuvent fonctionner sans après.
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.
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.
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
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.
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.
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.
Modèle par readiness
Socket unix