Le HAL Android (Partie 2 : après Oreo)

Dans notre dernier article, on a vu comment le système Android fonctionnait lors de l’accès au hardware spécifique. On a aussi mentionné que Google a reconçu la couche HAL avec le lancemment d’Android 8.0 Oreo. Dans ce nouvel article, on va parler des nouveautés introduites, et on va adapter notre exercice sur la vibration du portable avec du code source de la version 8.1.

Encore des Services

Dans cette nouvelle architecture, Google a compliqué un peu les choses. Avant, le problème était d’écrire les implémentations des interfaces hardware, c’était assez pour que le système appelle notre code. Maintenant on a plus de problèmes, non seulement il faut écrire les modules et les dispositifs, mais il faut aussi créer un service dynamique qui va les contenir.

Un service système ? J’aime bien votre raisonnement, mais non, pas tout à fait. Bien que l’idée et le mécanisme de communication soient similaires, les services HAL sont des serveurs de bas niveau plus spécifiques, qui peuvent être contactés par RPC. Il est important de noter que rien n’a changé de l’architecture de base, uniquement des aspects de communication et de déploiement.

Donc maintenant il faut commencer à imaginer les modules HAL comme des serveurs dynamiques. Ces serveurs sont exécutés pendant le démarrage du système, et ils peuvent même être gérés en temps d’exécution, pour économiser de l’énergie par exemple. Heureusement, de la même façon qu’avec le RPC traditionnel, on peut générer la majorité du code qui implémente la communication. Mais avant de vous effrayer avec des histoires de RPC et de serveurs bizarres, je veux vous parler des modes d’implémentation des modules.

Modes d’Implémentation

Comparaison des modes d’implémentation des modules HAL

Sur cette image on voit les 4 modes de fonctionnement d’un module HAL. Tout à gauche, on trouve l’implémentation avec libhardware telle qu’on l’a vu précédemment. Tout à droite, on trouve le mode idéal qui sera utilisé par tous les modules dans le futur, et c’est d’ailleurs celui qui est recommandé. Ceux au milieu, sont des combinaisons des deux modes “pures”, qui existent seulement pour faciliter la transition.

  • Mode 1 : Rien à dire, on a vu ce mode dans notre article précédent.
  • Mode 2 : Passthrough. C’est l’adaptation plus facile et rapide de nos modules legacy. Les services système communiquent avec le module comme si c’était sur RPC, mais en réalité, on fait un dlopen de la bibliothèque dynamique, et on charge le code dans la mémoire du service.
  • Mode 3 : Binderized tricheur. C’est un peu comme le mode 2, sauf que le module est enveloppé par un serveur par défaut, qui charge la bibliothèque dynamique dans son propre espace mémoire, fait l’appel, et renvoie les résultats.
  • Mode 4 : Le vrai Binderized. Notre module est un serveur qui exécute dans son propre processus et qui gère les appels des clients externes.

Ce qui est intéressant dans cette nouvelle approche, c’est le mélange stabilité-flexibilité qu’on gagne. Les interfaces ne sont pas définies avec un fichier .h, mais avec un langage spécifique qui s’appelle HIDL (HAL Interface Definition Language). Les services sont vraiment indépendants, vu qu’ils s’exécutent dans leur propre processus, ils peuvent être gérés par l’OS, et les mises à jour peuvent négocier la version des modules hardware pour garantir leur bon fonctionnement.

Le fait que les interfaces soient versionnées est aussi très important, cela permet d’assurer la compatibilité de différentes versions d’Android avec le même hardware, et avec une intervention minimale des dévéloppeurs (si besoin).

Mise à jour de notre exemple

Regardons maintenant comment cette nouvelle méthode modifie l’interaction entre les composants du système. On reprend alors notre exemple sur la vibration.

Le code du service système Java est vraiment très similaire (comme prévu). Les seuls changements qu’on voit au niveau de la communication avec le système natif c’est de nouvelles méthodes pour les fonctionalités introduites.

native static boolean vibratorExists();
native static void vibratorInit();
native static void vibratorOn(long milliseconds);
native static void vibratorOff();
native static boolean vibratorSupportsAmplitudeControl();
native static void vibratorSetAmplitude(int amplitude);
native static long vibratorPerformEffect(long effect, long strength);

Mais, comme avant, tout ce qui nous concerne pour cet exemple c’est les 4 méthodes principales qui n’ont pas changé. En revanche, la partie native du VibratorService a évolué.

...
#include <android/hardware/vibrator/1.0/IVibrator.h>
#include <android/hardware/vibrator/1.0/types.h>
#include <android/hardware/vibrator/1.1/IVibrator.h>

...

// Creates a Return<R> with STATUS::EX_NULL_POINTER.
template<class R>
inline Return<R> NullptrStatus() {
    using ::android::hardware::Status;
    return Return<R>{Status::fromExceptionCode(Status::EX_NULL_POINTER)};
}

// Helper used to transparently deal with the vibrator HAL becoming unavailable.
template<class R, class I, class... Args0, class... Args1>
Return<R> halCall(Return<R> (I::* fn)(Args0...), Args1&&... args1) {
    // Assume that if getService returns a nullptr, HAL is not available on the
    // device.
    static sp<I> sHal = I::getService();
    static bool sAvailable = sHal != nullptr;

    if (!sAvailable) {
        return NullptrStatus<R>();
    }

    // Return<R> doesn't have a default constructor, so make a Return<R> with
    // STATUS::EX_NONE.
    using ::android::hardware::Status;
    Return<R> ret{Status::fromExceptionCode(Status::EX_NONE)};

    // Note that ret is guaranteed to be changed after this loop.
    for (int i = 0; i < NUM_TRIES; ++i) {
        ret = (sHal == nullptr) ? NullptrStatus<R>()
                : (*sHal.*fn)(std::forward<Args1>(args1)...);

        if (ret.isOk()) {
            break;
        }

        ALOGE("Failed to issue command to vibrator HAL. Retrying.");
        // Restoring connection to the HAL.
        sHal = I::tryGetService();
    }
    return ret;
}

static void vibratorInit(JNIEnv /* env */, jobject /* clazz */)
{
    halCall(&IVibrator::ping).isOk();
}

static jboolean vibratorExists(JNIEnv* /* env */, jobject /* clazz */)
{
    return halCall(&IVibrator::ping).isOk() ? JNI_TRUE : JNI_FALSE;
}

static void vibratorOn(JNIEnv* /* env */, jobject /* clazz */, jlong timeout_ms)
{
    Status retStatus = halCall(&IVibrator::on, timeout_ms).withDefault(Status::UNKNOWN_ERROR);
    if (retStatus != Status::OK) {
        ALOGE("vibratorOn command failed (%" PRIu32 ").", static_cast<uint32_t>(retStatus));
    }
}

static void vibratorOff(JNIEnv* /* env */, jobject /* clazz */)
{
    Status retStatus = halCall(&IVibrator::off).withDefault(Status::UNKNOWN_ERROR);
    if (retStatus != Status::OK) {
        ALOGE("vibratorOff command failed (%" PRIu32 ").", static_cast<uint32_t>(retStatus));
    }
}
...

Il est clair que le service ne communique plus de la même façon avec le module de vibration. L’initialisation consiste à s’assurer que le serveur de vibration est disponible, en faisant un ping, mais la charge de la bibliothèque en mémoire et l’obtention de l’objet dispositif ne sont plus là.

Toutes les opérations sur le dispositif de vibration ont été transformées par les appels au service RPC qui implémentent l’interface définie dans les fichiers suivants :

#include <android/hardware/vibrator/1.0/IVibrator.h>
#include <android/hardware/vibrator/1.0/types.h>
#include <android/hardware/vibrator/1.1/IVibrator.h>

Il est intéréssant de noter que l’interface IVibrator.h est versionnée, alors qu’avant il n’y avait pas d’information sur ça, ce qui va nous permettre d’assurer la stabilité du système face à des incompatibilités. On peut toujours réduire la version du framework ou des drivers pour qu’elles “matchent”.

L’Interface

On arrive maintenant à la nouvelle couche HAL. Normalement, on devrait trouver ici :

  • Les fichiers .hal (l’interface) qui spécifient les types et les fonctions du module
  • Les fichiers .cpp qui implémentent le module et le serveur
  • Le fichier .h généré automatiquement à partir des fichiers .hal
  • Le fichier .rc pour démarrer le service pendant le boot du système

On a beaucoup de fichiers à regarder sur Oreo. On va se concentrer sur l’implémentation du module et la définition de l’interface, puisque le reste est soit pour la configuration (.rc), soit automatiquement généré (service.cpp, Vibrator.h). On regarde alors le fichier .hal principal.

<< IVibrator.hal >>
package android.hardware.vibrator@1.0;

interface IVibrator {
  on(uint32_t timeoutMs) generates (Status vibratorOnRet);
  off() generates (Status vibratorOffRet);
  supportsAmplitudeControl() generates (bool supports);
  setAmplitude(uint8_t amplitude) generates (Status status);
  perform(Effect effect, EffectStrength strength) generates (Status status, uint32_t lengthMs);
};

Cette interface définie (à l’aide du langage HIDL) les opérations de base du service. L’outil hidl-gen permet aux dévéloppeurs de transformer ces fichiers HIDL en interfaces .h avec une implémentation RPC de base. Tout ce qu’ils doivent faire pour mettre en oeuvre leur module c’est implémenter les fonctions qu’ils ont définies ici.

Dans le cas du service de vibration, on peut les trouver dans le fichier Vibration.cpp.

...
Vibrator::Vibrator(vibrator_device_t *device) : mDevice(device) {}

// Methods from ::android::hardware::vibrator::V1_0::IVibrator follow.
Return<Status> Vibrator::on(uint32_t timeout_ms) {
    int32_t ret = mDevice->vibrator_on(mDevice, timeout_ms);
    if (ret != 0) {
        ALOGE("on command failed : %s", strerror(-ret));
        return Status::UNKNOWN_ERROR;
    }
    return Status::OK;
}

Return<Status> Vibrator::off()  {
    int32_t ret = mDevice->vibrator_off(mDevice);
    if (ret != 0) {
        ALOGE("off command failed : %s", strerror(-ret));
        return Status::UNKNOWN_ERROR;
    }
    return Status::OK;
}

Return<bool> Vibrator::supportsAmplitudeControl()  {
    return false;
}

Return<Status> Vibrator::setAmplitude(uint8_t) {
    return Status::UNSUPPORTED_OPERATION;
}

Return<void> Vibrator::perform(Effect, EffectStrength, perform_cb _hidl_cb) {
    _hidl_cb(Status::UNSUPPORTED_OPERATION, 0);
    return Void();
}

IVibrator* HIDL_FETCH_IVibrator(const char * /*hal*/) {
    vibrator_device_t *vib_device;
    const hw_module_t *hw_module = nullptr;

    int ret = hw_get_module(VIBRATOR_HARDWARE_MODULE_ID, &hw_module);
    if (ret == 0) {
        ret = vibrator_open(hw_module, &vib_device);
        if (ret != 0) {
            ALOGE("vibrator_open failed: %d", ret);
        }
    } else {
        ALOGE("hw_get_module %s failed: %d", VIBRATOR_HARDWARE_MODULE_ID, ret);
    }

    if (ret == 0) {
        return new Vibrator(vib_device);
    } else {
        ALOGE("Passthrough failed to open legacy HAL.");
        return nullptr;
    }
}
...

Alors là j’espère que vous commencez à trouver tout ça assez similaire à ce qu’on a vu dans notre article précédent. Vous avez devinez ce que c’est ?

Bien sûr ! C’est ce qu’on appelait avant le service système ! Par exemple, la méthode HIDL_FETCH_IVibrator ressemble beaucoup à notre vibratorInit sur Android 7. Mais pourquoi est-ce qu’il est dans la couche HAL maintenant ? C’est une bonne question.

En fait, ces méthodes utilisent la bibliothèque du HAL de vibration, donc il faut qu’elles aient ce module disponible en mémoire. Mais on veut que les services HAL soient indépendants, on ne peut plus charger ces modules dans l’espace mémoire du processus client. C’est la puissance de notre nouvelle approche.

Et voilà, le reste est déjà connu. Il s’agit de notre interface .h classique, donc pas de surprise. Cependant, il y a une question que vous pouvez vous demander maintenant : quel est le mode de fonctionnement implémenté dans ce service ?

C’est difficile à dire, ça pourrait être le 2ème ou le 3ème ! Ces deux modes utilisent l’implémentation legacy (comme dans l’exemple), mais avec deux types d’appel différents. D’un côté, le passthrough charge le service dans la mémoire du processus client (c’est dommage, mais il est nécessaire pour des raisons de compatibilité et parfois performance), et d’un autre, le binderized utilise RPC.

En effet, si on regarde davantage les sources, on trouvera la réponse dans le fichier service.cpp :

...
int main() {
    return defaultPassthroughServiceImplementation<IVibrator>();
}

Donc c’est bien le 2ème mode qui est utilisé.

Conclusion

Félicitations ! Vous comprenez maintenant les secrets de l’architecture Android, et surtout ceux du HAL. Si vous avez encore des doutes ou si vous êtes assez courageux pour implémenter votre propre driver, je vous invite à regarder en détail la documentation, et bien évidemment le code source de la plateforme, qui n’est pas si compliqué que ça finalement !

Sources

1 Commentaire

  1. ala Répondre

    Bonjour

    je suis ALA ,
    maintenant en train de faire mon projet de fin d etude sur l automobile :
    realiser un driver camera vi interface fpdlink; puis implementation HAL camera avec EVS qui travaille avec camera et l’affichage en meme temps.
    Ma question : comment je peux ajouter le service EVS (c++) dans le service java CAR service ?

    et merci

Répondre à ala Annuler la réponse

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.