Skip to content

Instantly share code, notes, and snippets.

@Chubek
Last active January 27, 2024 15:15
Show Gist options
  • Save Chubek/27eec6435df6b30684dee201dc5edf4b to your computer and use it in GitHub Desktop.
Save Chubek/27eec6435df6b30684dee201dc5edf4b to your computer and use it in GitHub Desktop.
Zinteger.py: Dynamically-generated wrappers for ctypes integers

zinteger.py contains dynamically-generated wrappers for ctypes integers, with the ability to error-check in caes of overflow and underflow, in just less than 460 lines of code.

Usage

To use zintegre.py, place the file in your work directory. You can do it easily using this command:

wget -O <subfolder>/zinteger.py https://gist.githubusercontent.com/Chubek/27eec6435df6b30684dee201dc5edf4b/raw/83ab6def8aad495dd7ab2eb3a283656ef5d04959/zinteger.py

Then you can import one of the following 8 dynamically-generated wrappers. You may perform any of the unary, binary and in-place operators that the int objects have, with int objects, themselves, or a ctype of that the wrapped corresponds to.

For example, ZUint8 can be added/subtracted etc with ZUint8, int, and c_uint8/c_ubyte.

Example:

from zinteger import ZUint8, ZUint16, ZUint32, ZUint64
from zinteger import ZUint8, ZUint16, ZUint32, ZUint64
#or
from zinteger import *

# unsigned 8bit integer
u8 = ZUint8(12)
int1 = 12

print(u8 + int1) #24
print(u8 * 2) #24

# this will warn you of overflow
u16 = ZUint16(0xffff1)

# this will warn you of underflow
i8 = ZInt8(-0x81)

# this will abort because of overflow
i32 = ZInt32(0xffffffff, abort=True)

#this will not warn
i64 = ZUInt64(-1)

assert(hash(ZUint(8)) == hash(8), "Error")

assert(f"{Uint8(255):b} + {Uint8(255):012b}" == f"{255:b} + {255:08b}", "Error")

Mapping

Tables below shows the mapping of types between Zinteger objects and ctypes objects. Remember the third column may differ in your system.

Zinteger cint ctype
ZUint8 c_uint8
ZUint16 c_uint16
ZUint32 c_uint32
ZUint64 c_uint64
ZInt8 c_int8
ZInt16 c_int16
ZInt32 c_int32
ZInt64 c_int64

The difference between cint and ctype is, ctype is the actual identifier for the type in C, whereas cint is the typedef in stdint.h within your system. It may vary from system to system. c_uint64 may either map to c_ulong or c_ulonglong, for example.

Uses

This may not seem useful at first, but if you use Python as a utility for low-level programming like I do, it is necessary to set bounds for the integers. It has happened to me often that I mistakenly shifted a number beyond the bit limit that was required of that integer. My main use for Python these days is using it as a very apt, able utility belt for systems stuff. For example, I'm currently coding a bioinfromatics ABI in Assembly, and I neded to encode some values. I mistakenly shifted left by 64 bits instead of 4, which I meant to.

Since Python integers are stored on memory and are objects rather than identifiers (more on that a bit), it is considered bignum, and bignum can go as far as the memory allows. You can theoretically store an integer in one page of the memory.

Python's ctypes library provides an FFI with C and provides its numerical types to be used. They are not faster to use, exactly, because you still have to convert them to Python's int object before operations. But they do the job for type safety.

You may ask why I did not just use a library off PyPi? A simple code like this does not require a third-party package. I hold the license to this code and I can use it wherever I want. I neither condemn, nor condone heavy use of third-party packages in your codebase. Most especially, for code that you are writing for fun. it's better if you write everything yourself. But that's just my hubmle opinion and I do not wish to force it on anyone. As I say in Disclaimer, this code is heavily untested and I myself would use it with caution. Third-party libraries that have been thoroughly tested are a necessacity in high-stakes projects. But for educational and recreational projects absolutely minimal use of them is best.

Metaprogramming in Python

Metaprograms are those programs that emit, generate, compile, and interpret code. Compilers, assemblers, interpreters etc are Level-1 metaprogrammers, whereas stuff such as macros, preprocessors and metaclasses are level-2 metaprogammers. In GNU Assembler you can define macros in the .bss section of the memory. They will be used during assembling and will be phased out in form of uninitialized data in the aforementioned section. In C we have preprocessors (#define), which are literal string replacements --- completely unhygentic, the preprocessor just emits the macro. In Rust we have both hygenic (macro_rules!)) and unhygenic macros (idents imported from packages compiled with --crate-typ proc-macro with #[proc-macro] macro themselves). I am not at all familiar with Lisp sadly but it indeed has a ginormous macro system. Ruby is another example of a language with macro. I have coded in Nim a few times and Nim has two ways of metaprogramming, Templates, and Macros. Templates in Nim are simple unhygenic code emitters but Macros can actively modify the Abstract Syntax Tree. IMO Nim metaprogramming is the perfect approach. Metacode where you get to modify the AST with surgical precision is desired. C's preprocessors are useful but they are the high-level counterpart to .macro directive in GNU Assembler. In my project TransGatacca which I linked below, I make good use of GNU Assembler's macro system to achieve having the same code work in two Assembly languages. Now it's time to mention that there's another differentiation in types of metacoding. GAS macros are heteregenous metacodes, they are in a different language than the target language itself, whereas other macros we mentioned are homogenous, they use the same language. C++ has templates too, like Nim, but I'm a C guy and don't work much with Cooked Pizza Party. Another type of metaprogramming, according to Wikipedia, is multi-staged programming but again, my stack is C, Rust, x&a64 Assembly, Python and Go. Don't ask me anything about anything I don't know!

But these are all compile-time metaprograms. In Python we don't have conventional compilation. Everything is defined at runtime so macros don't make sense. What we have in Python are metaclasses, which are a form of runtime metaprogramming. We also have exec and eval, which can be used as some form of macro.

Everything in Python is an object, Classes are objects that can create instances of objects. User-defined functions are callable objects. Types are type objects. For example, int is a type object. In C, for example, int is a keyword. But in Python it is an object. Every object can be modified in runtime, so can be types, user-defined classes, and user-defined functions which are themselves objects.

You can theoretically, create a class and programmatically modify it. One may also do that with a function. Python's metaclasses can be used to create object factories, and these objects can be any of the ones mentioned in the page I link bewlow. Python does not emit code, it modifies the AST at runtime. Every object has a namespace dictionary, a name, a qualified name, and especial methods which are enclosed within a pair of double-underlines (line __name__ or __init__). Callable functions, too, are objects, and they can have their tree modified programatically. A function has a namespace __dict__ too, and also, a __defaults__ property which holds default keyword arguments. Python also has modules that have the same stuff, plus some other things such as an __all__ property which holds what should be imported from that module when from module import * is triggered. A module has a __name__. Every file is a module for itself and when you run a file with a shebang or by passing it to Python the name will be __main__. A module also has a property for the file it resides in. All these may or may not be writable. For example, a classes __dict__ namespaces is not writable, but a function's is.

We can create descriptors with metaclasses. Descriptors are objects whose access to them has been overriden through __get__, __set__, and __del__. When you define these special functions for a class, the behavior with which they are accessed changes according to your will. If class A has its __get__ method invariably return literal 2 everytime one of its instances is accessed as a property, it will return 2, for example, when we set B.a = A() and we do b = B(); print(b.a). This will print 2 in stdout.

Decorators can be used to modify the syntax three of the object they are decorated with. You can see the example of my __exec decorator, which takes a callable object, sets globals()[first kwarg of func] = func() and then returns the function verbatim.

The __generate* functions are factories for dunder methods, such as __add__ and the such, and __zinteger decorator is a decorator that sets the main fields of the main classes. It makes use of the type() function. type() can be used to dynamically generate classes if we use it with parameters type(objname, instmethod, {namespace})

I recommend giving the code a read. It's marely 460 lines. I tried to write it clean and concise. I recon if I wanted to repeat the same process for every class 8 times, the code basse would be in a several thousand lines. As a rule of thumb, the smaller the ratio of lines sof code against functionality is, the better.

The official documentation for Python's object data model, which metaclasses are a part of and allows for runtime metaprogramming, is the best resource to learn about them.

Disclaimer

The code in zinteger.py is not fully tested. I provide this code online with no warranty whatsoever. I recommend everyone to write their own utilities instead of using random snippets of code they find online. Please only use this code in high-stakes projects after full scrutiny.

Other Projects

Please take a look at my Github profile, my recent project PoxHash which is a block hash algorthim in C, Rust, Go, Python, Nim and JS, my utility Bash script DynoFiler and my current WIP project TransGatacca which is a bioinformatics ABI in x86-64 and Aarch64 Assemby languages with planned APIs in C, Rust and bindings in Python. You can contact me at Chubak#7400 in Discord (Discord link in profile). I'm also available for systems, network and scientific utility projects (rates in profile).

I hope you find this useful, educational and informative.

Thanks, Chubak

# Copyright (c) 2023 Chubak Bidpaa
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
from copy import copy
from ctypes import *
from math import inf
from operator import *
SELF_INSTANCE = 0
TYPE_INSTANCE = 1
INTG_INSTANCE = 2
OTHR_INSTANCE = -1
__TYPES_MAP = {
"ZUint8": c_uint8,
"ZUint16": c_uint16,
"ZUint32": c_uint32,
"ZUint64": c_uint64,
"ZInt8": c_int8,
"ZInt16": c_int16,
"ZInt32": c_int32,
"ZInt64": c_int64,
}
__TYPES_MAX = {
"ZUint8": 0xff,
"ZUint16": 0xffff,
"ZUint32": 0xffffffff,
"ZUint64": 0xffffffffffffffff,
"ZInt8": 0x7f,
"ZInt16": 0x7fff,
"ZInt32": 0x7fffffff,
"ZInt64": 0x7fffffffffffffff,
}
__TYPES_MIN = {
"ZUint8": 0,
"ZUint16": 0,
"ZUint32": 0,
"ZUint64": 0,
"ZInt8": -0x80,
"ZInt16": -0x80000,
"ZInt32": -0x80000000,
"ZInt64": -0x8000000000000000,
}
__DUNDERS_UNARY = {
"__hash__": hash,
"__hex__": hex,
"__bin__": bin,
"__oct__": oct,
"__not__": not_,
"__neg__": neg,
"__pos__": pos,
}
__DUNDERS_BINARY = {
"__add__": add,
"__sub__": sub,
"__mul__": mul,
"__trudiv__": truediv,
"__floordiv__": floordiv,
"__mod__": mod,
"__pow__": pow,
"__and__": and_,
"__or__": or_,
"__xor__": xor,
"__lshift__": lshift,
"__rshift__": rshift,
"__lt__": lt,
"__le__": le,
"__eq__": eq,
"__ne__": ne,
"__ge__": ge,
"__gt__": gt,
}
__DUNDERS_INPLACE = {
"__iadd__": iadd,
"__isub__": isub,
"__imul__": imul,
"__itrudiv__": itruediv,
"__ifloordiv__": ifloordiv,
"__imod__": imod,
"__ipow__": ipow,
"__iand__": iand,
"__ior__": ior,
"__ixor__": ixor,
"__ilshift__": ilshift,
"__irshift__": irshift,
}
__FMT = {
'b': bin,
'o': oct,
'x': hex,
}
def __exec(func):
globals()[func.__defaults__[0]] = func()
return func
@__exec
def __generate_unary_dunders(_="UNARY_DUNDERS"):
unary_dunderfuncs = {}
for unary_dunder, unary_op in __DUNDERS_UNARY.items():
def dunder_factory(unary_dunder, unary_op):
def dunderfn(self):
op = dunderfn.__dict__['op']
return op(self.val)
dunderfn.__dict__['op'] = unary_op
dunderfn.__name__ = unary_dunder
dunderfn.__qualname__ = unary_dunder
return dunderfn
unary_dunderfuncs[unary_dunder] = dunder_factory(
unary_dunder, unary_op)
return unary_dunderfuncs
@__exec
def __generate_binary_dunders(_="BINARY_DUNDERS"):
binary_dunderfuncs = {}
for binary_dunder, binary_op in __DUNDERS_BINARY.items():
def dunder_factory(binary_dunder, binary_op):
def dunderfn(self, other):
op = dunderfn.__dict__['op']
instance_check = self.__instancecheck__(other)
if instance_check == SELF_INSTANCE:
res = op(self.__maxand__(0), other.__maxand__(0))
elif instance_check == TYPE_INSTANCE:
res = op(self.__maxand__(0), self.__maxand__(other.value, on_self=False))
elif instance_check == INTG_INSTANCE:
res = op(self.__maxand__(0), self.__maxand__(other, on_self=False))
else:
raise TypeError(
f"Ilegal operation {op.__name__} between {self.clsname} and {other.__class__.__name__}")
return self.clstype(res) if type(res) != bool else res
dunderfn.__dict__['op'] = binary_op
dunderfn.__name__ = binary_dunder
dunderfn.__qualname__ = binary_dunder
return dunderfn
binary_dunderfuncs[binary_dunder] = dunder_factory(
binary_dunder, binary_op)
return binary_dunderfuncs
@__exec
def __generate_inplace_dunders(_="INPLACE_DUNDERS"):
inplace_dunderfuncs = {}
for inplace_dunder, inplace_op in __DUNDERS_INPLACE.items():
def dunder_factory(inplace_dunder, inplace_op):
def dunderfn(self, other):
op = dunderfn.__dict__['op']
instance_check = self.__instancecheck__(other)
if instance_check == SELF_INSTANCE:
res = op(self.__maxand__(0), other.__maxand__(0))
elif instance_check == TYPE_INSTANCE:
res = op(self.__maxand__(0), self.__maxand__(other.value, on_self=False))
elif instance_check == INTG_INSTANCE:
res = op(self.__maxand__(0), self.__maxand__(other, on_self=False))
else:
raise TypeError(
f"Ilegal operation {op.__name__} between {self.clsname} and {other.__class__.__name__}")
self.__integer = self.__tyy(res)
return self
dunderfn.__dict__['op'] = inplace_op
dunderfn.__name__ = inplace_dunder
dunderfn.__qualname__ = inplace_dunder
return dunderfn
inplace_dunderfuncs[inplace_dunder] = dunder_factory(
inplace_dunder, inplace_op)
return inplace_dunderfuncs
def __byte_zinteger(self, msb_leftmost=True) -> bytearray:
if self.min < 0:
raise OverflowError("Negative numbers cannot be decomposed to bytes")
value = self.val
decomposed_bytes = bytearray([])
shrn = 0
while True:
value >>= shrn
if value == 0:
break
byte = value & 255
decomposed_bytes += bytearray([byte])
shrn += 8
if msb_leftmost:
decomposed_bytes.reverse()
return bytes(decomposed_bytes)
def __maxd_zinteger(self, val: int, on_self=True) -> int:
val = self.val if on_self else val
if val < 0:
return val
return val & self.__max
def __init_zinteger(self, integer: int, mask=None, warn=True, abort=False):
if type(integer) not in [self.cty, self.sty, int]:
raise TypeError(f"Unallowed type for {self.clsname}: {type(integer)}")
integer &= mask if integer >= 0 else integer
if warn or abort:
do_exit = 0
if integer > self.max:
print(f'\033[1;33mWarning:\033[0m Overflow for {self.clsname}')
do_exit = "overflow"
if integer < self.min:
print(f'\033[1;33mWarning:\033[0m Underflow for {self.clsname}')
do_exit = "underflow"
if do_exit:
print(f"Exiting due to {do_exit}...")
exit(1)
self.__integer = self.cty(integer.value) if type(integer) in [
self.cty, self.sty] else self.cty(integer)
def __inst_zinteger(self, other) -> bool:
cls_other = other.__class__
cls_self = self.clstype
cls_type = self.ctype
cls_int = int(0).__class__
if cls_self == cls_other:
return SELF_INSTANCE
elif cls_type == cls_other:
return TYPE_INSTANCE
elif cls_int == cls_other:
return INTG_INSTANCE
return OTHR_INSTANCE
def __geti_zinteger(self, cls, instance=None):
return self.__integer
def __seti_zinteger(self, instance, value):
self.__integer = value
def __repr_zinteger(self) -> str:
return str(self.val)
def __strn_zinteger(self) -> str:
return str(self.val)
def __frmt_zinteger(self, spec: str) -> str:
fmtfn = __frmt_zinteger.__dict__['frmt_zintegers']
if any([spec.endswith(c) for c in ['b', 'o', 'x']]):
fmt = spec[-1]
prefix = "0" + fmt
spec = spec.rstrip(fmt)
if spec.startswith('0'):
spec = spec.lstrip('0')
numpad = int(spec)
return ('0' * numpad) + fmtfn[fmt](self).lstrip(prefix)
elif spec == '':
return fmtfn[fmt](self).lstrip(prefix)
else:
return ""
return ""
def __gtvl_zinteger(self, name: str) -> int:
if name.startswith("val") or name.startswith("int"):
return self.__integer.value
elif name.startswith("cint") or name.startswith("cval"):
return self.__integer
elif name.startswith("cty"):
return self.__tyy
elif name.startswith("ctyname"):
return self.__tyy.__name__
elif name.startswith("clsn") or name == "cnm":
return self.__class__.__name__
elif name.startswith("clst") or name == "clt":
return self.__class__
elif name.startswith("min"):
return self.__min
elif name.startswith("max"):
return self.__max
elif name.startswith("sty"):
return type(self)
elif name.startswith("nb"):
return len(format(self.__max, "b"))
elif name.startswith("test"):
return self(0).__test__
else:
raise AttributeError
def __docs_zinteger(ident_tyy, ident_slf, ident_min, ident_max):
docs_tynm = ident_tyy.__name__
docs_sign = "an unsigned" if ident_slf.startswith("ZUint") else "a signed"
docs_bitw = ident_slf.lstrip("Z").lstrip("U").lstrip("I").lstrip("int")
docs_strt = f"{ident_slf} takes {docs_sign}, {docs_bitw}-bit value of range [{ident_min}, {ident_max})"
docs_ctyy = f"This class maps to ctype {docs_tynm} on your system. It may differ on other systems."
docs_dndr = "ZIntmplements: " + ", ".join(__DUNDERS_BINARY.keys()) + ""
docs_fmtt = "__format__ is implemented for [padding-enabled] b, o, x flags"
docs_args = "Parameters:"
docs_arg1 = ";integer: the given in-range integer"
docs_arg2 = ";mask: the pre-masking bitmask (meaningless for negative integers)"
docs_retr = "Returns:"
docs_rett = "An instance of {}"
docs_sepr = "-------"
return "\n".join([
docs_strt, docs_sepr,
docs_ctyy, docs_dndr,
docs_fmtt, docs_sepr,
docs_args, docs_arg1,
docs_arg2, docs_sepr,
docs_retr, docs_rett
])
def __zinteger(cls: type):
dunderfuncs = {
**globals()['UNARY_DUNDERS'],
**globals()['BINARY_DUNDERS'],
**globals()['INPLACE_DUNDERS'],
}
ident_slf = cls.__name__
ident_min = __TYPES_MIN[ident_slf]
ident_max = __TYPES_MAX[ident_slf]
ident_tyy = __TYPES_MAP[ident_slf]
this_init = copy(__init_zinteger)
this_frmt = copy(__frmt_zinteger)
this_inst = copy(__inst_zinteger)
this_repr = copy(__repr_zinteger)
this_strn = copy(__strn_zinteger)
this_gtvl = copy(__gtvl_zinteger)
this_geti = copy(__geti_zinteger)
this_seti = copy(__seti_zinteger)
this_maxd = copy(__maxd_zinteger)
this_byte = copy(__byte_zinteger)
this_docs = __docs_zinteger(ident_tyy, ident_slf, ident_min, ident_max)
this_init.__defaults__ = (ident_max, *this_init.__defaults__[1:])
this_frmt.__dict__['frmt_zintegers'] = __FMT
cls = type(
ident_slf,
(), {
"__tyy": ident_tyy,
"__min": ident_min,
"__max": ident_max,
"__idt": ident_slf,
"__get__": this_geti,
"__set__": this_seti,
"__str__": this_strn,
"__init__": this_init,
"__repr__": this_repr,
"__bytes__": this_byte,
"__maxand__": this_maxd,
"__format__": this_frmt,
"__getattr__": this_gtvl,
"__instancecheck__": this_inst,
**dunderfuncs
}
)
def set_method_name(method: callable, name: str):
method.__name__ = name
method.__qualname__ = name
list(
map(
lambda args: set_method_name(*args),
[
(cls.__str__, "__str__"),
(cls.__get__, "__get__"),
(cls.__set__, "__set__"),
(cls.__init__, "__init__"),
(cls.__repr__, "__repr__"),
(cls.__bytes__, "__bytes__"),
(cls.__format__, "__format__"),
(cls.__maxand__, "__maxand__"),
(cls.__instancecheck__, "__instancecheck__"),
]
)
)
cls.__doc__ = this_docs
return cls
@__zinteger
class ZUint8:
pass
@__zinteger
class ZUint16:
pass
@__zinteger
class ZUint32:
pass
@__zinteger
class ZUint64:
pass
@__zinteger
class ZInt8:
pass
@__zinteger
class ZInt16:
pass
@__zinteger
class ZInt32:
pass
@__zinteger
class ZInt64:
pass
__all__ = [
ZUint8,
ZUint16,
ZUint32,
ZUint64,
ZInt8,
ZInt16,
ZInt32,
ZInt64,
]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment