Hoje em dia, por exemplo, no desenvolvimento de jogos, não há mais a necessidade de implementar independentemente a física dos objetos do zero, pois há um grande número de bibliotecas para isso. O Bullet já foi usado ativamente em muitos jogos AAA, projetos de realidade virtual, várias simulações e aprendizado de máquina. Sim, e ainda é usado hoje, sendo, por exemplo, um dos mecanismos Red Dead Redemption e Red Dead Redemption 2. Portanto, por que não verificar o Bullet com o PVS-Studio para ver quais erros a análise estática pode detectar em um projeto de grande escala, relacionados à simulação de física.
Essa biblioteca é
distribuída gratuitamente , para que todos possam usá-la opcionalmente em seus projetos. Além do Red Dead Redemption, esse mecanismo de física também é usado na indústria cinematográfica para criar efeitos especiais. Por exemplo, ele esteve envolvido nas filmagens de Sherlock Holmes por Guy Ritchie para calcular colisões.
Se este é o seu primeiro encontro com um artigo em que o PVS-Studio verifica projetos, farei uma pequena digressão.
O PVS-Studio é um analisador de código estático que ajuda a encontrar erros, falhas e possíveis vulnerabilidades no código fonte dos programas Java em C, C ++, C #. A análise estática é um tipo de processo automatizado de revisão de código.
Para aquecer
Exemplo 1:Vamos começar com um erro engraçado:
V624 Provavelmente, há uma impressão
incorreta na constante '3.141592538'. Considere usar a 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); .... }
Um pequeno erro de digitação no número Pi (3,141592653 ...), faltando o número "6" na 7ª posição na parte fracionária.
Talvez o erro na décima-milionésima casa decimal não leve a consequências tangíveis, mas você ainda deve usar as constantes da biblioteca existentes sem erros de digitação. Para Pi, há uma constante
M_PI no cabeçalho
math.h.Copiar e colar
Exemplo 2:Às vezes, o analisador permite encontrar o erro indiretamente. Portanto, por exemplo, aqui três argumentos relacionados
halfExtentsX, halfExtentsY, halfExtentsZ são passados para a função, mas o último não é usado em nenhum lugar da função. Você pode perceber que, ao chamar o método
addVertex , a variável
halfExtentsY é usada duas vezes. Portanto, talvez este seja um erro de copiar e colar e aqui um argumento esquecido deve ser usado.
V751 O parâmetro 'halfExtentsZ' não é usado dentro do corpo da função. 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], ....); .... }
Exemplo 3:O analisador também encontrou o seguinte fragmento interessante: eu o trago primeiro em sua forma original.
Vê esta última linha?
É muito estranho que o programador tenha decidido escrever uma condição tão longa em uma linha. Mas o fato de um erro muito provavelmente ter surgido nele não é de todo surpreendente.
O analisador emitiu os seguintes avisos nesta linha.
V501 Existem sub-expressões idênticas 'rotmat.Column1 (). Norm () <1.0001' à esquerda e à direita do operador '&&'. LinearR4.cpp 351
V501 Existem sub-expressões idênticas '0.9999 <rotmat.Column1 (). Norm ()' à esquerda e à direita do operador '&&'. LinearR4.cpp 351
Se escrevermos tudo em uma forma visual “tabular”, ficará claro que as mesmas verificações se aplicam à
Coluna1 . As duas últimas comparações mostram que há
Coluna1 e
Coluna2 . Provavelmente, a terceira e quarta comparações deveriam ter verificado o valor da
Coluna2 .
Column1().Norm() < 1.0001 && 0.9999 < Column1().Norm() && Column1().Norm() < 1.0001 && 0.9999 < Column1().Norm() &&(Column1() ^ Column2()) < 0.001 && (Column1() ^ Column2()) > -0.001
Nesta forma, a mesma comparação se torna muito mais perceptível.
Exemplo 4:Erro de tipo semelhante:
V501 Existem
subexpressões idênticas 'cs.m_fJacCoeffInv [0] == 0' à esquerda e à direita do operador '&&'. 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; } .... }
Nesse caso, o mesmo elemento da matriz é verificado duas vezes. Muito provavelmente, a condição deveria ter a seguinte aparência:
cs.m_fJacCoeffInv [0] == 0 && cs.m_fJacCoeffInv [1] == 0 . Este é um exemplo clássico de erro de copiar e colar.
Exemplo 5:Outra falha foi descoberta:
V517 O uso do
padrão 'if (A) {...} else if (A) {...}' foi detectado. Há uma probabilidade de presença de erro lógico. Verifique as linhas: 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); } .... } .... }
A função
enet_host_service , cujo resultado é atribuído a
serviceResult , retorna um se for
bem -
sucedido e -1 se falhar. Provavelmente, o ramo
mais se deveria ter respondido ao valor negativo de
serviceResult , mas a condição de verificação foi duplicada. Provavelmente, esse também é um erro de copiar e colar.
Um aviso do analisador semelhante, que não faz sentido descrever no artigo:
V517 O uso do
padrão 'if (A) {...} else if (A) {...}' foi detectado. Há uma probabilidade de presença de erro lógico. Verifique as linhas: 151, 190. PhysicsClientUDP.cpp 151
Além dos limites: indo além dos limites da matriz
Exemplo 6:Uma das coisas desagradáveis para procurar erros é sair da matriz. Geralmente, esse erro ocorre devido à indexação complexa no loop.
Aqui, na condição de loop, a variável
dofIndex é limitada a 128 a partir de cima e
dof a 4, inclusive. Mas
m_desiredState também contém apenas 128 elementos. Como resultado, o índice
[dofIndex + dof] pode levar à
saída da matriz.
A saturação da matriz V557 é possível. O valor do índice 'dofIndex + dof' pode chegar a 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]; .... } } .... }
Exemplo 7:Um erro semelhante, mas agora a soma leva a ele não ao indexar uma matriz, mas em uma condição. Se o nome do arquivo for o maior possível, o zero do terminal será gravado fora da matriz (
Erro Off-by-one ). Naturalmente, a variável
len será igual a
MAX_FILENAME_LENGTH apenas em casos excepcionais, mas isso não elimina o erro, mas simplesmente torna sua ocorrência rara.
A saturação da matriz V557 é possível. O valor do índice 'len' pode chegar a 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; }
Meça uma vez - corte sete vezes
Exemplo 8:Nos casos em que você precisa usar o resultado de uma determinada função várias vezes ou usar repetidamente uma variável que precisa acessar por meio de uma série de chamadas, use variáveis temporárias para otimizar e ler melhor o código. O analisador encontrou mais de 100 lugares no código onde essa correção pode ser feita.
V807 desempenho reduzido. Considere criar um ponteiro para evitar o uso da expressão 'm_app-> m_renderer-> getActiveCamera ()' repetidamente. 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(....); } }
Aqui, a mesma cadeia de chamadas é reutilizada, que pode ser substituída por um único ponteiro.
Exemplo 9:V810 desempenho reduzido. A função 'btCos (euler_out.pitch)' foi chamada várias vezes com argumentos idênticos. O resultado deve ser salvo em uma variável temporária, que pode ser usada ao chamar a função 'btAtan2'. btMatrix3x3.h 576
V810 desempenho reduzido. A função 'btCos (euler_out2.pitch)' foi chamada várias vezes com argumentos idênticos. O resultado deve ser salvo em uma variável temporária, que pode ser usada ao chamar a função '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)); } .... }
Nesse caso, você pode criar duas variáveis e armazenar os valores retornados pela função
btCos para
euler_out.pitch e
euler_out2.pitch nelas , em vez de chamar a função quatro vezes para cada argumento.
Vazamento
Exemplo 10:Muitos erros do seguinte tipo foram encontrados no projeto:
V773 O escopo de visibilidade do ponteiro 'importador' foi encerrado sem liberar a memória. É possível um vazamento de memória. SerializeSetup.cpp 94
void SerializeSetup::initPhysics() { .... btBulletWorldImporter* importer = new btBulletWorldImporter(m_dynamicsWorld); .... fclose(file); m_guiHelper->autogenerateGraphicsObjects(m_dynamicsWorld); }
Nenhuma memória foi liberada do ponteiro do
importador aqui. Isso pode causar vazamento de memória. E para o mecanismo físico, isso pode ser uma tendência ruim. Para evitar vazamentos, basta que a variável não seja mais necessária para adicionar o
importador de exclusão . Mas é claro que é melhor usar ponteiros inteligentes.
Aqui estão suas leis
Exemplo 11:O erro a seguir aparece no código devido ao fato de que as regras C ++ nem sempre coincidem com as regras matemáticas ou com o "senso comum". Observe por si mesmo onde está o erro em um pequeno trecho de código?
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; .... } .... }
O analisador gera o seguinte aviso:
V709 Comparação suspeita encontrada: 'f0 == f1 == m_fractureBodies.size ()'. Lembre-se de que 'a == b == c' não é igual a 'a == b && b == c'. btFractureDynamicsWorld.cpp 483
Parece que a condição verifica se
f0 é igual a
f1 e igual ao número de elementos em
m_fractureBodies . Parece que essa comparação deveria ter verificado se
f0 e
f1 estão no final da matriz
m_fractureBodies , pois contêm a posição do objeto encontrado pelo método
findLinearSearch () . No entanto, na realidade, essa expressão se transforma em uma verificação para ver se
f0 e
f1 são iguais e, em seguida, para verificar se
m_fractureBodies.size () é igual ao resultado de
f0 == f1 . Como resultado, o terceiro operando é comparado com 0 ou 1 aqui.
Belo erro! E, felizmente, bastante raro. Até agora, nós a
encontramos apenas em dois projetos abertos e, curiosamente, todos eles eram apenas motores de jogos.
Exemplo 12:Ao trabalhar com strings, geralmente é melhor usar os recursos fornecidos pela classe de
strings . Portanto, para os dois casos a seguir, é melhor substituir
strlen (MyStr.c_str ()) e
val = "" por
MyStr.length () e
val.clear (), respectivamente.
V806 desempenho reduzido. A expressão do tipo strlen (MyStr.c_str ()) pode ser reescrita como 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 Desempenho Diminuído. Considere substituir a expressão 'val = ""' por 'val.clear ()'. b3CommandLineArgs.h 40
void addArgs(int argc, char **argv) { .... std::string val; .... val = ""; .... }
Houve outros avisos, mas acho que você pode parar. Como você pode ver, a análise de código estático pode revelar uma ampla gama de vários erros.
É interessante ler sobre verificações únicas de projetos, mas essa não é a maneira correta de usar analisadores de código estático. E sobre isso, falaremos abaixo.
Erros encontrados antes de nós
Foi interessante no espírito do artigo recente “
Erros que a análise de código estático não encontra porque não é usada ” para tentar encontrar bugs ou deficiências que já foram corrigidos, mas que o analisador estático poderia detectar.
Não havia tantas solicitações de recebimento no repositório e muitas delas estão relacionadas à lógica interna do mecanismo. Mas também houve erros que o analisador pôde detectar.
Exemplo 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) { .... } }
O comentário de edição diz que era necessário verificar a matriz pelo fato de ela não estar vazia, mas, em vez disso, foi executada uma verificação sem ponteiro, que sempre retornava verdadeira. Isso é indicado pelo aviso PVS-Studio no tipo inicial de verificação:
V600 Considere inspecionar a condição. O ponteiro 'info.m_deviceExtensions' nem sempre é igual a NULL. b3OpenCLUtils.cpp 551
Exemplo 14:Você pode descobrir imediatamente qual é o problema na próxima função?
inline void Matrix4x4::SetIdentity() { m12 = m13 = m14 = m21 = m23 = m24 = m13 = m23 = m41 = m42 = m43 = 0.0; m11 = m22 = m33 = m44 = 1.0; }
O analisador gera os seguintes avisos:
V570 O mesmo valor é atribuído duas vezes à variável 'm23'. LinearR4.h 627
V570 O mesmo valor é atribuído duas vezes à variável 'm13'. LinearR4.h 627
Tarefas repetidas com essa forma de gravação são difíceis de rastrear a olho nu e, como resultado, alguns dos elementos da matriz não receberam o valor inicial. Este erro foi corrigido no formato tabular do registro de atribuição:
m12 = m13 = m14 = m21 = m23 = m24 = m31 = m32 = m34 = m41 = m42 = m43 = 0.0;
Exemplo 15:O erro a seguir em uma das condições da função
btSoftBody :: addAeroForceToNode () levou a um erro explícito. De acordo com o comentário na solicitação de recebimento, forças foram aplicadas a objetos do lado errado.
struct eAeroModel { enum _ { V_Point, V_TwoSided, .... END }; }; void btSoftBody::addAeroForceToNode(....) { .... if (....) { if (btSoftBody::eAeroModel::V_TwoSided) { .... } .... } .... }
O PVS-Studio também pode encontrar esse erro e exibir o seguinte aviso:
V768 A constante de enumeração 'V_TwoSided' é usada como uma variável de um tipo booleano. btSoftBody.cpp 542
A verificação corrigida é a seguinte:
if (m_cfg.aeromodel == btSoftBody::eAeroModel::V_TwoSided) { .... }
Em vez da equivalência da propriedade do objeto a um dos enumeradores, o
próprio enumerador
V_TwoSided foi verificado.
É claro que eu não vi todas as solicitações de recebimento, pois não defini essa tarefa para mim. Eu só queria mostrar que o uso regular de um analisador de código estático pode detectar erros em um estágio muito inicial. Essa é a maneira correta de usar a análise de código estático. A análise estática deve ser incorporada ao processo do DevOps e ser o principal filtro de bugs. Tudo isso está bem descrito no artigo "
Incorpore a análise estática ao processo e não procure por erros ".
Conclusão
A julgar por algumas solicitações pull, às vezes um projeto é verificado por meio de várias ferramentas de análise de código, no entanto, as edições são feitas não gradualmente, mas em grupos e em grandes intervalos de tempo. Em algumas solicitações, um comentário indica que foram feitas alterações apenas para suprimir avisos. Essa abordagem ao uso da análise reduz significativamente sua utilidade, pois é uma verificação constante do projeto que permite corrigir erros imediatamente, e não espere até que ocorram erros óbvios.
Se você deseja estar sempre informado sobre as novidades e os eventos de nossa equipe, assine nossos serviços sociais. Redes:
Instagram ,
Twitter ,
Vkontakte ,
Telegram .

Se você deseja compartilhar este artigo com um público que fala inglês, use o link da tradução:
PVS-Studio Observou o Bullet Engine do Red Dead Redemption