Archives pour septembre 2008

h1

Ecrire un programme basé sur libplayer

2008.09.26

Hello,

Cet article présente un exemple de l’utilisation de libplayer en C. Cette exemple se veut très simple et n’aborde qu’une petite partie des possibilités offertes par la librairie. Le but ici étant de vous montrer comment initialiser libplayer, comment lui donner quelque chose à lire pour également récupérer quelques informations et puis attendre la fin de la lecture pour quitter proprement. Il est vivement recommandé de consulter le fichier d’en-tête player.h pour une description de chaque fonction.

Initialisation d’un player

Avant toute chose, créons un fichier exemple.c avec la fonction principale main() et les éléments nécessaires pour utiliser libplayer.

static int
event_cb (player_event_t e, void *data)
{
  return 0;
}

int
main (int argc, char **argv)
{
  player_t *player;

  player = player_init (PLAYER_TYPE_MPLAYER,
                        PLAYER_AO_AUTO,
                        PLAYER_VO_AUTO,
                        PLAYER_MSG_WARNING,
                        0, event_cb);
  if (!player)
    return -1;

  /* ... */

  player_uninit (player);
  return 0;
}

‘argv’ nous sera utile pour spécifier à libplayer le fichier audio/video à lire. Le type player_t représente un lecteur audio/video, sans lui vous ne pourrez rien faire du tout. La fonction player_init() va nous permettre de créer une instance de player_t. Il est possible d’utiliser un callback pour récupérer des évènements venant de libplayer, la fonction event_cb() jouera donc ce rôle. player_uninit() va libérer correctement tout ce qui doit l’être. Ne quittez jamais une application sans “uninitialiser” le player!

Paramètres de player_init()

C’est ici que vous allez pouvoir donner quelques paramètres pour créer l’instance. Pour notre exemple on va choisir d’utiliser le wrapper MPlayer. Vous pouvez utiliser PLAYER_TYPE_XINE si vous préférez, mais n’utilisez pas GSTREAMER ou VLC qui ne sont pas encore suffisamment exploitables. Les deux paramètres suivant indiquent la sortie audio et la sortie vidéo à utiliser. Si vous avez défini MPlayer, je vous conseil de laisser en AUTO. Néanmoins avec xine il est nécessaire de les spécifier, un bon choix est PLAYER_AO_ALSA pour la sortie audio et PLAYER_VO_XV pour la sortie vidéo.
Le paramètre qui suit indique le niveau de verbosité. Le meilleur choix est le niveau “warning”, qui permet de se rendre compte des potentiels problèmes sans pour autant remplir le terminal de messages.
L’avant dernier argument permet d’indiquer dans quel fenêtre X11 la vidéo sera affichée. Dans notre cas on en a aucune, alors la valeur 0 indiquera à libplayer de créer une fenêtre en plein écran (désactivez Compiz si nécessaire, les fenêtres créées de cette manière posent un problème majeur avec ce gestionnaire).
Le dernier paramètre permet de transmettre l’adresse de notre callback. Il est optionnel, passez NULL si vous ne voulez pas l’utiliser. Par contre il ne sera plus possible de récupérer l’information de fin de lecture.

Que fait player_init()? Et bien il se passe beaucoup de choses, je vais donc résumer les étapes. Cette fonction va créer une instance de player_t puis va initialiser trois éléments importants. Tout d’abord libplayer étant MT-Safe, il y a un superviseur créé pour chaque instance, qui va sérialiser les actions venant de l’utilisateur de la librairie pour les transmettre au wrapper.
Il y a un gestionnaire d’évènement qui a pour tâche de transmettre certaines informations venant du wrapper ou de l’interne à libplayer, pour l’utilisateur via le callback (event_cb() dans notre cas).
La 3ème étapes importante étant d’initialiser le wrapper, donc MPlayer (fork du process), xine, GStreamer ou VLC.

Initialisation d’un mrl

Un MRL dans libplayer représente un média. Cela peut être un fichier, un DVD, un CD audio, etc,… Dans notre exemple on se contentera des fichiers. Tout d’abord il faut créer une instance pour un MRL puis l’assigner à un player.
En fonction du type de MRL, les arguments peuvent changer. Dans notre cas, il nous faut ceux qui définissent un fichier. Il nous faut également un pointeur pour notre MRL.

mrl_resource_local_args_t *args;
mrl_t *mrl;

Créons ces arguments dynamiquement pour les paramétrer (args sera automatiquement libéré par libplayer en temps voulu).

args = calloc (1, sizeof (mrl_resource_local_args_t));

Et enfin, donnons les arguments et créons l’instance de notre MRL. Si mrl_new() a pu créer l’instance sans erreur, il ne faut surtout pas libérer args!

args->location = strdup (argv[1]);
mrl = mrl_new (player, MRL_RESOURCE_FILE, args);

Détails de mrl_new()

Cette fonction va créer une instance en fonction de la ressource (FILE dans notre cas) puis interroger le player pour récupérer quelques informations. Il n’est pas nécessaire de spécifier si le fichier est uniquement audio, ou vidéo. libplayer devinera automatiquement en fonction des propriétés retournées par le wrapper (MPlayer par exemple).

Assigner le mrl au player

Maintenant que nous avons un mrl de prêt, nous pouvons l’assigner au player puis lancer la lecture.

player_mrl_set (player, mrl);
title = mrl_get_metadata (player, NULL,
                          MRL_METADATA_TITLE);
if (title)
  printf ("Title: %sn", title);
player_playback_start (player);

Quand une instance est assignée au player, elle est automatiquement libérée dés l’appel à player_uninit(). En ce qui concerne mrl_get_metadata(), le fait de passer un mrl NULL indique que nous voulons récupérer l’information depuis le MRL actuellement défini dans player (on aurait pu mettre ‘mrl’ à la place, ce qui reviendrait au même).

Attendre la fin de la lecture

Il y a deux moyens d’attendre la fin de la lecture. Par attente active ou par attente passive. Afin de faire les choses de manière élégante, je vous propose de mettre en œuvre une attente passive via un semaphore initialisé à 0.

Exemple complet

#include <semaphore.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

#include <player.h>

static sem_t sem;

static int
event_cb (player_event_t e, void *data)
{
  if (e == PLAYER_EVENT_PLAYBACK_FINISHED)
    sem_post (&sem);

  return 0;
}

int
main (int argc, char **argv)
{
  player_t *player;
  mrl_resource_local_args_t *args;
  mrl_t *mrl;
  int res = 0;
  char *title;

  if (argc < 2)
    return -1;

  player = player_init (PLAYER_TYPE_MPLAYER,
                        PLAYER_AO_AUTO,
                        PLAYER_VO_AUTO,
                        PLAYER_MSG_WARNING,
                        0, event_cb);
  if (!player)
    return -1;

  sem_init (&sem, 0, 0);

  args = calloc (1, sizeof (mrl_resource_local_args_t));
  if (!args)
  {
    res = -1;
    goto out;
  }

  args->location = strdup (argv[1]);
  mrl = mrl_new (player, MRL_RESOURCE_FILE, args);
  if (!mrl)
  {
    free (args);
    free (args->location);
    res = -1;
    goto out;
  }

  player_mrl_set (player, mrl);
  title = mrl_get_metadata (player,
                            NULL, MRL_METADATA_TITLE);
  if (title)
  {
    printf ("Title: %s\n", title);
    free (title);
  }
  player_playback_start (player);

  sem_wait (&sem);

 out:
  player_uninit (player);
  sem_destroy (&sem);
  return res;
}

Il est bien clair que cet exemple n’est pas parfait (et pas totalement protégé). Si la lecture ne se termine jamais correctement, le processus reste en attente indéfiniment. Si cela devrait arriver (un fichier illisible par exemple), un kill lui règlera son compte, mais n’oubliez pas de killer également le processus fils “mplayer”. Celui-ci n’étant jamais terminé sans un appel à player_uninit().

Pour compiler cet exemple:

  gcc `pkg-config libplayer --cflags --libs` exemple.c -o exemple

A bientôt,

Mathieu SCHROETER

h1

Heisenbug dans Enna

2008.09.03

Hello,

Depuis des mois, un bug apparaissait au déchargement de Enna. Ce bug avait la particularité de disparaitre avec l’utilisation de valgrind, d’où le nom d’heisenbug (par comparaison au principe d’incertitude d’Heisenberg). Il y a toujours des bugs difficiles à cerner, mais celui-ci est un peu différent et me permet de vous montrer une approche pour le débusquer, c’est pour cette raison que j’ai décidé de lui consacrer un article dans ce blog.

L’erreur consistait en une corruption de mémoire:

*** glibc detected ***
enna: corrupted double-linked list: 0x0000000000b3cd90 ***
======= Backtrace: =========
/lib/libc.so.6
/lib/libc.so.6(cfree+0x8c)
/usr/lib/libevas.so.0(evas_layer_free+0x17)
/usr/lib/libevas.so.0(evas_free+0xdd)
/usr/lib/libecore_evas.so.0(_ecore_evas_free+0x96)
/usr/lib/libecore_evas.so.0(_ecore_evas_x_shutdown+0x45)
/usr/lib/libecore_evas.so.0(ecore_evas_shutdown+0x3b)
enna(main+0x6ce)
/lib/libc.so.6(__libc_start_main+0xf4)
enna[0x407759]

Je vous épargne le “memory map” qui n’a aucun intérêt ici, mais comme vous pouvez le voir, le problème semble se situer dans le déchargement de ecore_evas. Et bien en réalité ce backtrace n’apporte rien du tout d’intéressant. Celui-ci change significativement d’allure en fonction des modules compilés dans Enna et complique donc méchamment l’affaire. Il n’est pas possible de s’y fier pour trouver l’origine du problème.
Là où ça devient intéressant c’est le comportement du programme avec valgrind. L’erreur n’apparait tout simplement pas. Enna se termine correctement comme si de rien n’était avec son message traditionnel (d’où l’heisenbug):

[Enna] [enna.c/430] Bye Bye !

On en était donc resté là depuis longtemps. Impossible de savoir d’où venait le problème, mais le fait de commenter certaines fonctions de déchargement de Enna permettait d’éviter cette corruption. Mais en aucun cas cela résolvait le bug à proprement parlé.

Valgrind

Pour s’y retrouver il fallait une astuce, et celle qui me paraissait la plus intéressante était de trouver le moyen de provoquer l’erreur avec valgrind.
Si valgrind bloque sur une corruption de mémoire, peut être que les informations récupérées en retour pourraient servir à la corriger une fois pour toute. Et il se trouve justement que valgrind à quelques atouts dans ses manches. Un petit tour dans le manpage nous indique deux options qui peuvent être utiles pour ce type de bug:

--malloc-fill=hex_number
--free-fill=hex_number

Ces deux arguments permettent de forcer les malloc() et free() de remplir par une valeur les bloques alloués et désalloués (sauf pour calloc() qui garde son comportement d’origine). Ainsi lors d’une erreur, il devient facile de repérer si un accès s’est fait sur un pointeur libéré.

Un petit essai sur Enna (valgrind --free-fill=0xab enna)

apporte une nouveauté:

Invalid read of size 8
at 0x409EE5: _e_smart_move (box.c:863)
by 0x66DCB3A: evas_object_move (evas_object_main.c:435)
by 0x40E03A: enna_pan_set (pan.c:100)
by 0x40F283: _e_smart_scrollbar_size_adjust (scrollframe.c:950)
by 0x40F4A0: _e_smart_child_del_hook (scrollframe.c:476)
by 0x66CAE5F: evas_object_event_callback_call (evas_callbacks.c:121)
by 0x66DD0F3: evas_object_del (evas_object_main.c:395)
by 0x410BF6: _e_smart_del (location.c:284)
by 0x66E64D4: evas_object_smart_del (evas_object_smart.c:465)
by 0x66DD0E4: evas_object_del (evas_object_main.c:394)
by 0x66D0C32: evas_layer_pre_free (evas_layer.c:63)
by 0x66D1B4A: evas_free (evas_main.c:112)
Address 0xabababababababab is not stack'd, malloc'd or (recently) free'd

Voyez l’adresse qui a été utilisée (0xabababababababab), il est clair qu’une fonction tente de travailler avec ce pointeur, mais celui-ci a été libéré auparavant. Un petit tour dans _e_smart_del (location.c:284) va nous apporter un élément de réponse et un fix provisoire (cf. r303). Néanmoins un nouveau bug apparait, mais cette fois je pouvais aisément retrouver l’endroit car il plantait précisément sur une fonction:

#5 0×000000000040b529 in enna_module_shutdown () at module.c:95

Et ce n’était plus directement un heisenbug car il apparaissait tout le temps, que se sois en debug ou non, et surtout un message de Ecore l’accompagnait. Je vous épargne donc les détails (cf. r301). Le problème était donc corrigé, enfin!

Conclusion

Avec un peu de recule cela me parait simple à résoudre. Au moins maintenant je sais comment m’y prendre, et j’espère que vous aussi, si un jour vous êtes confronté au même genre de corruption vous repenserez à valgrind --malloc-fill et --free-fill.

A bientôt,

Mathieu SCHROETER