C Bindings

Pointers.py provides type safe bindings for most of the C standard library. We can use one by just importing it:

from pointers import printf

printf("hello world\n")

Collisions

Some names are either already used by python or used by pointers.py, so they are prefixed with c_.

For example, raise and malloc are both used, so you can import their bindings by importing c_raise and c_malloc:

from pointers import c_raise, c_malloc

# ...

Pointers

Several things in the C standard library require pointers. To create your own, you can use to_c_ptr:

from pointers import to_c_ptr

ptr = to_c_ptr(1)  # creates a pointer to the c integer 1

Then, we can just pass it to the binding:

from pointers import to_c_ptr, time

ptr = to_c_ptr(1)
time(ptr)
print(time)  # unix timestamp

Strings

to_c_ptr automatically converts the passed type to a C type, but that can be misleading with strings.

When you pass a str, pointers.py has to convert it to a wchar_t*, not char*.

If you want a char*, you need to pass a bytes object:

from pointers import to_c_ptr

wcharp = to_c_ptr("test")  # wchar_t*
charp = to_c_ptr(b"test")  # char*

This is not the same for the bindings though. Pointers.py is able to convert a str to char* just fine:

from pointers import puts

puts("a")  # no need for a bytes object here

Note: Any pointer object that derives from BaseCPointer may be passed to a binding. Otherwise, you have to manually convert it.

Void Pointers

Some types cannot be convert to a Python type or they can point to anything. For this, pointers.py uses the VoidPointer class:

from pointers import c_malloc

ptr = c_malloc(0)  # ptr gets assigned to VoidPointer

FILE* is an example of a type that can't be converted:

from pointers import fopen, fprintf, fclose

file = fopen("/dev/null", "w") # assigns to the c FILE* type
fprintf(file, "hello world")
fclose(file)

You can pass void pointers the same way:

from pointers import c_malloc, printf

ptr = c_malloc(0)
printf("%p\n", ptr)

If you try and derefernce a void pointer, it just returns its memory address:

from pointers import c_malloc

ptr = c_malloc(0)
print(*ptr)

Casting

VoidPointer objects can be casted to a typed pointer with the cast function, like so:

from pointers import c_malloc, printf, cast, strcpy, c_free

ptr = c_malloc(3)
strcpy(ptr, "hi")
printf("%s\n", cast(ptr, bytes))  # bytes refers to char*, str refers to wchar_t*
c_free(ptr)

You can even dereference a casted VoidPointer to get its actual value:

ptr = c_malloc(3)
strcpy(ptr, "hi")
print(*cast(ptr, bytes))  # b'hi'
c_free(ptr)

Structs

Some bindings, such as div return a struct. For this, pointers.py has its own Struct class:

from pointers import div

a = div(10, 1)  # type is DivT, which inherits from Struct
print(a.quot)  # prints out 10

Functions

There are a few bindings which require a function. All you have to do is write a function, and then pass it to the binding:

from pointers import c_raise, signal, exit

def sighandler(signum: int):
    print(f"handling signal {signum}")
    exit(0)

signal(2, sighandler)
c_raise(2)  # send signal 2 to the program

Alternatively, you can create a function pointer using to_func_ptr:

from pointers import to_func_ptr, signal, exit, c_raise

def test(signum: int) -> None:
    print('hello world')
    exit(0)

signal(2, to_func_ptr(test))
c_raise(2)

Note: to_func_ptr requires the function to be type hinted in order to correctly generate the signature of the C function, so the following will not work:

def test(signum):
    ...

to_func_ptr(test)

Null Pointers

If you would like to pass a NULL pointer to a binding, you can use pointers.NULL or pass None:

from pointers import time, NULL

# these two do the same thing
print(time(NULL))
print(time(None))

Custom Bindings

You can create your own binding to a C function with ctypes and pointers.py.

The recommended way to do it is via the binds function:

from pointers import binds
import ctypes

dll = ctypes.CDLL("libc.so.6")  # c standard library for linux

# specifying the argtypes and restype isnt always required, but its recommended that you add it
dll.strlen.argtypes = (ctypes.c_char_p,)
dll.strlen.restype = ctypes.c_int

@binds(dll.strlen)
def strlen(text: str):
    ...

You can also use binding, but that isn't type safe:

from pointers import binding
import ctypes

# ...

strlen = binding(dll.strlen)  # no type safety when calling strlen!

Structs

We need to set the restype when returning a struct, so first you need to define a ctypes.Structure object:

import ctypes

dll = ctypes.CDLL("libc.so.6")

class div_t(ctypes.Structure):
    _fields_ = [
        ("quot", ctypes.c_int),
        ("rem", ctypes.c_int),
    ]

dll.div.restype = div_t

Then, we need to create a pointers.py Struct that corresponds:

from pointers import Struct

class DivT(Struct):
    quot: int
    rem: int

Then, we can finally define our binding:

from pointers import binds, Struct
import ctypes

dll = ctypes.CDLL("libc.so.6")

class div_t(ctypes.Structure):
    _fields_ = [
        ("quot", ctypes.c_int),
        ("rem", ctypes.c_int),
    ]

class DivT(Struct):
    quot: int
    rem: int

dll.div.restype = div_t

@binds(dll.div, struct=DivT) # this tells pointers.py that this struct will be returned
def div(numer: int, denom: int) -> DivT:
    ...

Why to use these bindings?

The pointers.py bindings are nicer to use opposed to something like ctypes:

Comparison between ctypes and pointers.py

# ctypes

import ctypes

dll = ctypes.CDLL("libc.so.6") # this isn't cross platform, only works on linux
dll.strlen.argtypes = (ctypes.c_char_p,)
dll.strlen.restypes = ctypes.c_int

print(dll.strlen(b"hello")) # not type safe and requires bytes object
# pointers.py

from pointers import strlen # this is cross platform

print(strlen("hello")) # type safe and doesnt force you to use bytes

Why not to use these bindings?

Versatility and speed. The pointers.py bindings can make it harder to use your own functions with, as it forces you to use its pointer API.

On top of that, the bindings are built on top of ctypes, which means that it cannot be faster. They also go through many type conversions in order to provide a nice API for the end user, which can slow things down significantly.