Skip to main content

A setuptools extension, for building cpython extensions in Zig or C with the Zig compiler

Project description

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

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

setuptools_zig-0.5.3.tar.gz (8.2 kB view details)

Uploaded Source

File details

Details for the file setuptools_zig-0.5.3.tar.gz.

File metadata

  • Download URL: setuptools_zig-0.5.3.tar.gz
  • Upload date:
  • Size: 8.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.10.5

File hashes

Hashes for setuptools_zig-0.5.3.tar.gz
Algorithm Hash digest
SHA256 95a4ca3946d33370f22e176194713ad0fffb786399a0703d10de840f58c03eb5
MD5 38f0b5ee09f44ad54d368bcd66200c8c
BLAKE2b-256 0790f2df7414d11832a3fe1d6cb0da1d8646eabafcc67a986c9d0aadc1c0baed

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page