>
>
>
Проверка кроссплатформенного фреймворка…

Святослав Размыслов
Статей: 90

Проверка кроссплатформенного фреймворка Cocos2d-x

Cocos2d — открытое программное обеспечение, фреймворк. Он может быть использован для построения игр, приложений и графических интерфейсов интерактивных кроссплатформенных приложений. Cocos2d содержит множество бранчей, известные из них Cocos2d-iPhone, Cocos2d-x, Cocos2d-html5 и Cocos2d-XNA.

В данной статье будут рассмотрены результаты проверки Cocos2d-x, фреймворка для C++, полученные с помощью PVS-Studio 5.18. Проект достаточно качественный, но всё же на некоторые места стоит обратить внимание. Исходный код взят с GitHub.

От malloc к new, от C к С++

Работа с графическими объектами, как правило, связана с обработкой массивов и матриц, память под которые выделяют динамически. В данном проекте для выделения памяти используются как вызовы функции 'malloc', так и оператор 'new'. У этих способов есть существенные различия в использовании, которые необходимо учитывать при их замене в коде. Далее будут описаны места, не совсем корректно использующие 'malloc' и 'new'.

V630 The 'malloc' function is used to allocate memory for an array of objects which are classes containing constructors and destructors. ccmotionstreak.cpp 122

Vec2::Vec2() : x(0.0f), y(0.0f) { }
Vec2::Vec2(float xx, float yy) : x(xx), y(yy) { }

bool MotionStreak::initWithFade(...)
{
  ....
  _pointVertexes = (Vec2*)malloc(sizeof(Vec2) * _maxPoints);
  _vertices = (Vec2*)malloc(sizeof(Vec2) * _maxPoints * 2);
  _texCoords = (Tex2F*)malloc(sizeof(Tex2F) * _maxPoints * 2);
  ....
}

Обычно с выделенной памятью работают как с массивом объектов, имеющих конструктор или деструктор. При таком выделении памяти для класса не будет вызван конструктор. При освобождении памяти с помощью функции free также не будет вызван деструктор. Это крайне подозрительно. В результате переменные 'x' и 'y' будут не инициализированы. Конечно, можно вызвать конструктор для каждого объекта "вручную" или явно инициализировать поля, но более правильным будет использование оператора 'new':

_pointVertexes = new Vec2[_maxPoints];
_vertices = new Vec2[_maxPoints * 2];

Аналогичные места:

  • V630 The 'malloc' function is used to allocate memory for an array of objects which are classes containing constructors and destructors. ccmotionstreak.cpp 124
  • V630 The 'malloc' function is used to allocate memory for an array of objects which are classes containing constructors. ccmotionstreak.cpp 125

V572 It is odd that the object which was created using 'new' operator is immediately casted to another type. ccactiontiledgrid.cpp 322

struct Tile
{
    Vec2    position;
    Vec2    startPosition;
    Size    delta;
};

Tile* _tiles;

void ShuffleTiles::startWithTarget(Node *target)
{
  ....
  _tiles = (struct Tile *)new Tile[_tilesCount];  // <=
  Tile *tileArray = (Tile*) _tiles;               // <=
  ....
}

Здесь оператор 'new' уже возвращает типизированный указатель и приведение его к этому же типу является бессмысленным.

Похожее место:

  • V572 It is odd that the object which was created using 'new' operator is immediately casted to another type. luabasicconversions.cpp 1301

V668 There is no sense in testing the 'pRet' pointer against null, as the memory was allocated using the 'new' operator. The exception will be generated in the case of memory allocation error. ccfloat.h 48

static __Float* create(float v)
{
  __Float* pRet = new __Float(v); // <=
  if (pRet)                       // <=
  {
    pRet->autorelease();
  }
  return pRet;
}

Если оператор 'new' не смог выделить память, то согласно стандарту языка Си++, генерируется исключение std::bad_alloc(). Таким образом проверять указатель на равенство нулю не имеет смысла, в отличии от возвращаемого значения функции 'malloc'. В проекте есть ещё 475 таких проверок!

V547 Expression '0 == commonInfo->eventName' is always false. Pointer 'commonInfo->eventName' != NULL. ccluaengine.cpp 436

struct CommonScriptData
{
  // Now this struct is only used in LuaBinding.
  int handler;
  char eventName[64];                                    // <=
  ....
};

int LuaEngine::handleCommonEvent(void* data)
{
  ....
  CommonScriptData* commonInfo = static_cast<....*>(data);
  if (NULL == commonInfo->eventName ||                   // <=
      0 == commonInfo->handler)
    return 0;
  ....
}

Условие (NULL == commonInfo->eventName) всегда будет ложным, так как массив 'eventName' объявлен локально. Если выделить память под массив фиксированного размера не удастся, то проблема будет выявлена ранее - при выделении памяти под структуру.

Ещё проверки:

  • V547 Expression '0 != commonInfo->eventSourceClassName' is always true. Pointer 'commonInfo->eventSourceClassName' != NULL. ccluaengine.cpp 442
  • V600 Consider inspecting the condition. The 'commonInfo->eventName' pointer is always not equal to NULL. ccluaengine.cpp 436
  • V600 Consider inspecting the condition. The 'commonInfo->eventSourceClassName' pointer is always not equal to NULL. ccluaengine.cpp 442

Страшный сон структурного программирования

V696 The 'continue' operator will terminate 'do { ... } while (FALSE)' loop because the condition is always false. Check lines: 125, 153. cccomaudio.cpp 125

bool ComAudio::serialize(void* r)
{
  bool ret = false;
  do
  {
    ....
    if (file != nullptr)
    {
      if (strcmp(file, "") == 0)
      {
         continue;                   // <=
      }
      ....
    }
  }while(0);
  return ret;
}

Анализатор обнаружил код, который может ввести в заблуждение программиста. Оператор continue в цикле "do { ... } while(0)" остановит цикл, а не возобновит его. Таким образом, после вызова оператора 'continue' будет проверено условие (0), и цикл завершится так как условие ложно. Если именно так задумано и ошибки нет, то код всё равно лучше изменить. Можно воспользоваться оператором 'break'.

Аналогичные циклы:

  • V696 The 'continue' operator will terminate 'do { ... } while (FALSE)' loop because the condition is always false. Check lines: 188, 341. cccomrender.cpp 188
  • V696 The 'continue' operator will terminate 'do { ... } while (FALSE)' loop because the condition is always false. Check lines: 276, 341. cccomrender.cpp 276
  • V696 The 'continue' operator will terminate 'do { ... } while (FALSE)' loop because the condition is always false. Check lines: 281, 341. cccomrender.cpp 281
  • V696 The 'continue' operator will terminate 'do { ... } while (FALSE)' loop because the condition is always false. Check lines: 323, 341. cccomrender.cpp 323

Форматированный вывод

V576 Incorrect format. Consider checking the fourth actual argument of the 'fprintf' function. The pointer to string of char type symbols is expected. ccconsole.cpp 341

#ifdef UNICODE
#define gai_strerror   gai_strerrorW            // <=
#else
#define gai_strerror   gai_strerrorA
#endif  /* UNICODE */

bool Console::listenOnTCP(int port)
{
  ....
  fprintf(stderr,"net_listen error for %s: %s", // <=
    serv, gai_strerror(n));                     // <=
  ....
}

Функция gai_strerror может быть определена как gai_strerrorW и gai_strerrorA, в зависимости от определения директивы UNICODE. В Visual Studio 2012, где проверялся проект, была определена юникодная функция, которая возвращает широкую строку, для печати которой необходимо использовать спецификатор '%S' (большая S), иначе будет напечатан только первый символ строки или бессмысленный текст.

Одинаковый результат условия

V583 The '?:' operator, regardless of its conditional expression, always returns one and the same value: ATLAS_REPEAT. atlas.cpp 219

spAtlas* spAtlas_readAtlas (....)
{
  ....
  page->uWrap = *str.begin == 'x' ? ATLAS_REPEAT :
    (*str.begin == 'y' ? ATLAS_CLAMPTOEDGE : ATLAS_REPEAT);
  page->vWrap = *str.begin == 'x' ? ATLAS_CLAMPTOEDGE :
    (*str.begin == 'y' ? ATLAS_REPEAT : ATLAS_REPEAT);     // <=
  ....
}

Возможно так написали для красоты, но, тем не менее, возврат одного значения в условии выглядит очень подозрительно.

Разыменование указателя

V595 The 'values' pointer was utilized before it was verified against nullptr. Check lines: 188, 189. ccbundlereader.h 188

template<>
inline bool BundleReader::readArray<std::string>(
  unsigned int *length, std::vector<std::string> *values)
{
  ....
  values->clear();             // <=
  if (*length > 0 && values)   // <=
  {
    for (int i = 0; i < (int)*length; ++i)
    {
      values->push_back(readString());
    }
  }
  return true;
}

Во многих местах кода, буквально сразу после разыменования, указатель проверяют на валидность. Вот некоторые из этих мест:

  • V595 The '_openGLView' pointer was utilized before it was verified against nullptr. Check lines: 410, 417. ccdirector.cpp 410
  • V595 The 'node' pointer was utilized before it was verified against nullptr. Check lines: 365, 374. cctween.cpp 365
  • V595 The 'rootEle' pointer was utilized before it was verified against nullptr. Check lines: 378, 379. ccfileutils.cpp 378
  • V595 The 'tolua_ret' pointer was utilized before it was verified against nullptr. Check lines: 429, 433. lua_cocos2dx_manual.cpp 429
  • V595 The 'tolua_ret' pointer was utilized before it was verified against nullptr. Check lines: 1858, 1861. lua_cocos2dx_manual.cpp 1858
  • V595 The 'tolua_ret' pointer was utilized before it was verified against nullptr. Check lines: 4779, 4781. lua_cocos2dx_manual.cpp 4779
  • V595 The '_fontAtlas' pointer was utilized before it was verified against nullptr. Check lines: 384, 396. cclabel.cpp 384
  • V595 The '_glprogramstate' pointer was utilized before it was verified against nullptr. Check lines: 216, 218. shadertest2.cpp 216
  • V595 The '_sprite' pointer was utilized before it was verified against nullptr. Check lines: 530, 533. sprite3dtest.cpp 530

Неслучайный тест

V636 The 'rand() / 0x7fff' expression was implicitly casted from 'int' type to 'float' type. Consider utilizing an explicit type cast to avoid the loss of a fractional part. An example: double A = (double)(X) / Y;. cpp-tests physicstest.cpp 307

static inline float frand(void) 
{
  return rand()/RAND_MAX; 
}

В исходных файлах для тестирования была обнаружена такая функция. Скорее всего планируется получать вещественные числа в диапазоне 0.0f до 1.0f, но результат функции rand() является целым числом, следовательно вещественная часть после деления отбрасывается. Функция возвращает только 0.0 или 1.0. Более того, так как функция rand() возвращает значение от 0 до RAND_MAX, вероятность получать результат 1.0 ничтожно мала.

Скорее тесты, использующие функцию frand() на самом деле ничего не тестируют. Хороший пример, как статический анализ дополняет тестирование с помощью юнит-тестов.

Заключение

Как я уже сказал в самом начале, Cocos2d-x содержит довольно мало подозрительных мест. Проект сравнительно молодой и инновационный, не содержит унаследованного от старых времён кода. Наверняка разработчики используют различные способы контроля качества и стараются следовать современным стандартам, и методологиям программирования.