Registering an object method as callback

Registering an object method as callback

This week, we are going to talk about programming. Support has recently received an interesting question: "how can you register an object method as an arrival callback". Our API documentation talks only about callback functions but, as we are going to see, most programming languages allow you to register object methods.




Our API is designed to allow the registration of callback functions for the following events:

  • module connections and disconnections
  • new values for the module functions (immediate and periodical)
  • changes in the module configuration
  • library and module logs


In the documentation and in the examples, we use functions, but in some cases we would rather use the method of an object. It's possible, but it's more or less difficult depending on the language.

First of all, let's refresh our minds on the difference between a function and a method. A function is a piece of code which you can call by its name. You can give it different data with parameters and it can return a value. To use a function, you simply need to know its name and to pass it the parameters that it needs. A method is a piece of code which is linked to an object. It works similarly to a function, with the difference than on top of the parameters, a method has an implicit reference to an object. It can therefore access the content of the later as well as the parameters that are passed to it. This is a simplified explanation. For more details on methods, you can read the Wikipedia page on the topic.

In our case, we need to register a callback, which means that the code isn't run immediately, but a reference to the code is stored to be run later. In the case of a function, we simply need to store a reference to the function, while for a method we need to store a reference to the method and a reference to the instance of the object that the method is going to use.

To register a callback on a method, we must therefore pass one more information: the reference of the object of the method. Depending on the language, it's more or less easy to do.

We are going to see how to register a callback on the deviceArrival method of a myObject object each time a module is connected. To do so, we are going to use the RegisterDeviceArrivalCallback method of our API in different programming languages.

Python, C#, and VB .Net

For these three languages, the compiler is smart enough to detect if it's dealing with a function or a method. We must simply pass the method without its parentheses as argument instead of the name of the function. In the present case myObject.deviceArrival.

The code in Python:

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


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



The code in C#:

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



The code in Visual Basic. Note that, as when using a function, we must use the AddressOf keyword to specify that we pass a reference to a function or to a method:

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 and TypeScript

In JavaScript or TypeScript, it is a little more complex. You can pass the method to be used directly like in Python, but when running the value of this inside the method is not defined.

For the method to work correctly, you must call the bind method when calling YAPI.RegisterDeviceArrivalCallback in order to specify the value of this. In the end, the code to be used is 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 selected a different approach. Instead of directly passing a reference on the method to be used, you must pass an array containing the object and the name of the method to be called.

The PHP code:

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




Java

Java is a particular case, because it's a programming language which is strongly object-oriented. There is no function, but only methods. The official way for callbacks is to use an interface. This mechanism is different from the other languages, because it's the object which must conform to the interface of our API.

To come back to our example, the MyClass class of our myObjet object implements the YAPI.DeviceArrivalCallback interface. We must then simply pass the myObjet object to the YAPI.RegisterDeviceArrivalCallback method. Our API doesn't need to store the name of the method, it knows that the object passed implements the YAPI.DeviceArrivalCallback interface and thus the name of the method is 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++ and Delphi

These two languages are the most complex, because it's not possible to directly register a method as a callback. The solution is to use an intermediary function which calls the method of our object.

To come back to our example, we must implement a wrapperForDeviceArrival function which calls the deviceArrival method of our myObjet object.


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);
    ..
}



The problem with this solution is that it imposes the use of a global variable which references our object. This has several disadvantages.

First, the code is not very readable. Second, we "pollute" the global namespace with symbols which we do not necessarily want to publish. For the later point, you can prefix the global variable and the function with the static keyword or encapsulate these two symbols in a static class, but this makes the code even less readable.

The other issue is that you need a global variable for each callback that you register. In the case of module connection callbacks, it's not a problem, but for new value callbacks, it's problematic because we don't necessarily know beforehand how many modules will be connected. The wrong solution is to use a global array which points to all the objects and for the callback function to find the correct object. A better solution is to use the set_userData and get_userData methods of the objects of our library to store a pointer on the object which must be used.

For example, the following code instantiates an object of type MyClass for each Yoctopuce sensor which is present. When registering the sensorValueChangeCallBackWarpper callback intermediary function, we store a pointer on the object with the set_userData method. in this way, in the sensorValueChangeCallBackWarpper function, we can retrieve the correct instance of the object with get_userData and call the method on the correct object.

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++;
    }
    ...
}



For Delphi, it's exactly the same solution. Only the syntax changes.

Lambda functions

Note also that most languages support anonymous functions, also called lambda functions. This new, very trendy, feature enables you to directly pass the body of the functions to the callback registration methods, without having to declare a function or a method. We will probably talk more about this is a future post, but you should know that you can use this feature with our libraries written in Java, C#, Python, PHP, JavaScript, and TypeScript.


Conclusion

This post isn't the most fun that we have ever written. It's more like a programming course than an actual post on our modules. However, we hope that this inventory will save you time during your future projects.

Add a comment No comment yet Back to blog












Yoctopuce, get your stuff connected.