A setuptools extension, for building cpython extensions in Zig or C with the Zig compiler
Project description
Setuptools-Zig
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_ZIGto point to a zig executablePY_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
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
95a4ca3946d33370f22e176194713ad0fffb786399a0703d10de840f58c03eb5
|
|
| MD5 |
38f0b5ee09f44ad54d368bcd66200c8c
|
|
| BLAKE2b-256 |
0790f2df7414d11832a3fe1d6cb0da1d8646eabafcc67a986c9d0aadc1c0baed
|