twitter/@realitix
github.com/realitix
realitix.github.io
def fib(int n): cdef int a = 0 cdef int b = 1 while b < n: print(b) a, b = b, a + b
from ctypes import cdll, Structure, c_int, c_double
lib = cdll.LoadLibrary('./vector.so')
# ctypes Point structure
class Point(Structure):
_fields_ = [('x', c_int), ('y', c_int)]
# Initialize Point[2] argument
points_array = (Point * 2)((1, 2), (3, 4))
# Get vector_size from library
vector_size_fn = lib.vector_size
vector_size_fn.restype = c_double
# Call vector_size with ctypes
size = vector_size_fn(points_array)
print('out = {}'.format(size))
from cffi import FFI
ffi = FFI()
ffi.cdef("int printf(const char *format, ...);")
C = ffi.dlopen(None)
arg = ffi.new("char[]", b"amazing")
C.printf("cffi is %s\n", arg)
$ python demo1.py
cffi is amazing
from cffi import FFI
ffibuilder = FFI()
ffibuilder.set_source("_demo2",
r"""#include <sys/types.h>
#include <pwd.h>
""")
ffibuilder.cdef("""
struct passwd {
char *pw_name;
char *pw_passwd;
int pw_uid;
...;
};
struct passwd *getpwuid(int uid);
struct passwd *getpwnam(const char *name);
""")
if __name__ == "__main__":
ffibuilder.compile(verbose=True)
$ python demo2_build.py
generating ./_demo2.c
running build_ext
building '_demo2' extension
x86_64-linux-gnu-gcc ... -c _demo2.c -o ./_demo2.o
x86_64-linux-gnu-gcc ... ./_demo2.o -o ./_demo2.cpython-35m-x86_64-linux-gnu.so
$ ls
demo2_build.py _demo2.c _demo2.o
_demo2.cpython-35m-x86_64-linux-gnu.so
$ cat demo2_run.py
from _demo2 import ffi, lib
# root name
p = lib.getpwuid(0)
print(ffi.string(p.pw_name))
# realitix uid
p = lib.getpwnam(b"realitix")
print(p.pw_uid)
$ python demo2_run.py
b'root'
1000
├── converters.c -> 423
├── custom_functions.c -> 103
├── custom_structs.c -> 59
├── extension_functions.c -> 32
├── functions.c -> 8
├── header.c -> 11
├── init.c -> 68
├── init_unions
│ ├── vkclearcolorvalue.c -> 69
│ └── vkclearvalue.c -> 36
├── macros.c -> 111
├── main.c -> 122
├── objects.c -> 99
└── jfilter.py -> 542
Total: 1683
- What you see is the folder structure of the C extension.
- For the C extension, there are too much things involved. To keep code maitainable, I had to split it up.
- Line of code is a good indicator here but what is more important is the complexity of the code.
- Like you can see, it's pretty huge!
- Moreover, with C code, you have to manage memory yourself and this is error prone.
vulkan.template.py -> 340
- Now with cffi
- Despite of it looks, what you see on screen means a lot !
- With cffi, I didn't need to think about architecture, it was obvious, all in one file.
- One more point for cffi!
PyModule_AddIntConstant(module, {{name}}, {{value}});
{{name}} = {{value}}
Constant is the easier part and both case are simple:
In C we just use the CPython API to add constant to our module BUT we have to take care about variable type.
With Cffi, it's just Python and its dynamically typped variables !
- Only with constants, you don't go far away...
- Next step is to create bindings for structs.
- BUT there is no struct concept in Python, there are classes which is similar.
- With CPython, you have to handle the new, del, init, get for each member.
- You have to use malloc to allocate memory for your object
- In del you must free your object
- In init, you have to parse each parameter with CPython utils function.
- And in Get, you have to convert C struct to Python object
- I can assure you it takes me lot of time to figure out how to properly do it! Code is so long that I can't show you an exemple on screen
def _new(ctype, **kwargs):
_type = ffi.typeof(ctype)
ptrs = {}
for k, v in kwargs.items():
# convert tuple pair to dict
ktype = dict(_type.fields)[k].type
if ktype.kind == 'pointer':
ptrs[k] = _cast_ptr(v, ktype)
init = dict(kwargs, **{k: v for k, (v, _) in ptrs.items()})
return ffi.new(_type.cname + '*', init)[0]
- With cffi now, it's forty lines, cffi handles for us object allocation and deletion.
- I just used plain Python to handle all "struct" initialization in a generic way.
- I repeat to be sure that you understand, this generic new function works for all structs.
- With CPython API, the step I showed you must be done for each struct, and there are hundred of structs
- When you realize this benefit, you just fall in love with cffi.
What I showed you until here is the ABI mode, let's try the fast and robust API mode now.
We are going to wrap a C library with the API / out-of-line mode
This mode is robust and fast but needs compilation
- The library we are going to wrap is shaderc.
- Shaderc is a free software done by Google guys and gives you the ability to compile Glsl language to spirv binary.
- GLSL and SPIRV formats are used to create programs for your graphic card.
├── _cffi_build
│ ├── pyshaderc_build.py
│ └── shaderc.h
├── pyshaderc
│ └── __init__.py
└── setup.py
- I usually create a folder dedicated to cffi named _cffi_build and I recommand you to do so to keep a clean separation of concerns.
- shaderc.h contains shaderc function signatures and structs, we just copy/paste the header provided by google and cffi handles it.
- pyshaderc_build is used to generate the Python extension
- Next we have the pyshader folder containing our wrapper and finally we'll take a look at the setup file with a special cffi feature.
ffi = FFI()
with open('shaderc.h') as f:
ffi.cdef(f.read())
Like you see, we first read content of the shaderc header and pass it to the cdef function
That's all for the definition !
Three lines and cffi knows how to handle this library ! Beautiful !
ffi = FFI()
with open('shaderc.h') as f:
source = f.read()
ffi.set_source('_pyshaderc', source, libraries=['shaderc_combined'])
if __name__ == '__main__':
ffi.compile()
- With theses few lines, we tell cffi to compile the source with statically linked library 'shaderc_combined' and to name the extension _pyshaderc.
- shaderc_combined is a static library that we have to compile before running this cffi part.
- I don't show you how because it's not directly related to cffi. You can look on github if you are interested in.
- Like you can see, there is no C code invovled, we just tell cffi to open a Python portal to the shaderc library
- Here again, it looks like magic
- Currently, our extension is ready but we remain dependent of cffi.
- For example, if you want to pass a pointer to a function, you have to use cffi.
- When using your wrapper, your users shouldn't need to know cffi, it's just a dependancy.
- So we are going to write a small Python code especially for that.
- This code is located in pyshader/__init__.py and is loaded automatically when you import your module.
from pyshaderc._pyshaderc import ffi, lib
- First we load our extension _pyshaderc which contains ffi and lib modules.
- lib contains all functions and struct related to our library.
- We need ffi too to interface with our lib.
def compile_into_spirv(raw, stage, suppress_warnings=False):
# initialize compiler
compiler = lib.shaderc_compiler_initialize()
# compile
result = lib.shaderc_compile_into_spv(compiler, raw, len(raw), stage, b"main")
- Let's examine the "compile_into_spirv" function.
- Remember, the shaderc library allows to compile glsl to spirV
- All functions defined in our extension can be accessed through "lib".
- If you take a look at the compile line, we can pass pure Python types like int or string and cffi converts it for us.
- Clean, intuitive...
- Note that with Python3, string must be bytes.
length = lib.shaderc_result_get_length(result)
output_pointer = lib.shaderc_result_get_bytes(result)
tmp = bytearray(length)
ffi.memmove(tmp, output_pointer, length)
spirv = bytes(tmp)
return spirv
- Shaderc returns a pointer to us, we have to tell cffi to convert it to a bytes Python object.
- We first get back the pointer and the length but we can't directly copy the data to a bytes in Python because bytes type is immutable, so we have to copy data into a bytearray, and pass it to our bytes object.
- Take a look at the ffi.memmove function, it's similar to the memcpy C function but directly in Python.
I'm doing memory manipulation directly in Python !
setup(
...
cffi_modules=["_cffi_build/pyshaderc_build.py:ffi"]
)
- Now! We can enjoy the result! Let's play a simple game using Vulkan wrapper and Pyshaderc wrapper!
- This simple game is written fully with Python and make use of the new shiny Vulkan API.
- I hope that like me you enjoyed this talk. If one day you need to use a C library or to improve performance of your code, think about cffi.
- Thank you for your attention, have a nice day.