Task Wrapper - October 18, 2019
If a class requires a task as a member, it causes some problems. A pros::Task
requires a callback function to run, but due to some limitations in c++, the address of that function needs to be known in compile-time. This means that the task callback needs to be static
, which means that it does not belong to the class instance and it can’t access class members. To fix this, a pattern named trampoline is used. Trampoline is the act of passing a opaque pointer to the class object through the task to be received by the static function, having the static function cast the pointer to the correct class type, and calling a member function to execute the task.
Since implementing a trampoline requires a solid amount of boilerplate, I have written an abstract task wrapper that does this for me. To be able to run using unit tests and CI, I am using OkapiLib’s CrossPlatformTask
as the task object.
Note how the this
pointer is passed when task is constructed:
task = std::make_unique<CrossplatformThread>(trampoline, this, iname.c_str());
Then, the pointer is cast by the trampoline and a virtual member loop()
is called, which is then resolved by dynamic binding:
void TaskWrapper::trampoline(void* iparam) {
pros::delay(20);
static_cast<TaskWrapper*>(iparam)->loop();
}
Here is the full TaskWrapper
implementation:
/**
* A utility class that wraps a task trampoline. To use, simply inherit your class from TaskWrapper
* and override the `loop` method. To start the task, the `startTask` method must be called, either
* from the constructor or from outside the class.
*/
class TaskWrapper {
protected:
explicit TaskWrapper(const std::shared_ptr<Logger>& ilogger = Logger::getDefaultLogger());
TaskWrapper(const TaskWrapper& itask) = delete;
TaskWrapper(TaskWrapper&& itask) = default;
virtual ~TaskWrapper() = default;
/**
* Override this function to implement a custom task loop.
* Will throw if not overridden.
*/
virtual void loop();
public:
/**
* Start the task.
*
* @param iname The task name, optional.
*/
virtual void startTask(const std::string& iname = "TaskWrapper");
/**
* Kill the task.
*/
virtual void killTask();
/**
* Get the task name.
*
* @return The name.
*/
virtual std::string getName();
protected:
std::shared_ptr<Logger> logger {nullptr};
private:
static void trampoline(void* iparam);
std::unique_ptr<CrossplatformThread> task {nullptr};
};
Source file:
TaskWrapper::TaskWrapper(const std::shared_ptr<Logger>& ilogger) : logger(ilogger) {}
void TaskWrapper::loop() {
std::string msg("TaskWrapper::loop: loop is not overridden");
LOG_ERROR(msg);
throw std::runtime_error(msg);
}
void TaskWrapper::startTask(const std::string& iname) {
if (task) LOG_INFO("TaskWrapper::startTask: restarting task: " + iname);
task = std::make_unique<CrossplatformThread>(trampoline, this, iname.c_str());
}
void TaskWrapper::killTask() {
task = nullptr;
}
std::string TaskWrapper::getName() {
return task->getName();
};
void TaskWrapper::trampoline(void* iparam) {
pros::delay(20);
static_cast<TaskWrapper*>(iparam)->loop();
}