Sending Member Functions as Callbacks to Global Functions

So I was writing a library for the I2C MCP23017 IO expander to use on the Arduino platform. Everything went fine until I stumbled onto a problem. The way I have written the library is by using C++ class that includes all the member variables and functions. Each object created of the class will fully represent a single IO expander IC. If you have two ICs attached to the IC bus, you can create two objects to manipulate them. MCP23017 supports change interrupts on all of its 16 GPIO pins. I wanted to implement that feature in my library.

To make that work, I need to use the Arduino’s attachInterrupt() function. This function accepts a C type callback function that will be invoked when an interrupt occurs at the set pin. Below are the three formats to invoke this function, and the first one is what we’re interested in.

attachInterrupt(digitalPinToInterrupt(pin), ISR, mode);  //(recommended)
attachInterrupt(interrupt, ISR, mode);  //(not recommended)
attachInterrupt(pin, ISR, mode);

The ISR is the callback function we have to send, and accepts the form void (*isr)(void). Means no returns and no input parameters. Now the problem is, since we use objects for each IO expander, all our functions are part of the class (member functions). When an interrupt occurs at a pin of the IO expander, its own isrSupervisor() should be called. That function is responsible for determining where the interrupt occurred and check for other conditions. We want to send this function to the attachInterrupt() function of Arduino. See what’s wrong there ? We have a member function to send to a function that accepts C type callbacks, which is not going to happen.

To understand why that’s a problem, you need to understand how function pointer works. Just like every data is saved on the memory at specific addressable locations, functions are also saved in memory as entry points. A function entry point is the address in the memory where the instruction sequence of that function starts. This is why we can have function pointers and use them as callbacks. A function pointer is simply a pointer that stores the entry point of a function. static and global functions have absolute entry points that you can read and directly use as callbacks. But this is not the case with member functions. Member functions are always situated relative to the object entry point or object pointer. That means, you need the object pointer to access the member function. The implicit object pointer is called this in C++. The this pointer is passed in the background when you invoke a member function like obj.function(). If so, is it possible to send a member function to a function that accepts an absolute function pointer ? Yes, you can. After all, pointers are just memory addresses right ? But that takes a little work. For now, let’s see what happens when we try to send a member function as callback without any conversions.

//our global function
void sayHello(void(*func)(void)) {
    func();  //invokes the member function explicitly
}

That’s our global function to which we have to send our member function. This assumes that our member function returns nothing (void) and takes no input parameters (void). In case you didn’t know, this is how – <return type> (*<function name>)(<parameter type>) – we declare function pointers. Now let’s send the member function,

//assume that we're doing this inside another member function of the same class
sayHello(printHello);

printHello() is a member function of a class helloClass. I’m not showing the entire class definition to keep it simple. The declaration of that function looks like the following,

void printHello();

If we send this function as callback, we will be greeted with the following error.

error: invalid use of non-static member function 'void helloClass::printHello()'

The error says it. We’re trying to send a non-static member function to another function, which is not acceptable. The error also points to a possible solution – if the member function is non-static, well then make it static. Static member functions are like any other global functions, they have an absolute entry point. A static member function will also be same for all the objects created, because there will be only one instance of that function. Another limitation is static member functions can only access other static member variables, not the non-static ones!

None of that are going to be useful in our case. We need to create separate objects for each IO expander, with unique function and variables set. If you remember I said before that all pointers are just memory addresses. Then is there a way to convert out member function pointer to a type that the target function will accept ? Yes, we can and that involves tricking the compiler. We can convert the member function pointer as shown below,

typedef void (*func)(void); //the only pointer type the global accepts

func funcPtr = (func) &helloClass::printHello;
 //cast the pointer to type we need
sayHello(funcPtr);  //send the callback to global function

As you can see, we created a custom type for the pointer we need and casted the member function to that type (you might get a warning for doing this, but just ignore it). Now the compiler is okay with sending this pointer to the global function because there is not type mismatch. But does this work ? Will the member function be invoked correctly from the global scope ? Yes, it will. The function will be executed just fine.

But there’s a catch! Trying to access any non-static member variables will result in a segmentation fault or segfault. The variables will return garbage values. It is because we’re missing something important – the this pointer. We accessed the member function explicitly, without using an object, and therefore we’re missing that this pointer without which we can’t access any member variables. So how to we pass the this pointer ? How would the member function know which object it belongs to ? The solution, I came up with is shown in the following program.

//===================================================================================//

//Sending member functions as callbacks to global function that accepts C type callbacks

//written by @vishnumaiea
//date : 08:03 PM 04-10-2020, Sunday

//===================================================================================//
//includes

#include <functional>
#include <iostream>

//-----------------------------------------------------------------------------------//
//defines

#define MAX_OBJ_COUNT 10    //max 10 objects just for demo, later could use vectors

//-----------------------------------------------------------------------------------//
//globals

class helloClass;   //forward declaration
typedef void (*func)(void); //the only pointer type the global accetps
typedef void (helloClass::*classPtr)(); //pointer to the parent class

classPtr voidPtr[MAX_OBJ_COUNT] = {0};  //class ptrs are converted to void ptrs and saved in this array
helloClass* objPtr[10] = {0};   //array of ptrs to parent class objects
int ptrCount = 0;   //voidPtr count
int objCount = 0;   //object count

//-----------------------------------------------------------------------------------//
//this function saves the member function ptr and the object ptr (this pointer)
//to their arrays

int addPtr(helloClass* thisPtr) {
    if(ptrCount >= (MAX_OBJ_COUNT - 1)) {   //limit the max ptr entries
        objPtr[MAX_OBJ_COUNT - 1] = thisPtr;    //save the object ptr
        return (MAX_OBJ_COUNT - 1); //index is always -1 of count
    }
    
    objPtr[ptrCount] = thisPtr; //save the object ptr
    ptrCount++;
    // std::cout << "ptrCount : " << ptrCount << std::endl;
    return ptrCount - 1;    //index is always -1 of count
}

//-----------------------------------------------------------------------------------//
//this is the global function that accepts the member func pointer and use it as a
//callback (C type). this is the part of the external code we can not change.
//isr means Interrupt Service Routine

void sayHello(void(*isr)(void)) {
    isr();  //invokes the member function explicitly
}

//-----------------------------------------------------------------------------------//
//example class

class helloClass {
    public:
    int ptrLocation;    //the ptr index variable to access the arrays
    int number; //some member data to later testing
    
    helloClass() {
        objCount++; //could've made this static, but for some reason, static is not working in online compiler
        
        ptrLocation = addPtr(this); //send the object ptr for saving get the index location
        
        //pritns the this pointer
        std::cout << "Obj " << objCount-1 << " this : " << this << std::endl;
        
        // std::cout << "ptrLocation : " << ptrLocation << std::endl;
        // voidPtr[ptrLocation] = reinterpret_cast<void*> (&helloClass::printHello);
        
        //to just demonstrate the conditional switching of functions
        if(ptrLocation == 1) {
            number = 47;
            voidPtr[ptrLocation] = &helloClass::printJello; //save the member function ptr
        }
        else {
            number = 37;
            voidPtr[ptrLocation] = &helloClass::printHello; //save the member function ptr
        }
    };
    
    //this function actually decides which memeber function to be called
    void sendHello() {
        // func funcPtr = (func) voidPtr[ptrLocation];
        
        //convert the member function ptr to void pointer
        //func is the only type the global function accepts
        func funcPtr = (func) voidPtr[ptrLocation];
        sayHello(funcPtr);  //send the callback to global function
    }
    
    //these functions actually do the work as callbacks
    //but since these functions are accessed explicitly using their function
    //entry points, there's no automatic way of determining which object they belong to
    //because no "this" ptr is being given to them
    void printHello() {
        //this demonstrates the segfault issue when trying to access a memebr varibale
        //this happens because the funtion entry point and its corresponding
        //object pointers are not same.
        //this will print garbage value due to segfault
        std::cout << "Hello World : " << number << "\n";
        
        //the following statements shows the different pointers, both
        //entry point of this function and the object ptr
        //for simplicity, we're accessing them from the arrays directly using the index value
        std::cout << "Func entry : " << (void*)&helloClass::printHello << "\n";
        std::cout << "voidPtr[0] : " << (void*)voidPtr[0] << "\n";  //output same as above
        std::cout << "objPtr[0] : " << (void*)objPtr[0] << "\n";    //the object for this callback
        
        //below we use the object ptr to access the member variable
        //this time, no segfaults because we're using the right object ptr
        std::cout << "number : " << objPtr[0]->number << "\n\n";
    }
    
    void printJello() {
        std::cout << "Hello Jello : " << number << "\n";
        
        std::cout << "Func entry : " << (void*)&helloClass::printJello << "\n";
        std::cout << "voidPtr[1] : " << (void*)voidPtr[1] << "\n";
        std::cout << "objPtr[1] : " << (void*)objPtr[1] << "\n";
        std::cout << "number : " << objPtr[1]->number << "\n\n";
    }
};

//-----------------------------------------------------------------------------------//
//two objects

helloClass foo;
helloClass bar;
    
//-----------------------------------------------------------------------------------//

int main() {
    std::cout << "\n";
    foo.sendHello();
    bar.sendHello();
}

//===================================================================================//

Leave a Reply

Your email address will not be published. Required fields are marked *

This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.

The reCAPTCHA verification period has expired. Please reload the page.