Different ways to implement callbacks

Concepts:

Virtual Address Memory Layouts:

A stack typically refers to the region of memory used by a process for storing local variables, function arguments, and return addresses.

The stack is a data structure that follows the Last-In-First-Out (LIFO) principle, which means that the most recently added item is the first one to be removed. When a function is called, a new stack frame is created on top of the current stack frame, and the arguments and local variables of the function are stored in this new stack frame. When the function returns, its stack frame is removed from the stack, and the program counter jumps to the return address stored in the previous stack frame.

The stack typically grows downward in memory, meaning that new stack frames are added to the lower addresses in memory, while the stack pointer points to the top of the stack.

  • Frame Stack Address of a function: refers to the address that stores the local variables and saved state of the function. It is created on the stack when a function is called and destroyed when the function returns.
  • Return Address of a function: is the memory location where the program should continue execution after the function has finished executing. When a function is called, the return address is stored on the stack along with the function frame. When the function is done executing, the return address is used to jump back to the calling function. The return address of the function is also on the stack.
  • Memory Address of a function: is the location in memory where the actual executable code of the function is stored. It is a fixed address that is determined at compile time or at runtime, depending on the programming language and the computer.
Images taken from The Linux Programming by Micheal Kerrish.

References: Chapter6.4 Virtual Memory Address of a Process, 6.5 The Stack and Stack Frames from "The Linux Programming" by MiChael Kerrish.

Function Pointer:

A function pointer is a variable that stores the memory address of a function. They can be passed around as arguments to other functions, returned as values from functions, and stored in variables.

  • Unlike normal pointers, a function pointer points to code, not data. Typically a function pointer stores the start of executable code
  • Unlike normal pointers, we do not allocate de-allocate memory using function pointers.

References: Function pointer is C.

Function pointers are often used in callback functions, where a function is passed as an argument to another function and is called by the second function at a later time.

Function pointers are also used to save stack memory.

When a function is called in C or C++, a new stack frame is created for the function's local variables and arguments. This stack frame is allocated from the program's stack memory, which is a limited resource.

If the same code is called multiple times with different arguments, the program will need to allocate a new stack frame for each call, even though the code being executed is identical. This can result in inefficient use of memory.

By using a function pointer to call a single function that can handle multiple cases, the program can avoid duplicating the code and reusing the same stack frame for multiple calls. This can result in significant memory savings, especially in programs that make frequent calls to the same code with different arguments.

Additionally, using function pointers can also make the code more flexible and modular, since different functions can be swapped in and out as needed, without requiring major changes to the calling code.

Callback function:

  • A callback function is a function that is passed as an argument to another function, and is then called by that function at some point during its execution
  • The purpose of the callback function is to provide a way for the caller to customize the behavior of the callee, by specifying a function that will be executed at a certain point in the code.
  • This potentially supports asynchronous operation: the calling entity can continue another operation on its process/threads while the running function perfoms its logic on its process/threads and return the result via the callback. The callback is another operation that the calling entity can now perform.

Implementation

Define, assign and use a function pointer

  • declare a function pointer. For example:
int (*operationPtr)(const int, const int) = nullptr;
  • assign a function pointer to address of a function. For example:
operationPtr = &add;
  • invoke a function via function pointer. For example:
operationPtr(a,b);

Code:

The following program defines function add, subtract, calculate which are all called by function pointers, operationPtr, calculatePtr. Output prints out the addresses that the function pointer points to, the address of the frame stack, and return address of the function being called.

#include <iostream>
using namespace std;


int add (const int a, const int b){
    cout<<"adding "<<a<<" + "<<b<<" = "<<a+b<<endl;
    cout<<"return address of add function "<<__builtin_return_address(0)<<endl;
    cout<<"address of the add function frame in stack "<<__builtin_frame_address(0)<<endl;
    cout<<"address of the calling function frame in stack "<<__builtin_frame_address(1)<<endl;
    return a+b;
}
int subtract (const int a, const int b){
    cout<<"subtract "<<a<<" - "<<b<<" = "<<a-b<<endl;
    cout<<"return address of subtract function "<<__builtin_return_address(0)<<endl;
    cout<<"address of the subtract function frame in stack "<<__builtin_frame_address(0)<<endl;
    cout<<"address of the calling function frame in stack "<<__builtin_frame_address(1)<<endl;
    return a-b;
}


// define function pointer
int (*operationPtr)(const int, const int) = nullptr;
void (*calculatePtr)(const int, const int, const string) = nullptr;

void calculate(const int a, const int b, const string operation){
    cout<<"return address of calculate function: "<<__builtin_return_address(0)<<endl;
    cout<<"address of the calculate function frame in stack "<<__builtin_frame_address(0)<<endl;
    if (operation=="add"){
        // assign function pointer to address of function
        operationPtr = &add;
        // invoke function via function pointer
        printf("operationPtr pointing to add function at address: 0x%p \n", operationPtr);
        operationPtr(a,b);
    }
        
    else if (operation == "subtract"){
        operationPtr = &subtract;
        printf("operationPtr pointing to subtract function at address: 0x%p \n",operationPtr);
        operationPtr(a,b);
    }       
    else
        return;
}

int main(){
    calculatePtr = &calculate; 
    calculatePtr(4,5,"add");
    calculatePtr(10,12, "subtract");
    calculatePtr(34,21, "add");
    calculatePtr(45,6, "subtract");
}

Output:

return address of calculate function: 0x55985b9e8737
address of the calculate function frame in stack 0x7ffd118f31d0
operationPtr pointing to add function at address: 0x0x55985b9e82e9 
adding 4 + 5 = 9
return address of add function 0x55985b9e866d
address of the add function frame in stack 0x7ffd118f31b0
address of the calling function frame in stack 0x7ffd118f31d0
return address of calculate function: 0x55985b9e878f
address of the calculate function frame in stack 0x7ffd118f31d0
operationPtr pointing to subtract function at address: 0x0x55985b9e843c 
subtract 10 - 12 = -2
return address of subtract function 0x55985b9e86c8
address of the subtract function frame in stack 0x7ffd118f31b0
address of the calling function frame in stack 0x7ffd118f31d0
return address of calculate function: 0x55985b9e87e7
address of the calculate function frame in stack 0x7ffd118f31d0
operationPtr pointing to add function at address: 0x0x55985b9e82e9 
adding 34 + 21 = 55
return address of add function 0x55985b9e866d
address of the add function frame in stack 0x7ffd118f31b0
address of the calling function frame in stack 0x7ffd118f31d0
return address of calculate function: 0x55985b9e883f
address of the calculate function frame in stack 0x7ffd118f31d0
operationPtr pointing to subtract function at address: 0x0x55985b9e843c 
subtract 45 - 6 = 39
return address of subtract function 0x55985b9e86c8
address of the subtract function frame in stack 0x7ffd118f31b0
address of the calling function frame in stack 0x7ffd118f31d0

The frame stack address is the location where the function information is stored. The frame stack address is printed out by __builtin_frame_address(0).

The logs indicate that:

  • when calculate is called: the stack frame is at 0x7ffd118f31d0.
  • when add or substract is called: the stack frame is at 0x7ffd118f31b0.
  • this happens becauses the stack grows and shrinks when the function add or substract is called and exits

The return address of the function is the location on the stack where the program continues after a function finishes. It is printed out by __builtin_return_address(0).

The logs indicates the return address of all the functions are different in the program.

The function pointer is assigned to point at the memory address of the function, which is stored at a fixed and special place, determined at compile time. It is printed out by printf("operationPtr pointing to subtract function at address: 0x%p \n",operationPtr).

The logs indicates that memory address of the functions is static. For the duration of the program, the memory address of

  • calculation function is not printed but should be the same every time the function is called.
  • add function is 0x0x55985b9e82e9
  • subtract function is 0x0x55985b9e843c

References:

GNU online docs. Getting address of Return or Frame Address of the function.

Use function pointer to address a function of a different class

Note: If the function that we want to assign address to belong to a different class, we have to do extra steps:

  • define the function pointer that has the correct type, which is the function defined in the scope of the class.
  • initialize an instance of the class
  • invoke the function via the instance of the class

Code:

In the below program, I put add and subtract into the class. Thus, I have to define the function pointer to the correct type, which is Operation::*operationPtr. I also have to create an instance of Operation and then invoke the function pointer using the instance that was created.

#include <iostream>
using namespace std;

class Operation{
    public:
    int add (const int a, const int b){
    cout<<"adding "<<a<<" + "<<b<<" = "<<a+b<<endl;
    return a+b;
    }
    int subtract (const int a, const int b){
        cout<<"subtract "<<a<<" - "<<b<<" = "<<a-b<<endl;
        return a-b;
    }
};


// define function pointer
int (Operation::*operationPtr)(const int, const int) = nullptr;
void (*calculatePtr)(const int, const int, const string) = nullptr;


void calculate(const int a, const int b, const string operation){
    cout<<"address of calculate function"<<&calculatePtr<<endl;
    Operation *opt = new Operation();
    if (operation=="add"){
        // assign function pointer to address of function
        operationPtr = &Operation::add;
        cout<<"address of add function"<<&operationPtr<<endl;
        // invoke function via function pointer
        (opt->*operationPtr)(a,b);
    }
        
    else if (operation == "subtract"){
        operationPtr = &Operation::subtract;
        cout<<"address of subtraction function"<<&operationPtr<<endl;
        (opt->*operationPtr)(a,b);
    }       
    else
        return;
}

int main(){
    calculatePtr = &calculate;
    calculatePtr(4,5,"add");
    calculatePtr(10,12, "subtract");
}

Outputs:

address of calculate function0x405230
address of add function0x405220
adding 4 + 5 = 9
address of calculate function0x405230
address of subtraction function0x405220
subtract 10 - 12 = 2

This output again indicates that there are only 2 stack frames in call stack despite that multiple functions calls were made.

Using function pointer to as callback argument.

The following program implements function pointer as callback.

#include <iostream>
#include <cstdlib>
#include <ctime>

using namespace std;

int generateRandomNumber() {
    srand(time(0));
    return rand() % 100 + 1;
}
// int (*callback)() is the function pointer
void printNumber(int (*callback)()) {
    int num = callback();
    cout << "The random number is: " << num << endl;
}

int main() {
    printNumber(generateRandomNumber);
    return 0;
}

In the printNumber function, int (*callback)() is a function pointer. The syntax int (*callback)() declares callback as a pointer to a function that takes no arguments and returns an int.

The (*callback) syntax is used to call the function pointed to by callback. When printNumber is called and generateRandomNumber is passed as the callback argument, (*callback)() will be equivalent to generateRandomNumber(), and will generate and return a random number.

So, in essence, the (*callback) syntax allows the printNumber function to be flexible and work with any function that matches its signature (i.e., a function that takes no arguments and returns an int), rather than being limited to just one specific function.*

Using function pointer on callback that has argument.

Notes: if I decide to modify the callback so that it accepts an argument, I need to change the callback function definition, and the function which accepts the callback. In addition, note that the parameter value is hard-coded, and provided to callback from within the calling function. The following codes illustrate such scenario:

#include <iostream>
#include <cstdlib>
#include <ctime>

using namespace std;

int generateRandomNumberLargerThan(int x) {
    srand(time(0));
    cout<<"generate a random number larger than "<<x<<endl;
    return rand() % 100 + x;
}
// int (*callback)() is the function pointer
void printNumber(int (*callback)(int)) {
    int num = callback(5);                                      // provide paramenter for callback from within calling function
    cout << "The random number is: " << num << endl;
}

int main() {
    printNumber(generateRandomNumberLargerThan);
    return 0;
}

Using typedef and function wrapper to define Callback:

In this example, typedef std::function<int()> Callback defines a type alias Callback for std::function that takes no arguments and returns an int. This makes the declaration of the printNumber function more flexible, as it can accept any callable target that matches the signature (i.e., takes no arguments and returns an int). The generateRandomNumber function can still be passed as the callback argument, just like in the previous example.

#include <iostream>
#include <cstdlib>
#include <ctime>
#include <functional>

using namespace std;

int generateRandomNumber() {
    srand(time(0));
    return rand() % 100 + 1;
}

typedef std::function<int()> Callback;   // defines a type alias Callback

void printNumber(Callback callback) {
    int num = callback();
    cout << "The random number is: " << num << endl;
}

int main() {
    printNumber(generateRandomNumber);
    return 0;
}

Implementing callback as class's member function:

The following program implements callbacks as a member of a class MyLogic. The callback argument in the function definition is defined by std::function.

#include <iostream>
#include <cstdlib>
#include <ctime>
#include <functional>

using namespace std;

class MyLogic {
public:
    int generateRandomNumber() {
        srand(time(0));
        return rand() % 100 + 1;
    }
};

class MyLog {
public:
    // function<int()> syntax defines a callback function that returns int, 
    // takes void argument
    void printNumber(function<int()> callback) {
        int num = callback();
        cout << "The random number is: " << num << endl;
    }
};

int main() {
    MyLogic logic;
    MyLog log;
    log.printNumber(bind(&MyLogic::generateRandomNumber, &logic));
    return 0;
}

In addition, std::bind is used to create a callable object that invokes NyLogic::generateRandomNumber on the logic object. This callable object is then passed as the argument to printNumber. Note that we must pass the reference &logic to the argument of binds because it is required on cplusplus:

If fn is a pointer to member, the first argument expected by the returned function is an object of the class *fn is a member (or a reference to it, or a pointer to it)

Since generateRandomNumber is a member function of MyLogic, a reference of MyLogic object, &logic, is required.

Using lamda function to pass callback function as an argument

We usually use callback to pass data, return data, or invoke a logic implemented in another class. The following creates 2 classes MyLog and MyLogic. MyLogic prints a number that is generated by myLogic

We can't directly pass logic.generateRandomNumber as the argument to printNumber because generateRandomNumber is a non-static member function and it has an implicit this pointer argument. To pass generateRandomNumber as a callback to printNumber, you need to capture the this pointer of the logic object and pass it along with the call to generateRandomNumber. This can be done using a lambda expression:

#include <iostream>
#include <cstdlib>
#include <ctime>
#include <functional>

using namespace std;

class myLogic {
public:
    int generateRandomNumber() {
        srand(time(0));
        return rand() % 100 + 1;
    }
};

class myLog {
public:
    void printNumber(function<int()> callback) {
        int num = callback();
        cout << "The random number is: " << num << endl;
    }
};

int main() {
    myLogic logic;
    myLog log;
    log.printNumber(
        [&logic]() { return logic.generateRandomNumber(); }  // lamda function
        );
    return 0;
}

In this example, the lambda expression [&logic]() { return logic.generateRandomNumber(); } captures the logic object by reference (i.e., &logic) and returns the result of calling logic.generateRandomNumber(). This lambda expression can be passed as the argument to printNumber, and it will be automatically converted to a std::function object that can be called like a regular function.

Using lamda function as callback that has argument

I modify the function to add an argument. To properly pass this function as an parameter into printNumber, I used

[&logic](int x) { return logic.generateRandomNumberLargerThan(x); }

The lamda function captures a reference of logic object. It then can invoke the function generateRandomNumberLargerThan with the object logic. Note that the integer argument is defined in (int x). The parameter value is input via 'x'.

Code:

#include <iostream>
#include <cstdlib>
#include <ctime>
#include <functional>

using namespace std;

class myLogic {
public:
    int generateRandomNumberLargerThan(int x) {
        cout<<"Generate a random number larger than "<<x<<endl;
        srand(time(0));
        return rand() % 100 + x;
    }
};

class myLog {
public:
    void printNumber(function<int(int)> callback) {
        int num = callback(100);                                      
        cout << "The random number is: " << num << endl;
    }
};

int main() {
    myLogic logic;
    myLog log;
    log.printNumber(
        [&logic](int x) { return logic.generateRandomNumberLargerThan(x); }  // lamda function
        );
    return 0;
}

Using lamda function to define anonymous function as callback

The following program uses lamda expression to define an anonymous function that generates a random number. We don't need to declare and implement generateRandomNumber function beforehand.

#include <iostream>
#include <cstdlib>
#include <ctime>
#include <functional>

using namespace std;

class myLog {
public:
    void printNumber(function<int()> callback) {
        int num = callback();
        cout << "The random number is: " << num << endl;
    }
};

int main() {
    myLog log;
    log.printNumber([]() {                       // lamda function
        srand(time(0));
        return rand() % 100 + 1;
    });
    return 0;
}

Using lamda on anonymous function with arguments

The following program illustrates a syntax that is used on lamda function, when the callback has parameter.

#include <iostream>
#include <cstdlib>
#include <ctime>
#include <functional>

using namespace std;

class myLog {
public:
    void printNumber(function<int(int)> callback) {
        int num = callback(100);
        cout << "The random number is: " << num << endl;
    }
};

int main() {
    myLog log;
    
    log.printNumber([](int x) {                       // lamda function
    cout<<"create a random number larger than "<<x<<endl;
        srand(time(0));
        return rand() % 100 + x;
    });
    return 0;
}

The following syntax define an anonymous function with an integer value.

[](int x) {                       // lamda function
    //function logic;
})

Leave a Reply

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