“Pegadinha”: Cuidado ao encadear múltiplas chamadas de métodos

Num dos testes antigos do PySide, havia uma inocente linha de código com QFile().metaObject().methodCount(), que na nova versão estava causando uma falha de segmentação dentro da Qt. O que estava acontecendo era que o QMetaObject retornado pelo metaObject() estava sendo apagado pelo QFile() criado, invalidando a área de memória que methodCount() tentava acessar. Agora por que diabos ele estava sendo deletado, já que eu chamava o método direto nele? A resposta está no modo como o CPython é implementado, sendo uma máquina virtual de pilha.

Usando o módulo dis nessa linha, temos o seguinte resultado:

0 LOAD_GLOBAL              0 (QFile)
3 CALL_FUNCTION            0
6 LOAD_ATTR                1 (metaObject)
9 CALL_FUNCTION            0
12 LOAD_ATTR                2 (foo)
15 CALL_FUNCTION            0
18 POP_TOP
19 LOAD_CONST               0 (None)
22 RETURN_VALUE

Dissecando instrução por instrução e seus efeitos na pilha, vamos assumir que esteja inicialmente vazia. Apenas as 4 primeiras instrução são necessárias:

  • LOAD_GLOBAL (QFile) – Topo da pilha é a classe QFile
  • CALL_FUNCTION – Remove QFile do topo e coloca o resultado da chamada, no caso, a nova instância de QFile, com refcount 1
  • LOAD_ATTR(metaObject) – Remove a instância de QFile do topo (decrementa o refcount) e coloca o resultado de getattr(instância, ‘metaObject’) no topo. Nesse caso, o resultado é um “bound method” A chamada a getattr incrementa a referência da instância de QFile, logo ela não morre.
  • CALL_FUNCTION – Remove o metodo metaObject do topo e coloca o resultado, no caso a instância de QMetaObject retornada. Ao remover o método, a referência à instância de QFile é removida, chegando a 0. Então o destrutor do binding chama o destrutor de C++, que por sua vez deleta o objeto C++ do QMetaObject, invalidando o ponteiro usado pelo binding.

Ou seja, devido essas instruções, não se pode garantir que um objeto criado anonimamente numa chamada de metodo e usado imediatamente irá estar “vivo” em chamadas subsequentes.

Vale notar que esse problema aparece em outras implementações de Python baseadas no CPython, como o Stackless e o Unladen Swallow. Implementações que usam outros tipos de máquina virtual como o Jython, IronPython e Pypy não sofrem desse problema.