Invalid use of non-static member function, yes i added the ()

Hi,

I am trying to build a generic AWS IoT "Thing" library. The Thing should be able to manage the WiFi, setup MQTT, handle OTA updates, etc. The end-user would implement some virtual methods i.e. handleMessage, drawLCD, readSensors from the Thing class, supply some secrets, and the thing should work. I am having some issues passing a non-static member function to the ArduinoMQTTClient which itself is a variable of the class. I would like the user to be able to provide their own method which I would then like to pass to the onMessage function of the ArduinoMQTTClient. I am a bit new to c++ and could use any advice as to how to implement this kind of feature. In javascript it would be trivial, but I am struggling to figure this out in c++. Here is the code, it is a bit long, but I wanted to provide a complete example that would compile. The main issue is in the connectMQTT function of the Thing.cpp class. The error I am seeing is:

lib/Thing/Thing.cpp: In member function 'void Thing::connectMQTT()':
lib/Thing/Thing.cpp:59:24: error: invalid use of non-static member function 'virtual void Thing::handleMessage(int)'
   59 |   mqttClient.onMessage(handleMessage);
      |                        ^~~~~~~~~~~~~
In file included from lib/Thing/Thing.cpp:2:
lib/Thing/Thing.h:54:16: note: declared here
   54 |   virtual void handleMessage(int messageSize);
      |                ^~~~~~~~~~~~~
*** [.pio/build/nodemcuv2/libdd0/Thing/Thing.cpp.o] Error 1

Any help or suggestions are greatly appreciated! Thanks in advance!!

The code:

#include <ESP8266WiFi.h>
#include <ESP8266HTTPClient.h>

// WiFiHelper.h 
class WiFiHelper
{
public:
  WiFiHelper(String ssid,
             String wiFiPassword,
             const char *certificatePemCrt,
             const char *privatePemKey,
             const char *caPemCrt);
  void setupWiFi();
  WiFiClientSecure wiFiClient;

private:
  String ssid;
  String wiFiPassword;
  void setCurrentTime();
  BearSSL::X509List client_crt;
  BearSSL::PrivateKey client_key;
  BearSSL::X509List rootCert;
};

//WiFiHelper.cpp
//#include <WiFiHelper.h>

WiFiHelper::WiFiHelper(
    String ssid,
    String wiFiPassword,
    const char *certificatePemCrt,
    const char *privatePemKey,
    const char *caPemCrt) : wiFiClient(),
                            client_crt(certificatePemCrt),
                            client_key(privatePemKey),
                            rootCert(caPemCrt)
{
  this->ssid = ssid;
  this->wiFiPassword = wiFiPassword;
}

void WiFiHelper::setupWiFi()
{
  Serial.print("Connecting to ");
  Serial.println(ssid);
  WiFi.begin(ssid, wiFiPassword);
  // WiFi.waitForConnectResult();
  while (WiFi.status() != WL_CONNECTED)
  {
    //Wait for the WiFI connection completion
    delay(1000);
    Serial.println("Waiting for connection...");
  }
  Serial.print("WiFi connected, IP address: ");
  Serial.println(WiFi.localIP());

  setCurrentTime();

#if defined(ARDUINO_ARCH_ESP8266)
  wiFiClient.setClientRSACert(&client_crt, &client_key);
  wiFiClient.setTrustAnchors(&rootCert);
#elif defined(ARDUINO_ARCH_ESP32)
  wiFiClient.setCACert(caPemCrt);
  wiFiClient.setCertificate(certificatePemCrt);
  wiFiClient.setPrivateKey(privatePemKey);
#endif
}

void WiFiHelper::setCurrentTime()
{
  Serial.println("");
  Serial.println("Setting up NTP server...");
  configTime(3 * 3600, 0, "pool.ntp.org", "time.nist.gov");

  Serial.print("Waiting for NTP time sync: ");
  time_t now = time(nullptr);
  while (now < 8 * 3600 * 2)
  {
    delay(500);
    Serial.print(".");
    now = time(nullptr);
  }
  Serial.println("");
  struct tm timeinfo;
  gmtime_r(&now, &timeinfo);
  Serial.print("Current time: ");
  Serial.print(asctime(&timeinfo));

  Serial.println("NTP server setup complete!");
  Serial.println("");
}

// Thing.h
#include <Arduino.h>
#include <ArduinoMqttClient.h>
//#include <WiFiHelper.h>
#include <ArduinoJson.h>

struct ThingConfig
{
  String deviceName;
  String mqttBroker;
  String wiFiSsid;
  String wiFiPassword;
  const char *certificatePemCrt;
  const char *privatePemKey;
  const char *caPemCrt;
  // how do i make this function an input to Thing?
  // void (*callback)(int) handleMessage;
};

struct MQTTTopics
{
  char getTopic[64];
  char getAcceptedTopic[64];
  char getRejectedTopic[64];
  char updateTopic[64];
  char updateDeltaTopic[64];
  char updateAcceptedTopic[64];
  char updateDocumentsTopic[64];
  char updateRejectedTopic[64];
  char deleteTopic[64];
  char deleteAcceptedTopic[64];
  char deleteRejectionTopic[64];
  char updateServerTopic[64];
  int numberOfSubscriptionTopics = 9;
  char *subscriptionTopics[9] = {
      getAcceptedTopic,
      getRejectedTopic,
      updateDeltaTopic,
      updateAcceptedTopic,
      updateDocumentsTopic,
      updateRejectedTopic,
      deleteAcceptedTopic,
      deleteRejectionTopic,
      updateServerTopic};
};

// Thing.h
class Thing
{
public:
  Thing(ThingConfig config);
  void loop();
  void setup();
  void publishMessage(String topic, String message);
  DynamicJsonDocument state;
  virtual void handleMessage(int messageSize);
  virtual void drawLCD();
  virtual void readSensors();
  virtual void updateState();

private:
  ThingConfig config;
  MQTTTopics topics;
  WiFiHelper wiFiHelper;
  MqttClient mqttClient;
  unsigned long lastMillis;
  void setupWiFi();
  void connectMQTT();
  void setupShadowTopics();
  void publishState();
};

// Thing.cpp

//#include <Thing.h>

Thing::Thing(ThingConfig config) : state(2048),
                                   wiFiHelper(config.wiFiSsid,
                                              config.wiFiPassword,
                                              config.certificatePemCrt,
                                              config.privatePemKey,
                                              config.caPemCrt),
                                   mqttClient(wiFiHelper.wiFiClient)
{
  this->config = config;
  setupShadowTopics();
  // default some stuff to the state
  state["state"]["reported"]["messageInterval"] = 30000;
}

void Thing::setupWiFi()
{
  Serial.println("");
  Serial.println("Setting up WiFi...");
  wiFiHelper.setupWiFi();
  Serial.println("WiFi setup complete!");
  Serial.println("");
}

void Thing::connectMQTT()
{
  Serial.println("");
  Serial.println("Setting up MQTT...");
  Serial.println("-> broker: " + String(config.mqttBroker));

  while (!mqttClient.connect(config.mqttBroker.c_str(), 8883))
  {
    // failed, retry
    delay(500);
    Serial.print(".");
  }
  Serial.println();

  // subscribe to the aws topics
  for (int i = 0; i < topics.numberOfSubscriptionTopics; i++)
  {
    Serial.print("-> subscribing to topic ");
    Serial.println(topics.subscriptionTopics[i]);
    mqttClient.subscribe(topics.subscriptionTopics[i]);
  }

  auto func = [this](int messageSize)
  {
    Serial.println(messageSize);
    // String topic = mqttClient.messageTopic();
    handleMessage(messageSize);
  };

  // void (*functionPointer)(void);

  mqttClient.onMessage(handleMessage);

  // void MqttClient::onMessage(void (*)(int))
  mqttClient.onMessage([](int messageSize)
                       {
                         Serial.println(messageSize);
                         // how do i call handleMessage? it is a method in the Thing class.
                         // adding this to the brackets gives an error
                         // error: cannot convert 'Thing::connectMQTT()::<lambda(int)>' to 'void (*)(int)'
                         // it works without this line...
                         //  this->handleMessage(messageSize);
                       });

  //error: invalid use of non-static member function 'void Thing::handleMessage(int)'
  // but i cannot make it static bc i need access to things in the class.
  // mqttClient.onMessage(handleMessage);

  // mqttClient.onMessage(std::bind(&Thing::handleMessage, this, std::placeholders::_1));
  Serial.println("MQTT setup complete!");
  Serial.println();
}

/**
 * Handle an MQTT message, default implementation just prints the message.
 */
void Thing::handleMessage(int messageSize)
{
  String topic = mqttClient.messageTopic();

  Serial.print("Message received on topic: ");
  Serial.println(topic);

  String message = "";
  // Get the whole JSON string to parse.
  while (mqttClient.available())
  {
    message.concat((char)mqttClient.read());
  }

  DynamicJsonDocument doc(2048);
  deserializeJson(doc, message);
  serializeJson(doc, Serial);
}

/**
 * Setup everything.
 */
void Thing::setup()
{
  Serial.begin(115200);
  setupWiFi();
  connectMQTT();
}

/**
 * Do the loop.
 */
void Thing::loop()
{
  if (!mqttClient.connected())
  {
    // MQTT client is disconnected, connect
    connectMQTT();
  }

  // poll for new MQTT messages and send keep alive
  mqttClient.poll();

  readSensors();
  updateState();
  drawLCD();

  unsigned long messageInterval = state["state"]["reported"]["messageInterval"];
  // publish a message every message interval
  if (millis() - lastMillis > messageInterval)
  {
    lastMillis = millis();
    publishState();
  }
}

void Thing::updateState()
{
  state["state"]["reported"]["elapsedTime"] = millis();
}

void Thing::drawLCD()
{
}

void Thing::readSensors()
{
}

void Thing::setupShadowTopics()
{
  const char *clientName = config.deviceName.c_str();
  sprintf(topics.getTopic, "$aws/things/%s/shadow/name/%s/get", clientName, clientName);
  sprintf(topics.getAcceptedTopic, "$aws/things/%s/shadow/name/%s/get/accepted", clientName, clientName);
  sprintf(topics.getRejectedTopic, "$aws/things/%s/shadow/name/%s/get/rejected", clientName, clientName);
  sprintf(topics.updateTopic, "$aws/things/%s/shadow/name/%s/update", clientName, clientName);
  sprintf(topics.updateDeltaTopic, "$aws/things/%s/shadow/name/%s/update/delta", clientName, clientName);
  sprintf(topics.updateAcceptedTopic, "$aws/things/%s/shadow/name/%s/update/accepted", clientName, clientName);
  sprintf(topics.updateDocumentsTopic, "$aws/things/%s/shadow/name/%s/update/documents", clientName, clientName);
  sprintf(topics.updateRejectedTopic, "$aws/things/%s/shadow/name/%s/update/rejected", clientName, clientName);
  sprintf(topics.deleteTopic, "$aws/things/%s/shadow/name/%s/delete", clientName, clientName);
  sprintf(topics.deleteAcceptedTopic, "$aws/things/%s/shadow/name/%s/accepted", clientName, clientName);
  sprintf(topics.deleteRejectionTopic, "$aws/things/%s/shadow/name/%s/rejected", clientName, clientName);
  sprintf(topics.updateServerTopic, "maxis-iot/%s/update", clientName);
}

void Thing::publishState()
{
  String message;
  serializeJson(state, message);
  publishMessage(topics.updateTopic, message);
}

void Thing::publishMessage(String topic, String message)
{
  Serial.println("Publishing message to topic: " + topic);
  Serial.println(message);
  mqttClient.beginMessage(topic);
  mqttClient.print(message);
  mqttClient.endMessage();
}

// main.cpp
#include <Arduino.h>
//#include <secrets.h>
//#include <Thing.h>

ThingConfig config{
  deviceName : "deviceName",
  mqttBroker : AWS_IOT_ENDPOINT,
  wiFiSsid : WIFI_SSID,
  wiFiPassword : WIFI_PASSWORD,
  certificatePemCrt : certificatePemCrt,
  privatePemKey : privatePemKey,
  caPemCrt : caPemCrt,
};

class VenusThing : public Thing
{
public:
  VenusThing(ThingConfig config) : Thing(config) {}
  void handleMessage(int message)
  {
    // deviceName implementation of Thing::handleMessage
  }

  void drawLCD()
  {
    // deviceName implementation of Thing::drawLCD
  }

  void readSensors()
  {
    // deviceName implementation of Thing::readSensors
  }
};

VenusThing thing(config);

void setup()
{
  thing.setup();
}

void loop()
{
  thing.loop();
}

a not static class member function can be called only on an object of that class. on what object should be called handleMessage which you try to pass to onMessage?

onMessage is from the mqttClient which is an instance of ArduinoMQTTClient in the thing class. handle message is the callback that should be executed by the ArduinoMQTTClient class.

Unfortunately, the onMessage() function takes a pointer to a regular (aka free) function:

void onMessage(void(*)(int));

Pointers to member functions are odd ducks and don't have the same signature. Hence, they can't be used where a free function pointer is required. You can read about pointers to member functions here: https://isocpp.org/wiki/faq/pointers-to-members

I think I have figured it out for my use case. Instead of storing the user defined onMessage callback in the Thing class itself, I exposed a method that allows the user to set the mqttClient.onMessage themselves externally to the class. In the Thing.cpp class i added

/**
 * This will allow the user to set the callback themselves vs storing it as a class member which does not work.
 */
void Thing::setOnMessageCallback(void (*callback)(int))
{
  mqttClient.onMessage(callback);
}

And it allows the user of the thing to do this kind of pattern:

VenusThing thing(config);

// callback is defined externally to the Thing class, but can still use the thing properties.
void onMessageCallback(int messageSize)
{
  String topic = thing.mqttClient.messageTopic();

  Serial.print("Message received on topic: ");
  Serial.println(topic);

  String message = "";
  // Get the whole JSON string to parse.
  while (thing.mqttClient.available())
  {
    message.concat((char)thing.mqttClient.read());
  }

  DynamicJsonDocument doc(2048);
  deserializeJson(doc, message);
  serializeJson(doc, Serial);
}

void setup()
{
  thing.setup();
  // register the void(*callback)(int) on the mqttClient
  thing.setOnMessageCallback(onMessageCallback);
}

void loop()
{
  thing.loop();
}

Thanks! That was really useful and has been bookmarked!