Событийное программирование при помощи "слабых" функций (позднего связывания)

Тема в разделе "Arduino & Shields", создана пользователем DIYMan, 18 янв 2018.

  1. DIYMan

    DIYMan Guest

    Кому-то - баян, кому-то, возможно, покажется достойным применения. Итак: некоторые библиотеки (боже, как бесит это слово!) работают в поле событийного интерфейса путём назначения коллбэков, простейший пример тому - Wire.onReceive(handler). Этот подход к реализации событий многим известен и многими практикуется, однако, наряду с достоинствами - у этого подхода есть и определённые недостатки, хотя бы тот факт, что указатель на функцию-обработчик надо где-то хранить (а это место в оперативной памяти).

    В дополнение к этому - существует ещё один вполне себе годный подход, который применяется при объявлении той же функции yield, а именно - позднее связывание. Если совсем короче, то - любой функции можно назначить альяс (синоним, заглушку, если хотите), и если функция не определена - то на стадии линковки все вызовы такой функции заменятся на вызовы заглушки. Что нам это даёт? Как минимум - упрощение кода, вместо:

    Код (C++):
    if(onReceiveHandler) onReceiveHandler(bla, bla,bla);
    Достаточно написать просто

    Код (C++):
    onReceiveHandler(bla,bla,bla);
    Из ограничений, навскидку - тот факт, что таким образом реализованная событийность будет всегда ссылаться на одну функцию, вне зависимости от экземпляра класса. Но и это - не всегда минус, скажем так.

    Как это работает и как это сделать? Всё просто - объявляете функцию:

    Код (C++):
    extern "C" {
    void MY_RECEIVE(int bytesReceived);
    }

    Потом - делаете её альяс:

    Код (C++):
    extern "C" {
    static void __nohandler(int dummy){}
    }
    Потом - указываете, что функция - позднего связывания и имеет заглушку:

    Код (C++):
    void MY_RECEIVE(int) __attribute__ ((weak, alias("__nohandler")));
    Всё - теперь вы в любом месте проекта сможете позвать MY_RECEIVE (например, с параметром 10 - MY_RECEIVE(10); ) - и не париться, куда попадёт вызов: если желающий принимать это событие определит функцию, а именно - напишет в исходном коде:

    Код (C++):
    void MY_RECEIVE(int bytesReceived)
    {
    Serial.print(F("bytes received: "));
    Serial.println(bytesReceived);
    }
    то ваш вызов функции MY_RECEIVE попадёт именно в реализованный подписчиком обработчик. Если же функции MY_RECEIVE нигде не определено - её вызов переадресуется на заглушку, а именно - на функцию __nohandler. Что, кстати, даёт нам ещё один инструмент - обработка событий по умолчанию ;)

    Надеюсь, данная поверхностная заметка будет кому-либо полезна.
     
    ZAZ-965, ИгорьК, arkadyf и 2 другим нравится это.
  2. b707

    b707 Гуру

    Интересно, спасибо.
    По-моему, главный плюс этого подхода, по сравнению с колбеками - отсуствие необходимости проверять, определена ли функция и отрабатывать ситуацию пустого колбека. А тут все получается автоматически.

    "Одна функция , независимо от экземпляра класса" - поправь меня. разве нельзя внутри функции вызвать что-то типа _self, чтобы определить, какой экземпляр ее вызвал?
     
  3. DIYMan

    DIYMan Guest

    Считаю также плюсом наличие обработчика по умолчанию - та самая заглушка ;)

    Можно, конечно. Просто достаточно определить прототип события, как надо, например:
    Код (C++):
    extern "C" {
    void ON_RECEIVE(MyClass& sender, int bytesReceived);
    }
    И потом просто в классе, который вызывает событие, писать:
    Код (C++):
    void MyClass::readFromPort()
    {
    ....
    ON_RECEIVE(*this,10); // говорим подписчикам на событие, что именно мы приняли 10 байт
    }
    Просто функция ON_RECEIVE остаётся одна на все экземпляры классов, в отличие от типичных коллбэков, где каждому экземпляру класса можно назначить свой коллбэк:
    Код (C++):
    void myHandler1(){}
    void myHandler2(){}

    MyClass cl1;
    MyClass cl2;

    cl1.onReceive(myHandler1);
    cl2.onReceive(myHandler2);
    Короче - применимо и то, и то, всё зависит от задач и контекста ;) Указанным мной способом удобно делать закладки "на будущее", например, периодически из любого кода дёргать что-то там, чтобы не было тормозов, а в обработчике делать это самое "чтобы не было тормозов" (именно для этого авторами обвязки Wiring была предоставлена yield - чтобы "библиотеки" дёргали её в момент длительных операций; даже внутри delay есть вызов yield ;)).
     
    ИгорьК нравится это.
  4. ИгорьК

    ИгорьК Гуру

    "жопа есть, а слова - нет"?
     
  5. DIYMan

    DIYMan Guest

    А то :) Вот что жопа есть - это в точку, что называется :)