Using RAII to register a callback in C ++

I am using API to receive notification. Something like:

NOTIF_HANDLE register_for_notif(CALLBACK func, void* context_for_callback);
void unregister_for_notif(NOTIF_HANDLE notif_to_delete);

      

I want to wrap it in some decent RAII class that will set an event after receiving a notification. My problem is how to sync it. I wrote something like this:

class NotifClass
{
public:
    NotifClass(std::shared_ptr<MyEvent> event):
        _event(event),
        _notif_handle(register_for_notif(my_notif_callback, (void*)this))
        // initialize some other stuff
    {
        // Initialize some more stuff
    }

    ~NotifClass()
    {
        unregister_for_notif(_notif_handle);
    }

    void my_notif_callback(void* context)
    {
        ((NotifClass*)context)->_event->set_event();
    }

private:
    std::shared_ptr<MyEvent> _event;
    NOTIF_HANDLE _notif_handle;
};

      

But I'm worried about the callback being called during build \ destroy (maybe in this particular example shared_ptr will be ok with it, but maybe it won't be the same with other built classes).

I'll say it again: I don't need a very specific solution for this special class, but a more general solution for RAII when passing a callback.

+3


source to share


5 answers


Your sync issues are a little misplaced.

To summarize your problem, you have a library with which you can register a callback function and (via a void * pointer or similar) some resources that a function acts on via a function register()

. This same library also provides a function unregister()

.

Within your code, you cannot and should not try to protect against the possibility that a library might call your callback function after or when it is unregistered with a function unregister()

: it is the library's responsibility to ensure that the callback cannot be triggered while it exists, or after his non-registration. The library should worry about synchronicity, mutexes, and the rest of those gubins, not you.

The two responsibilities of your code are as follows:

  • make sure you create the resources the callback acts on before registering it, and
  • make sure you cancel the callback before disposing of the resources that the callback is acting on.

This reverse build versus destruction is exactly what C ++ does with its member variables, and why compilers warn you when you initialize them in the "wrong" order.

In terms of your example, you need to make sure that 1) register_for_notif()

is called after the shared pointer is initialized, and 2) unregister_for_notif()

is called before the std :: shared_ptr (or whatever) is destroyed.



The key to the latter is understanding the destruction order of the destructor. See the Destruction Sequence section on the next page of cppreference.com to review .

  • The body of the destructor is executed first;
  • then the compiler calls destructors for all non-static nonvariant class members in reverse order of declaration.

Your example code is therefore "safe" (or as safe as possible) because it unregister_for_notif()

is called in the body of the destructor before the member variable is destroyed std::shared_ptr<MyEvent> _event

.

An alternative (and in a sense more understandable way) to do this would be to separate the notification descriptor from the resources on which the callback function is running, breaking it into its own class. For example. something like:

class NotifHandle {
 public:
   NotifHandle(void (*callback_fn)(void *), void * context)
       : _handle(register_for_notif(callback_fn, context)) {}

   ~NotifHandle() { unregister_for_notif(_handle); }

 private:
   NOTIF_HANDLE _handle;
};

class NotifClass {
 public:
   NotifClass(std::shared_ptr<MyEvent> event)
       : _event(event),
         _handle(my_notif_callback, (void*)this) {}

   ~NotifClass() {}

   static void my_notif_callback(void* context) {
     ((NotifClass*)context)->_event->set_event();
   }

private:
    std::shared_ptr<MyEvent> _event;
    NotifHandle _handle;
};

      

The order in which the member variable is declared is important: it is NotifHandle _handle

declared after the resource std::shared_ptr<MyEvent> _event

, so the notification will be unregistered until the resource is destroyed.

+3


source


You can do this by streaming access to a static container that contains pointers to your live instances. The constructor of the RAII class adds to the container this

, and the destructor removes it. The callback function checks the container context and returns it if it is missing. It will look something like this (not tested):

class NotifyClass {
public:
   NotifyClass(const std::shared_ptr<MyEvent>& event)
      : event_(event) {
      {
         // Add to thread-safe collection of instances.
         std::lock_guard<std::mutex> lock(mutex_);
         instances_.insert(this);
      }

      // Register the callback at the end of the constructor to
      // ensure initialization is complete.
      handle_ = register_for_notif(&callback, this);
   }

   ~NotifyClass() {
      unregister_for_notif(handle_);

      {
         // Remove from thread-safe collection of instances.
         std::lock_guard<std::mutex> lock(mutex_);
         instances_.erase(this);
      }

      // Guaranteed not to be called from this point so
      // further destruction is safe.
   }

   static void callback(void *context) {
      std::shared_ptr<MyEvent> event;
      {
         // Ignore if the instance does not exist.
         std::lock_guard<std::mutex> lock(mutex_);
         if (instances_.count(context) == 0)
            return;

         NotifyClass *instance = static_cast<NotifyClass*>(context);
         event = instance->event_;
      }

      event->set_event();
   }

   // Rule of Three. Implement if desired.
   NotifyClass(const NotifyClass&) = delete;
   NotifyClass& operator=(const NotifyClass&) = delete;
private:
   // Synchronized associative container of instances.
   static std::mutex mutex_;
   static std::unordered_set<void*> instances_;

   const std::shared_ptr<MyEvent> event_;
   NOTIF_HANDLE handle_;
};

      

Note that the callback increments the shared pointer and releases the lock on the container before using the shared pointer. This prevents a possible deadlock if the startup MyEvent

can create or destroy the instance synchronously NotifyClass

.




Technically, the above may fail due to address reuse. That is, if one instance is NotifyClass

destroyed and a new instance is immediately created at the same memory address, then an API callback that targets the old instance can presumably be delivered to the new instance. For certain uses, perhaps even for most uses, it doesn't matter. If it matters, then the static keys of the container should be made globally unique. This can be done by replacing the set with a map and passing the map key instead of a pointer to the API, for example:

class NotifyClass {
public:
   NotifyClass(const std::shared_ptr<MyEvent>& event)
      : event_(event) {
      {
         // Add to thread-safe collection of instances.
         std::lock_guard<std::mutex> lock(mutex_);
         key_ = nextKey++;
         instances_[key_] = this;
      }

      // Register the callback at the end of the constructor to
      // ensure initialization is complete.
      handle_ = register_for_notif(&callback, reinterpret_cast<void *>(key_));
   }

   ~NotifyClass() {
      unregister_for_notif(handle_);

      {
         // Remove from thread-safe collection of instances.
         std::lock_guard<std::mutex> lock(mutex_);
         instances_.erase(key_);
      }

      // Guaranteed not to be called from this point so
      // further destruction is safe.
   }

   static void callback(void *context) {
      // Ignore if the instance does not exist.
      std::shared_ptr<MyEvent> event;
      {
         std::lock_guard<std::mutex> lock(mutex_);
         uintptr_t key = reinterpret_cast<uintptr_t>(context);
         auto i = instances_.find(key);
         if (i == instances_.end())
            return;

         NotifyClass *instance = i->second;
         event = instance->event_;
      }

      event->set_event();
   }

   // Rule of Three. Implement if desired.
   NotifyClass(const NotifyClass&) = delete;
   NotifyClass& operator=(const NotifyClass&) = delete;
private:
   // Synchronized associative container of instances.
   static std::mutex mutex_;
   static uintptr_t nextKey_;
   static std::unordered_map<unsigned long, NotifyClass*> instances_;

   const std::shared_ptr<MyEvent> event_;
   NOTIF_HANDLE handle_;
   uintptr_t key_;
};

      

+2


source


There are two general common solutions for RAII callbacks. One of them is a common interface to shared_ptr

your object. The other is std::function

.

Using a common interface allows one smart_ptr to manage the lifetime of all callbacks for an object. This is similar to the observer pattern.

class Observer
{
public:
    virtual ~Observer() {}
    virtual void Callback1() = 0;
    virtual void Callback2() = 0;
};

class MyEvent 
{
public:
    void SignalCallback1() 
    {
        const auto lock = m_spListener.lock();
        if (lock) lock->Callback1();
    }

    void SignalCallback2() 
    {
        const auto lock = m_spListener.lock();
        if (lock) lock->Callback2();
    }

    void RegisterCallbacks(std::shared_ptr<Observer> spListener) 
    {
        m_spListener = spListener;
    }
private:
    std::weak_ptr<Observer> m_spListener;
};

class NotifClass : public Observer
{
public:
    void Callback1() { std::cout << "NotifClass 1" << std::endl; }
    void Callback2() { std::cout << "NotifClass 2" << std::endl; }
};

      

Usage example.

MyEvent source;
{
    auto notif = std::make_shared<NotifClass>();
    source.RegisterCallbacks(notif);
    source.SignalCallback1(); // Prints NotifClass 1
}
source.SignalCallback2(); // Doesn't print NotifClass 2

      

If you are using a C style element pointer, you need to worry about the object address and the principal callback. std::function

can encapsulate these two things with a lambda. This allows you to control the lifetime of each callback individually.

class MyEvent 
{
public:
    void SignalCallback() 
    {
        const auto lock = m_spListener.lock();
        if (lock) (*lock)();
    }

    void RegisterCallback(std::shared_ptr<std::function<void(void)>> spListener) 
    {
        m_spListener = spListener;
    }

private:
    std::weak_ptr<std::function<void(void)>> m_spListener;

};

class NotifClass
{
public:
    void Callback() { std::cout << "NotifClass 1" << std::endl; }
};

      

Usage example.

MyEvent source;
// This doesn't need to be a smart pointer.
auto notif = std::make_shared<NotifClass>();
{
    auto callback = std::make_shared<std::function<void(void)>>(
        [notif]()
    {
        notif->Callback();
    });
    notif = nullptr; // note the callback already captured notif and will keep it alive
    source.RegisterCallback(callback);
    source.SignalCallback(); // Prints NotifClass 1
}
source.SignalCallback(); // Doesn't print NotifClass 1

      

+2


source


AFAICT, you are concerned about what my_notif_callback

can be called in parallel to the destructor, or context

maybe a dangling pointer. This is a legitimate problem and I don't think you can solve it with a simple blocking mechanism.

Instead, you probably have to use a combination of shared and weak pointers to avoid such dangling pointers. For example, to solve a problem, you can store an event in widget

which is shared_ptr

, and then you can create weak_ptr

in widget

and pass it as a context register_for_notif

.

In other words, NotifClass

has share_ptr

in widget

, and context weak_ptr

in widget

. If you cannot block weak_ptr

, the class is already destroyed:

class NotifClass
{
public:
    NotifClass(const std::shared_ptr<MyEvent>& event):
            _widget(std::make_shared<Widget>(event)),
            _notif_handle(register_for_notif(my_notif_callback, (void*)new std::weak_ptr<Widget>(_widget)))
            // initialize some other stuff
    {
            // Initialize some more stuff
    }

    ~NotifClass()
    {
            unregister_for_notif(_notif_handle);
    }

    static void my_notif_callback(void* context)
    {
            auto ptr = ((std::weak_ptr<Widget>*)context)->lock();
            // If destructed, do not set the event.
            if (!ptr) {
                return;
            }
            ptr->_event->set_event();
    }

private:
    struct Widget {
        Widget(const std::shared_ptr<MyEvent>& event)
                : _event(event) {}

        std::shared_ptr<MyEvent> _event;
    };

    std::shared_ptr<Widget> _widget;
    NOTIF_HANDLE _notif_handle;
};

      

Note that any functionality you want to add to yours NotifClass

must actually go to widget

. If you don't have such additional functionality, you can skip the indirect widget

and use weak_ptr

to event

as context:

class NotifClass
{
public:
    NotifClass(const std::shared_ptr<MyEvent>& event):
            _event(event),
            _notif_handle(register_for_notif(my_notif_callback, (void*)new std::weak_ptr<MyEvent>(event)))
            // initialize some other stuff
    {
            // Initialize some more stuff
    }

    ~NotifClass()
    {
            unregister_for_notif(_notif_handle);
    }

    static void my_notif_callback(void* context)
    {
            auto ptr = ((std::weak_ptr<MyEvent>*)context)->lock();
            // If destructed, do not set the event.
            if (!ptr) {
                return;
            }
            ptr->set_event();
    }

private:
    std::shared_ptr<MyEvent> _event;
    NOTIF_HANDLE _notif_handle;
};

      

0


source


Moderator Warning: To request me please delete this post, just edit it!

make sure the callback object is fully constructed before registering it. The facility makes the callback object a separate class and the registration / deregistration wrapper a separate class. Then you can associate both classes with a member or base class relationship.

struct A
{CCallBackObject m_sCallback;
        CRegistration m_sRegistration;
        A (void)
             : m_sCallback (),
             m_sRegistration (& m_sCallback)
       {
       }
};

As an added benefit, you can reuse the registration / deregistration wrapper ...

If the callback could happen on a different thread, I would revisit this software to avoid it. For example. it is possible to finish stopping the main thread (for example, destroying this object) until all worker threads are finished / finished.

0


source







All Articles