2

I would like to implement a workaround to use a non-static class as a call-back function. I am working with Eclipse Paho MQTT code. The following type is implemented and used as callback:

typedef void MQTTAsync_onSuccess(void* context, MQTTAsync_successData* response);

MQTTAsync_onSuccess* onSuccess;

onSuccess = myStaticCallback;

void myStaticCallback (void* context, MQTTAsync_successData* response)
{
   //...callback actions...
}

I want to wrap this C API (without modifying the existing MQTT C API) and implement non-static / non-centralized callback function that belongs to an object/class.

typedef void MQTTAsync_onSuccess(void* context, MQTTAsync_successData* response);

class myMQTTClass
{
   private:
      void myCallback (void* context, MQTTAsync_successData* response);
      MQTTAsync_onSuccess* onSuccess;

   public:
      void foo (void)
      {
          this->onSuccess = this->myCallback;
      }
}

As you might guess, the code above causes the error: cannot convert myCallback from type 'void (myMQTTClass::) (void*, MQTTAsync_successData*)' to type 'void (*)(void*, MQTTAsync_successData*)'.

Any guidance as to how to address this issue or any workaround is greately appreciated. I would be willing to provide any possible missing information. Thanks in advance.

EDIT: Actual code with some omissions

   namespace rover
    {
       typedef struct
       {
        char * clientID;
        char * topic;
        char * payload;
        int         qos;        // 1
        long int    timeout;    // Such as 10000L usec
       } RoverMQTT_Configure_t;

       class RoverPahoMQTT
       {
           public:
              RoverPahoMQTT (char * host_name, int port, RoverMQTT_Configure_t MQTT_Configure);

           private:

             /**
             * @brief Host name used for connecting to the Eclipse Paho MQTT server
             */
            char * HOST_NAME;

            /**
             * @brief Port used for connecting to the Eclipse Paho MQTT server
             */
            int PORT;

            RoverMQTT_Configure_t rover_MQTT_configure;

            /* Internal attributes */
            MQTTAsync client;

            /**
             * @brief Connect options
             */
            MQTTAsync_connectOptions conn_opts;

            /**
             * @brief Disconnect options
             */
            MQTTAsync_disconnectOptions disc_opts;

             //...
             static void onPublisherConnect (void* context, MQTTAsync_successData* response);
             void onPublisherConnect_ (MQTTAsync_successData* response);

           //...

       }
    }

    int rover::RoverPahoMQTT::publish (void)
    {
        this->flushFlags ();

        this->conn_opts = MQTTAsync_connectOptions_initializer;
        this->client = new MQTTAsync;
        int rc;

        char my_addr[20];
        this->constructAddress (my_addr);
        printf ("address: %s", my_addr);
        MQTTAsync_create (  &(this->client),
                            my_addr,
                            this->rover_MQTT_configure.clientID,
                            MQTTCLIENT_PERSISTENCE_NONE,
                            NULL);

        MQTTAsync_setCallbacks(this->client, NULL, onConnectionLost, NULL, NULL);

        conn_opts.keepAliveInterval = 20;
        conn_opts.cleansession = 1;
        conn_opts.onSuccess = rover::RoverPahoMQTT::onPublisherConnect;
        conn_opts.onFailure = onConnectFailure;
        conn_opts.context = this->client;

        if ((rc = MQTTAsync_connect(this->client, &(this->conn_opts))) != MQTTASYNC_SUCCESS)
        {
            printf("Failed to start connect, return code %d\n", rc);
            return rc;
        }

        /*printf("Waiting for publication of %s\n"
             "on topic %s for client with ClientID: %s\n",
             PAYLOAD, TOPIC, CLIENTID);*/
        while (!mqtt_finished)
            usleep(this->rover_MQTT_configure.timeout);

        MQTTAsync_destroy(&client);
        return rc;
    }

    void rover::RoverPahoMQTT::onPublisherConnect_(MQTTAsync_successData* response)
    {
        MQTTAsync_responseOptions opts = MQTTAsync_responseOptions_initializer;
        MQTTAsync_message pubmsg = MQTTAsync_message_initializer;
        int rc;

        printf("Successful connection\n");

        opts.onSuccess = onPublisherSend;
        opts.context = client;

        pubmsg.payload = &default_MQTT_configure.payload;
        pubmsg.payloadlen = strlen(default_MQTT_configure.payload);
        pubmsg.qos = default_MQTT_configure.qos;
        pubmsg.retained = 0;
        deliveredtoken = 0;

        if ((rc = MQTTAsync_sendMessage(client, default_MQTT_configure.topic, &pubmsg, &opts)) != MQTTASYNC_SUCCESS)
        {
            printf("Failed to start sendMessage, return code %d\n", rc);
            exit(EXIT_FAILURE);
        }
    }

    void rover::RoverPahoMQTT::onPublisherConnect (void* context, MQTTAsync_successData* response)
    {
        rover::RoverPahoMQTT* m = (rover::RoverPahoMQTT*) context;
        m->onPublisherConnect_(response);
        //((rover::RoverPahoMQTT*)context)->onPublisherConnect_(response);
        // ^^^HERE IS THE SEGMENTATION FAULT

    }
3
  • Use a static class member function and provide this as the context. Commented Dec 9, 2017 at 17:40
  • You need the static callback as a middle man, and it should probably be extern "C" (since it's clearly a C level callback). If you control the context parameter then use that to identify your class instance. If not, then you can either use some MQTTA-specific means of instance identification, or for a small number of possible concurrent instances identify via the callback function address (then using a unique callback function per instance), or for single-threaded execution, a "current instance" global variable, or ditto thread local such variable for multi-threading, or something else. Commented Dec 9, 2017 at 17:41
  • Hmm, actually I want nothing static here because I want to use several objects which might be overrided with several different callback functions. I did not how to modify context. I can simply do this->onSuccess = myCallback(this) but I know that won't work. Commented Dec 9, 2017 at 17:50

2 Answers 2

1

As clearly stated here, the callback has to be

registered with the client library by passing it as an argument in MQTTAsync_responseOptions

and the context argument is a

pointer to the context value originally passed to MQTTAsync_responseOptions, which contains any application-specific context.

I suggest a common interface for your classes, which provides a static method that matches the callback prototype:

class myMQTTClass
{
public:
  static void callback(void* context, MQTTAsync_successData* response)
  {
    myMQTTClass * m = (myMQTTClass*)context;
    m->myCallback(response);
  }
protected:
  virtual void myCallback(MQTTAsync_successData* response) = 0;
};

You can now implement different behaviours in subclasses:

class myMQTTClassImpl : public myMQTTClass
{
protected:
  void myCallback(MQTTAsync_successData *response)
  {
    std::cout << "success!!!" << std::endl;
  }
};

Let's see how to use it:

int main()
{
  myMQTTClass * m = new myMQTTClassImpl();

  MQTTAsync_responseOptions options;
  options.onSuccess = myMQTTClass::callback;
  options.context = m;
}

Edit (refers to actual code posted):

In your publish method, this is right:

conn_opts.onSuccess = rover::RoverPahoMQTT::onPublisherConnect;

this is wrong:

conn_opts.context = this->client;

it should be:

conn_opts.context = this;
Sign up to request clarification or add additional context in comments.

5 Comments

I like the approach but for some reason this causes a segmentation fault, maybe we are not allowed to use the context variable like this as; options.context refers to a MQTTAsync client; object in your example.
I did, I'd appreciate if you could have a look at it
Answer edited, you have to assign a RoverPahoMQTT instance to context field of MQTTAsync_responseOptions struct options, and not a MQTTAsync instance (this->client).
That does not make sense to me, conn_opts.context is void** and I'm assigning rover::RoverPahoMQTT* if I assign it to this. That gave an error.
According to the documentation it's void *, and just set a pointer to it and it will compile. But it has to be the pointer to the right object (the one having the onPublisherConnect method ...)
1

If you can use the context parameter to point to the object you want the callback to fire on you could simply make the callback function static and forward to an instance function. If the context parameter is needed for other data then I would use libffi to generate a closure and associate the object pointer with the closure's saved arguments.

There is no correct way to pass an actual instance function to that callback and be sure that it will work (even if you made the instance function be something like void MyCallback(MQTTAsync_successData*) and then forcibly cast that to the callback type you aren't guaranteed that the calling convention would match.

For the first (where you can use the context arg to point to 'this'):

class MyCallback
{
static void CallbackFunc(void * ptr, MQTTAsync_successData* data)
{
((MyCallback*)ptr)->RealCallback(data);
}
void RealCallback(MQTTAsync_successData*)
{}
};

You would then assign &MyCallback::CallbackFunc to the function pointer.

libffi is quite a bit more complicated.

2 Comments

Seems like a solution. But, the line ((MyCallback*)ptr)->RealCallback(data); caused a segmentation fault.
Also, would this work with multiple classes having self callback functions?

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.