>
>
>
64-битная версия Loki

Андрей Александреску
Статей: 1

Андрей Карпов
Статей: 671

Евгений Рыжков
Статей: 125

Питер Кюмель
Статей: 1

Рич Спозато
Статей: 1

64-битная версия Loki

Статья представляет собой отчет о проверки библиотеки 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-репозитория и приступили к работе.

Сборка 64-битной Loki в Microsoft Visual Studio 2005

Анализатор кода 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.

Проверка 64-битной Loki с помощью Viva64

Для того чтобы убедится в совместимости Loki с 64-битными системами, выполним анализ кода с помощью Viva64. Анализатор кода Viva64 предназначен для разработки новых 64-битных приложений и для миграций существующих 32-битных на 64-битную платформу.

Запуск Viva64 для анализа Loki выявил список из 89 потенциально-опасных синтаксических конструкций. Это не значит, что в библиотеке Loki содержится 89 ошибок, связанных с 64-битным кодом. Но все эти 89 мест должен просмотреть разработчик для того, чтобы понять, действительно ли там возможна ошибка или нет. Разумеется, мы проанализировали эти ошибки.

1 Некорректно используемая константа LONG_MIN

Начнем с ошибки, связанной с некорректно используемой константой 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 это единственное место, которое заслуживавет исправления. Описанные далее замечания могут быть интересны, но не имеют важности.

2 Некоторые виды магических чисел - ошибка или нет?

Одной из проблем, выявляемой анализатором 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':

Конкретно в этом месте проблемы, конечно же, нет. Но диагностика магических чисел в анализаторе кода нужна именно для нахождения таких конструкций в коде.

3 Прибавление int к указателю - потенциальная проблема

Файл 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());

4 Использование int для индексации массивов некорректно

Для доступа к большим массивам данным (более INT_MAX элементов) надо использовать типы ptrdiff_t или size_t. В файле SmallObj\SmallObjBench.cpp присутсвует довольно большой макрос LOKI_SMALLOBJ_BENCH_ARRAY, в котором работа с массивом ведется через индексацию по int. Хотя это лучше исправить, в данном месте это не является ошибкой.

5 Правильные аргументы функций

Внутри файла CachedFactory\CachedFactoryTest.cpp объявлена функция:

template< class Cache >
milliSec typicalUse(Cache &CC, unsigned objectKind, 
                    unsigned maxObjectCount, unsigned maxIteration)

Для параметра objectKind лучше использовать тип size_t. Но поскольку этот код относится к тестам, то какой-либо серьезного недостатка в этом нет.

Библиотека Loki совместима с 64-битными системами - значит программа, использующая ее также совместима?

Все немногие обнаруженные проблемы библиотеки 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:

  • Спасибо компании ООО "СиПроВер", которая проанализировала код библиотеки Loki и выполнила верификацию ее для работы в 64-битном режиме, в частности: Андрею Карпову и Евгению Рыжкову.
  • Спасибо команде Loki: Андрею Александреску, Питеру Кюмелю, Ричу Спозато за совместную работу над проверкой и редактированием статьи, а также ценные замечания.
  • Спасибо Ричу Спозато, за координирование всей работы.

Библиографический список