De nos jours, pour, par exemple, développer des jeux, il n'est plus nécessaire d'implémenter indépendamment la physique des objets à partir de zéro, car il existe un grand nombre de bibliothèques pour cela. Bullet était autrefois activement utilisé dans de nombreux jeux AAA, projets de réalité virtuelle, diverses simulations et apprentissage automatique. Oui, et il est toujours utilisé aujourd'hui, étant, par exemple, l'un des moteurs Red Dead Redemption et Red Dead Redemption 2. Alors pourquoi ne pas vérifier Bullet avec PVS-Studio pour voir quelles erreurs l'analyse statique peut détecter dans un projet à si grande échelle, liés à la simulation physique.
Cette bibliothèque est
librement distribuée , afin que chacun puisse éventuellement l'utiliser dans ses projets. En plus de Red Dead Redemption, ce moteur physique est également utilisé dans l'industrie cinématographique pour créer des effets spéciaux. Par exemple, il a participé au tournage de Sherlock Holmes de Guy Ritchie pour calculer les collisions.
Si c'est votre première rencontre avec un article où PVS-Studio vérifie les projets, alors je vais faire une petite digression.
PVS-Studio est un analyseur de code statique qui aide à trouver les erreurs, les lacunes et les vulnérabilités potentielles dans le code source des programmes C, C ++, C #, Java. L'analyse statique est une sorte de processus automatisé de révision de code.
Pour se réchauffer
Exemple 1:Commençons par une erreur amusante:
V624 Il y a probablement une
faute d' impression dans la constante '3.141592538'. Pensez à utiliser la constante M_PI de <math.h>. PhysicsClientC_API.cpp 4109
B3_SHARED_API void b3ComputeProjectionMatrixFOV(float fov, ....) { float yScale = 1.0 / tan((3.141592538 / 180.0) * fov / 2); .... }
Une petite faute de frappe dans le nombre Pi (3.141592653 ...), manquant le nombre "6" à la 7ème position dans la partie fractionnaire.
Peut-être que l'erreur à la dix millionième décimale n'entraînera pas de conséquences tangibles, mais vous devriez toujours utiliser les constantes de bibliothèque existantes sans fautes de frappe. Pour Pi, il existe une constante
M_PI de l'en-tête
math.h.Copier coller
Exemple 2:Parfois, l'analyseur vous permet de trouver l'erreur indirectement. Ainsi, par exemple, ici trois arguments liés
halfExtentsX, halfExtentsY, halfExtentsZ sont passés à la fonction, mais ce dernier n'est utilisé nulle part dans la fonction. Vous pouvez remarquer que lors de l'appel de la méthode
addVertex , la variable
halfExtentsY est utilisée deux fois. C'est peut-être une erreur de copier-coller et ici un argument oublié devrait être utilisé.
V751 Le paramètre 'halfExtentsZ' n'est pas utilisé dans le corps de la fonction. 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], ....); .... }
Exemple 3:L'analyseur a également trouvé le fragment intéressant suivant, je vais l'apporter d'abord dans sa forme originale.
Vous voyez cette dernière ligne?
Il est très étrange que le programmeur ait décidé d'écrire une condition aussi longue sur une seule ligne. Mais le fait qu'une erreur s'y soit probablement glissée n'est pas du tout surprenant.
L'analyseur a émis les avertissements suivants sur cette ligne.
V501 Il existe des sous-expressions identiques 'rotmat.Column1 (). Norm () <1.0001' à gauche et à droite de l'opérateur '&&'. LinearR4.cpp 351
V501 Il existe des sous-expressions identiques '0.9999 <rotmat.Column1 (). Norm ()' à gauche et à droite de l'opérateur '&&'. LinearR4.cpp 351
Si nous écrivons tout sous une forme «tabulaire» visuelle, il deviendra clair que les mêmes vérifications s'appliquent à
Colonne1 . Les deux dernières comparaisons montrent qu'il y a
Colonne1 et
Colonne2 . Très probablement, les troisième et quatrième comparaisons auraient dû vérifier la valeur de
Colonne2 .
Column1().Norm() < 1.0001 && 0.9999 < Column1().Norm() && Column1().Norm() < 1.0001 && 0.9999 < Column1().Norm() &&(Column1() ^ Column2()) < 0.001 && (Column1() ^ Column2()) > -0.001
Sous cette forme, la même comparaison devient beaucoup plus perceptible.
Exemple 4:Erreur de type similaire:
V501 Il existe des sous-expressions identiques «cs.m_fJacCoeffInv [0] == 0» à gauche et à droite de l'opérateur «&&». 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; } .... }
Dans ce cas, le même élément de tableau est revérifié. Très probablement, la condition aurait dû ressembler à ceci:
cs.m_fJacCoeffInv [0] == 0 && cs.m_fJacCoeffInv [1] == 0 . Il s'agit d'un exemple classique d'erreur de copier-coller.
Exemple 5:Une autre lacune a été découverte:
V517 L'utilisation du
modèle 'if (A) {...} else if (A) {...}' a été détectée. Il y a une probabilité de présence d'erreur logique. Vérifiez les lignes: 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); } .... } .... }
La fonction
enet_host_service , dont le résultat est affecté à
serviceResult , renvoie un en cas de
succès et -1 en cas d'échec. Très probablement, la branche
else if aurait dû répondre à la valeur négative de
serviceResult , mais la condition de vérification a été dupliquée. Il s'agit très probablement aussi d'une erreur de copier-coller.
Un avertissement similaire à l'analyseur, qui n'a pas de sens à décrire dans l'article:
V517 L'utilisation du
modèle 'if (A) {...} else if (A) {...}' a été détectée. Il y a une probabilité de présence d'erreur logique. Vérifiez les lignes: 151, 190. PhysicsClientUDP.cpp 151
Au-delà des limites: dépasser les limites du tableau
Exemple 6:L'une des choses désagréables à rechercher des erreurs est de sortir du tableau. Cette erreur se produit souvent en raison d'une indexation complexe dans la boucle.
Ici, dans la condition de boucle, la variable
dofIndex est limitée à 128 par le haut et
dof à 4 inclus. Mais
m_desiredState ne contient également que 128 éléments. Par conséquent, l'index
[dofIndex + dof] peut conduire à la
sortie du tableau.
V557 Le dépassement de matrice est possible. La valeur de l'indice «dofIndex + dof» pourrait atteindre 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]; .... } } .... }
Exemple 7:Une erreur similaire, mais maintenant la sommation y conduit non pas lors de l'indexation d'un tableau, mais dans une condition. Si le nom de fichier est aussi long que possible, le terminal zéro sera écrit en dehors du tableau (
erreur Off-by-one ). Naturellement, la variable
len sera égale à
MAX_FILENAME_LENGTH uniquement dans des cas exceptionnels, mais cela n'élimine pas l'erreur, mais rend simplement son occurrence rare.
V557 Le dépassement de matrice est possible. La valeur de l'index 'len' pourrait atteindre 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; }
Mesurer une fois - couper sept fois
Exemple 8:Dans les cas où vous devez utiliser le résultat d'une certaine fonction plusieurs fois ou utiliser à plusieurs reprises une variable à laquelle vous devez accéder via une série d'appels, vous devez utiliser des variables temporaires pour optimiser et mieux lire le code. L'analyseur a trouvé plus de 100 emplacements dans le code où une telle correction peut être effectuée.
V807 Performances
réduites . Envisagez de créer un pointeur pour éviter d'utiliser à plusieurs reprises l'expression '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(....); } }
Ici, la même chaîne d'appel est réutilisée, qui peut être remplacée par un seul pointeur.
Exemple 9:Performances
réduites du V810 . La fonction 'btCos (euler_out.pitch)' a été appelée plusieurs fois avec des arguments identiques. Le résultat devrait éventuellement être enregistré dans une variable temporaire, qui pourrait ensuite être utilisée lors de l'appel de la fonction 'btAtan2'. btMatrix3x3.h 576
Performances
réduites du V810 . La fonction 'btCos (euler_out2.pitch)' a été appelée plusieurs fois avec des arguments identiques. Le résultat devrait éventuellement être enregistré dans une variable temporaire, qui pourrait ensuite être utilisée lors de l'appel de la fonction '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)); } .... }
Dans ce cas, vous pouvez créer deux variables et stocker les valeurs renvoyées par la fonction
btCos pour
euler_out.pitch et
euler_out2.pitch , au lieu d'appeler la fonction quatre fois pour chaque argument.
Fuite
Exemple 10:De nombreuses erreurs du type suivant ont été trouvées dans le projet:
V773 La portée de visibilité du pointeur «importateur» a été fermée sans libérer la mémoire. Une fuite de mémoire est possible. SerializeSetup.cpp 94
void SerializeSetup::initPhysics() { .... btBulletWorldImporter* importer = new btBulletWorldImporter(m_dynamicsWorld); .... fclose(file); m_guiHelper->autogenerateGraphicsObjects(m_dynamicsWorld); }
Ici, aucune mémoire n'a été libérée du pointeur d'
importation . Cela peut provoquer une fuite de mémoire. Et pour le moteur physique, cela peut être une mauvaise tendance. Pour éviter les fuites, il suffit que la variable ne soit plus nécessaire pour ajouter un
importateur de suppression . Mais bien sûr, il vaut mieux utiliser des pointeurs intelligents.
Voici vos lois
Exemple 11:L'erreur suivante apparaît dans le code car les règles C ++ ne coïncident pas toujours avec les règles mathématiques ou le «bon sens». Remarquez par vous-même où l'erreur se trouve dans un petit extrait de code?
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; .... } .... }
L'analyseur génère l'avertissement suivant:
V709 Comparaison suspecte trouvée: 'f0 == f1 == m_fractureBodies.size ()'. N'oubliez pas que «a == b == c» n'est pas égal à «a == b && b == c». btFractureDynamicsWorld.cpp 483
Il semblerait que la condition vérifie que
f0 est égal à
f1 et égal au nombre d'éléments dans
m_fractureBodies . Il semble que cette comparaison aurait dû vérifier si
f0 et
f1 sont à la fin du tableau
m_fractureBodies , car ils contiennent la position de l'objet trouvé par la méthode
findLinearSearch () . Cependant, en réalité, cette expression se transforme en une vérification pour voir si
f0 et
f1 sont égales, puis en une vérification si
m_fractureBodies.size () est égal au résultat de
f0 == f1 . Par conséquent, le troisième opérande est comparé à 0 ou 1 ici.
Belle erreur! Et, heureusement, assez rare. Jusqu'à présent, nous ne l'avons
rencontrée que dans deux projets ouverts et, fait intéressant, tous n'étaient que des moteurs de jeu.
Exemple 12:Lorsque vous travaillez avec des chaînes, il est souvent préférable d'utiliser les fonctionnalités fournies par la classe de
chaînes . Ainsi, pour les deux cas suivants, il est préférable de remplacer respectivement
strlen (MyStr.c_str ()) et
val = "" par
MyStr.length () et
val.clear () .
Performances
réduites du V806 . L'expression du type strlen (MyStr.c_str ()) peut être réécrite en 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 Performances
réduites . Pensez à remplacer l'expression 'val = ""' par 'val.clear ()'. b3CommandLineArgs.h 40
void addArgs(int argc, char **argv) { .... std::string val; .... val = ""; .... }
Il y a eu d'autres avertissements, mais je pense que vous pouvez arrêter. Comme vous pouvez le voir, l'analyse de code statique peut révéler un large éventail d'erreurs diverses.
La lecture des vérifications de projet ponctuelles est intéressante, mais ce n'est pas la bonne façon d'utiliser des analyseurs de code statiques. Et à ce sujet, nous parlerons ci-dessous.
Erreurs trouvées devant nous
Il était intéressant dans l'esprit du récent article «
Erreurs que l'analyse de code statique ne trouve pas parce qu'il n'est pas utilisé » d'essayer de trouver des bogues ou des défauts qui ont déjà été corrigés, mais que l'analyseur statique pourrait détecter.
Il n'y avait pas autant de demandes d'extraction dans le référentiel, et beaucoup d'entre elles sont liées à la logique interne du moteur. Mais il y avait aussi des erreurs que l'analyseur pouvait détecter.
Exemple 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) { .... } }
Le commentaire d'édition indique qu'il était nécessaire de vérifier le tableau pour le fait qu'il n'est pas vide, mais à la place une vérification de pointeur inutile a été effectuée, qui renvoyait toujours true. Ceci est indiqué par l'avertissement PVS-Studio sur le type de contrôle initial:
V600 Envisagez d'inspecter l'état. Le pointeur 'info.m_deviceExtensions' n'est toujours pas égal à NULL. b3OpenCLUtils.cpp 551
Exemple 14:Pouvez-vous découvrir immédiatement quel est le problème dans la fonction suivante?
inline void Matrix4x4::SetIdentity() { m12 = m13 = m14 = m21 = m23 = m24 = m13 = m23 = m41 = m42 = m43 = 0.0; m11 = m22 = m33 = m44 = 1.0; }
L'analyseur génère les avertissements suivants:
V570 La même valeur est affectée deux fois à la variable 'm23'. LinearR4.h 627
V570 La même valeur est affectée deux fois à la variable 'm13'. LinearR4.h 627
Les affectations répétées avec cette forme d'enregistrement sont difficiles à suivre à l'œil nu et, par conséquent, certains éléments de la matrice n'ont pas reçu la valeur initiale. Cette erreur a été corrigée dans la forme tabulaire de l'enregistrement d'affectation:
m12 = m13 = m14 = m21 = m23 = m24 = m31 = m32 = m34 = m41 = m42 = m43 = 0.0;
Exemple 15:L'erreur suivante dans l'une des conditions de la fonction
btSoftBody :: addAeroForceToNode () a conduit à un bogue explicite. Selon le commentaire de la demande de traction, des forces ont été appliquées aux objets du mauvais côté.
struct eAeroModel { enum _ { V_Point, V_TwoSided, .... END }; }; void btSoftBody::addAeroForceToNode(....) { .... if (....) { if (btSoftBody::eAeroModel::V_TwoSided) { .... } .... } .... }
PVS-Studio peut également trouver cette erreur et afficher l'avertissement suivant:
V768 La constante d'énumération 'V_TwoSided' est utilisée comme variable de type booléen. btSoftBody.cpp 542
Le contrôle corrigé est le suivant:
if (m_cfg.aeromodel == btSoftBody::eAeroModel::V_TwoSided) { .... }
Au lieu de l'équivalence de la propriété d'objet à l'un des énumérateurs, l'énumérateur
V_TwoSided lui-même a été vérifié.
Il est clair que je n'ai pas vu toutes les demandes de tirage, car je ne me suis pas fixé une telle tâche. Je voulais juste montrer que l'utilisation régulière d'un analyseur de code statique peut détecter des erreurs à un stade très précoce. C'est la bonne façon d'utiliser l'analyse de code statique. L'analyse statique doit être intégrée au processus DevOps et être le filtre principal des bogues. Tout cela est bien décrit dans l'article "
Intégrez l'analyse statique dans le processus, et ne cherchez pas de bugs avec ."
Conclusion
À en juger par certaines demandes de tirage, un projet est parfois vérifié à l'aide de divers outils d'analyse de code, mais les modifications sont effectuées non pas progressivement, mais en groupes et à de grands intervalles de temps. Dans certaines demandes, un commentaire indique que des modifications ont été apportées uniquement pour supprimer les avertissements. Cette approche de l'utilisation de l'analyse réduit considérablement son utilité, car c'est une vérification constante du projet qui vous permet de corriger les erreurs immédiatement et de ne pas attendre l'apparition de bogues évidents.
Si vous souhaitez être toujours au courant des actualités et événements de notre équipe, abonnez-vous à nos services sociaux. Réseaux:
Instagram ,
Twitter ,
Vkontakte ,
Telegram .

Si vous souhaitez partager cet article avec un public anglophone, veuillez utiliser le lien de traduction:
PVS-Studio a examiné le moteur de puces de Red Dead Redemption