Enregistrer une méthode d'objet comme callback

Enregistrer une méthode d'objet comme callback

Cette semaine, on va vous parler de programmation. Récemment, le support a reçu une question qui nous a interpellé : "Comment enregistrer une méthode d'un objet comme callback d'arrivée". La documentation de notre API parle uniquement de fonction de callback mais, comme nous allons le voir, la plupart des langages de programmation permettent aussi d'enregistrer une méthode d'un objet.



Notre API est conçue pour permettre l’enregistrement de fonctions de callback pour les événements suivants:

  • les connexions et déconnexions de modules
  • les nouvelles valeurs des fonctions des modules (immédiates et périodiques)
  • les changements de configuration des modules
  • les logs de la libraire et des modules


Dans la documentation et les exemples, nous utilisons des fonctions, mais dans certaines situations on aimerait plutôt utiliser la méthode d'un objet. C'est possible, mais c'est plus ou moins compliqué selon des langages.

Tout d'abord, faisons un rapide petit rappel de la différence entre une fonction et une méthode. Une fonction est une partie de code qui peut être appelé par son nom. Il est possible de lui passer différentes données à l'aide de paramètres et elle peut retourner une valeur de retour. Pour utiliser une fonction, on a simplement besoin de connaître son nom et de lui passer les paramètres dont elle a besoin. Une méthode est une partie de code qui est liée à un objet. Son fonctionnement est identique à une fonction à la différence qu'en plus des paramètres, une méthode possède une référence implicite à un objet. Elle peut donc accéder au contenu de ce dernier en plus des paramètre qui lui sont passés. Ceci est une explication simplifiée. Pour plus de détail sur les méthodes, vous pouvez lire la page Wikipedia sur le sujet.

Dans dans notre cas, nous avons besoin d’enregistrer un callback, c'est-à-dire que le code n'est pas exécuté immédiatement, mais une référence sur le code est stockée pour être exécuté plus tard. Dans le cas d'une fonction, on a simplement besoin de stocker une référence sur la fonction, alors que pour une méthode on a besoin de stocker une référence sur la méthode et une référence sur l'instance de l'objet que la méthode va utiliser.

Pour enregistrer un callback sur une méthode il faut donc passer une information en plus: la référence sur l'objet de la méthode. En fonction des langages, c'est plus ou moins facile.

Nous allons voir comment enregistrer un callback sur la méthode deviceArrival d'un objet myObject pour chaque branchement d'un module. Pour ce faire nous allons utiliser la méthode RegisterDeviceArrivalCallback de notre API dans différents langages de programmations.


Python, C# et VB .Net


Pour ces trois langages, le compilateur est suffisamment malin pour détecter s'il s’agit d'une méthode ou d'une fonction. Il faut simplement passer la méthode sans les parenthèses en argument au lieu du nom de la fonction. En l’occurrence myObject.deviceArrival.

Le code en Python

class MyClass(object):
    def deviceArrival(self, m):
        print('Device %s plugged' % m.get_serialNumber())


myObject = MyClass()
YAPI.RegisterDeviceArrivalCallback(myObject.deviceArrival)



Le code en C#.

class MyClass
{
    void deviceArrival(YModule m)
    {
        Console.WriteLine("Device arrival : " + m.get_serialNumber());
    }
}
MyClass myObject = new MyClass();
YAPI.RegisterDeviceArrivalCallback(myObject.deviceArrival);



Le code en Visual Basic. Notez que, comme pour l'utilisation d'une fonction, il faut utiliser le mot clef AddressOf pour spécifier que l'on passe une référence à une fonction ou une méthode.

Class MyClas
  Sub deviceArrival(ByVal m As YModule)
    Console.WriteLine("Device Arrival : " + m.get_serialNumber())
  End Sub
end class

Dim myObject As MyClas = New MyClas()
YAPI.RegisterDeviceArrivalCallback(AddressOf myObject.deviceArrival)




JavaScript et TypeScript


En JavaScript ou TypeScript, c'est un petit peu plus compliqué. Il est possible de passer directement la méthode à utiliser comme en Python, mais lors de l’exécution valeur de this à l’intérieur de la méthode ne sera pas définie.

Pour que la méthode fonctionne correctement, il faut appeler la méthode bind au moment de l'appel à YAPI.RegisterDeviceArrivalCallback afin de spécifier la valeur de this. Au final le code à utiliser est myObject.deviceArrival.bind(myObject).

class MyClass
{
    async deviceArrival(module)
    {
        console.log('Device arrival: ' + await module.get_serialNumber());
    }
}
let myObject = new MyClass();
await YAPI.RegisterDeviceArrivalCallback(myObject.deviceArrival.bind(myObject));




PHP


PHP a choisi une approche différente. Au lieu de passer directement une référence sur la méthode à utiliser il faut passer un tableau qui contient l'objet et le nom de la méthode à appeler.

Le code PHP:

class MyClass
{
    function deviceArrival($module)
    {
        $serial = $module->get_serialNumber();
        print("New device: {$serial}\n");
    }
}
$myObject = new MyClass();
YAPI::RegisterDeviceArrivalCallback(array($myObject, 'deviceArrival'));




Java

Java est un cas particulier, car c'est un langage de programmation fortement orienté objet. Il n'y a pas de fonction, mais uniquement des méthodes. La méthode officielle pour les callbacks est d'utiliser une interface. Ce mécanisme est différent des autres langages, car c'est l'objet qui doit se conformer à l'interface de notre API.

Pour reprendre notre exemple, il faut que la classe MyClass> de notre objet myObjet implémente l'interface YAPI.DeviceArrivalCallback. Il suffit ensuite juste de passer l'objet myObject à la méthode YAPI.RegisterDeviceArrivalCallback. Notre API n'a pas besoin de stocker le nom de la fonction, elle sait que l'objet qu'on lui a passé implémente l'interface YAPI.DeviceArrivalCallback et donc le nom de la méthode est yDeviceArrival.

class MyClass implements YAPI.DeviceArrivalCallback
{
    @Override
    public void yDeviceArrival(YModule module)
    {
        System.out.println("Device arrival : " + module);
    }
}

MyClass myObject = new MyClass();
YAPI.RegisterDeviceArrivalCallback(myObject);




C++ et Delphi


Ces deux langages sont les plus compliqués, car il n'est pas possible d’enregistrer directement une méthode comme callback. La solution est d’utiliser une fonction intermédiaire qui appelle la méthode de notre objet.

Pour reprendre notre exemple, il faut implémenter une fonction wrapperForDeviceArrival qui va appeler la méthode deviceArrival de notre objet myObjet.

class MyClass
{
public:

    void deviceArrival(YModule* m)
    {
        cout << "Device arrival : " << serial << m->get_serialNumber(); << endl;
    }
};

MyClass* myObject;

void wrapperForDeviceArrival(YModule* m)
{
    myObject->deviceArrival(m);
}

int main(int argc, const char* argv[])
{
    ...
    myObject = new MyClass();
    YAPI::RegisterDeviceArrivalCallback(wrapperForDeviceArrival);
    ..
}




Le problème de cette solution est qu'elle impose l'utilisation d'une variable globale qui référence notre objet. Cela présente plusieurs désavantages.

Premièrement, le code n'est pas très lisible. Deuxièmement, on "pollue" le namespace global avec des symboles que l'on n’a pas forcément envie de publier. Pour ce dernier point, il est possible de préfixer la variable globale et la fonction du mot clef static ou encapsuler ces deux symboles dans une classe statique mais cela rend le code encore moins lisible.

L'autre problème est qu'il faut une variable globale par callback que l'on enregistre. Dans le cas des callbacks de branchement des modules, ce n'est pas un problème, mais pour les callbacks de nouvelles valeurs des modules, c'est problématique car on ne sait pas forcément combien de modules seront branchés à l'avance. La mauvaise solution est d'utiliser un tableau global qui pointe sur tous les objets et que la fonction callback retrouve le bon objet. Une meilleure solution est d'utiliser la méthode set_userData et get_userData des objets de notre librairie pour stocker un pointeur sur l'objet qui doit être utilisé.


Par exemple, le code suivant instancie un objet de type MyClass pour chaque senseur Yoctopuce présent. Lors de l'enregistrement de la fonction intermédiaire de callback sensorValueChangeCallBackWarpper, on stocke un pointeur sur l'objet à l'aide de la méthode set_userData. De cette manière dans la fonction sensorValueChangeCallBackWarpper on peut retrouver la bonne instance de l'objet avec get_userData et appeler la méthode sur le bon objet.

class MyClass
{
    int dummy;
public:
    explicit MyClass(int val) : dummy(val) {}

    void sensorValueChangeCallBack(YSensor* fct, const string& value)
    {
        cout << "Object:" << dummy << " new value: " << value << endl;
    }
};


void sensorValueChangeCallBackWarpper(YSensor* fct, const string& value)
{
    MyClass* myCbject = (MyClass*)fct->get_userData();
    myCbject->sensorValueChangeCallBack(fct, value);
}

int main(int argc, const char* argv[])
{
    ...

    int count = 0;
    YSensor* sensor = YSensor::FirstSensor();
    while (sensor) {
        MyClass* myCbject = new MyClass(count);
        sensor->set_userData(myCbject);
        sensor->registerValueCallback(sensorValueChangeCallBackWarpper);

        sensor = sensor->nextSensor();
        count++;
    }
    ...
}



Pour Delphi, c'est exactement la même solution. Seule la syntaxe change.

Lambda fonctions


Notez aussi que la plupart des langages supportent les fonctions anonymes, aussi appelées lambda fonctions. Cette nouvelle fonctionnalité, très à la mode, permet de passer directement le corps de la fonction aux méthodes d'enregistrement de callback sans avoir à déclarer de fonction ou de méthode. Nous en reparlerons probablement dans un futur article, mais sachez que cette fonctionnalité est utilisable avec nos librairies écrites en Java, C#, Python, PHP, JavaScript et TypeScript.


Conclusion


Cet article n'est pas le plus ludique que nous ayons fait. Il s'apparente plus à un cours de programmation qu'à un article sur nos modules. Toutefois, nous espérons que cet état des lieux vous permettra de gagner du temps lors de vos futurs projets.

Commenter aucun commentaire Retour au blog












Yoctopuce, get your stuff connected.