Класс, объявляющий интерфейс для взаимодействия с приложением, представлен в Листинг 97.
namespace sensor
{
class ISensorControl
{
public:
virtual ~ ISensorControl () = default;
virtual void initialize() = 0; // (1)
virtual void shutDown() = 0; // (2)
virtual void assignDriver(DriverPointer driver) = 0; // (3)
virtual DriverPointer getAssignedDriver() = 0; // (4)
virtual DriverPointer getSensorDriver(SensorNumber number) = 0; // (5)
virtual void addSensor(SensorType type, SensorNumber number) = 0; // (6)
virtual void deleteSensor(SensorNumber number) = 0; // (7)
virtual bool isSensorExist(SensorNumber number) = 0; // (8)
virtual bool isSensorOperable(SensorNumber number) = 0; // (9)
virtual SensorValue getSensorValue(SensorNumber number) = 0; // (10)
virtual void querySensorValue(SensorNumber number, SensorValueCallback callback) = 0; // (11)
virtual void readSensorValues(SensorValueCallback callback) = 0; // (12)
virtual SensorValue getMinValue(SensorNumber first, SensorNumber last) = 0; // (13)
virtual SensorValue getMaxValue(SensorNumber first, SensorNumber last) = 0; // (14)
virtual void setAlert(SensorNumber number, SensorAlertCallback callback, SensorValue alertValue, AlertRule alertRule, CheckAlertTimeout checkTimeoutSeс = 1) = 0; // (15)
virtual void resetAlert(SensorNumber number) = 0; // (16)
static ISensorControl* createControl(); // (17)
};
};
В строке 1 и 2 объявлены методы для запуска и останова. В строках 3 и 4 объявлены методы для назначения и получения драйвера. Этот драйвер должен быть создан и назначен в самом начале работы, поскольку он будет передаваться новым создаваемым датчикам. Узнать назначенный драйвер для соответствующего датчика можно в методе 5.
В строках 6 и 7 объявлены методы для добавления и удаления датчика. В методе 8 можно проверить, существует ли датчик с переданным номером, в методе 9 можно проверить, является ли датчик работоспособным.
В строке 10 объявлен метод для чтения текущего показания датчика. В методе 11 осуществляется асинхронный запрос показания датчика, значение будет возвращаться через передаваемый обратный вызов. В строке 12 осуществляется опрос показаний всех работоспособных датчиков, значения также возвращаются через обратный вызов. С помощью методов, объявленных в строках 13 и 14, можно получить минимальное и максимальное значение для набора датчиков с номерами из указанного диапазона.
В строке 15 назначается отслеживание пороговых значений, в строке 16 отслеживание выключается. С помощью метода, объявленного в строке 17, можно создать экземпляр соответствующего интерфейсного класса.
Класс, реализующий интерфейс, приведен в Листинг 98.
namespace sensor
{
class ISensor;
class IDriver;
class CommandQueue;
class AlertControl;
class SensorContainer;
class SensorControl: public ISensorControl
{
public:
SensorControl();
~SensorControl();
void initialize() override;
/* Other Interface methods – they are not displayed here*/
private:
SensorContainer* sensorContainer_; // (1)
CommandQueue* commandQueue_; // (2)
AlertControl* alertControl_; // (3)
bool isInitialized_; // (4)
DriverPointer driver_; // (5)
void checkInitialize(); // (6)
void checkDriver(); // (7)
};
}; //namespace sensor
В строке 1 объявлен контейнер для хранения датчиков, в строке 2 – класс для выполнения асинхронных запросов, в строке 3 – класс для отслеживания пороговых значений. Соответствующие указатели создаются в конструкторе и уничтожаются в деструкторе. Индикатор 4 указывает, была ли выполнена инициализация.
В строке 6 объявлен вспомогательный метод, который проверяет, была ли выполнена инициализация (если нет, выбрасывает исключение). В строке 7 аналогичный метод проверяет, был ли установлен драйвер.
Рассмотрим, как здесь используются обратные вызовы. Для начала самый простой случай – чтение показаний работоспособных датчиков (Листинг 99).
void SensorControl::readSensorValues(SensorValueCallback callback)
{
checkInitialize(); // (1)
sensorContainer_->forEachSensor([callback](SensorNumber number, SensorPointer sensor) // (2)
{
if (sensor->isOperable()) // (3)
{
callback(number, sensor->getValue()); // (4)
}
}
);
}
В строке 1 производится проверка, инициализирован ли класс. Если класс не проинициализирован, то функция выбросит исключение.
В строке 2 происходит перебор элементов контейнера, в качестве обратного вызова используется лямбда-выражение. Контейнер будет вызывать лямбда-выражение, в которое он будет передавать номер датчика и указатель на экземпляр класса. В теле выражения проверяется, является ли датчик работоспособным (строка 3), и если да, то выполняется соответствующий обратный вызов (строка 4).
Рассмотрим теперь поиск максимального и минимального значения для заданного диапазона номеров датчиков. Вначале разработаем вспомогательный класс, который будет последовательно принимать на вход показания датчиков и искать среди них максимальное и минимальное значение (Листинг 100).
class FindMinMaxValue
{
public:
enum MinMaxSign { MIN_VALUE = 0, MAX_VALUE = 1 }; // (1)
FindMinMaxValue(SensorNumber first, SensorNumber last, MinMaxSign sign) : // (2)
sign_(sign), first_(first), last_(last), count_(0)
{
if (sign == MIN_VALUE)
{
result_ = std::numeric_limits<SensorValue>::max(); // (3)
}
else
{
result_ = std::numeric_limits<SensorValue>::min(); // (4)
}
arrayFunMinMax_[MIN_VALUE] = &FindMinMaxValue::CompareMin; // (5)
arrayFunMinMax_[MAX_VALUE] = &FindMinMaxValue::CompareMax; // (6)
}
void operator()(SensorNumber number, SensorPointer sensor) // (7)
{
if ( sensor->isOperable() && (number >= first_ && number <= last_) ) // (8)
{
(this->*arrayFunMinMax_[sign_])(sensor->getValue()); // (9)
count_++; // (10)
}
}
SensorValue result() { return result_; } // (11)
size_t count() { return count_; } // (12)
private:
SensorNumber first; // (13)
SensorNumber last; // (14)
MinMaxSign sign; // (15)
SensorValue result; // (16)
size_t count; // (17)
using FunMinMax = void (FindMinMaxValue::*)(SensorValue value); // (18)
void CompareMin(SensorValue value) // (19)
{
if (result_ > value)
{
result_ = value;
}
}
void CompareMax(SensorValue value) // (20)
{
if (result_ < value)
{
result_ = value;
}
}
FunMinMax arrayFunMinMax_[2]; // (21)
};
В строке 2 объявлен конструктор, который принимает на вход следующие параметры: минимальное значение диапазона номеров; максимальное значение диапазона номеров; параметр, указывающий, что необходим поиск минимального либо максимального значения. В конструкторе инициализируются переменные класса: минимальное значение диапазона (объявлено в строке 13); максимальное значение диапазона (объявлено в 14); параметр для поиска (объявлено в 15); итоговый результат (объявлено в 16); количество датчиков, которые участвовали в поиске (объявлено в 17). В зависимости от переданного параметра начальный результат инициализируется соответственно максимальным либо минимальным значением (строки 3 и 4). Кроме того, инициализируется массив указателей на функцию (строки 5 и 6, объявление в 21). Данные функции предназначены для сравнения и запоминания максимального либо минимального значений (объявлены в 19 и 20).
Анализ очередного значения происходит в перегруженном операторе 7. На вход подаются номер датчика и указатель на датчик. Если датчик работоспособный и его номер попадает в заданный диапазон номеров (строка 8), то в зависимости от параметра поиска через указатель вызывается соответствующая функция для анализа (строка 9), а также увеличивается счетчик просмотренных датчиков (строка 10). Функции 11 и 12 возвращают итоговые результаты.
Итак, класс для анализа готов. Теперь можно вызвать метод для итерации по элементам контейнера, и в качестве обратного вызова передать экземпляр соответствующего вспомогательного класса. Метод будет вызывать перегруженный оператор, и таким образом, мы узнаем минимальное либо максимальное значение (Листинг 101).
SensorValue SensorControl::getMinValue(SensorNumber first, SensorNumber last)
{
checkInitialize();
FindMinMaxValue fmv(first, last, FindMinMaxValue::MIN_VALUE);
sensorContainer_->forEachSensor(fmv);
return fmv.result();
}
SensorValue SensorControl::getMaxValue(SensorNumber first, SensorNumber last)
{
checkInitialize();
FindMinMaxValue fmv(first, last, FindMinMaxValue::MAX_VALUE);
sensorContainer_->forEachSensor(fmv);
return fmv.result();
}
Уже после того, как классы модуля были разработаны, протестированы и начали использоваться в системе, появилось новое требование – ввести поддержку системного API. Как известно, в интерфейсах системных API можно использовать только внешние функции и простые структуры данных в стиле C; классы и другие специфические конструкции C++ использовать нельзя (см. п. 1.4.2). Так что же, все теперь придется переписывать? Можно предложить следующее решение: использовать интерфейс API как оболочку для вызова методов класса. Концептуальный пример приведен в Листинг 102.
using ControlPointer = std::unique_ptr<sensor::ISensorControl>;
ControlPointer g_SensorControl(sensor::ISensorControl::createControl());
void initialize () // This function is declared in the header file as part of API interface
{
g_SensorControl->initialize();
}
Однако не все так просто, перед нами встают следующие проблемы.
1. В исходной реализации мы использовали специфические типы C++, такие, как std::function, smart pointers и т. п., что не допускается в интерфейсах системных API. Какие типы использовать взамен?
2. Для обработки ошибок в исходной реализации мы использовали исключения. Как сейчас обрабатывать ошибки, ведь в интерфейсах API исключения недопустимы?
3. В исходной реализации мы в каждом потоке могли объявить отдельный интерфейсный класс и работать с ним независимо от остальных потоков. Как теперь обеспечить многопоточную работу, ведь отдельные потоки вызывают одни и те же интерфейсные функции?
4. В исходной реализации драйвер настраивался путем создания нового класса и передаче его в интерфейсный класс. Как теперь настраивать драйвер, если в интерфейсах API нельзя использовать классы?
5. Как организовать обратные вызовы?
Рассмотрим, как эти проблемы можно решить.
В исходной реализации общие типы объявлены в SensorDef.h, но мы не можем просто перенести их в интерфейс API из-за использования специфических конструкций С++. Поэтому нам придется повторить эти объявления в стиле C с использованием простых типов, которые можно будет использовать в интерфейсных функциях. Объявления представлены в Листинг 103.
#ifdef _WINDOWS // (1)
#ifdef LIB_EXPORTS
#define LIB_API __declspec(dllexport)
#else
#define LIB_API __declspec(dllimport)
#endif
#else
#define LIB_API
#endif
typedef uint32_t SensorNumber; // (2)
typedef double SensorValue; // (3)
typedef uint32_t CheckAlertTimeout; // (4)
typedef uint32_t SensorType; // (5)
typedef uint32_t DriverType; // (6)
typedef uint32_t AlertRule; // (7)
typedef void(*SensorValueCallback)(SensorNumber, SensorValue, void*); // (8)
typedef CheckAlertTimeout(*SensorAlertCallback)(SensorNumber, SensorValue, void*); // (9)
typedef SensorValue(*OnSimulateReadValue)(SensorNumber, int, void*); // (10)
typedef int (*OnSimulateOperable)(SensorNumber, void*); // (11)
enum eSensorType // (12)
{
SENSOR_SPOT = 0,
SENSOR_SMOOTH = 1,
SENSOR_DERIVATIVE = 2,
};
enum eDriverType // (13)
{
DRIVER_SIMULATION = 0,
DRIVER_USB = 1,
DRIVER_ETHERNET = 2
};
enum eAlertRule // (14)
{
ALERT_MORE = 0,
ALERT_LESS = 1
};
В строке 1 объявлены определения для экспортируемых функций. Эти объявления необходимы для компиляции динамической библиотеки в среде Windows, для других платформ они неактуальны.
В строках 2–4 объявлены типы, которые будут использоваться для входных параметров интерфейсных функций. Это те же объявления, которые использовались в исходной реализации (SensorDef.h, см. п. 6.2.2).
В строках 5–7 вместо перечислений C++ объявляются простые числовые типы. В экспортируемых функциях нежелательно использовать перечисления как типы входных параметров, потому что размер этих типов в C явно не определен. Вместо этого перечисления используются в качестве числовые констант, они объявлены соответственно в строках 12–14.
В строках 8–11 объявлены типы указателей на функцию для выполнения обратных вызовов. Как видим, в отличие от исходной реализации здесь присутствует дополнительный параметр для указания контекста вызова.
Исходя из концепции «API как оболочка», сигнатура интерфейсных функций API должна повторять сигнатуру методов интерфейсного класса. Однако здесь мы сталкиваемся с некоторыми проблемами, одна из которых – это обработка ошибок.
В исходной реализации мы обрабатывали ошибки с помощью исключений. Теперь исключения использовать нельзя, в системных API они недопустимы. Тем не менее, вызываемая функция должна как-то уведомить о возникновении ошибки, для чего могут использоваться следующие способы:
1) функция возвращает результат, для которого некоторое предопределенное значение говорит о том, что произошла ошибка. Код ошибки возвращается с помощью отдельного вызова;
2) код ошибки возвращается через дополнительный параметр функции;
3) все функции возвращают результат выполнения, который является кодом ошибки.
Ни один способов не является идеальным, каждый имеет свои достоинства и недостатки. Так, в первом способе возникают сложности, если результат, возвращаемый функцией, не имеет значений, которые недопустимы и могут сигнализировать о возникновении ошибки36. Во втором способе для всех вызовов придется использовать дополнительную переменную – код ошибки, даже если он нас не интересует. В третьем способе, если функция возвращает результат, то для него приходится использовать отдельный входной параметр, что не всегда удобно.
В нашем случае мы выберем третий способ, исходя из следующих соображений: объявления функций будут выглядеть единообразно; возникновение ошибки можно узнать непосредственно в момент вызова, (например, в операторе if); если функция не возвращает значений, то ей не нужно передавать никакие дополнительные параметры. Объявления интерфейсных функций с возвратом ошибок представлены в Листинг 104.
typedef unsigned int ErrorCode;
LIB_API ErrorCode initialize();
LIB_API ErrorCode shutDown();
LIB_API ErrorCode assignDriver(DriverType type);
LIB_API ErrorCode getAssignedDriver(DriverType* type);
LIB_API ErrorCode getSensorDriver(SensorNumber number, DriverType* type);
LIB_API ErrorCode addSensor(SensorType type, SensorNumber number);
LIB_API ErrorCode deleteSensor(SensorNumber number);
LIB_API ErrorCode isSensorExist(SensorNumber number, int* isExist);
LIB_API ErrorCode isSensorOperable(SensorNumber number, int* isOperable);
LIB_API ErrorCode getSensorValue(SensorNumber number, SensorValue* value);
LIB_API ErrorCode querySensorValue(SensorNumber number, SensorValueCallback callback, void* pContextData);
LIB_API ErrorCode readSensorValues(SensorValueCallback callback, void* pContextData);
LIB_API ErrorCode getMinValue(SensorNumber first, SensorNumber last, SensorValue* value);
LIB_API ErrorCode getMaxValue(SensorNumber first, SensorNumber last, SensorValue* value);
LIB_API ErrorCode setAlert(SensorNumber number, SensorAlertCallback callback, SensorValue alertValue, AlertRule alertRule, CheckAlertTimeout checkTimeoutSeс, void* pContextData);
LIB_API ErrorCode resetAlert(SensorNumber number);
LIB_API ErrorCode setSimulateReadCallback(OnSimulateReadValue callback, void* pContextData);
LIB_API ErrorCode setSimulateOperableCallback(OnSimulateOperable callback, void* pContextData);
В реализации этих функций мы будем возвращать код ошибки, получая его из перехваченного исключения. В качестве примера рассмотрим реализацию функции для получения значения датчика (Листинг 105).
ErrorCode getSensorValue(SensorNumber number, SensorValue* value)
{
ErrorCode error = ERROR_NO; // (1)
try
{
*value = g_SensorControl->getSensorValue(number); // (2)
}
catch (sensor::sensor_exception& e) // (3)
{
error = e.code(); // (4)
}
return error; // (5)
}
В строке 1 объявляем переменную – код возврата. В строке 2 осуществляем вызов метода класса, который заключен в блок try. В строке 3 осуществляется перехват исключения, в строке 4 присваивается код ошибки, который возвращается в строке 5.
Итак, мы придумали, как в интерфейсных функциях осуществлять обработку ошибок. Теперь перед нами встает следующая проблема: как настраивать типы драйверов, ведь в исходной реализации для этого используются классы? Прежде чем перейти к решению этой задачи, остановимся на реализации многопоточной работы, поскольку используемые там конструкции нам понадобятся в дальнейшем.
В исходной реализации в каждом потоке мы могли создать свой экземпляр класса ISensorControl и работать с ним независимо. В случае API это не работает, потому что экземпляр класса в реализации интерфейса объявляется глобальным, и все интерфейсные функции обращаются к одному и тому же экземпляру класса. Выходом здесь будет выделение отдельной области памяти для экземпляра класса в рамках одного потока, т. е. использование локальной памяти потока.
До появления стандарта C++ 11 использовать локальную память потока было непросто: для этого требовалось явное обращение к функциям операционной системы, что усложняло реализацию и делало код платформенно-зависимым. В C++ 11 появилось ключевое слово thread_local, и это сильно упростило жизнь: если в объявлении переменной добавить указанный спецификатор, то она становится локальной в рамках потока, т. е. каждый новый создаваемый поток будет иметь независимый экземпляр соответствующей переменной. Таким образом, достаточно экземпляр интерфейсного класса ISensorControl объявить как thread_local, и теперь для каждого потока будет существовать отдельный независимый экземпляр класса (Листинг 106).
using ControlPointer = std::unique_ptr<sensor::ISensorControl>;
thread_local ControlPointer g_SensorControl(sensor::ISensorControl::createControl());
В исходной реализации в начале работы мы создавали необходимый класс драйвера, который затем передавали интерфейсному классу (Листинг 107). Но в интерфейсах системных API мы классы использовать не можем, как поступить в этом случае? Можно предложить следующее решение: класс драйвера создавать внутри API, а в функцию настройки передавать идентификатор, в соответствии с которым будет создан соответствующий драйвер (Листинг 108).
ISensorControl sensorControl = ISensorControl::createControl;
DriverPointer driver = IDriver::createDriver(DRIVER_SIMULATION);
driver->initialize();
sensorControl->assignDriver(driver);
thread_local sensor::DriverPointer g_DriverSimulation; // (1)
thread_local sensor::DriverPointer g_DriverUSB; // (2)
thread_local sensor::DriverPointer g_DriverEthernet; // (3)
void CreateDriver(sensor::DriverType driverType, sensor::DriverPointer& driverPointer) // (4)
{
if (!driverPointer)
{
driverPointer = sensor::IDriver::createDriver(driverType);
driverPointer->initialize();
}
g_SensorControl->assignDriver(driverPointer);
}
ErrorCode assignDriver(DriverType driverType) // (5)
{
ErrorCode error = ERROR_NO;
try
{
EnumConverter<sensor::DriverType> conv;
conv.convert (driverType,
{sensor::DriverType::Simulation, sensor::DriverType::Usb, sensor::DriverType::Ethernet}); // (6)
if (conv.error())
{
return ERROR_INVALID_ARGUMENT;
}
switch (conv.result()) // (7)
{
case sensor::DriverType::Simulation:
{
CreateDriver(sensor::DriverType::Simulation, g_DriverSimulation);
}
break;
case sensor::DriverType::Usb:
{
CreateDriver(sensor::DriverType::Usb, g_DriverUSB);
}
break;
case sensor::DriverType::Ethernet:
{
CreateDriver(sensor::DriverType::Ethernet, g_DriverEthernet);
}
break;
}
}
catch (sensor::sensor_exception& e)
{
error = static_cast<ErrorCode>(e.code());
}
return error;
}
В строках 1–3 объявляются указатели для хранения классов всех возможных типов драйверов. В строке 4 объявлена вспомогательная функция для создания драйвера. Эта функция проверяет, создан ли драйвер соответствующего типа, при необходимости создает, инициализирует и передает его в интерфейсный класс.
В строке 5 приведена реализация интерфейсной функции для настройки драйвера. В строке 6 конвертируется переданное числовое значение в перечисление C++ (будет рассмотрено ниже). В строке 7 объявлен оператор switch, в котором анализируется полученное значение перечисления, и вызывается вспомогательная функция с соответствующими параметрами.
В функции API для задания типа драйвера используются числовые значения, а в интерфейсном классе используются перечисления C++. Для того, чтобы сконвертировать числовое значение в перечисление, используется вспомогательный класс EnumConverter (Листинг 109)
template <typename Enum> // (1)
class EnumConverter
{
public:
template<typename ConvValueType> // (2)
void convert(ConvValueType value, std::initializer_list<Enum> list) // (3)
{
isError_ = true;
for (Enum item : list) // (4)
{
if (static_cast<ConvValueType>(item) == value) // (5)
{
result_ = item; // (6)
isError_ = false;
break;
}
}
};
bool error() const { return isError_; }
Enum result() const { return result_; }
private:
bool isError_;
Enum result_;
};
В строке 1 объявлен шаблонный класс, параметром которого является тип перечисления. Конвертация происходит в функции 2, которая объявлена в виде шаблона, параметром шаблона является тип числового значения для конвертации. Функция принимает число, которое должно быть сконвертировано, а также список значений перечисления (строка 3). Реализация пробегает по всем элементам списка (строка 4) и, если какой-то из элементов списка перечисления равен переданному значению, запоминает это значение перечисления в качестве результата (строки 5,6).