Обычные пользователи любят графические движки, потому что с ними удобно работать. Команда PVS-Studio любит графические движки, потому что там часто попадаются интересные фрагменты кода. По просьбе одного из читателей нашего блога в этой статье будут рассмотрены результаты анализа графического фреймворка Ogre3D. Каждому по интересному срабатыванию из проекта, и пусть никто не уйдёт обиженным!

Введение
Маги, огры, колдовство и замки злодеев. Звучит как отличный сеттинг для фильма с жанром фэнтези. Сегодня спасать принцесс мы не будем, но с некоторыми Ограми познакомимся.
Ogre3D (Object-Oriented Graphics Rendering Engine) является графическим движком с открытым исходным кодом, который можно найти на GitHub. Проект написан на языке программирования С++ и используется в играх и 3D визуализации.
Какие ошибки нашёл PVS-Studio?
Всего при анализе Ogre3D PVS-Studio выдал 562 срабатывания первого и второго уровней. Были включены только предупреждения общего назначения (GA). Про фильтрацию срабатываний можно узнать в нашей документации. Это не мало, но и не много, учитывая, что большинство срабатываний пришлось на V730. Такое диагностическое правило говорит о том, что не все члены класса инициализируются в конструкторе. Сложно определить, так и задумывалось или нет, для этого необходимо знать тонкости реализации проекта.
Не всё так однозначно
Некоторые срабатывания анализатора мне показались довольно интересными, давайте я вам их покажу. Начнём со вкусного.
V1064 The '1' operand of integer division is less than the '100000' one. The result will always be zero. OgreAutoParamDataSource.cpp 1094
typedef Vector<4, Real> Vector4; const Vector4& AutoParamDataSource::getShadowSceneDepthRange(size_t index) const { static Vector4 dummy(0, 100000, 100000, 1/100000); // .... }
Здесь вектор dummy должен хранить числа с плавающей точкой. В данном случае конструктор принимает 4 аргумента типа float. Однако результат операции 1/100000 будет равен не дроби, а нулю, поскольку слева и справа от оператора деления находятся целочисленные значения.
Исправим ситуацию:
const Vector4& AutoParamDataSource::getShadowSceneDepthRange(size_t index) const { static Vector4 dummy(0, 100000, 100000, 1.0f/100000); // .... }
Теперь всё работает корректно.
V506 Pointer to local variable 'varyingName' is stored outside the scope of this variable. Such a pointer will become invalid. OgreGLES2RenderToVertexBuffer.cpp 268
typedef std::string String; void GLES2RenderToVertexBuffer::bindVerticesOutput(Pass* pass) { // .... const GLchar *names[64]; for (unsigned short e = 0; e < elemCount; e++) { const VertexElement* element = declaration->getElement(e); String varyingName = getSemanticVaryingName(element->getSemantic(), element->getIndex()); names[e] = varyingName.c_str(); // <= } // .... }
В данном коде у нас есть массив из 64 указателей на тип const GLchar, куда в цикле сохраняются указатели на внутренние хранилища контейнеров типа String. Проблема заключается в том, что сами контейнеры типа String объявляются и инициализируются внутри самого цикла. После выхода из области видимости они уничтожаются вместе с внутренними хранилищами, что делает сохраненные в names указатели невалидными.
Эту ошибку можно поправить, выделив в куче память под новое хранилище, перекопировав туда строку из контейнера String и уже сохранив указатель на новое хранилище. Но не проще ли сделать массив не указателей, а типа String? Так и поступим:
void GLES2RenderToVertexBuffer::bindVerticesOutput(Pass* pass) { // .... String names[64]; for (unsigned short e = 0; e < elemCount; e++) { const VertexElement* element = declaration->getElement(e); names[e] = getSemanticVaryingName(element->getSemantic(), element->getIndex()); } // .... }
V614 Uninitialized variable 'lodLevel.reductionValue' used. main.cpp 806
Структура LodLevel:
struct _OgreLodExport LodLevel { // .... VertexReductionMethod reductionMethod; Real reductionValue; // .... };
И код, который использует эту структуру:
numLod = opts.numLods; LodLevel lodLevel; // <= lodLevel.distance = 0.0; for (unsigned short iLod = 0; iLod < numLod; ++iLod) { lodLevel.reductionMethod = opts.usePercent ? LodLevel::VRM_PROPORTIONAL : LodLevel::VRM_CONSTANT; if (opts.usePercent) { lodLevel.reductionValue += opts.lodPercent * 0.01f; // <= } else { lodLevel.reductionValue += (Ogre::Real)opts.lodFixed; // <= } lodLevel.distance += opts.lodDist; lodConfig.levels.push_back(lodLevel); }
В этом фрагменте кода объявляется структура LodLevel, которая не имеет определённого пользователем конструктора по умолчанию и инициализаторов нестатических полей классов. Соответственно, нестатическое поле не инициализируется, после чего происходит его чтение.
Если мы хотим, чтобы все поля инициализировались значениями по умолчанию, то можно воспользоваться одним из методов:
определить свой конструктор по умолчанию;
добавить инициализатор поля по умолчанию (начиная с C++11);
воспользоваться value initialization при объявлении экземпляра структуры (начиная с C++11).
Наиболее предпочтительным является третий метод, т.к. не меняет тривиальность типа, а это может быть важно:
LodLevel lodlevel {};
V595 The 'params' pointer was utilized before it was verified against nullptr. Check lines: 95, 101. OgreGpuProgramManager.cpp 95
Resource* GpuProgramManager::createImpl(...., const NameValuePairList* params) { auto langIt = params->find("language"); auto typeIt = params->find("type"); if (langIt == params->end()) langIt = params->find("syntax"); if (!params || langIt == params->end() || typeIt == params->end()) { OGRE_EXCEPT(Exception::ERR_INVALIDPARAMS, "You must supply 'language' or 'syntax' and 'type' parameters"); } }
В этом фрагменте кода переданный указатель params сначала разыменовывается, а только потом проверяется на валидность. Классическая ошибка. Код будет работать, пока в функцию кто-нибудь не толкнёт nullptr. Перенесём проверку:
Resource* GpuProgramManager::createImpl(...., const NameValuePairList* params) { if (!params) { OGRE_EXCEPT(Exception::ERR_INVALIDPARAMS, "Params can't be nullptr"); } auto langIt = params->find("language"); auto typeIt = params->find("type"); if (langIt == params->end()) langIt = params->find("syntax"); if (langIt == params->end() || typeIt == params->end()) { OGRE_EXCEPT(Exception::ERR_INVALIDPARAMS, "You must supply 'language' or 'syntax' and 'type' parameters"); } // .... }
V547 Expression 'x == 0' is always true/false. OgreTerrain.cpp 3750
Terrain::NeighbourIndex Terrain::getNeighbourIndex(long x, long y) { if (x < 0) { if (y < 0) return NEIGHBOUR_SOUTHWEST; else if (y > 0) return NEIGHBOUR_NORTHWEST; else return NEIGHBOUR_WEST; } else if (x > 0) { if (y < 0) return NEIGHBOUR_SOUTHEAST; else if (y > 0) return NEIGHBOUR_NORTHEAST; else return NEIGHBOUR_EAST; } if (y < 0) { if (x == 0) // <= return NEIGHBOUR_SOUTH; } else if (y > 0) { if (x == 0) // <= return NEIGHBOUR_NORTH; } return NEIGHBOUR_NORTH; }
Здесь переменная x проверяется на 0 после ложных проверок x > 0 и x < 0. Эта проверка бессмысленна, так как мы и так можем попасть в эту часть кода только при x == 0. Простая математика. Уберём лишние проверки и немного упростим код:
Terrain::NeighbourIndex Terrain::getNeighbourIndex(long x, long y) { if (x < 0) { // .... } else if (x > 0) { // .... } else if (y < 0) return NEIGHBOUR_SOUTH; else if (y > 0) return NEIGHBOUR_NORTH; else return NEIGHBOUR_NORTH; }
Теперь фрагмент выглядит значительно лучше и нет явно лишних проверок.
V609. Possible division or mod by zero. OgreInstanceBatchHW_VTF.cpp 392
Внимательно посмотрите на следующий код:
static const uint16 c_maxTexWidthHW = 4096; const size_t numBones = std::max<size_t>(1, baseSubMesh->blendIndexToBoneIndexMap.size()); // .... const size_t maxUsableWidth = c_maxTexWidthHW – (c_maxTexWidthHW % (numBones * mRowLength)); // .... size_t texHeight = numWorldMatrices * mRowLength / maxUsableWidth; // <=
В строке объявления переменной maxUsableWidth её значение может быть от 0 до 4096. Следовательно, если значением maxUsableWidth внезапно окажется ноль, то мы получим деление на ноль в указанном месте. Бабах! А ведь на первый взгляд код написан без ошибок. Он даже скомпилируется и будет работать до тех пор, пока в переменную maxUsableWidth не проскочит ноль. Такое может быть, если результат произведения numBones * mRowLength будет больше 4096.
Переменная numBones инициализируется размером вектора blendIndexToBoneIndexMap. Вполне возможно, что разработчики вне класса контролируют количество элементов этого контейнера. Может быть, им просто везёт, и вектор недостаточно большой. Тем не менее может случиться, что вектор будет размером больше 4096. Тогда выстрелит деление на ноль и программа упадёт.
V557 Array overrun is possible. The 'j' index is pointing beyond array bound. OgreAnimationTrack.cpp 219
Классический выход за границы контейнера:
void AnimationTrack::_buildKeyFrameIndexMap( const std::vector<Real>& keyFrameTimes) { // .... size_t i = 0, j = 0; while (j <= keyFrameTimes.size()) // <= { mKeyFrameIndexMap[j] = static_cast<ushort>(i); while (i < mKeyFrames.size() && mKeyFrames[i]->getTime() <= keyFrameTimes[j]) // <= ++i; ++j; } }
Индекс j, по которому мы получаем доступ к элементам контейнера keyFrameTimes, инкрементируется до значения, равного размеру контейнера.

Исправим ошибку:
while (j < keyFrameTimes.size()) { // .... }
Статический анализатор нашёл несколько таких же ошибок и в других местах. Вот срабатывание на файле OgreSerializer.cpp, где в массиве 255 элементов, но мы пытаемся получить доступ к 256-му элементу:
String Serializer::readString(const DataStreamPtr& stream, size_t numChars) { OgreAssert(numChars <= 255, ""); char str[255]; stream->read(str, numChars); str[numChars] = '\0'; return str; }
Здесь вообще очень странный код. Похоже на то, что его нигде не используют и забыли вычистить при рефакторинге, но вдруг всё же кто-то воспользуется функцией? Разберём ошибки в ней. В первую очередь в функции происходит выход за границу массива, потому что мы пытаемся присвоить значение '\0' несуществующему 256-му символу. Во-вторых, число прочитанных символов, возвращаемое функцией read, может быть меньше размера буфера str, в таком случае между символом '\0' и строкой, прочитанной функцией read, будет неинициализированная память. Такую функцию можно было бы переписать так:
String Serializer::readString(const DataStreamPtr& stream, size_t numChars) { OgreAssert(numChars <= 255, ""); String str(numChars, '\0'); numChars = stream->read(&str[0], numChars); str.erase(numChars); return str; }
Теперь у нас нет выхода за границу массива, а всю неинициализированную память мы забиваем символами '\0' и обрезаем функцией erase. Также, начиная с С++23, такой паттерн можно будет переписать через функцию resize_and_overwrite.
V1048 The 'mVSOutPosition' variable was assigned the same value. OgreShaderExTriplanarTexturing.cpp 168
void TriplanarTexturing::copyFrom(....) { const TriplanarTexturing& rhsTP = static_cast<const TriplanarTexturing&>(rhs); mPSOutDiffuse = rhsTP.mPSOutDiffuse; mPSInDiffuse = rhsTP.mPSInDiffuse; mVSInPosition = rhsTP.mVSInPosition; // <= mVSOutPosition = rhsTP.mVSOutPosition; // <= mVSOutNormal = rhsTP.mVSOutNormal; mVSInNormal = rhsTP.mVSInNormal; mPSInNormal = rhsTP.mPSInNormal; mVSInPosition = rhsTP.mVSInPosition; // <= mVSOutPosition = rhsTP.mVSOutPosition; // <= }
Классическая опечатка после копипаста. Дважды присваивается одно и то же значение переменным-членам.
V560 Part of conditional expression is always true/false. OgreTerrainLodManager.cpp 62
void TerrainLodManager::open(const String& filename) { if (!filename.empty() && filename.length() > 0) mDataStream = Root::getSingleton() .openFileStream(filename, mTerrain->_getDerivedResourceGroup()); }
Здесь разработчик проверяет контейнер std::string на пустоту и на длину больше 0. Одну из частей условия можно убрать:
void TerrainLodManager::open(const String& filename) { if (!filename.empty()) mDataStream = Root::getSingleton() .openFileStream(filename, mTerrain->_getDerivedResourceGroup()); }
Подозрительные места
Также я хочу поделиться некоторыми подозрительными местами, на которые ругается анализатор PVS-Studio, но трудно сказать, ошибка это или нет. Сразу отмечу, что анализатор сработал правильно, однако разработчики могли намеренно написать подобный код. Но лучше я просто покажу вам эти срабатывания.
V703 It is odd that the 'mProgramID' field in derived class 'GLGpuNvparseProgram' overwrites field in base class 'GLGpuProgram'. Check lines: OgreGLGpuNvparseProgram.h:63, OgreGLGpuProgram.h:60.
class _OgreGLExport GLGpuProgram : public GpuProgram, public GLGpuProgramBase { // .... protected: GLuint mProgramID; // <= }; class _OgreGLExport GLGpuNvparseProgram : public GLGpuProgram { // .... GLuint getProgramID(void) const { return mProgramID; // <= } // .... private: GLuint mProgramID; // <= };
Здесь класс-наследник объявляет переменную с таким же именем, как и protected-переменная в родительском классе. Это ведёт к скрытию и является прямым путём к ошибкам. При возврате mProgramID из функции getProgramID мы получим значение из класса-наследника, а не из базового класса. Неизвестно, задумывалось так или нет, но разработчикам лучше проверить это место.
Можно переименовать одно из полей или явно указать обращение к конкретному полю:
// Теперь обращение идёт к переменной базового класса GLuint getProgramID(void) const { return GLGpuProgram::mProgramID; }
Первый способ, конечно, предпочтительнее и правильнее.
V547 Expression 'i != end' is always true. OgreScriptTranslator.cpp 787
bool ScriptTranslator::getMatrix4( AbstractNodeList::const_iterator i, AbstractNodeList::const_iterator end, Matrix4 *m) { int n = 0; while (i != end && n < 16) { if (i != end) // <= { Real r = 0; if (getReal(*i, &r)) (*m)[n / 4][n % 4] = r; else return false; } else { return false; } ++i; ++n; } return true; }
Очень странный код. Могу выделить в нём как минимум две проблемы:
Условие i != end проверяется дважды. Если условие в while будет true, то условие в if будет тоже всегда true. Проверка лишняя.
Ветка else недостижима. При этом возвращает false.
Сложно предложить решение в таких случаях, если не знать, что должна делать функция, но можно было бы упростить, не меняя существующую логику:
bool ScriptTranslator::getMatrix4( AbstractNodeList::const_iterator i, AbstractNodeList::const_iterator end, Matrix4 *m) { int n = 0; while (i != end && n < 16) { Real r = 0; if (!getReal(*i, &r)) return false; (*m)[n / 4][n % 4] = r; ++i; ++n; } return true; }
V1053 Calling the 'destroyAllDeclarations' virtual function in the destructor may lead to unexpected result at runtime. OgreDefaultHardwareBufferManager.h 118
Объявление виртуальных функций класса:
class _OgreExport HardwareBufferManagerBase : public BufferAlloc { protected: // .... /// Internal method for destroys all vertex declarations. virtual void destroyAllDeclarations(void); /// Internal method for destroys all vertex buffer bindings. virtual void destroyAllBindings(void); // .... }
Объявление деструктора:
class _OgreExport DefaultHardwareBufferManager : public HardwareBufferManager { // .... ~DefaultHardwareBufferManager() { // have to do this before mImpl is gone destroyAllDeclarations(); destroyAllBindings(); } // .... }
Здесь мы вызываем в деструкторе две виртуальных функции. Пока что это ни на что не влияет. Однако если мы унаследуемся от этого класса и переопределим эти функции, то в деструкторе класса DefaultHardwareBufferManager будут по-прежнему использоваться виртуальные функции из базового класса. Это может привести к неожиданным результатам для разработчика. Использование виртуальных функций в деструкторах считается плохой практикой и опасным местом в коде. У нас есть отдельная статья, посвящённая этому случаю.
V530 The return value of function 'back' is required to be utilized. OgreGLXConfigDialog.cpp 410
class GLXConfigurator { public: // .... std::list<ConfigCallbackData> mConfigCallbackData // .... } void GLXConfigurator::SetRenderer(RenderSystem *r) // .... mConfigCallbackData.back(); // .... }
Здесь зачем-то мы вызываем функцию *back *контейнера std::list, чтобы получить ссылку на последний элемент, но никак её не используем и не сохраняем. Странное место. Возможно, что имелось в виду что-то другое.
V570 Variable is assigned to itself. OgreETCCodec.cpp 242
bool ETCCodec::decodePKM(const DataStreamPtr& stream, DecodeResult& result) const { // .... void *destPtr = output->getPtr(); stream->read(destPtr, imgData->size); destPtr = static_cast<void*>(static_cast<uchar*>(destPtr)); // <= // .... }
Указатель destPtr кастуется к другому типу указателя, потом к своему типу и присваивается самому себе. Очень странное место. Возможно, это старый код, который забыли убрать.
V1065 Expression can be simplified: check similar operands. OgrePage.cpp 117
bool Page::isHeld() const { unsigned long nextFrame = Root::getSingleton().getNextFrameNumber(); unsigned long dist; if (nextFrame < mFrameLastHeld) { // we must have wrapped around dist = mFrameLastHeld + (std::numeric_limits<unsigned long>::max() - mFrameLastHeld); // <= } else dist = nextFrame - mFrameLastHeld; // 5-frame tolerance return dist <= 5; }
Тоже очень подозрительное место. Во-первых, выражение может быть упрощено: достаточно просто присвоить переменной dist значение из std::numeric_limits. Во-вторых, при истинном условии переменной dist присваивается значение, которое очевидно больше 5. Куда понятнее и лучше было бы сделать примерно так:
bool Page::isHeld() const { unsigned long nextFrame = Root::getSingleton().getNextFrameNumber(); if (nextFrame >= mFrameLastHeld) { // 5-frame tolerance return (nextFrame – mFrameLastHeld) <= 5; } return false; }
Согласитесь, так код выглядит куда приятнее и понятнее.
Заключение
Подводя итог, можно сказать, что качество большей части кода Ogre3D можно назвать если не отличным, то хорошим. Подавляющее число ошибок находится в одних и тех же файлах, при этом в остальных файлах ошибок практически не было найдено. Возможно, это результат наличия новичка в команде разработчиков, которому поручили написать определенные файлы, а ревью проводили редко или невнимательно.
Также около четверти срабатываний анализатора PVS-Studio пришлось на диагностическое правило V730, о котором сложно что-то сказать однозначно. Мы не знаем подробности реализации, так может быть задумано или нет. Однозначно можно сказать лишь то, что при использовании анализатора PVS-Studio большинство перечисленных выше ошибок не попали бы в релиз и могли бы быть исправлены ещё на этапе написания кода.
Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: ссылка.
