Menu

Tree [8fe70a] default tip /
 History

Read Only access


File Date Author Commit
 _doc 2025-11-13 Anthon van der Neut Anthon van der Neut [8fe70a] updated to Python 3.14 and Zig 0.15
 .hgtags 2020-12-04 Anthon van der Neut Anthon van der Neut [85a4f0] Added tag 0.1.3 for changeset 5686501bd8f6
 LICENSE 2020-11-27 Anthon van der Neut Anthon van der Neut [bd93a8] initial
 README.md 2025-11-13 Anthon van der Neut Anthon van der Neut [8fe70a] updated to Python 3.14 and Zig 0.15
 setup.py 2025-11-13 Anthon van der Neut Anthon van der Neut [8fe70a] updated to Python 3.14 and Zig 0.15
 setuptools_zig.py 2025-11-13 Anthon van der Neut Anthon van der Neut [8fe70a] updated to Python 3.14 and Zig 0.15

Read Me

Setuptools-Zig

image
image
image

A setuptools extension for building cpython extensions written in Zig
and/or C, with the Zig compiler. Even though
setuptools as package has been fazed out with newer pythons, it can be
used with its pip wheel replacment as well.

This extension expects to find the zig command in your PATH. If it
is not there, or if you need to select a specific version, you can set
the environment variable PY_ZIG to the full path of the executable.
E.g.:

PY_ZIG=/usr/local/bin/zig

Alternatively you can add 'ziglang' to setup_requires. When setuptools-zig finds
that ziglang is installed as well, it will prepend the directory to the search
path for binaries (PY_ZIG overrides that as well).

This version of the module has been tested with Zig 0.11 through 0.15, it should
work with other versions (as long as you adapt your Zig code). It has
been tested with Python 3.8 - 3.12, on Ubuntu 22.04
and macOS 24.6.0 (the zig code only example), both platforms with binary zig installs.
Due to changes in Python, there is some PY_MINOR_VERSION dependent code in
the setup part of the example.

The package setuptools-zig is available on PyPI, but doesn\'t need
to be installed
, as it is a setup requirement. Once your setup.py
has the apropriate entries, building an sdist or bdist_wheel will
automatically downloaded the package (cached in the .eggs directory).

Setup.py

Your setup.py file should look like:

from setuptools import Extension
from setuptools import setup

setup(
    name=NAME,
    version='MAJ.MIN.PATCH',
    python_requires='>=3.8.18',
    build_zig=True,
    ext_modules=[Extension(NAME, [XX1, XX2])],
    setup_requires=['setuptools-zig'],  # or e.g. ['setuptools-zig', 'ziglang==0.12.1'],
)

with NAME replaced by the string that is your package name. MAJ, MIN,
and PATCH your package\'s version, and XX1, XX2 being your source files
(you can have just one, or more one source file).

With that adapted to your project:

python -m pip wheel --use-pep517 -w dist --disable-pip-version-check --no-deps .

will result in a .whl file in your dist directory. That wheel file
can be installed in a virtualenv, and the functions defined in the
package imported and used. By default the compile and/or link commands
executed will be shown, their output only when errors occur. Verbosity
can be increased specifying [-v]{.title-ref} or [-vv]{.title-ref}, after
[bdist_wheel]{.title-ref}.

The python you are using (if you have multiple, select one by specify its full path in
the above command), should have headers installed. This is usually the case when you install
from source (what I do on Linux), or use pyenv (what I use on macOS), but
not necessarily if you use a system installed python on Linux. Since the
selected python knows where it headers and libraries live, you don't
have to specify any of that on the zig commandline
.

Using Zig as a C compiler

Create your setup.py:

from setuptools import Extension
from setuptools import setup

setup(
    name='c_sum',
    version='1.0.0',
    python_requires='>=3.8.18',
    build_zig=True,
    ext_modules=[Extension('c_sum', ['sum.c', ])],
    setup_requires=['setuptools-zig>=0.3.0'],

and sum.c:

/* based on https://docs.python.org/3.9/extending/extending.html */

#define PY_SSIZE_T_CLEAN
#include <Python.h>

PyObject* sum(PyObject* self, PyObject* args) {
    long a, b;

    if (!PyArg_ParseTuple(args, "ll", &a, &b))
              return NULL;
    return PyLong_FromLong(a+b);
}


static struct PyMethodDef methods[] = {
    {"sum", (PyCFunction)sum, METH_VARARGS},
    {NULL, NULL}
};

static struct PyModuleDef zigmodule = {
    PyModuleDef_HEAD_INIT,
    "sum",
    NULL,
    -1,
    methods
};

PyMODINIT_FUNC PyInit_c_sum(void) {
    return PyModule_Create(&zigmodule);
}

install the resulting wheel using pip and test with:

python -c "from c_sum import sum; print(sum(20, 22))"

Using Zig with .zig and .c

The Zig compiler can easily mix and match (see section macOS), here we
use the C code to provide the interface and do the "heavy lifting" of
calculating the sum in Zig.

setup.py:

from setuptools import Extension
from setuptools import setup

setup(
    name='c_zig_sum',
    version='1.0.3',
    python_requires='>=3.8.18',
    build_zig=True,
    ext_modules=[Extension('c_zig_sum', ['c_int.c', 'sum.zig', ])],
    setup_requires=['setuptools-zig>=0.3.0'],
)

c_int.c:

/* based on https://docs.python.org/3.9/extending/extending.html */

#define PY_SSIZE_T_CLEAN
#include <Python.h>

PyObject* sum(PyObject* , PyObject*);

/*
PyObject* sum(PyObject* self, PyObject* args) {
    long a, b;

    if (!PyArg_ParseTuple(args, "ll", &a, &b))
        return NULL;
    return PyLong_FromLong(a+b);
}
*/


static struct PyMethodDef methods[] = {
    {"sum", (PyCFunction)sum, METH_VARARGS},
    {NULL, NULL}
};

static struct PyModuleDef zigmodule = {
    PyModuleDef_HEAD_INIT,
    "c_zig_sum",
    NULL,
    -1,
    methods
};

PyMODINIT_FUNC PyInit_c_zig_sum(void) {
    return PyModule_Create(&zigmodule);
}

sum.zig:

const c = @cImport({
    @cDefine("PY_SSIZE_T_CLEAN", "1");
    @cInclude("Python.h");
});

pub export fn sum(self: [*]c.PyObject, args: [*]c.PyObject) [*c]c.PyObject {
    var a: c_long = undefined;
    var b: c_long = undefined;
    _ = self;
    if (!(c.PyArg_ParseTuple(args, "ll", &a, &b) != 0)) return null;
    return c.PyLong_FromLong((a + b));
}

Zig code only

The orignal converted code is rather ugly to read. There were no
differences in the program specific Zig code converted from C between
Python 3.7/3.8/3.9/3.10/3.11 (apart from the header), but 3.12/3.13 and 3.14
require some changes and the differences are solved with dependency on PY_MINOR_VERSION
as imported from Python.h (writing 3.13t/3.14t compatible code will require more
changes).

Only the part under the
comment line should need to be adapted for your project.

setup.py:

from setuptools import Extension
from setuptools import setup

setup(
    name='zig_sum',
    version='1.0.4',
    python_requires='>=3.8.18',
    build_zig=True,
    ext_modules=[Extension('zig_sum', ['sum.zig' ])],
    # setup_requires=['setuptools-zig>=0.3.0', 'ziglang<0.15'],
    setup_requires=['setuptools-zig>=0.3.0'],
)

sum.zig:

// translated using zig from the C version

const c = @cImport({
    @cDefine("PY_SSIZE_T_CLEAN", {});
    @cInclude("Python.h");
});

const PyObject = c.PyObject;

const PyModuleDef_Base = extern struct {
    ob_base: PyObject,
    m_init: ?*const fn () callconv(.c) [*c]PyObject = null,
    m_index: c.Py_ssize_t = 0,
    m_copy: [*c]PyObject = null,
};

const PyModuleDef_HEAD_INIT = if ((c.PY_MAJOR_VERSION > 2) and (c.PY_MINOR_VERSION > 13))
    PyModuleDef_Base {
        .ob_base = PyObject {
            .unnamed_0 = .{ .ob_refcnt_full = 1 },
            .ob_type = null,
        }
    }
else if ((c.PY_MAJOR_VERSION > 2) and (c.PY_MINOR_VERSION > 11))
    PyModuleDef_Base {
        .ob_base = PyObject {
            .unnamed_0 = .{ .ob_refcnt = 1 },
            .ob_type = null,
        }
    }
else
    PyModuleDef_Base {
        .ob_base = PyObject {
            .ob_refcnt = 1,
            .ob_type = null,
        }
    };

const PyMethodDef = extern struct {
    ml_name: [*c]const u8 = null,
    ml_meth: c.PyCFunction = null,
    ml_flags: c_int = 0,
    ml_doc: [*c]const u8 = null,
};

const PyModuleDef = extern struct {
    // m_base: c.PyModuleDef_Base,
    m_base: PyModuleDef_Base = PyModuleDef_HEAD_INIT,
    m_name: [*c]const u8,
    m_doc: [*c]const u8 = null,
    m_size: c.Py_ssize_t = -1,
    m_methods: [*]PyMethodDef,
    m_slots: [*c]c.struct_PyModuleDef_Slot = null,
    m_traverse: c.traverseproc = null,
    m_clear: c.inquiry = null,
    m_free: c.freefunc = null,
};

/////////////////////////////////////////////////

pub export fn sum(self: [*]PyObject, args: [*]PyObject) [*c]PyObject {
    var a: c_long = undefined;
    var b: c_long = undefined;
    _ = self;
    if (!(c.PyArg_ParseTuple(args, "ll", &a, &b) != 0)) return null;
    return c.PyLong_FromLong((a + b));
}

pub var methods = [_]PyMethodDef{
    PyMethodDef{
        .ml_name = "sum",
        .ml_meth = @ptrCast(&sum),
        .ml_flags = @as(c_int, 1),
        .ml_doc = null,
    },
    PyMethodDef{
        .ml_name = null,
        .ml_meth = null,
        .ml_flags = 0,
        .ml_doc = null,
    },
};

pub var zigmodule = PyModuleDef{
    .m_name = "zig_sum",
    .m_methods = &methods,
};

pub export fn PyInit_zig_sum() [*c]c.PyObject {
    // return c.PyModule_Create(@ptrCast([*c]c.struct_PyModuleDef, &zigmodule));
    return c.PyModule_Create(@as([*c]c.struct_PyModuleDef, @ptrCast(&zigmodule)));
}

macOS

Running zig build-lib on macOS will result in a .dylib file that
cannot be loaded by Python. Instead setuptools-zig will run
zig build-obj on the individual source files (as combining .c and
.zig files results in an error) and then combines the .o files using
clang -bundle generating a loadable [.so]{.title-ref} file.

cleanup

Running zig build-obj sum.zig in Zig 0.10.0 generates both sum.o and
sum.o.o. This extension tries to clean up those extra files.