Cannot pass a member function as a callback function Cause and Solution

Many Arduino functions and libraries have a problem where member functions cannot be passed as callback functions (strictly speaking, members cannot be accessed from callback functions), and this has been bothering me quite a bit.
This article describes the cause and solution of this problem.
Built-in callback function usage
It is mainly used when you want to temporarily execute an arbitrary function separately from the main loop. For example, when data is received, or when the state of an IO pin changes.
In Arduino, you can use the attachInterrupt
function to interrupt when the state of a pin changes. If it is a global function, you can call it without any problem.
void onChange()
{
// When pin 1 changes, this is where it is called.
}
void setup()
{
attachInterrupt(digitalPinToInterrupt(1), onChange, CHANGE);
}
Why can’t member functions call back?
Most Arduino libraries pass callback functions as function pointers. Since member function pointers and function pointers have different types, member functions cannot be passed as callback functions.
In essence, you cannot reference an instance from a callback function (a regular static function). Put another way, it is because you cannot access the this pointer from a callback function.
As an example, consider a class of sensor called rotary encoder that reads the angle of rotation. The rotary encoder outputs a pulse signal, so the pulse signal should be detected by an interrupt.
class Encoder
{
int pinA;
int pinB;
int count;
public:
Encoder(int pinA, int pinB)
: pinA{ pinA }
, pinB{ pinB }
, count{}
{}
void begin()
{
attachInterrupt(digitalPinToInterrupt(pinA), onChange, CHANGE); // error
attachInterrupt(digitalPinToInterrupt(pinB), onChange, CHANGE); // error
}
int getCount() const
{
return count; // In fact, exclusive control is required
}
private:
void onChange()
{
this->count += ...
}
};
Since we are not able to pass instance information to attachInterrupt
, if we were able to call back a member function, we would not be able to determine which instance to call onChange
on.
static Encoder enc0{ 0, 1 };
static Encoder enc1{ 2, 3 };
It may be a little easier to understand if you understand this pointer. The compiler interprets a member function call as a normal function call. In this case, a hidden argument this
appears. This is the identity(?) of this pointer. This is the this
pointer.
The previous class is interpreted as follows. The class is actually name-qualified, so this is just an image.
struct Encoder
{
int pinA;
int pinB;
int count;
};
void begin(Encoder* this)
{
attachInterrupt(digitalPinToInterrupt(this->pinA), onChange, CHANGE);
attachInterrupt(digitalPinToInterrupt(this->pinB), onChange, CHANGE);
}
void getCount(const Encoder* this)
{
return this->count;
}
void onChange(Encoder* this)
{
this->count += ...
}
The caller passes a pointer to the instance.
static Encoder enc{ 0, 1 };
void setup()
{
begin(&enc);
}
void loop()
{
int count = getCount(&enc);
}
You can see that calling a member function requires a pointer (&enc
) pointing to the calling instance. However, attachInterrupt
takes only a function pointer as an argument, so it does not have the ability to pass a pointer to the instance. For this reason, you cannot access the member.
Solutions
Since it is impossible to pass a member function to a function that takes a function pointer in this way, the goal is to use a regular function as a callback function and pass a pointer of the instance to the function via a static variable. Since different situations call for different solutions, here are a few.
Prerequisite Knowledge
Lambda expressions are function objects and can also be defined within functions.
int main() { auto f = [ /*capture list*/ ]( /*args*/ ){ /*statement*/ }; f(); }
A lambda expression with an empty capture list can be pointed to by a function pointer.
attachInterrupt(..., [](){ /* ここが呼ばれる */ }, CHANGE);
Static variables can be referenced from lambda expressions without capture.
int main() { static int value; []() { value = 100; } }
Static member functions can be called back. However, access to the member is not possible because there is no this pointer.
class Encoder { void begin() { attachInterrupt(..., onChange, CHANGE); } static void onChange() { } };
If a void pointer can be passed to the callback function / Easy
Some boards and libraries have functions that allow you to pass an arbitrary pointer to a callback function. this pointer can be passed to a callback function so that you can access its members.
M5Stack : attachInterruptArg
Raspberry Pi Pico : attachInterruptParam
※When board is used earlephilhower/arduino-pico
class Encoder
{
int pinA;
int pinB;
int count;
public:
Encoder(int pinA, int pinB)
: pinA{ pinA }
, pinB{ pinB }
, count{}
{}
// Depending on the implementation of attachInterruptArg, in most
// cases the pin number and this pointer are held together.
// If the pin number is copied, the source and the target will
// have the same pin number, and the this pointer of either
// will be overwritten, making it unusable.
Encoder(const Encoder&) = delete;
void begin()
{
attachInterruptArg(pinA, onChange, this, CHANGE); // Register this pointer
attachInterruptArg(pinB, onChange, this, CHANGE);
}
private:
static void onChange(void* param)
{
// The registered this pointer is passed to param,
// so it can be cast and restored
Encoder* thisPtr = static_cast<Encoder*>(param);
thisPtr->count += ...
}
};
static Encoder enc0{ 0, 1 };
static Encoder enc1{ 2, 3 };
static Encoder enc2{ 4, 5 };
void setup()
{
enc0.begin();
enc1.begin();
enc2.begin();
}
void loop()
{
int count0 = enc0.getCount();
int count1 = enc1.getCount();
int count2 = enc2.getCount();
}
If you can pass std::function / Easy
std::function
is a class that integrates member functions, captured lambda expressions, and other functions.
To pass member functions, you need to use the std::bind
function, which can be a bit complicated if there are arguments. Instead, I personally find it easier to pass a lambda expression that captures this pointer as follows
class Encoder
{
<略>
void begin()
{
attachInterrupt(pinA, [this]() { this->onChange(); }, CHANGE);
attachInterrupt(pinB, [this]() { this->onChange(); }, CHANGE);
}
void onChange()
{
this->count += ...
}
};
If each instance has a unique value / OK
When multiple instances are instantiated, each instance has a unique value (in this case, the pin number). In this case, an associative array is created with the pin number as the key to obtain a pointer pointing to the instance.
static Encoder enc0{ 0, 1 }; // < pinA = 0
static Encoder enc1{ 2, 3 }; // < pinA = 2
static Encoder enc2{ 4, 5 }; // < pinA = 4
Create a static array for the number of pins as shown below and reference it from the callback function.
class Encoder
{
int pinA;
int pinB;
int count;
public:
Encoder(int pinA, int pinB)
: pinA{ pinA }
, pinB{ pinB }
, count{}
{}
// Deleted because if copied, the source and itself will
// have the same pin number and one of them will be unusable
Encoder(const Encoder&) = delete;
void begin()
{
darkAttachInterrupt(pinA);
darkAttachInterrupt(pinB);
}
private:
void onChange()
{
count += ...
}
void darkAttachInterrupt(int pin)
{
static Encoder* these[100];
these[pin] = this; // Set to get this pointer from pin number
switch (pin)
{
case 0: attachInterrupt(digitalPinToInterrupt(0), [](){ these[0]->onChange(); }, CHANGE); break;
case 1: attachInterrupt(digitalPinToInterrupt(1), [](){ these[1]->onChange(); }, CHANGE); break;
case 2: attachInterrupt(digitalPinToInterrupt(2), [](){ these[2]->onChange(); }, CHANGE); break;
case 3: attachInterrupt(digitalPinToInterrupt(3), [](){ these[3]->onChange(); }, CHANGE); break;
... // OMG
case 59: attachInterrupt(digitalPinToInterrupt(59), [](){ these[59]->onChange(); }, CHANGE); break;
}
}
};
If only one instance is created / OK
An example is the receive interrupt of an I2C slave. If you are connecting to multiple I2C buses, there could be more than one instance, but since such a situation is unlikely, we will give up on it.
The pointer is still held in a static variable, but it is simpler because it does not need to be an array.
#include <Wire.h>
class I2cSlaveReader
{
TwoWire& wire;
public:
I2cSlaveReader(TwoWire& wire)
: wire(wire)
{}
// Multiple instances prohibited
I2cSlaveReader(const I2cSlaveReader&) = delete;
void begin()
{
static I2cSlaveReader* self;
self = this;
wire.onReceive([](int n){
self->onReceive(n);
});
}
private:
void onReceive(int n)
{
// I2C receive interrupt
}
};
static I2cSlaveReader reader{ Wire };
void setup()
{
reader.begin();
}
When creating multiple instances / OMG
Multiple instances can be handled by evolving the above classes.
One static variable in the class is created per class. Therefore, if multiple instances are created, the “this” pointer will be overwritten and multiple instances cannot be handled.
Therefore, templates are used.
template <int Unique>
class Hell
{
static int value;
};
static Hell<0> hell0;
static Hell<1> hell1;
static Hell<2> hell2;
If you instantiate a template argument with a different value or type, the compiler will instantiate the template as a separate class as follows.
class Hell_0
{
static int value;
};
class Hell_1
{
static int value;
};
class Hell_2
{
static int value;
};
static Hell_0 hell0;
static Hell_1 hell1;
static Hell_2 hell2;
Multiple static variables are created in this manner. This specification is used. Since it is too much to ask the user to specify unique values, use the __COUNTER__
macro.
template <int Unique>
class HellImpl
{
public:
HellImpl(const HellImpl&) = delete;
void begin()
{
static HellImpl* self;
self = this;
hoge.onReceive([](){
self->onReceive();
});
}
private:
void onReceive()
{
}
};
#define Hell HellImpl<__COUNTER__>
static Hell hell0;
static Hell hell1;
static Hell hell2;
Because of the template, the types of hell0
hell1
hell2
are all different types. Since it is difficult to pass instances to functions and so on, interface classes are prepared so that the types can be handled in an integrated manner.
class IHell
{
public:
virtual void begin() = 0;
};
template <int Unique>
class HellImpl
: public IHell
{
public:
HellImpl(const HellImpl&) = delete;
void begin() override
{
static HellImpl* self;
self = this;
hoge.onReceive([](){
self->onReceive();
});
}
private:
void onReceive()
{
}
};
#define Hell HellImpl<__COUNTER__>
static Hell hell0;
static Hell hell1;
static Hell hell2;
void invokeBegin(IHell& hell) // All can be referenced by IHell& type
{
hell.begin();
}
void setup()
{
invokeBegin(hell0);
invokeBegin(hell1);
invokeBegin(hell2);
}
Conclusion
The method of preparing an array of pin numbers is actually used in the Teensy encoder library 1. The author wrote the following comment.
this giant function is an unfortunate consequence of Arduino’s attachInterrupt function not supporting any way to pass a pointer or other context to the attached function.
On the user side, we can only do as bad as this article does, so I hope the libraries will do their best 🤔 This issue has been discussed at length on GitHub, but has not been addressed ((+_+)) 2 3.
Thank you for taking a look at this rather lengthy article.
Wrapper function implementation for attachInterrupt
https://github.com/PaulStoffregen/Encoder/blob/c083e9cbd6400f7e72a794c0d371a00a09d2a25d/Encoder.h#L387-L943 ↩︎
issue - Enable use of attachInterrupt() in classes #85
Pull Request(Closed) - Support extra parameter on attachInterrupt() #58