Zig + Python Easy & Optimized
How to create Zig Binding for Python to create compiled and optimized libraries
ZigLang is a low level programming language that is very interesting also for his C and C++ integration that allow you to not need to rewrite enything.
Python is not well known as language with good speed of code so you can speed up it in many ways for example using Cpython, Cython or Numba. The bindings with a low level language is another way.
In this post we will see step by step how to setup a python library written with Zig, fully example in this repo.
Prepare a Zig+Python docker environment
You just need an environment with python3 and Zig installed, following a guide to setup it with Docker from alpine.
We need to create a Dockerfile:
FROM python:3.10-alpine
# Install ZIG
# RUN apk add libgdiplus --repository=http://dl-cdn.alpinelinux.org/alpine/edge/testing/
RUN echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/testing" >> /etc/apk/repositories
RUN apk update && apk add zig@testing
# Install Python dev
RUN apk add python3-dev
# Workdir
WORKDIR /code
And a docker-compose.yml
version: "3.7"
services:
zig:
build: .
volumes:
- .:/code
Now we can build and run our environment:
docker compose build
docker compose run zig ash
Create the library
To create the library we need to create the folder simple_module and inside we put the following files.
We can start from the zig code that use the python bindings simple.zig:
const py = @cImport({
@cDefine("PY_SSIZE_T_CLEAN", {});
@cInclude("Python.h");
});
const std = @import("std");
const print = std.debug.print;
const PyObject = py.PyObject;
const PyMethodDef = py.PyMethodDef;
const PyModuleDef = py.PyModuleDef;
const PyModuleDef_Base = py.PyModuleDef_Base;
const Py_BuildValue = py.Py_BuildValue;
const PyModule_Create = py.PyModule_Create;
const METH_NOARGS = py.METH_NOARGS;
const PyArg_ParseTuple = py.PyArg_ParseTuple;
const PyLong_FromLong = py.PyLong_FromLong;
fn sum(self: [*c]PyObject, args: [*c]PyObject) callconv(.C) [*]PyObject {
_ = self;
var a: c_long = undefined;
var b: c_long = undefined;
if (!(py._PyArg_ParseTuple_SizeT(args, "ll", &a, &b) != 0)) return Py_BuildValue("");
return py.PyLong_FromLong((a + b));
}
fn mul(self: [*c]PyObject, args: [*c]PyObject) callconv(.C) [*]PyObject {
_ = self;
var a: c_long = undefined;
var b: c_long = undefined;
if (PyArg_ParseTuple(args, "ll", &a, &b) == 0) return Py_BuildValue("");
return PyLong_FromLong((a * b));
}
fn hello(self: [*c]PyObject, args: [*c]PyObject) callconv(.C) [*]PyObject {
_ = self;
_ = args;
print("welcom to ziglang\n", .{});
return Py_BuildValue("");
}
fn printSt(self: [*c]PyObject, args: [*c]PyObject) callconv(.C) [*]PyObject {
_ = self;
var input: [*:0]u8 = undefined;
if (PyArg_ParseTuple(args, "s", &input) == 0) return Py_BuildValue("");
print("you entered: {s}\n", .{input});
return Py_BuildValue("");
}
fn returnArrayWithInput(self: [*c]PyObject, args: [*c]PyObject) callconv(.C) [*]PyObject {
_ = self;
var N: u32 = undefined;
if (!(py._PyArg_ParseTuple_SizeT(args, "l", &N) != 0)) return Py_BuildValue("");
var list: [*c]PyObject = py.PyList_New(N);
var i: u32 = 0;
while (i < N) : (i += 1) {
const python_int: [*c]PyObject = Py_BuildValue("i", i);
_ = py.PyList_SetItem(list, i, python_int);
}
return list;
}
var Methods = [_]PyMethodDef{
PyMethodDef{
.ml_name = "sum",
.ml_meth = sum,
.ml_flags = @as(c_int, 1),
.ml_doc = null,
},
PyMethodDef{
.ml_name = "mul",
.ml_meth = mul,
.ml_flags = @as(c_int, 1),
.ml_doc = null,
},
PyMethodDef{
.ml_name = "hello",
.ml_meth = hello,
.ml_flags = METH_NOARGS,
.ml_doc = null,
},
PyMethodDef{
.ml_name = "printSt",
.ml_meth = printSt,
.ml_flags = @as(c_int, 1),
.ml_doc = null,
},
PyMethodDef{
.ml_name = "returnArrayWithInput",
.ml_meth = returnArrayWithInput,
.ml_flags = @as(c_int, 1),
.ml_doc = null,
},
PyMethodDef{
.ml_name = null,
.ml_meth = null,
.ml_flags = 0,
.ml_doc = null,
},
};
var module = PyModuleDef{
.m_base = PyModuleDef_Base{
.ob_base = PyObject{
.ob_refcnt = 1,
.ob_type = null,
},
.m_init = null,
.m_index = 0,
.m_copy = null,
},
.m_name = "simple",
.m_doc = null,
.m_size = -1,
.m_methods = &Methods,
.m_slots = null,
.m_traverse = null,
.m_clear = null,
.m_free = null,
};
pub export fn PyInit_simple() [*]PyObject {
return PyModule_Create(&module);
}
We need to create the class ZigBuilder for the setup into builder.py
import os
from setuptools.command.build_ext import build_ext
class ZigBuilder(build_ext):
def build_extension(self, ext):
assert len(ext.sources) == 1
if not os.path.exists(self.build_lib):
os.makedirs(self.build_lib)
mode = "Debug" if self.debug else "ReleaseFast"
self.spawn(
[
"zig",
"build-lib",
"-O",
mode,
"-lc",
f"-femit-bin={self.get_ext_fullpath(ext.name)}",
"-fallow-shlib-undefined",
"-dynamic",
*[f"-I{d}" for d in self.include_dirs],
ext.sources[0],
]
)
Now it is possible to create the setup.py
from setuptools import setup, Extension
from builder import ZigBuilder
simple = Extension("simple", sources=["simple.zig"])
setup(
name="simple",
version="0.0.1",
description="a experiment create Python module in Zig",
ext_modules=[simple],
cmdclass={"build_ext": ZigBuilder},
)
Testing the lib
The easyest way to test it is to made a python script that use it in each is method so I write also test.py:
import subprocess
failed = subprocess.call(["pip", "install", "-e", "."])
assert not failed
import simple
a = input("enter a: ")
b = input("enter b: ")
print("sum: ",simple.sum(int(a),int(b)))
print("multiple: ",simple.mul(int(a),int(b)))
print("type sum: ", type(simple.sum(int(a),int(b))))
simple.hello()
simple.printSt(input("enter something: "))
print(simple.returnArrayWithInput(100))
So into our environment we can run it with:
$ python test.py
I suggest in a real case to use a real testcase written in python.
Conclusion
Rust have his position that solve many problems and I thing that it is the sobstitute of C++. But to recover Rust also after some weeks that you do not use it is hard. In my opinion Zig can become a good sobstitute for C and have life near Rust in the future. That language is also more simply and friendly so I think that can be more used also from pythonist.