Deixando o interpretador Python maluco por diversão e lucro, ou Como se livrar de um colega de trabalho desenvolvedor Python

Você está lá, em apenas mais um dia comum de trabalho (ou diversão), debugando um sistema complexo e tentando entender porque diabos algo que deveria acontecer não acontece. Você consegue reduzir o seu problema a uma linha de código, que não se comporta como você espera:

>>> print 42 == 0
True

O resultado foi verdadeiro, ou seja, 42 realmente é igual a 0 no momento em que esta comparação roda! Mas como assim?! Em Python não é possível reabrir a classe dos números (int), reescrever os métodos de comparação para fazer isso dar certo.
Lembre-se que esta comparação estranha está no meio de um sistema complexo. Assim:

algo_que_deixa_interpretador_maluco()
print 42 == 0

Explicação: A função algo_que_deixa_interpretador_maluco está modificando o objeto “número inteiro 42” (mesmo os números são objetos). Mas os números são objetos imutáveis, como assim você modificou o valor de 42? Um número inteiro em Python (na verdade, CPython) é representado por uma struct do C, que entre outras coisas, contém um long do C com o mesmo valor  – o atributo ob_ival.

Simplificando uma longa história, quando temos uma comparação de números, ela é repassada para o nível do C, e os atributos ob_ival da struct C são comparados com o operador == do C. Basta então mudarmos o ob_ival do objeto “número 42” que podemos influenciar na comparação.

E para mudar o valor de ob_ival? Como é possível acessar e modificar o estado interno e a memória do interpretador? Com o módulo ctypes, da biblioteca padrão, podemos acessar do Python os tipos do C. Basta então conseguirmos achar na memória a struct do número 42. Mas isso é fácil, a função builtin id retorna exatamente o lugar na memória onde o objeto está. Com isso, temos tudo que precisamos.

import ctypes

if hasattr(ctypes.pythonapi, 'Py_InitModule4'):
    Py_ssize_t = ctypes.c_int
elif hasattr(ctypes.pythonapi, 'Py_InitModule4_64'):
    Py_ssize_t = ctypes.c_int64
else:
    raise TypeError("Cannot determine type of Py_ssize_t")

class PyObject(ctypes.Structure):
    pass # incomplete type

class PyObject(ctypes.Structure):
    _fields_ = [('ob_refcnt', Py_ssize_t),
                ('ob_type', ctypes.POINTER(PyObject))]

class PyIntObject(PyObject):
    _fields_ = [('ob_ival', ctypes.c_long)]

forty_two_object = PyIntObject.from_address(id(42))
forty_two_object.ob_ival = 0

print 42 == 0

Neste código, estamos primeiro conseguindo o endereço do objeto 42 (linha 20), depois criando a struct do ctypes (ponte entre o Python e a struct C), depois alteramos o ob_ival do 42 para 0, na memória do interpretador (linha 21).

A partir daí, quando o Python fizer a comparação entre o número 42 e 0 (linha 23), ela será verdadeira, já que seus ob_ival são iguais.

Extensões em C ou mesmo código Python com ctypes mal testados podem causar consequências imprevisíveis e bugs dificílimos de reproduzir, mesmo onde parece impossível. Mas também tem usos interessantes: Pense em um dos inúmeros jogos que suportam Python como Civilization IV, Battlefield 2 e Piratas do Caribe Online. É possível fazer um script usando o mesmo princípio que modifica, por exemplo, as variáveis de controle de vidas do jogador, posição etc.

Publicado em C, hack, Python. Tags: , , . 11 Comments »

xor swap

Motivado pelo Log4Dev, quero contar sobre um truque legal: o xor swap (também conhecido por triple-xor trick).

O xor swap é um truque para trocar o conteúdo de 2 variáveis sem usar uma terceira. Em C, o jeito normal de trocar dois ints (x e y) é:

temp = x;
x = y;
y = temp;

Com o xor swap, o código fica:

x = x ^ y;
y = x ^ y;
x = x ^ y;

E aí pronto, suas variáveis estão invertidas. Pode acreditar (ou ler uma explicação bem mais detalhada). Legal né?

E qual a aplicação disso? Bem, um dia os microcontroladores tiveram quantidades bem escassas de memória, e jogar um int fora para variável temporária assim estava fora de cogitação. Hoje muitos microcontroladores tem uma RAM razoável e a perda de legibilidade e manutenibilidade acarretadas não vale mais a pena para estes controladores. Para os que vivem no cruel mundo dos 128 bytes de RAM, ainda vale a pena.

Publicado em C. Tags: . Leave a Comment »