В мае 2016 года немецкая компания Crytek решила опубликовать на GitHub исходный код игрового движка CryEngine V. Проект находится в стадии активной разработки, что влечёт за собой появление множества ошибок в коде. Мы уже проверяли проект с помощью PVS-Studio для Windows, а теперь смогли проверить проект с помощью PVS-Studio для Linux. Материала снова набралось на большую статью с описанием только очень серьёзных ошибок.
Введение
CryEngine — игровой движок, созданный немецкой компанией Crytek в 2002 году, и первоначально используемый в шутере от первого лица Far Cry. На CryEngine разных версий сделано много отличных игр от нескольких игровых студий, которые лицензировали движок: Far Cry, Crysis, Entropia Universe, Blue Mars, Warface, Homefront: The Revolution, Sniper: Ghost Warrior, Armored Warfare, Evolve и многие другие. В марте 2016 года компания Crytek анонсировала выход своего нового движка CryEngine V и вскоре опубликовала исходный код на GitHub.
Для проверки открытого исходного кода использовался статический анализатор PVS-Studio версии 6.14 для Linux. Теперь разработчикам кроссплатформенных проектов стало ещё удобнее следить за качеством кода с помощью одного инструмента анализа кода. Скачать версию для Linux можно в виде архива или пакета для пакетного менеджера. Для большинства дистрибутивов можно настроить установку и обновление, используя наш репозиторий.
В статью вошли предупреждения общего назначения и только уровня достоверности «High» (есть ещё Medium и Low). Честно говоря, я не осилил досмотреть внимательно и все «High» предупреждения, т.к. почти сразу насобирал ошибок для статьи при быстром просмотре. За проект я брался несколько раз и долго не мог найти время написать обзорную статью, поэтому про приведённые баги могу сказать, что они живут в коде уже не один месяц. Также некоторые ошибки не исправили из предыдущей статьи о проверке проекта.
Скачать и проверить исходный код в Linux было очень просто. Вот список всех необходимых команд:
mkdir ~/projects && cd ~/projects
git clone https://github.com/CRYTEK/CRYENGINE.git
cd CRYENGINE/
git checkout main
chmod +x ./download_sdks.py
./download_sdks.py
pvs-studio-analyzer trace -- sh ./cry_waf.sh build_linux_x64_clang_profile -p gamesdk
pvs-studio-analyzer analyze -l /path/to/PVS-Studio.lic -o ~/projects/CRYENGINE/cryengine.log -r ~/projects/CRYENGINE/ -C clang++-3.8 -C clang-3.8 -e ~/projects/CRYENGINE/Code/SDKs -j4
plog-converter -a GA:1,2 -t tasklist -o ~/projects/CRYENGINE/cryengine_ga.tasks ~/projects/CRYENGINE/cryengine.log
Файл отчёта cryengine_ga.tasks можно открыть и просмотреть в QtCreator. Что же удалось найти в исходном коде CryEngine V?
Несчастная функция Active()
V501 There are identical sub-expressions to the left and to the right of the '==' operator: bActive == bActive LightEntity.h 124
void SetActive(bool bActive)
{
if (bActive == bActive)
return;
m_bActive = bActive;
OnResetState();
}
Из-за опечатки функция ничего не делает. Мне кажется, если бы был конкурс «Мисс Опечатка», то этот фрагмент кода точно бы занял первое место. Думаю, эта ошибка имеет все шансы попасть в раздел "C/C++ bugs of the month".
Но это ещё не всё, вот функция из другого класса:
V501 There are identical sub-expressions 'm_staticObjects' to the left and to the right of the '||' operator. FeatureCollision.h 66
class CFeatureCollision : public CParticleFeature
{
public:
CRY_PFX2_DECLARE_FEATURE
public:
CFeatureCollision();
....
bool IsActive() const { return m_terrain ||
m_staticObjects ||
m_staticObjects; }
....
bool m_terrain;
bool m_staticObjects;
bool m_dynamicObjects;
};
Тут в функции IsActive() два раза используется переменная m_staticObjects, хотя рядом есть неиспользуемая переменная m_dynamicObjects. Возможно, в коде хотели использовать именно её.
Code above has no bugs
V547 Expression 'outArrIndices[i] < 0' is always false. Unsigned type value is never < 0. CGFLoader.cpp 881
static bool CompactBoneVertices(....,
DynArray<uint16>& outArrIndices, ....) // <= uint16
{
....
outArrIndices.resize(3 * inFaceCount, -1);
int outVertexCount = 0;
for (int i = 0; i < verts.size(); ++i)
{
....
outArrIndices[....] = outVertexCount - 1;
}
// Making sure that the code above has no bugs // <= LOL
for (int i = 0; i < outArrIndices.size(); ++i)
{
if (outArrIndices[i] < 0) // <= LOL
{
return false;
}
}
return true;
}
Эта ошибка достойна отдельного раздела. Вообще, в коде CryEngine ну очень много мест, где беззнаковые переменные бессмысленно сравнивают с нулём. Таких мест сотни, но этому фрагменту хочется уделить особое внимание, т.к. код писался намерено.
Так вот, есть массив беззнаковых чисел — outArrIndices. Далее массив заполняется по некому алгоритму. После чего выполняется гениальная проверка каждого элемента массива, чтобы среди них не было ни одного отрицательного числа. Элементы массива имеют тип uint16.
Ошибки при работе с памятью
V512 A call of the 'memcpy' function will lead to underflow of the buffer 'hashableData'. GeomCacheRenderNode.cpp 285
void CGeomCacheRenderNode::Render(....)
{
....
CREGeomCache* pCREGeomCache = iter->second.m_pRenderElement;
....
uint8 hashableData[] =
{
0, 0, 0, 0, 0, 0, 0, 0,
(uint8)std::distance(pCREGeomCache->....->begin(), &meshData),
(uint8)std::distance(meshData....->....begin(), &chunk),
(uint8)std::distance(meshData.m_instances.begin(), &instance)
};
memcpy(hashableData, pCREGeomCache, sizeof(pCREGeomCache));
....
}
Обратите внимание на аргументы функции memcpy(). Объект pCREGeomCache планируют скопировать в массив hashableData, но случайно вычислили с помощью оператора sizeof не размер объекта, а размер указателя. Из-за ошибки объект копируется не полностью, а всего 4 или 8 байт.
V568 It's odd that 'sizeof()' operator evaluates the size of a pointer to a class, but not the size of the 'this' class object. ClipVolumeManager.cpp 145
void
CClipVolumeManager::GetMemoryUsage(class ICrySizer* pSizer) const
{
pSizer->AddObject(this, sizeof(this));
for (size_t i = 0; i < m_ClipVolumes.size(); ++i)
pSizer->AddObject(m_ClipVolumes[i].m_pVolume);
}
Похожую ошибку допустили, вычислив вместо размера класса, размер указателя this. Правильный вариант: sizeof(*this).
V530 The return value of function 'release' is required to be utilized. ClipVolumes.cpp 492
vector<unique_ptr<CFullscreenPass>> m_jitteredDepthPassArray;
void CClipVolumesStage::PrepareVolumetricFog()
{
....
for (int32 i = 0; i < m_jitteredDepthPassArray.size(); ++i)
{
m_jitteredDepthPassArray[i].release();
}
m_jitteredDepthPassArray.resize(depth);
for (int32 i = 0; i < depth; ++i)
{
m_jitteredDepthPassArray[i] = CryMakeUnique<....>();
m_jitteredDepthPassArray[i]->SetViewport(viewport);
m_jitteredDepthPassArray[i]->SetFlags(....);
}
....
}
Если обратиться к документации по классу std::unique_ptr, то функция release() должна использоваться примерно следующим образом:
std::unique_ptr<Foo> up(new Foo());
Foo* fp = up.release();
delete fp;
Скорее всего, в данном фрагменте кода хотели использовать функцию reset() вместо release().
V549 The first argument of 'memcpy' function is equal to the second argument. ObjectsTree_Serialize.cpp 1135
void COctreeNode::LoadSingleObject(....)
{
....
float* pAuxDataDst = pObj->GetAuxSerializationDataPtr(....);
const float* pAuxDataSrc = StepData<float>(....);
memcpy(pAuxDataDst, pAuxDataDst, min(....) * sizeof(float));
....
}
Забыли передать pAuxDataSrc в функцию memcpy(). Вместо этого в качестве источника и приёмника используют одну и ту же переменную pAuxDataDst. Никто не застрахован от опечаток. Кстати, жалеющие могут проверить свою внимательность, пройдя наш тест по выявлению подобных ошибок: q.viva64.com.
Странный код
V501 There are identical sub-expressions to the left and to the right of the '||' operator: val == 0 || val == — 0 XMLCPB_AttrWriter.cpp 363
void CAttrWriter::PackFloatInSemiConstType(float val, ....)
{
uint32 type = PFSC_VAL;
if (val == 0 || val == -0) // <=
type = PFSC_0;
else if (val == 1)
type = PFSC_1;
else if (val == -1)
type = PFSC_N1;
....
}
Разработчики планировали сравнить вещественную переменную val с нулём и отрицательным нулём (signed zero / negative zero), но сделали это неправильно. Объявив целочисленные константы, значения нулей стали одинаковыми.
Скорее всего, код стоит исправить следующим образом, объявив константы вещественного типа:
if (val == 0.0f || val == -0.0f)
type = PFSC_0;
С другой стороны, условное выражение является избыточным, т.к. переменную достаточно сравнивать с обычным нулём. По этой причине оригинальный код выполняется так, как ожидает программист.
Но если необходимо идентифицировать именно отрицательный ноль, то правильно это сделать с помощью функции std::signbit.
V501 There are identical sub-expressions 'm_joints[i].limits[1][j]' to the left and to the right of the '-' operator. articulatedentity.cpp 1326
int CArticulatedEntity::Step(float time_interval)
{
....
for (j=0;j<3;j++) if (!(m_joints[i].flags & angle0_locked<<j)&&
isneg(m_joints[i].limits[0][j]-m_joints[i].qext[j]) +
isneg(m_joints[i].qext[j]-m_joints[i].limits[1][j]) +
isneg(m_joints[i].limits[1][j]-m_joints[i].limits[1][j]) < 2)
{
....
}
В последней части условного выражения присутствует вычитание из переменной m_joints[i].limits[1][j] самой себя. Код выглядит подозрительным. В выражении много индексов, возможно, в одном из них ошибка.
Ещё одно такое место:
- V501 There are identical sub-expressions 'm_joints[op[1]].limits[1][i]' to the left and to the right of the '-' operator. articulatedentity.cpp 513
V590 Consider inspecting this expression. The expression is excessive or contains a misprint. GoalOp_Crysis2.cpp 3779
void COPCrysis2FlightFireWeapons::ParseParam(....)
{
....
bool paused;
value.GetValue(paused);
if (paused && (m_State != eFP_PAUSED) &&
(m_State != eFP_PAUSED_OVERRIDE))
{
m_NextState = m_State;
m_State = eFP_PAUSED;
m_PausedTime = 0.0f;
m_PauseOverrideTime = 0.0f;
}
else if (!paused && (m_State == eFP_PAUSED) && // <=
(m_State != eFP_PAUSED_OVERRIDE)) // <=
{
m_State = m_NextState;
m_NextState = eFP_STOP;
m_PausedTime = 0.0f;
m_PauseOverrideTime = 0.0f;
}
....
}
Условное выражение написано таким образом, что результат не зависит от подвыражения m_State != eFP_PAUSED_OVERRIDE. Хотя кому я рассказываю, этот фрагмент кода разработчики так и не исправили после первой статьи.
Если кому интересно, то подобную разновидность ошибок я уже описывал ранее в отдельной статье "Логические выражения в C/C++. Как ошибаются профессионалы".
V529 Odd semicolon ';' after 'for' operator. boolean3d.cpp 1077
int CTriMesh::Slice(...)
{
....
pmd->pMesh[0]=pmd->pMesh[1] = this; AddRef();AddRef();
for(pmd0=m_pMeshUpdate; pmd0->next; pmd0=pmd0->next); // <=
pmd0->next = pmd;
....
}
Ещё один фрагмент кода, неисправленный с момента предыдущей проверки. По коду по-прежнему не ясно: тут ошибочное форматирование или логическая ошибка?
Про указатели
V522 Dereferencing of the null pointer 'pCEntity' might take place. BreakableManager.cpp 2396
int CBreakableManager::HandlePhysics_UpdateMeshEvent(....)
{
CEntity* pCEntity = 0;
....
if (pmu && pSrcStatObj && GetSurfaceType(pSrcStatObj))
{
....
if (pEffect)
{
....
if (normal.len2() > 0)
pEffect->Spawn(true, pCEntity->GetSlotWorldTM(...); // <=
}
}
....
if (iForeignData == PHYS_FOREIGN_ID_ENTITY)
{
pCEntity = (CEntity*)pForeignData;
if (!pCEntity || !pCEntity->GetPhysicalProxy())
return 1;
}
....
}
Анализатор обнаружил разыменование нулевого указателя. Код функции написан или отрефакторен таким образом, что образовалась ветка кода, в которой будет использован указатель pCEntity, инициализированный нулём.
Теперь рассмотрим вариант потенциального разыменования нулевого указателя.
V595 The 'pTrack' pointer was utilized before it was verified against nullptr. Check lines: 60, 61. AudioNode.cpp 60
void CAudioNode::Animate(SAnimContext& animContext)
{
....
const bool bMuted = gEnv->IsEditor() && (pTrack->GetFlags() &
IAnimTrack::eAnimTrackFlags_Muted);
if (!pTrack || pTrack->GetNumKeys() == 0 ||
pTrack->GetFlags() & IAnimTrack::eAnimTrackFlags_Disabled)
{
continue;
}
....
}
Автор этого фрагмента кода сначала воспользовался указателем pTrack, а на следующей же строке кода проверяет его валидность перед разыменованием. Скорее всего, это потенциальное место некорректной работы программы.
Предупреждений V595 достаточно много, и в статью они не поместятся. Очень часто такой код является настоящей ошибкой, но благодаря везению, работает корректно.
V571 Recurring check. The 'if (rLightInfo.m_pDynTexture)' condition was already verified in line 69. ObjMan.cpp 70
// Safe memory helpers
#define SAFE_RELEASE(p){ if (p) { (p)->Release(); (p) = NULL; } }
void CObjManager::UnloadVegetationModels(bool bDeleteAll)
{
....
SVegetationSpriteLightInfo& rLightInfo = ....;
if (rLightInfo.m_pDynTexture)
SAFE_RELEASE(rLightInfo.m_pDynTexture);
....
}
В этом фрагменте кода нет серьёзной ошибки, но не стоит писать лишний код, если в специальный макрос уже включены соответствующие проверки.
Ещё одно место с лишним кодом:
- V571 Recurring check. The 'if (m_pSectorGroups)' condition was already verified in line 48. PartitionGrid.cpp 50
V575 The 'memcpy' function doesn't copy the whole string. Use 'strcpy / strcpy_s' function to preserve terminal null. SystemInit.cpp 4045
class CLvlRes_finalstep : public CLvlRes_base
{
....
for (;; )
{
if (*p == '/' || *p == '\\' || *p == 0)
{
char cOldChar = *p;
*p = 0; // create zero termination
_finddata_t fd;
bool bOk = FindFile(szFilePath, szFile, fd);
if (bOk)
assert(strlen(szFile) == strlen(fd.name));
*p = cOldChar; // get back the old separator
if (!bOk)
return;
memcpy((void*)szFile, fd.name, strlen(fd.name)); // <=
if (*p == 0)
break;
++p;
szFile = p;
}
else ++p;
}
....
}
Возможно, в этом коде допущена ошибка. При копировании строки теряется последний терминальный ноль. В данном случае, необходимо копировать strlen() + 1 символ, либо использовать специальные функции для копирования строк: strcpy или strcpy_s.
Проблемы с запятой
V521 Such expressions using the ',' operator are dangerous. Make sure the expression '!sWords[iWord].empty(), iWord ++' is correct. TacticalPointSystem.cpp 3243
bool CTacticalPointSystem::Parse(....) const
{
string sInput(sSpec);
const int MAXWORDS = 8;
string sWords[MAXWORDS];
int iC = 0, iWord = 0;
for (; iWord < MAXWORDS; !sWords[iWord].empty(), iWord++) // <=
{
sWords[iWord] = sInput.Tokenize("_", iC);
}
....
}
Обратите внимание на секцию цикла for с счётчиками. Что там делает логическое выражение? Скорее всего, его стоит перенести в другое место и написать так:
for (; iWord < MAXWORDS && !sWords[iWord].empty(); iWord++) {...}
V521 Such expressions using the ',' operator are dangerous. Make sure the expression is correct. HommingSwarmProjectile.cpp 187
void CHommingSwarmProjectile::HandleEvent(....)
{
....
explodeDesc.normal = -pCollision->n,pCollision->vloc[0];
....
}
Ещё один странный фрагмент кода с оператором ','.
Подозрительные условия
V571 Recurring check. The 'if (pos == npos)' condition was already verified in line 1530. CryString.h 1539
//! Find last single character.
// \return -1 if not found, distance from beginning otherwise.
template<class T>
inline typename CryStringT<T>::....::rfind(....) const
{
const_str str;
if (pos == npos)
{
// find last single character
str = _strrchr(m_str, ch);
// return -1 if not found, distance from beginning otherwise
return (str == NULL) ?
(size_type) - 1 : (size_type)(str - m_str);
}
else
{
if (pos == npos)
{
pos = length();
}
if (pos > length())
{
return npos;
}
value_type tmp = m_str[pos + 1];
m_str[pos + 1] = 0;
str = _strrchr(m_str, ch);
m_str[pos + 1] = tmp;
}
return (str == NULL) ?
(size_type) - 1 : (size_type)(str - m_str);
}
Анализатор обнаружил повторную проверку переменной pos. Из-за такой ошибки часть кода никогда не выполняется. Также в функции есть дублирующийся код, поэтому функцию стоит переписать получше.
Такой код благополучно продублировали ещё в одном месте:
- V571 Recurring check. The 'if (pos == npos)' condition was already verified in line 1262. CryFixedString.h 1271
V523 The 'then' statement is equivalent to the 'else' statement. ScriptTable.cpp 789
bool CScriptTable::AddFunction(const SUserFunctionDesc& fd)
{
....
char sFuncSignature[256];
if (fd.sGlobalName[0] != 0)
cry_sprintf(sFuncSignature, "%s.%s(%s)", fd.sGlobalName,
fd.sFunctionName, fd.sFunctionParams);
else
cry_sprintf(sFuncSignature, "%s.%s(%s)", fd.sGlobalName,
fd.sFunctionName, fd.sFunctionParams);
....
}
Независимо от содержимого строки, её всё равно пытаются распечатать одинаковым образом. Такого кода тоже очень много в проекте, вот некоторые предупреждения:
- V523 The 'then' statement is equivalent to the 'else' statement. BudgetingSystem.cpp 718
- V523 The 'then' statement is equivalent to the 'else' statement. D3DShadows.cpp 627
- V523 The 'then' statement is equivalent to the 'else' statement. livingentity.cpp 967
Неопределённое поведение
V610 Undefined behavior. Check the shift operator '<<'. The left operand '-1' is negative. physicalplaceholder.h 25
class CPhysicalEntity;
const int NO_GRID_REG = -1<<14;
const int GRID_REG_PENDING = NO_GRID_REG+1;
const int GRID_REG_LAST = NO_GRID_REG+2;
Анализатор умеет находить несколько типов ошибок, приводящих к неопределённому поведению. Согласно современному стандарту языка, сдвиг отрицательного числа влево приводит к неопределённому поведению.
Вот ещё несколько сомнительных мест:
- V610 Undefined behavior. Check the shift operator '<<'. The left operand '~(TFragSeqStorage(0))' is negative. UDPDatagramSocket.cpp 757
- V610 Undefined behavior. Check the shift operator '<<'. The right operand ('cpu' = [0..1023]) is greater than or equal to the length in bits of the promoted left operand. CryThreadUtil_posix.h 115
- V610 Undefined behavior. Check the shift operator '>>'. The right operand is negative ('comp' = [-1..3]). ShaderComponents.cpp 399
- V610 Undefined behavior. Check the shift operator '<<'. The left operand '-1' is negative. trimesh.cpp 4126
- V610 Undefined behavior. Check the shift operator '<<'. The left operand '-1' is negative. trimesh.cpp 4559
- V610 Unspecified behavior. Check the shift operator '>>'. The left operand '-NRAYS' is negative. trimesh.cpp 4618
- V610 Undefined behavior. Check the shift operator '<<'. The left operand '-1' is negative. tetrlattice.cpp 324
- V610 Undefined behavior. Check the shift operator '<<'. The left operand '-1' is negative. tetrlattice.cpp 350
- V610 Undefined behavior. Check the shift operator '<<'. The left operand '-1' is negative. tetrlattice.cpp 617
- V610 Undefined behavior. Check the shift operator '<<'. The left operand '-1' is negative. tetrlattice.cpp 622
Другой тип неопределённого поведения связан с неоднократным изменением переменной между двумя точками следования:
V567 Undefined behavior. The 'm_current' variable is modified while being used twice between sequence points. OperatorQueue.cpp 101
boolCOperatorQueue::Prepare(....)
{
++m_current &= 1;
m_ops[m_current].clear();
return true;
}
К сожалению, такое место не единственное в коде:
- V567 Undefined behavior. The 'm_commandBufferIndex' variable is modified while being used twice between sequence points. XConsole.cpp 180
- V567 Undefined behavior. The 'itail' variable is modified while being used twice between sequence points. trimesh.cpp 3119
- V567 Undefined behavior. The 'ihead' variable is modified while being used twice between sequence points. trimesh.cpp 3126
- V567 Undefined behavior. The 'ivtx' variable is modified while being used twice between sequence points. boolean3d.cpp 957
- V567 Undefined behavior. The 'ivtx' variable is modified while being used twice between sequence points. boolean3d.cpp 965
- V567 Undefined behavior. The 'ivtx' variable is modified while being used twice between sequence points. boolean3d.cpp 983
- V567 Undefined behavior. The 'm_iNextAnimIndex' variable is modified while being used twice between sequence points. HitDeathReactionsDefs.cpp 192
Вопросы к разработчикам
В коде CryEngine V я нашёл интересный способ общения разработчиков через комментарии в коде.
Вот самый забавный комментарий, который я нашёл с помощью предупреждения:
V763 Parameter 'enable' is always rewritten in function body before being used.
void CNetContext::EnableBackgroundPassthrough(bool enable)
{
SCOPED_GLOBAL_LOCK;
// THIS IS A TEMPORARY HACK TO MAKE THE GAME PLAY NICELY,
// ASK peter@crytek WHY IT'S STILL HERE
enable = false;
....
}
Далее я решил с помощью поиска по содержимому файлов поикать подобные тексты и выписал несколько мест:
....
// please ask me when you want to change [tetsuji]
....
// please ask me when you want to change [dejan]
....
//if there are problems with this function, ask Ivo
uint32 numAnims =
pCharacter->GetISkeletonAnim()->GetNumAnimsInFIFO(layer);
if (numAnims)
return pH->EndFunction(true);
....
//ask Ivo for details
//if (pCharacter->GetCurAnimation() &&
// pCharacter->GetCurAnimation()[0] != '\0')
// return pH->EndFunction(pCharacter->GetCurAnimation());
....
/////////////////////////////////////////////////////////////////
// Strange, !do not remove... ask Timur for the meaning of this.
/////////////////////////////////////////////////////////////////
if (m_nStrangeRatio > 32767)
{
gEnv->pScriptSystem->SetGCFrequency(-1); // lets get nasty.
}
/////////////////////////////////////////////////////////////////
// Strange, !do not remove... ask Timur for the meaning of this.
/////////////////////////////////////////////////////////////////
if (m_nStrangeRatio > 1000)
{
if (m_pProcess && (m_pProcess->GetFlags() & PROC_3DENGINE))
m_nStrangeRatio += cry_random(1, 11);
}
/////////////////////////////////////////////////////////////////
....
// tank specific:
// avoid steering input around 0.5 (ask Anton)
....
CryWarning(VALIDATOR_MODULE_EDITOR, VALIDATOR_WARNING,
"....: Wrong edited item. Ask AlexL to fix this.");
....
// If this renders black ask McJohn what's wrong.
glGenerateMipmap(GL_TEXTURE_2D);
....
Ну и самый главный вопрос к разработчикам: почему они не пользуются специализированными инструментами для повышения качества кода? Я, конечно, имею в виду PVS-Studio. :)
Еще раз хочу отметить, что в статье приведена только часть ошибок. Я не досмотрел до конца даже предупреждения уровня High. Так что проект ещё ждет тех, кто внимательно проверит его. Я, к сожалению, не могу найти столько времени, меня ждут десятки других проектов.
Заключение
За много лет разработки статического анализатора кода я пришёл к выводу, что в командах разработчиков с некоторого количества людей уже невозможно писать код без ошибок. Нет, я не против Code Review, но посчитайте сами, сколько времени понадобится руководителю, чтобы сделать обзор кода 10 человек? А на следующий день? А большего числа людей? В таких условиях Code Review необходим при изменении только ключевых компонентов продукта. В больших объёмах такой подход уже крайне неэффективен. Автоматизированная проверка кода статическими анализаторами поможет значительно выправить ситуацию в лучшую сторону. Это не замена существующим тестам, а совсем иной подход к контролю качества кода (к слову, в тестах тоже находятся ошибки с помощью статического анализа кода). Исправление ошибок во время написания кода почти ничего не стоит, в отличии от тех, которые находятся на этапе работы тестировщиков, а ошибки в выпущенном продукте несут уже колоссальные расходы.
Скачать и попробовать PVS-Studio можно по этой ссылке.
Обсудить варианты лицензирования, цены и варианты скидок можно написав нам в поддержку.
Не огорчайте единорога своим кодом…
Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Svyatoslav Razmyslov. Critical errors in CryEngine V code
Комментарии (30)
Yeah
04.04.2017 12:33+1Хороший хитрый план: зачем исправлять ошибки, выложим код на Github, пусть сообщество правит. Ну тут хоть открыты проект, а игры, которые выпускают до ужаса забагованными — это просто бич отрасли
SvyatoslavMC
04.04.2017 14:09+3И Pull Request'ы отрпавили с исправлениями, но у них там завал, никто не закрывает задачи и не рассматривает исправления. Хотя код постоянно редактируют.
Mingun
04.04.2017 18:58-1Да, переписку ведут :) Нет бы, чтобы сразу написать, в чём был затык и для чего сделан хак — везде только «ask somehow for details».
lookid
05.04.2017 11:19+1Ваше утветржение очень глупое и показывает ваш узкой кругозор и узкое мышление.
1) Этот код может вообще не вызываться и висеть рудиментом.
2) Фиксить баги сложнее, чем писать функционал. И никто в опсосных проектах не будет фиксить баги просто так.
3) Выложили в опенсорс для того, чтобы люди могли изучить код и посмотреть что-нибудь. Всё равно такого зверя писать с ходу никто не будет.
4) Чем больше проект, тем больше в нем приколов и недоумений.
iXCray
04.04.2017 12:43+2Есть подозрение, что здесь не ошибка, а поиск конца списка. Очевидно, что для большей прозрачности и защиты от случайных ошибок нужно было бы применить while вместо вырожденного варианта for.
for(pmd0=m_pMeshUpdate; pmd0->next; pmd0=pmd0->next); // <= pmd0->next = pmd;
CrazyNiger
04.04.2017 12:56+5Или хотя бы форматирование поправить, что бы было:
for(pmd0=m_pMeshUpdate; pmd0->next; pmd0=pmd0->next); // <= pmd0->next = pmd;
daiver19
04.04.2017 18:14+1Статический анализ, безусловно, нужен и полезен, но:
Нет, я не против Code Review, но посчитайте сами, сколько времени понадобится руководителю, чтобы сделать обзор кода 10 человек? А на следующий день? А большего числа людей? В таких условиях Code Review необходим при изменении только ключевых компонентов продукта. В больших объёмах такой подход уже крайне неэффективен.
Внезапно, код может ревьювить не только руководитель и ревью масштабируется для любого количества людей (доказано Google).SvyatoslavMC
04.04.2017 20:23Внезапно, код может ревьювить не только руководитель и ревью масштабируется для любого количества людей (доказано Google).
Под руководителями понимаются и ведущие разработчики какого-нибудь подразделения, их может быть достаточно много, но мой вывод всё равно актуален. А вообще я просто хотел оставить ссылки на 5 статей о проверке Google Chromium (1, 2, 3, 4, 5, ...,), так сказать, в качестве опровержения)daiver19
04.04.2017 20:29+1Ну так ревью ортогональны багам вообще-то, ревьювер не должен целенаправленно выискивать опечатки. Речь просто о том, что ревью нормально масштабируются на большие проекты и много людей. Но в силу ортогональности наличию багов это не имеет никакого значения в контексте статического анализа.
SvyatoslavMC
04.04.2017 21:15+2Просто Вы разделяете эти процессы. А когда общаешься с разработчиком, заведомо противником статического анализа, то в ход идёт единственный козырь — Code Review. Мой вывод основывается как раз на таких многочисленных прецедентах.
SvyatoslavMC
04.04.2017 21:27Да и не в опечатках дело, как Вам ошибка из раздела статьи «Code above has no bugs»? Чья ответственность?
daiver19
04.04.2017 23:28+3Вот тот чудо-кусок кода — это, в принципе, говнокод, и его должно отлавливать ревью. В общем, мне не понять людей, которые противопоставляют ревью и статический анализ. Как ревьювер, я только за то, чтобы как можно больше стилистических, логических и прочих ошибок было найдено автоматически.
Ronnie_Gardocki
04.04.2017 19:11+1Народ, я вот сам из мира JS со всеми этими новомодными баззвордами и тулзами и так далее. И мне вот не особо понятно — как в столь серьезном проекте, написанном явно не студентами (надеюсь), могут находиться столь примитивные ошибки, наподобие той что можно узреть в функции setActive, там где переменная сравнивается сама с собой?
Я у себя на проекте если пробел после запятой не поставлю, мне линтер из монитора в лицо плюнет и выдаст ошибку на весь экран вместо вебсайта. А уж задетектить сравнение переменной с самой собой, ну это же вообще детский сад, мне кажется все эти навороченные ИДЕ, которые грузятся пару лет, должны уметь делать такое по умолчанию (в то время как я сижу с Sublime Text и парочкой плагинов сверху, и весь линтинг динамически происходит там же не отходя от кассы).
В общем моя не понимать, как такой код попадает в продакшен (я уж молчу про тесты и все такое).SvyatoslavMC
04.04.2017 20:37+2могут находиться столь примитивные ошибки
Не соглашусь насчёт примитивных. Это они сейчас выглядят простыми и смешными, в дебрях кода это вполне себе потенциальные проблемы. Характер возникновения некоторых ошибок действительно примитивен.
Если Вы взгляните на наш обновляемый список статьей, то ещё больше ужаснётесь. Ошибки далеко не только прерогатива студентов, но и всех подряд в этой сфере.
навороченные ИДЕ, которые грузятся пару лет, должны уметь делать такое по умолчанию
Статический анализ прибавит ещё десяток лет к загрузке, поэтому эта ниша не для них, а для специальных инструментов. Современным IDE ещё шаблоны C++ разбирать учиться и учиться.
4e1
04.04.2017 21:11+1welcome to C++ world :)
предполагаю основная причина — анализатору плюсового кода «на лету» тяжело быть эфективным. Полноценный анализ возможен только при составлении AST, это фактически компиляция, и она занимает время, как пример — можно посмотреть на clang-плагин в QtCreator.Dionis_mgn
04.04.2017 21:56Да, линтер C++ — довольно требовательная к ресурсам штука. Но не настолько, чтобы заметить какие-либо во время редактирования кода. По крайней мере, до тех пор, пока не начать редактировать заголовочные файлы. Есть и другие проблемы: экосистема C++ не имеет стандартизированного средства сборки проектов, менеджера зависимостей. Там до сих пор нет модулей! Проверять файл без учёта его зависимостей… ну это такое. Всё это усложняет процессы разработки, внедрения и пользования подобными ништяками.
Но, как я уже говорил, Clang делает успехи в этом направлении.
DjPhoeniX
04.04.2017 22:37+1Мне больше непонятно, как это прошло через компилятор. Как я вижу, используется clang-3.8. У меня он (
-Wall -Wextra
) сходу ругается и на V547, и местами на V501. И многое другое, что в статьях про PVS говорится. А clang-4 покрывает чуть ли не половину перечисленного.Andrey2008
04.04.2017 22:54-1Предположу, что слишком много шума (ложных срабатываний) у компиляторов, вот и не пользуются. Это когда рассматриваешь отдельные случаи, то все кажется просто и даже в выводе компилятора можно найти нужное. Но на практике всё это теряется в шуме бессмысленных предупреждений. Мы же очень много работаем не только над поиском многих паттернов ошибок, но и ещё очень много работаем над исключениями. Чтобы шума было меньше. Это ОЧЕНЬ важно.
QtRoS
04.04.2017 22:37Жесть, неужели проект такой большой, что такие конкретные и серьезные баги не проявляются на каждом шагу…
DjPhoeniX
04.04.2017 22:55Реквестирую дайджест «самые интересные ошибки, найденные анализатором PVS-Studio в коде PVS-Studio». Скорее всего, он у вас в pre-commit/pre-push hooks, но тем не менее… :)
Andrey2008
04.04.2017 22:58Ну например в "C# баги месяца" попало такое. :)
Maccimo
04.04.2017 23:41Магические числа с комментариями вместо именованных констан, ай-яй-яй!
У вас ругани на такое в диагностиках разве нет?Andrey2008
04.04.2017 23:52Ругаться на неименованные константы нельзя, кроме особых случаев. Вернее, ругаться можно на все константы, но как только в анализаторе появятся такие предупреждения, он превратится в тыкву.
Andrey2008
05.04.2017 00:08-1Кстати, мы никогда не утверждали, что пишем идеальный код и у нас есть смелость сказать про это. Думаю, в этом есть польза. Польза конечно не том, что код не идеален, а в том, что признаем это. Благодаря этому у нас нет гордыни и мы спокойно используем различные методики для повышения качества и надежности кода (разные виды тестов, проверяем себя с помощью PVS-Studio, Clang и т.п.). Мы просто столько везде слышим пафосного, что вот мы то о-го-го и таких-то ошибок не совершаем. Вот только откуда же тогда вот эти 10893 ошибки… Никак инопланетяне по ночам в код их подмешивают. Впрочем, ничего нового, все мы считаем себя чуть лучше, чем есть средний программист.
Neraverin
После чтения Ваших статей у меня всегда возникает вопрос — а Вы уверены, что PVS-Studio мало покупают потому, что не верят в его эффективность? Мне кажется, что в способности PVS-Studio найти проблемы в коде уже никто не сомневается. Может надо что-то еще продвигать? Например, стоимость ошибки разработчика.
Andrey2008
Сложно. Но мы попробуем.
SvyatoslavMC
Есть кое-какой материал о стоимости ошибки в коде, из которого следует вполне очевидный вывод (продвигаемый нами): стоимость исправления ошибки в коде сразу в разы дешевле, чем когда ошибка доходит до тестеров разных уровней и тем более до клиентов. Что-то конкретное сказать для проверенного проекта сложно без внутренней информации о компании.
EviGL
Думаю, стоит поспрашивать ваших лояльных клиентов на предмет историй про стоимость ошибок «до» и «после». Историю можно обезличить, чтобы всякие там корпоративные тайны не страдали.