setuptools-zig 0.5.3
A setuptools extension, for building cpython extensions in Zig or C with the Zig compiler
License: MIT
Requires Python: >=3



# Setuptools-Zig

[![image](https://sourceforge.net/p/setuptools-zig/code/ci/default/tree/_doc/_static/license.svg?format=raw)](https://opensource.org/licenses/MIT)
[![image](https://sourceforge.net/p/setuptools-zig/code/ci/default/tree/_doc/_static/pypi.svg?format=raw)](https://pypi.org/project/setuptools-zig/)
[![image](https://sourceforge.net/p/oitnb/code/ci/default/tree/_doc/_static/oitnb.svg?format=raw)](https://bitbucket.org/ruamel/oitnb/)

A setuptools extension for building cpython extensions written in Zig
and/or C, with the [Zig compiler](https://ziglang.org). Even though
setuptools as package has been phased out with newer pythons, it can be
used with its `pip wheel` replacment as well and provides flexibility
that can only be achieved by Totally Obfuscated Markup Language, by
re-implementing a(nother) domain language.


There are three ways for this package to find the `zig` command:

- include a build dependency on ziglang in your project (e.g. `ziglang<0.16`)
- set the environment var `PY_ZIG` to point to a zig executable `PY_ZIG=/opt/zig/current/zig`
- make sure the zig command is in your `PATH`

Setting `PY_ZIG` overrides adding `ziglang`.

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 to work with Python 3.9 - 3.12, on Ubuntu 22.04 (Intel, Arm), Alpine (Intel) 
and macOS 24.6.0 (the zig code only example),  all platforms with binary zig installs.
Due to changes in Python, there is some PY_MINOR_VERSION dependent code in
the `setup.py` part of the example.

The package `setuptools-zig` is available on PyPI, but **does not need
to be installed**, as it is only a setup requirement. Once your `setup.py`
has the apropriate entries, building wheel  will
automatically download 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.10.19',
    build_zig=True,
    ext_modules=[Extension(
        name=NAME,
        sources=[XX1, XX2],
        # extra_compile_args=['-O', 'Debug'],
    )],
    setup_requires=['setuptools-zig'],  # or e.g. ['setuptools-zig>-0.4', '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.1',
    python_requires='>=3.8.18',
    build_zig=True,
    ext_modules=[Extension('c_sum', ['sum.c', ])],
    setup_requires=['setuptools-zig>=0.3.0', 'ziglang<0.16'],
```

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, "sum two ints"},
    {NULL, NULL, 0, NULL}
};

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

PyMODINIT_FUNC PyInit_c_sum(void) {
    PyObject *m = PyModule_Create(&zigmodule);
    if (m == NULL) {
        return NULL;
    }
#ifdef Py_GIL_DISABLED
    PyUnstable_Module_SetGIL(m, Py_MOD_GIL_NOT_USED);
#endif
    return m;
}
```

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.4', 'ziglang<0.16'],
)
```

`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) {
    PyObject *m = PyModule_Create(&zigmodule);
    if (m == NULL) {
        return NULL;
    }
#ifdef Py_GIL_DISABLED
    PyUnstable_Module_SetGIL(m, Py_MOD_GIL_NOT_USED);
#endif
    return m;
}
```

`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 original 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, as do the free-threaded Python.
These differences were solved with dependency on PY_MINOR_VERSION
as imported from `Python.h`, as well `Py_GIL_DISABLED` being defined.

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', ziglang<0.16],
)
```

`sum.zig`:

```
// selected and merged from the various translated C version generated using:
// zig translate-c -I /opt/python/<VER>/include/python3.*/ sum.c

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 > 12) and @hasDecl(c, "Py_GIL_DISABLED"))
    if ((c.PY_MAJOR_VERSION > 2) and (c.PY_MINOR_VERSION > 13))
        PyModuleDef_Base {
            .ob_base = PyObject {
                .ob_tid = 0,
                .ob_flags = 4,
                .ob_mutex = c.PyMutex{
                    ._bits = 0,
                },
                .ob_gc_bits = 0,
                .ob_ref_local = @as(c_uint, 4294967295),
                .ob_ref_shared = 0,
                .ob_type = null,
            }
        }
    else
        PyModuleDef_Base {
            .ob_base = PyObject {
                .ob_tid = 0,
                .ob_mutex = c.PyMutex{
                    ._bits = 0,
                },
                .ob_gc_bits = 0,
                .ob_ref_local = @as(c_uint, 4294967295),
                .ob_ref_shared = 0,
                .ob_type = null,
            }
        }
else 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 {
    const m: [*c]PyObject = c.PyModule_Create(@as([*c]c.struct_PyModuleDef, @ptrCast(&zigmodule)));
    if (m == null)
        return null;
    if ((c.PY_MAJOR_VERSION > 2) and (c.PY_MINOR_VERSION > 12) and @hasDecl(c, "Py_GIL_DISABLED")) {
        _ = c.PyUnstable_Module_SetGIL(m, c.Py_MOD_GIL_NOT_USED);
    }
    return m;
}
```

### 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.