Статья представляет собой отчет о проверки библиотеки Loki на совместимость с 64-битными системами с помощью анализатора кода Viva64 компании ООО "СиПроВер". Содержатся рекомендации пользователям библиотеки. Статья будет полезна также пользователям других библиотек, построенных на шаблонах, так как раскрывает особенности анализа подобных библиотек.
Библиотека Loki разработана Андреем Александреску как часть книги "Современное проектирование на С++: Обобщенное программирование и прикладные шаблоны проектирования". Аннотация к книге гласит: "В книге изложена новая технология программирования, представляющая собой сплав обобщенного программирования, метапрограммирования шаблонов и объектно-ориентированного программирования на C++. Настраиваемые компоненты, созданные автором, высоко подняли уровень абстракции, наделив язык C++ чертами языка спецификации проектирования, сохранив всю его мощь и выразительность".
Компания ООО "СиПроВер", создающая анализатор кода Viva64 для разработки 64-битных приложений, активно общается с авторами различных программных проектов. Однажды к нам обратился Рич Спозато (Rich Sposato), один из администраторов проекта Loki, и попросил проверить код библиотеки на предмет совместимости с 64-битными системами с помощью нашего инструмента Viva64. Мы сразу же согласились. Ведь это не только возможность принести пользу сообществу разработчиков, но и настоящий "тест выносливости" для нашего анализатора кода Viva64. Всем известно, что библиотека Loki написана с применением самых современных и мощных возможностей языка Си++. И если анализатор Viva64 справится с Loki, то и другие более простые проекты не вызовут никаких проблем.
Надо заметить, что данная статья основана на версии Loki от мая 2009 года (новее, чем официальная на тот момент версия Loki 0.1.7), поэтому в более новых версиях Loki указанные проблемы будут устранены.
Итак, мы скачали последнюю версию Loki из SVN-репозитория и приступили к работе.
Анализатор кода Viva64 интегрируется в среду разработки Microsoft Visual Studio, поэтому разумно было собрать версию Loki именно для этой среды. В составе пакета Loki есть готовые файлы решений для Visual Studio 2005 и Visual Studio 2008. Однако на момент мая 2009 года эти решения содержат лишь 32-битные конфигурации. Поэтому необходимо в Visual Studio создать конфигурации для платформы x64. Добавив нужные конфигурации можно запустить компиляцию 64-битной версии.
Библиотека Loki предназначена для работы на большом количестве различных платформ и собирается с помощью многих наиболее известных компиляторов. Именно поэтому 64-битная версия Loki скомпилировалась практически сразу же. Из 20 проектов, входящих в решение, не собрался только один:
Build: 19 succeeded, 1 failed, 0 up-to-date, 0 skipped
Это был проект SafeFormat:
16> Build started: Project: SafeFormat, Configuration: Debug x64
16>Compiling...
16>main.cpp
16>.\main.cpp(255) : error C3066: there are multiple ways
that an object of this type can be called with these arguments
16> ..\..\include\loki/SafeFormat.h(109): could be
'Loki::PrintfState<Device,Char>
&Loki::PrintfState<Device,Char>::operator ()(bool)'
...
16> while trying to match the argument list '(UInt)'
Текст ошибки несколько сокращен, поскольку полностью он займет целую страницу. Посмотрим на код, который приводит к ошибке:
void test_dword()
{
typedef signed int Int;
typedef unsigned int UInt;
typedef signed long Long;
typedef unsigned long ULong;
Int i(0);
UInt ui(0);
Long l(0);
ULong ul(0);
Printf("%d")(i);
Printf("%d")(ui); // Проблема в этой строке
Printf("%d")(l);
Printf("%d")(ul);
}
Printf - это функция, внутри которой используется макрос LOKI_PRINTF_STATE_FORWARD. С помощью этого макроса задается ряд вспомогательных функций. В нем-то и кроется проблема. Внутри файла SafeFormat.h вместо следующего фрагмента:
#if (defined(_WIN32) || defined(_WIN64))
LOKI_PRINTF_STATE_FORWARD(unsigned long)
#else
необходимо написать так:
#if (defined(_WIN32) || defined(_WIN64))
#if (defined(_WIN64))
LOKI_PRINTF_STATE_FORWARD(unsigned int)
#endif
LOKI_PRINTF_STATE_FORWARD(unsigned long)
#else
После этого исправления ошибка компиляции пропадет и все 20 проектов библиотеки скомпилируются.
Однако среди прочих диагностических сообщений (warnings) компилятора мы видим сообщение, касающееся 64-битного кода:
2>.\CachedFactoryTest.cpp(204) : warning C4267: 'argument' :
conversion from 'size_t' to 'const int', possible loss of data
2> .\CachedFactoryTest.cpp(238) : see reference to function
template
instantiation 'milliSec typicalUse<Cache>
(Cache &,unsigned int,unsigned int,unsigned int)' being compiled
2> with
2> [
2> Cache=CRandomEvict
2> ]
2> .\CachedFactoryTest.cpp(252) : see reference to function
template
instantiation 'void displayTypicalUse<CRandomEvict>
(Cache &,unsigned int,unsigned int,unsigned int)' being compiled
2> with
2> [
2> Cache=CRandomEvict
2> ]
Это сообщение говорит о потенциально опасном преобразовании типа size_t внутри функции typicalUse() в файле CachedFactoryTest.cpp:
// Registering objects
for(size_t i=0;i<objectKind;i++)
CC.Register(i, createProductNull);
Граница цикла (переменная objectKind) представлена типом unsigned. Поэтому использовать для счетчика цикла переменную типа size_t не имеет смысла. После исправления типа счетчика цикла проблема исчезнет:
// Registering objects
for(unsigned i=0;i<objectKind;i++)
CC.Register(i, createProductNull);
После этих небольших исправлений мы имеем 64-битную библиотеку, которая успешно компилируется и не выдает никаких диагностических сообщений (warnings) насчет 64-битности. Но насколько в действительности код библиотеки корректен? Это мы определим с помощью нашего анализатора кода Viva64.
Для того чтобы убедится в совместимости Loki с 64-битными системами, выполним анализ кода с помощью Viva64. Анализатор кода Viva64 предназначен для разработки новых 64-битных приложений и для миграций существующих 32-битных на 64-битную платформу.
Запуск Viva64 для анализа Loki выявил список из 89 потенциально-опасных синтаксических конструкций. Это не значит, что в библиотеке Loki содержится 89 ошибок, связанных с 64-битным кодом. Но все эти 89 мест должен просмотреть разработчик для того, чтобы понять, действительно ли там возможна ошибка или нет. Разумеется, мы проанализировали эти ошибки.
Начнем с ошибки, связанной с некорректно используемой константой LONG_MIN в следующей функции:
char* RenderWithoutSign(LOKI_SAFEFORMAT_SIGNED_LONG n,
char* bufLast, unsigned int base, bool uppercase)
Она находится в файле SafeFormat.h. Проблема в строке:
if (n != LONG_MIN) {
Тип LOKI_SAFEFORMAT_SIGNED_LONG объявляется как тип, способный вмещать 64-битные значения в 64-битной системе. В Unix-системах (с моделью данных LP64) для этого используют тип long. Но в 64-битных Windows-системах (модель данных LLP64) тип long остался 32-битным. Поэтому в Loki тип LOKI_SAFEFORMAT_SIGNED_LONG объявлен так:
#if defined(_WIN32) || defined(_WIN64)
#define LOKI_SAFEFORMAT_SIGNED_LONG intptr_t
#define LOKI_SAFEFORMAT_UNSIGNED_LONG uintptr_t
#else
#define LOKI_SAFEFORMAT_SIGNED_LONG signed long
#define LOKI_SAFEFORMAT_UNSIGNED_LONG unsigned long
#endif
Поскольку тип long в 64-битных Windows-системах остался 32-битным, то и константа LONG_MIN задает минимальное значение 32-битной переменной. А это значит, что ее использование некорректно при работе с 64-битными типами (в нашем случае с intptr_t). Решением будет использование собственной константы. Например, исправление может выглядеть так:
#if defined(_WIN32) || defined(_WIN64)
# define LOKI_SAFEFORMAT_SIGNED_LONG intptr_t
#if defined(_WIN64)
# define LOKI_SAFEFORMAT_SIGNED_LONG_MIN_VALUE LLONG_MIN
# define LOKI_SAFEFORMAT_SIGNED_LONG_MAX_VALUE LLONG_MAX
#else
# define LOKI_SAFEFORMAT_SIGNED_LONG_MIN_VALUE LONG_MIN
# define LOKI_SAFEFORMAT_SIGNED_LONG_MAX_VALUE LONG_MAX
#endif
...
#else
# define LOKI_SAFEFORMAT_SIGNED_LONG signed long
# define LOKI_SAFEFORMAT_SIGNED_LONG_MIN_VALUE LONG_MIN
# define LOKI_SAFEFORMAT_SIGNED_LONG_MAX_VALUE LONG_MAX
...
#endif
И, соответственно, строку
if (n != LONG_MIN) {
надо заменить на строку
if (n != LOKI_SAFEFORMAT_SIGNED_LONG_MIN_VALUE) {
К счастью и к похвале создателей библиотеки Loki это единственное место, которое заслуживавет исправления. Описанные далее замечания могут быть интересны, но не имеют важности.
Одной из проблем, выявляемой анализатором Viva64 являются магические числа. С точки зрения миграции кода с 32-битной на 64-битную платформу есть ряд чисел, использование которых наиболее опасно. Возможно, где-то в коде программист рассчитывает на определенный размер типа данных, что может вызвать проблему. Те, кто смотрят на сообщения от анализаторов кода, часто жалуются на бессмысленность подобных сообщений. Действительно, какой смысл ругаться анализатору кода на число 4 в подобных строках:
::Loki::ScopeGuard guard4 = ::Loki::MakeGuard( &HasFour, 1, 2, 3, 4 );
::Loki::ScopeGuard guard5 = ::Loki::MakeGuard( &HasFive, 1, 2, 3, 4, 5
);
Но бывают такие конструкции, которые надо анализировать очень тщательно. Например, в файле SafeFormat\main.cpp видим код:
case 'X':
// TestCase(formatSpec, RandomInt(-10000, 10000));
// don't test negative values on 64bit systems, because
// snprintf does not support 64 Bit values
TestCase(formatSpec,
RandomInt( -10000 * (sizeof(size_t)>4 ? 0 : 1) , 10000));
break;
case 'e':
Конкретно в этом месте проблемы, конечно же, нет. Но диагностика магических чисел в анализаторе кода нужна именно для нахождения таких конструкций в коде.
Файл flex\simplestringstorage.h содержит функцию:
void resize(size_type newSize, E fill)
{
const int delta = int(newSize - size());
if (delta == 0) return;
if (delta > 0)
{
if (newSize > capacity())
{
reserve(newSize);
}
E* e = &*end();
flex_string_details::pod_fill(e, e + delta, fill);
}
pData_->pEnd_ = pData_->buffer_ + newSize;
}
Анализатор Viva64 сообщает о потенциальной проблеме здесь:
flex_string_details::pod_fill(e, e + delta, fill);
Недостаток кода заключается в том, что к указателю e прибавляется переменная delta типа int. Это потенциальная проблема, так как функция pod_fill не сможет обрабатывать объем данных более двух гигабайт (INT_MAX символов). Конечно же, конкретно здесь ошибки, видимо, нет, поскольку вряд ли бывают строки более двух гигабайт. Но лучше все-таки заменить тип переменной delta с int на ptrdiff_t:
const ptrdiff_t delta = ptrdiff_t(newSize - size());
Для доступа к большим массивам данным (более INT_MAX элементов) надо использовать типы ptrdiff_t или size_t. В файле SmallObj\SmallObjBench.cpp присутсвует довольно большой макрос LOKI_SMALLOBJ_BENCH_ARRAY, в котором работа с массивом ведется через индексацию по int. Хотя это лучше исправить, в данном месте это не является ошибкой.
Внутри файла CachedFactory\CachedFactoryTest.cpp объявлена функция:
template< class Cache >
milliSec typicalUse(Cache &CC, unsigned objectKind,
unsigned maxObjectCount, unsigned maxIteration)
Для параметра objectKind лучше использовать тип size_t. Но поскольку этот код относится к тестам, то какой-либо серьезного недостатка в этом нет.
Все немногие обнаруженные проблемы библиотеки Loki, перечисленные ранее, легко исправимы. Значит ли это, что если в Loki нет никаких 64-битных проблем (а это именно так), то и приложение, использующее эту библиотеку, безопасно с точки зрения 64-битного кода? К сожалению нет!
Все дело в том, что библиотека Loki крайне активно использует шаблоны. А когда анализатор кода разбирает код шаблона, он не всегда может диагностировать проблему. Для полной уверенности анализатору необходимо выполнить инстанцирование шаблонных классов и функций.
Приведем просто пример, не относящийся к библиотеке Loki. Анализатор Viva64 среди прочих проблем в коде может обнаруживать неоптимальные структуры данных:
template <class T>
struct TClass
{
int m_a;
T m_b;
int m_c;
};
Если здесь T имеет тип int, то структура оптимальна. Если же T имеет тип size_t, то структура займет 24 байта, вместо возможных 16 байтах. Тогда в случае большого количества подобных объектов лучше было бы переписать:
template <class T>
struct TClass
{
T m_b;
int m_a;
int m_c;
};
Но проверить это анализатор может только выполнив инстанцирование шаблона. То есть имея лишь объявление класса в заголовке выявит проблему нельзя.
Другой пример, опять же не относящийся к Loki, связанный с приведением типов:
template<typename T1, typename T2>
class TemplateClass
{
public:
void test1()
{
m_a.m_value = m_b.m_value; // Есть ли здесь ошибка?
}
private:
T1 m_a;
T2 m_b;
};
В данном коде ошибка приведения типов может быть, а может и не быть, в зависимости от того, с какими параметрами будет выполнено инстанцирование шаблона TemplateClass. Только анализируя код функции, не выполняя инстанцирование, анализатор не может сообщить об ошибке.
Перечисленные два примера шаблонных классов не относятся к библиотеке Loki, однако важны для понимания принципов работы анализаторов кода. Особенность шаблонных библиотек типа Loki заключается в том, что даже если библиотека полностью совместима с 64-битными системами, это не значит что код, который ее использует, корректен. Это в корне меняет подход к верификации приложений. Если при работе с обычной (не шаблонной) библиотекой достаточно быть уверенным, что библиотека корректна с точки зрения 64-бит для того, чтобы быть уверенным в корректности всего приложения, то с шаблонными библиотеками такой уверенности уже быть не может.
Все это означает, что хотя библиотека Loki и не содержит проблем 64-битного кода, пользовательское приложение, которое ее использует должно быть дополнительно проверено анализатором кода на отсутствие подобных проблем. Ведь ошибки зависят от того, с какими параметрами выполняется инстанцирование шаблонов.
По результатам работы сотрудников компании ООО "СиПроВер" над проверкой на совместимость с 64-бибтными системами библиотеки Loki сделаны следующие выводы:
Библиотека полностью совместима с 64-битными системами и не содержит потенциальных ошибок. Указанные в данной статье ошибки, скорее всего, будут очень быстро исправлены.
Анализатор кода Viva64, предназначенный для разработки новых 64-битных и миграции старых 32-битных приложений показал себя очень хорошо при проверке сложного шаблонного кода библиотеки. Это говорит о высоком качестве анализатора кода.
Хотя библиотека Loki и не содержит 64-битных проблем, они возможны в пользовательских приложениях, использующих Loki. Поскольку конечный код зависит от того, с какими параметрами инстанцировались шаблоны, то обязательно надо проверять пользовательские приложения анализатором кода. Только тогда можно быть уверенным в полной совместимости приложения с 64-битными системами.
Благодарим всех, кто принимал участие в анализе библиотеки Loki и в оценке этой работы в команде Loki: