Zig + Python Easy & Optimized

How to create Zig Binding for Python to create compiled and optimized libraries

Nicola Landro
4 min readSep 1, 2023

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 and ZIg

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.

--

--

Nicola Landro

Linux user and Open Source fun. Deep learning PhD. , Full stack web developer, Mobile developer, cloud engineer and Musitian.