如今,无需为开发游戏而从头开始实现对象的物理原理,因为有许多用于此目的的库。 Bullet在许多AAA游戏,虚拟现实项目,各种模拟和机器学习中得到了积极的应用。 并且它仍在使用,例如,它是Red Dead Redemption和Red Dead Redemption 2引擎之一。 因此,为什么不使用PVS-Studio检查Bullet来查看静态分析可以在如此大规模的物理模拟项目中检测到哪些错误。
该库是
免费分发的 ,因此每个人都可以根据需要在自己的项目中使用它。 除《荒野大镖客:救赎》外,该物理引擎还用于电影行业中以创建特殊效果。 例如,在盖伊·里奇(Guy Ritchie)的“夏洛克·福尔摩斯”(Sherlock Holmes)拍摄中使用了它来计算碰撞。
如果这是您第一次见到PVS-Studio检查项目的文章,我将做一点题外话。
PVS-Studio是静态代码分析器,可以帮助您发现C,C ++,C#,Java程序的源代码中的错误,缺陷和潜在漏洞。 静态分析是一种自动的代码检查过程。
热身
范例1:让我们从一个有趣的错误开始:
V624'3.141592538 '常量中可能存在打印错误。 考虑使用<math.h>中的M_PI常量。 PhysicsClientC_API.cpp 4109
B3_SHARED_API void b3ComputeProjectionMatrixFOV(float fov, ....) { float yScale = 1.0 / tan((3.141592538 / 180.0) * fov / 2); .... }
Pi值(3.141592653 ...)有一个小错字。 小数部分的第7位数字缺失-必须等于6。
也许,小数点后第1百万分之一的错误不会导致任何重大后果,但是仍然应该使用没有拼写错误的现有库常量。 从
math.h标头中有一个Pi号的
M_PI常量。
复制粘贴
范例2:有时,分析仪可以让您间接找到错误。 例如,此处将三个相关的参数HalfExtentsX,halfExtentsY,halfExtentsZ传递给函数,但后者未在函数中的任何地方使用。 您可能会注意到,在调用
addVertex方法时,halfExtentsY变量被使用了两次。 因此,可能是复制粘贴错误,应该在此处使用被遗忘的参数。
V751参数“ halfExtentsZ”未在功能体内使用。 TinyRenderer.cpp 375
void TinyRenderObjectData::createCube(float halfExtentsX, float halfExtentsY, float halfExtentsZ, ....) { .... m_model->addVertex(halfExtentsX * cube_vertices_textured[i * 9], halfExtentsY * cube_vertices_textured[i * 9 + 1], halfExtentsY * cube_vertices_textured[i * 9 + 2], cube_vertices_textured[i * 9 + 4], ....); .... }
范例3:分析器还检测到以下有趣的片段,我将首先以初始形式显示它。
看到这个looooooooooong行吗?
程序员决定将这么长的条件写到一行中是很奇怪的。 但是不足为奇的是,它也很可能漏掉了。
分析仪在此行生成了以下警告。
V501在'&&'运算符的左侧和右侧有相同的子表达式'rotmat.Column1()。Norm()<1.0001'。 线性R4.cpp 351
V501在&&运算符的左侧和右侧有相同的子表达式'0.9999 <rotmat.Column1()。Norm()'。 线性R4.cpp 351
如果我们以清晰的“表格”形式将其全部写下,我们可以看到所有相同的检查都适用于
Column1 。 最后两个比较显示有
Column1和
Column2 。 最有可能的是,第三次和第四次比较应该已经检查了
Column2的值。
Column1().Norm() < 1.0001 && 0.9999 < Column1().Norm() && Column1().Norm() < 1.0001 && 0.9999 < Column1().Norm() &&(Column1() ^ Column2()) < 0.001 && (Column1() ^ Column2()) > -0.001
在这种形式下,相同的比较变得更加明显。
范例4:同类错误:
V501在'&&'运算符的左侧和右侧有相同的子表达式'cs.m_fJacCoeffInv [0] == 0'。 b3CpuRigidBodyPipeline.cpp 169
float m_fJacCoeffInv[2]; static inline void b3SolveFriction(b3ContactConstraint4& cs, ....) { if (cs.m_fJacCoeffInv[0] == 0 && cs.m_fJacCoeffInv[0] == 0) { return; } .... }
在这种情况下,将检查同一数组元素两次。 该条件最有可能看起来像这样:
cs.m_fJacCoeffInv [0] == 0 && cs.m_fJacCoeffInv [1] == 0 。 这是复制粘贴错误的经典示例。
范例5:还发现存在这样的缺陷:
V517检测到使用'if(A){...} else if(A){...}'模式。 存在逻辑错误的可能性。 检查行:79、112。main.cpp 79
int main(int argc, char* argv[]) { .... while (serviceResult > 0) { serviceResult = enet_host_service(client, &event, 0); if (serviceResult > 0) { .... } else if (serviceResult > 0) { puts("Error with servicing the client"); exit(EXIT_FAILURE); } .... } .... }
函数
enet_host_service的结果分配给
serviceResult ,如果成功完成,则返回1,如果失败,则返回-1。 最可能的是,
else if分支应该对
serviceResult的负值作出反应,但是检查条件已重复。 可能也是复制粘贴错误。
对于分析器也有类似的警告,但是在本文中对其进行更仔细的研究是没有意义的。
V517检测到使用'if(A){...} else if(A){...}'模式。 存在逻辑错误的可能性。 检查行:151、190。PhysicsClientUDP.cpp 151
最重要的是:超出数组边界
范例6:要搜索的令人不快的错误之一是数组溢出。 由于循环中的复杂索引,经常会发生此错误。
在此,在循环条件下,
dofIndex变量的上限为128,而
dof的上限为4。 但是
m_desiredState也仅包含128个项目。 结果,
[dofIndex + dof]索引可能会导致数组溢出。
V557阵列可能超限。 “ dofIndex + dof”索引的值可以达到130。PhysicsClientC_API.cpp 968
#define MAX_DEGREE_OF_FREEDOM 128 double m_desiredState[MAX_DEGREE_OF_FREEDOM]; B3_SHARED_API int b3JointControl(int dofIndex, double* forces, int dofCount, ....) { .... if ( (dofIndex >= 0) && (dofIndex < MAX_DEGREE_OF_FREEDOM ) && dofCount >= 0 && dofCount <= 4) { for (int dof = 0; dof < dofCount; dof++) { command->m_sendState.m_desiredState[dofIndex+dof] = forces[dof]; .... } } .... }
范例7:一个类似的错误,但现在是由于不是在对数组建立索引时而是在某种情况下求和而引起的。 如果文件的名称具有最大长度,则终端零将被写入数组外部(
Off-by-One Error )。 当然,只有在特殊情况下,
len变量才等于
MAX_FILENAME_LENGTH ,但是它不能消除错误,只是使其很少见。
V557阵列可能超限。 “ len”索引的值可能达到1024。PhysicsClientC_API.cpp 5223
#define MAX_FILENAME_LENGTH MAX_URDF_FILENAME_LENGTH 1024 struct b3Profile { char m_name[MAX_FILENAME_LENGTH]; int m_durationInMicroSeconds; }; int len = strlen(name); if (len >= 0 && len < (MAX_FILENAME_LENGTH + 1)) { command->m_type = CMD_PROFILE_TIMING; strcpy(command->m_profile.m_name, name); command->m_profile.m_name[len] = 0; }
测量一次,切七次
范例8:如果您需要多次使用某个函数的工作结果或使用一个变量,而该变量需要通过整个调用链才能访问,则应使用临时变量进行优化和提高代码可读性。 分析仪已在代码中找到100多个可以进行更正的位置。
V807性能
下降 。 考虑创建一个指针,以避免重复使用'm_app-> m_renderer-> getActiveCamera()'表达式。 InverseKinematicsExample.cpp 315
virtual void resetCamera() { .... if (....) { m_app->m_renderer->getActiveCamera()->setCameraDistance(dist); m_app->m_renderer->getActiveCamera()->setCameraPitch(pitch); m_app->m_renderer->getActiveCamera()->setCameraYaw(yaw); m_app->m_renderer->getActiveCamera()->setCameraPosition(....); } }
相同的调用链在这里使用了很多次,可以用一个指针代替。
范例9:V810性能
下降 。 使用相同的参数多次调用了“ btCos(euler_out.pitch)”函数。 结果可能应该保存到一个临时变量中,然后可以在调用“ btAtan2”函数时使用它。 btMatrix3x3.h 576
V810性能
下降 。 使用相同的参数多次调用了“ btCos(euler_out2.pitch)”函数。 结果可能应该保存到一个临时变量中,然后可以在调用“ btAtan2”函数时使用它。 btMatrix3x3.h 578
void getEulerZYX(....) const { .... if (....) { .... } else { .... euler_out.roll = btAtan2(m_el[2].y() / btCos(euler_out.pitch), m_el[2].z() / btCos(euler_out.pitch)); euler_out2.roll = btAtan2(m_el[2].y() / btCos(euler_out2.pitch), m_el[2].z() / btCos(euler_out2.pitch)); euler_out.yaw = btAtan2(m_el[1].x() / btCos(euler_out.pitch), m_el[0].x() / btCos(euler_out.pitch)); euler_out2.yaw = btAtan2(m_el[1].x() / btCos(euler_out2.pitch), m_el[0].x() / btCos(euler_out2.pitch)); } .... }
在这种情况下,您可以创建两个变量并将
btCos函数返回的值保存为
euler_out.pitch和
euler_out2.pitch ,而不是为每个参数调用该函数四次。
泄漏
范例10:在项目中检测到许多以下类型的错误:
V773在不释放内存的情况下退出了“导入程序”指针的可见性范围。 可能发生内存泄漏。 SerializeSetup.cpp 94
void SerializeSetup::initPhysics() { .... btBulletWorldImporter* importer = new btBulletWorldImporter(m_dynamicsWorld); .... fclose(file); m_guiHelper->autogenerateGraphicsObjects(m_dynamicsWorld); }
此处尚未从
导入程序指针释放内存。 这可能会导致内存泄漏。 对于物理引擎而言,这可能是一个坏趋势。 为避免泄漏,在变量变得不必要之后添加
删除导入器就足够了。 但是,当然,最好使用智能指针。
C ++依靠自己的代码生存
示例11:下一个错误出现在代码中,因为C ++规则并不总是与数学规则或“常识”一致。 您会注意到这个小代码片段在哪里包含错误?
btAlignedObjectArray<btFractureBody*> m_fractureBodies; void btFractureDynamicsWorld::fractureCallback() { for (int i = 0; i < numManifolds; i++) { .... int f0 = m_fractureBodies.findLinearSearch(....); int f1 = m_fractureBodies.findLinearSearch(....); if (f0 == f1 == m_fractureBodies.size()) continue; .... } .... }
分析仪生成以下警告:
V709发现可疑比较:“ f0 == f1 == m_fractureBodies.size()”。 请记住,“ a == b == c”不等于“ a == b && b == c”。 btFractureDynamicsWorld.cpp 483
看来该条件检查
f0等于
f1并等于
m_fractureBodies中的项目
数 。 似乎该比较应该检查
f0和
f1是否位于
m_fractureBodies数组的末尾,因为它们包含由
findLinearSearch()方法找到的对象位置。 但是实际上,该表达式变成了检查
f0和
f1是否等于
m_fractureBodies.size()的检查,然后是检查
m_fractureBodies.size()是否等于结果
f0 == f1的检查 。 结果,此处的第三个操作数将与0或1进行比较。
美丽的错误! 而且,幸运的是,非常罕见。 到目前为止,我们仅在两个开源项目中
碰面 ,有趣的是,它们都是游戏引擎。
示例12:使用字符串时,通常最好使用
字符串类提供的功能。 因此,在接下来的两种情况下,最好分别使用
MyStr.length()和
val.clear()替换
strlen(MyStr.c_str())和val =“” 。
V806性能
下降 。 strlen(MyStr.c_str())类型的表达式可以重写为MyStr.length()。 RobotLoggingUtil.cpp 213
FILE* createMinitaurLogFile(const char* fileName, std::string& structTypes, ....) { FILE* f = fopen(fileName, "wb"); if (f) { .... fwrite(structTypes.c_str(), strlen(structTypes.c_str()), 1, f); .... } .... }
V815性能
下降 。 考虑用“ val.clear()”替换表达式“ val =“”“。 b3CommandLineArgs.h 40
void addArgs(int argc, char **argv) { .... std::string val; .... val = ""; .... }
还有其他警告,但我认为我们可以在这里停止。 如您所见,静态代码分析可以检测各种错误。
阅读一次性项目检查很有趣,但是使用静态代码分析器不是正确的方法。 我们将在下面谈论它。
发现的错误
尝试查找已经修复但可以由最近的文章“
由于未使用静态代码分析而无法发现错误 ”的静态分析器可以检测到的bug或缺陷很有趣。
存储库中没有很多拉取请求,其中许多与引擎的内部逻辑有关。 但是也存在分析仪可以检测到的错误。
示例13: char m_deviceExtensions[B3_MAX_STRING_LENGTH]; void b3OpenCLUtils_printDeviceInfo(cl_device_id device) { b3OpenCLDeviceInfo info; b3OpenCLUtils::getDeviceInfo(device, &info); .... if (info.m_deviceExtensions != 0) { .... } }
该请求的注释说,您需要检查数组是否为空的事实,而是执行了无意义的指针检查,该检查始终返回true。 这是PVS-Studio关于原始支票的警告告诉您的内容:
V600考虑检查状况。 “ info.m_deviceExtensions”指针始终不等于NULL。 b3OpenCLUtils.cpp 551
示例14:您能找出下一个功能有什么问题吗?
inline void Matrix4x4::SetIdentity() { m12 = m13 = m14 = m21 = m23 = m24 = m13 = m23 = m41 = m42 = m43 = 0.0; m11 = m22 = m33 = m44 = 1.0; }
分析仪会生成以下警告:
V570相同的值两次分配给'm23'变量。 线性R4.h 627
V570相同的值两次分配给'm13'变量。 线性R4.h 627
用这种记录形式的重复分配很难用肉眼跟踪,结果,一些矩阵元素没有得到初始值。 此错误已通过作业记录的表格形式更正:
m12 = m13 = m14 = m21 = m23 = m24 = m31 = m32 = m34 = m41 = m42 = m43 = 0.0;
示例15:btSoftBody函数:: addAeroForceToNode()的条件之一中的以下错误导致了明显的错误。 根据拉动请求中的注释,力从错误的一侧施加到对象上。
struct eAeroModel { enum _ { V_Point, V_TwoSided, .... END }; }; void btSoftBody::addAeroForceToNode(....) { .... if (....) { if (btSoftBody::eAeroModel::V_TwoSided) { .... } .... } .... }
PVS-Studio还会发现此错误并生成以下警告:
V768枚举常量'V_TwoSided'用作布尔型变量。 btSoftBody.cpp 542
固定检查如下所示:
if (m_cfg.aeromodel == btSoftBody::eAeroModel::V_TwoSided) { .... }
而不是将对象的属性等效于枚举器之一,
而是检查
V_TwoSided枚举器本身。
显然,我并没有考虑所有的请求请求,因为那不是重点。 我只是想向您展示,定期使用静态代码分析器可以在早期阶段检测到错误。 这是使用静态代码分析的正确方法。 静态分析必须内置到DevOps流程中,并且必须是主要的错误过滤器。 “
在过程中引入静态分析,而不仅仅是使用它来查找错误 ”一文中对此进行了很好的描述。
结论
根据某些请求,有时会通过各种代码分析工具来检查项目,但是校正不是逐步进行的,而是成组且间隔较大。 在某些请求中,注释指示所做的更改仅是为了禁止显示警告。 这种使用分析的方法大大降低了其有用性,因为对项目的定期检查使您可以立即纠正错误,而不必等待任何明显的错误出现。
关注我们并订阅我们的社交媒体帐户和渠道:
Instagram ,
Twitter ,
Facebook ,
Telegram 。 无论您身在何处,我们都希望与您在一起,并让您始终保持稳定。