Le HAL Android (Partie 1 : avant Oreo)

Dans cet article on va se focaliser sur une partie précise de l’architecture Android : le HAL, et en particulier celui avant Project Treble. On va regarder ce qu’il fait, et son interaction avec d’autres composants proches dans le stack. Pour comprendre ce qu’on va expliquer ici, il ne faut pas avoir peur du stack Android, dont on a parlé dans notre article précédent.

Un petit rappel

Architecture Android

Pour commencer, je vous rappelle que le HAL est une couche d’interfaces placée entre les services système et le kernel. Cela veut dire que les services système vont appeler les modules HAL, qui vont communiquer avec les drivers (qui contrôlent le hardware).

Vous vous demandez peut-être qu’est-ce qu’il y a alors d’intéressant sur cette couche qui n’a l’air de rien faire. D’accord, c’est vrai que c’est une couche légère, mais elle est indispensable pour le bon fonctionnement du système. Ce qui est encore plus étonnant, c’est que notre HAL est tellement important, que Google a décidé de le reconcevoir pour assurer la bonne évolution d’Android. Ces changements font partis du fameux Project Treble.

Pour l’instant, on veut vous montrer ce qu’on devrait faire à la place d’un fabricant de dispositifs, pour adapter, disons, Android 7.1 à notre hardware.

Noter que ceci n’est pas un tutoriel, mais plutôt une sorte de visite guidée du HAL, et des composants du système qui en font partis.

Motivation

Imaginez que vous avez créé un super OS mobile que tout le monde va utiliser et avec lequel vous allez conquérir le monde. Votre OS doit marcher sur n’importe quel dispositif construit maintenant ou dans le futur. Qu’est-ce que vous pourriez faire pour assurer la compatibilité hardware-OS ?

Bien sûr, on pense aux interfaces, on pense au HAL. Donc on écrit une dizaine de fichiers .h avec les opérations dont notre OS a besoin pour fonctionner (ex. accès au WiFi, connexion bluetooth, ouvrir la caméra, prendre une photo, lire l’état de la batterie, etc), on les met dans un répertoire, et dans nos services du niveau supérieur on fait un dlopen des implémentations (qui doivent être là) et on les utilise.

Voilà l’approche d’Android avant Oreo. Voyons comment ça marche dans un exemple concret : la vibration lors d’une notification reçue.

Les Services

Tout d’abord, sur Nougat comme sur Oreo, l’événement est détécté dans la couche application, l’appli qui la reçoit cherche l’instance du service Vibrator avec le mécanisme du binder, et appelle la méthode vibrate. On regarde donc le code du service VibratorService.

public class VibratorService extends IVibratorService.Stub
        implements InputManager.InputDeviceListener {

    ...

    native static boolean vibratorExists();
    native static void vibratorInit();
    native static void vibratorOn(long milliseconds);
    native static void vibratorOff();

    ...

    @Override // Binder call
    public void vibrate(int uid, String opPkg, long milliseconds, int usageHint,
            IBinder token) {
        if (mContext.checkCallingOrSelfPermission(android.Manifest.permission.VIBRATE)
                != PackageManager.PERMISSION_GRANTED) {
            throw new SecurityException("Requires VIBRATE permission");
        }
        verifyIncomingUid(uid);
        ...
        try {
            synchronized (mVibrations) {
                removeVibrationLocked(token);
                doCancelVibrateLocked();
                addToPreviousVibrationsLocked(vib);
                startVibrationLocked(vib);
            }
        } finally {
            Binder.restoreCallingIdentity(ident);
        }
    }

    ...

    // Lock held on mVibrations
    private void startVibrationLocked(final Vibration vib) {
        ...
        if (vib.mTimeout != 0) {
            doVibratorOn(vib.mTimeout, vib.mUid, vib.mUsageHint);
            mH.postDelayed(mVibrationRunnable, vib.mTimeout);
        } else {
            ...
        }
    }

    ...

    private void doVibratorOn(long millis, int uid, int usageHint) {
        synchronized (mInputDeviceVibrators) {
            ...
            if (vibratorCount != 0) {
                ...
            } else {
                vibratorOn(millis);
            }
        }
    }

    ...
}

Notre VibratorService contient 4 méthodes natives, notamment 2 méthodes pour commencer la vibration et l’arrêter. Bien sûr, il n’est pas tout à fait idiot, il y a beaucoup d’autres fonctionnalités qu’on a caché pour montrer le code qui nous intéresse, telles que la gestion de concurrence, l’alimentation, la file d’attente de requêtes, l’historique, entre autres. Donc ce service assure l’accès atomique à la vibration, il valide la requête et les permis, et finalement, si tout se passe bien, il appelle la méthode native pour activer la vibration.

...
#include <hardware/vibrator.h>
...

namespace android
{

static hw_module_t *gVibraModule = NULL;
static vibrator_device_t *gVibraDevice = NULL;

static void vibratorInit(JNIEnv /* env */, jobject /* clazz */)
{
    if (gVibraModule != NULL) {
        return;
    }

    int err = hw_get_module(VIBRATOR_HARDWARE_MODULE_ID, (hw_module_t const**)&gVibraModule);

    if (err) {
        ALOGE("Couldn't load %s module (%s)", VIBRATOR_HARDWARE_MODULE_ID, strerror(-err));
    } else {
        if (gVibraModule) {
            vibrator_open(gVibraModule, &gVibraDevice);
        }
    }
}

static jboolean vibratorExists(JNIEnv* /* env */, jobject /* clazz */)
{
    if (gVibraModule && gVibraDevice) {
        return JNI_TRUE;
    } else {
        return JNI_FALSE;
    }
}

static void vibratorOn(JNIEnv* /* env */, jobject /* clazz */, jlong timeout_ms)
{
    if (gVibraDevice) {
        int err = gVibraDevice->vibrator_on(gVibraDevice, timeout_ms);
        if (err != 0) {
            ALOGE("The hw module failed in vibrator_on: %s", strerror(-err));
        }
    } else {
        ALOGW("Tried to vibrate but there is no vibrator device.");
    }
}

static void vibratorOff(JNIEnv* /* env */, jobject /* clazz */)
{
    if (gVibraDevice) {
        int err = gVibraDevice->vibrator_off(gVibraDevice);
        if (err != 0) {
            ALOGE("The hw module failed in vibrator_off(): %s", strerror(-err));
        }
    } else {
        ALOGW("Tried to stop vibrating but there is no vibrator device.");
    }
}
...
};

Voilà la contrepartie de plus bas niveau de notre service de vibration. On peut voir que, comme on pouvait l’anticiper, toutes les 4 méthodes natives sont implémentées.

Si vous regardez le code attentivement, vous noterez qu’il y a deux appels intéressants dans la fonction vibratorInit. Tout d’abord, on trouve :

hw_get_module(VIBRATOR_HARDWARE_MODULE_ID, (hw_module_t const**)&gVibraModule);

Cet appel permet au système de charger la bibliothèque dynamique en mémoire (en gros : le driver), mais elle ne nous donne pas accès au dispositif physique de vibration. C’est pour cela que plus tard dans la fonction on trouve :

vibrator_open(gVibraModule, &gVibraDevice);

Mais cette fonction n’est pas définie dans le service de vibration, donc d’où vient-elle ? Vous avez peut-être remarqué qu’on n’a rien dit sur la ligne de code suivante :

#include <hardware/vibrator.h>

C’est notre premier contact avec le HAL ! Et c’est ici que la fonction vibrator_open est définie.

Toutes les interfaces du HAL se trouvent dans le répertoire /hardware/libhardware/. Comme prévu initialement, on va creuser encore plus pour regarder à quoi ressemble cette couche.

L’Interface

Bienvenu sur libhardware, on se rapproche de plus en plus vers le centre de la planète Android. Ici, on retrouve nos interfaces HAL sous le répertoire include/hardware, et quelques implémentations par défaut, dans modules.

Regardons à l’intérieur de notre interface HAL de vibration :

...
#include <hardware/hardware.h>
...

struct vibrator_device;
typedef struct vibrator_device {
    struct hw_device_t common;
    int (*vibrator_on)(struct vibrator_device* vibradev, unsigned int timeout_ms);
    int (*vibrator_off)(struct vibrator_device* vibradev);
} vibrator_device_t;

static inline int vibrator_open(const struct hw_module_t* module, vibrator_device_t** device)
{
    return module->methods->open(module, VIBRATOR_DEVICE_ID_MAIN, (struct hw_device_t**)device);
}
...

Donc, on sait que le driver va forcément avoir une structure vibrator_device, qui nous permet d’allumer ou éteindre le dispositif. Mais en plus on a une fonction déjà implémentée qui spécifie comment ouvrir un dispositif de vibration, étant donné que le module de vibration est dans la mémoire.

Ce bout de code est très puissant. Il définit la forme d’un dispositif de vibration, il met des contraintes sur l’implémentation des drivers, et il permet de récupérer un dispositif utilisable.

Noter que la structure hw_module_t est définie dans une autre interface hardware.h, qui contient la spécification commune aux modules HAL. Cette structure en particulier a une méthode open qui nous assure d’obtenir un dispositif à partir d’un module chargé.

Le Driver

On est maintenant dans le coeur de l’architecture. On est tellement loin du monde des applications qu’on peut trouver du code spécifique aux fabricants. Normalement, c’est ici que le code à éxécuter varie d’un dispositif à l’autre, et c’est pour cette raison que notre dernier exemple sera une implémentation par défaut donnée par Google. Donc voilà l’implémentation du driver de vibration, que vous pouvez trouver dans le répertoire modules de libhardware.

#include <hardware/vibrator.h>
#include <hardware/hardware.h>

#include <cutils/log.h>

#include <malloc.h>
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <math.h>

static const char THE_DEVICE[] = "/sys/class/timed_output/vibrator/enable";

static int vibra_exists() {
    int fd;

    fd = TEMP_FAILURE_RETRY(open(THE_DEVICE, O_RDWR));
    if(fd < 0) {
        ALOGE("Vibrator file does not exist : %d", fd);
        return 0;
    }

    close(fd);
    return 1;
}

static int sendit(unsigned int timeout_ms)
{
    // Écrit la valeur dans le fichier THE_DEVICE
    ...
}

static int vibra_on(vibrator_device_t* vibradev __unused, unsigned int timeout_ms)
{
    /* constant on, up to maximum allowed time */
    return sendit(timeout_ms);
}

static int vibra_off(vibrator_device_t* vibradev __unused)
{
    return sendit(0);
}

static int vibra_close(hw_device_t *device)
{
    free(device);
    return 0;
}

static int vibra_open(const hw_module_t* module, const char* id __unused,
                      hw_device_t** device __unused) {
    if (!vibra_exists()) {
        ALOGE("Vibrator device does not exist. Cannot start vibrator");
        return -ENODEV;
    }

    vibrator_device_t *vibradev = calloc(1, sizeof(vibrator_device_t));

    if (!vibradev) {
        ALOGE("Can not allocate memory for the vibrator device");
        return -ENOMEM;
    }

    vibradev->common.tag = HARDWARE_DEVICE_TAG;
    vibradev->common.module = (hw_module_t *) module;
    vibradev->common.version = HARDWARE_DEVICE_API_VERSION(1,0);
    vibradev->common.close = vibra_close;

    vibradev->vibrator_on = vibra_on;
    vibradev->vibrator_off = vibra_off;

    *device = (hw_device_t *) vibradev;

    return 0;
}

/*===========================================================================*/
/* Default vibrator HW module interface definition                           */
/*===========================================================================*/

static struct hw_module_methods_t vibrator_module_methods = {
    .open = vibra_open,
};

struct hw_module_t HAL_MODULE_INFO_SYM = {
    .tag = HARDWARE_MODULE_TAG,
    .module_api_version = VIBRATOR_API_VERSION,
    .hal_api_version = HARDWARE_HAL_API_VERSION,
    .id = VIBRATOR_HARDWARE_MODULE_ID,
    .name = "Default vibrator HAL",
    .author = "The Android Open Source Project",
    .methods = &vibrator_module_methods,
};

Ici il n’y a plus de mystère, ce fichier ressemble plus à un exercice de C de L2, et le comportement des fonctions est évident. C’est aussi intéressant de noter qu’on fait une espèce de binding dynamique sur le device et le module, vu qu’on assigne des pointeurs de fonction qui correspondent à ce que notre implémentation fait. Cela donne une flexibilité énorme.

Notez aussi que tout ce qu’il fallait faire pour faire vibrer le dispositif c’était d’écrire un nombre dans un fichier.

Sources

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.