Search code examples
pythoncctypes

Python unsigned integers operations like C


import ctypes

def main():
    
    key = 0xDEADBEEF
    
    print(f"({type(key)})  {key = }")
    key = key * 5 + 2
    print(f"({type(key)})  {key = }")
    key = key * 5 + 2
    print(f"({type(key)})  {key = }")
    key = key * 5 + 2
    print(f"({type(key)})  {key = }")
    
    key = ctypes.c_uint(0xDEADBEEF)
    
    print(f"({type(key)})  {key.value = }")
    key = ctypes.c_uint(key.value * 5 + 2)
    print(f"({type(key)})  {key.value = }")
    key = ctypes.c_uint(key.value * 5 + 2)
    print(f"({type(key)})  {key.value = }")
    key = ctypes.c_uint(key.value * 5 + 2)
    print(f"({type(key)})  {key.value = }")


if __name__ == '__main__':
    main()

If you run this it outputs:

(<class 'int'>)  key = 3735928559
(<class 'int'>)  key = 18679642797
(<class 'int'>)  key = 93398213987
(<class 'int'>)  key = 466991069937
(<class 'ctypes.c_ulong'>)  key.value = 3735928559
(<class 'ctypes.c_ulong'>)  key.value = 1499773613
(<class 'ctypes.c_ulong'>)  key.value = 3203900771
(<class 'ctypes.c_ulong'>)  key.value = 3134601969

Why does it keep growing on python?

Is there any way to replicate what C does without using ctypes or is it just how python works?

Edit:

Okay, thanks to @n.1.8e9-where's-my-sharem I've understood that in C uint get's capped at 32 bits and to get this same effect in python we can use number modulo 2^32



def main():
    key_a = 0xDEADBEEF
    key_b = uint32(0xDEADBEEF)
    printBinary(key_a)
    printBinary(key_b)
    print('--')
    key_a = key_a * 5 + 2
    key_b = uint32(key_b * 5 + 2)
    printBinary(key_a)
    printBinary(key_b)
    print('--')
    key_a = key_a * 5 + 2
    key_b = uint32(key_b * 5 + 2)
    printBinary(key_a)
    printBinary(key_b)
    print('--')
    key_a = key_a * 5 + 2
    key_b = uint32(key_b * 5 + 2)
    printBinary(key_a)
    printBinary(key_b)
    print('--')
    key_a = key_a * 5 + 2
    key_b = uint32(key_b * 5 + 2)
    printBinary(key_a)
    printBinary(key_b)
    

def printBinary(argument) -> None:
    binary_repr = f"{argument:<15} in binary {argument:>43b}"
    print(binary_repr)

def uint32( n, num_bits=32):
    return n % (2 ** num_bits)

if __name__ == '__main__':
    main()

So if we run the previous code we get this:


3735928559      in binary            11011110101011011011111011101111
3735928559      in binary            11011110101011011011111011101111
--
18679642797     in binary         10001011001011001001011101010101101
1499773613      in binary             1011001011001001011101010101101
--
93398213987     in binary       1010110111110111101111010010101100011
3203900771      in binary            10111110111101111010010101100011
--
466991069937    in binary     110110010111010110101100011101011110001
3134601969      in binary            10111010110101100011101011110001
--
2334955349687   in binary  100001111110100110001011110010011010110111
2788107959      in binary            10100110001011110010011010110111

Where we can see that with the function uint32 we can keep the first 32 bits of the key.


Solution

  • Python integers can be of any magnitude (they are bignums). In many other programming languages, integers are of fixed size and overflow/wrap around modulo 2number_of_bits.

    If you want to replicate the behaviour of these languages in Python, the easiest way is to add an explicit reduction modulo 2number_of_bits. There are several equivalent ways to express this reduction:

    n = n % (2**nbits)
    n = n % (1<<nbits)
    n = n & ((1<<nbits) - 1)