Skip to content

Commit fef0070

Browse files
committed
Demo: Add a C/Cython API
1 parent 6708168 commit fef0070

23 files changed

Lines changed: 351 additions & 10 deletions

.github/workflows/buildwheel.yml

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,52 @@ jobs:
329329
- run: spin run -- pytest --doctest-glob='*.rst' doc/source
330330
- run: spin docs
331331

332+
test_cython_c_api:
333+
name: 'Test C API with Cython consumers'
334+
runs-on: ubuntu-24.04
335+
steps:
336+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
337+
with:
338+
persist-credentials: false
339+
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
340+
with:
341+
python-version: '3.13'
342+
- run: sudo apt-get update
343+
- run: sudo apt-get install libflint-dev
344+
- run: pip install "cython==3.3.0" meson-python ninja
345+
- run: pip wheel --no-build-isolation --no-deps --wheel-dir=/tmp/python-flint-wheel .
346+
- run: pip install /tmp/python-flint-wheel/python_flint-*.whl
347+
- name: Build examples with Cython 3.1 through 3.3
348+
run: |
349+
for version in 3.1.0 3.2.9 3.3.0; do
350+
pip install "cython==$version"
351+
pip install --no-build-isolation --no-deps --force-reinstall ./examples/cython/capi
352+
pip install --no-build-isolation --no-deps --force-reinstall ./examples/cython/flint
353+
(cd /tmp && python "$GITHUB_WORKSPACE/examples/cython/capi/test_capi.py")
354+
(cd /tmp && python "$GITHUB_WORKSPACE/examples/cython/flint/test_flint.py")
355+
done
356+
- name: Capsule-only example has no FLINT dependency
357+
run: |
358+
python - <<'PY'
359+
import _flint_capi_example
360+
import subprocess
361+
dependencies = subprocess.check_output(
362+
["ldd", _flint_capi_example.__file__], text=True)
363+
print(dependencies)
364+
assert "libflint" not in dependencies
365+
PY
366+
- name: Installed API files are present
367+
run: |
368+
cd /tmp
369+
python - <<'PY'
370+
import flint
371+
from pathlib import Path
372+
package = Path(flint.__file__).parent
373+
assert (package / "__init__.pxd").is_file()
374+
assert (package / "__init__.cython-31.pxd").is_file()
375+
assert (Path(flint.get_include()) / "python_flint" / "fmpz.h").is_file()
376+
PY
377+
332378
# Test build with minimum Cython and meson-python versions.
333379
test_old_build_requires:
334380
name: 'Test old Cython/meson-python'
@@ -349,7 +395,7 @@ jobs:
349395
# We don't need to specify ninja as a requirement in pyproject.toml
350396
# because without --no-build-isolation meson-python handles it
351397
# automatically in get_requirements_for_build_wheel().
352-
- run: 'pip install "cython==3.0.11" "meson-python==0.18" "ninja<1.11"'
398+
- run: 'pip install "cython==3.1.0" "meson-python==0.18" "ninja<1.11"'
353399
- run: pip install --no-build-isolation .
354400
- run: python -m flint.test --verbose
355401

README.md

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -150,9 +150,8 @@ Compatible versions:
150150
| `0.7.0` | 16th Mar 2025 | `3.11-3.13` | `3.0-3.2` | `3.0.11-3.1.0a1` |
151151
| `0.6.0` | 1st Feb 2024 | `3.9-3.12` | `3.0` only | `3.0` only |
152152

153-
The requirement for Cython 3.1 is only for CPython's free-threaded build.
154-
Otherwise Cython 3.0 is fine. Cython 3.2 is required for a stable ABI build of
155-
python-flint.
153+
Cython 3.1 or newer is required. Cython 3.2 is required for a stable ABI build
154+
of python-flint.
156155

157156
As of python-flint 0.7.0, CPython 3.13 [PEP
158157
703](https://peps.python.org/pep-0703/) free-threaded (no-GIL) builds of

doc/source/build.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,7 @@ active Python environment before running ``pip install``.
229229
To build without build isolation with ``python-flint >= 0.7.0`` the
230230
dependencies should first be installed in the active Python environment::
231231

232-
pip install Cython==3.0 meson meson-python ninja
232+
pip install Cython==3.1 meson meson-python ninja
233233
pip install --no-build-isolation .
234234

235235
The ``meson`` build system will detect the versions of ``FLINT`` and Cython

doc/source/c_api.rst

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
Experimental C API
2+
==================
3+
4+
python-flint provides an experimental version 1 C API for ``fmpz``. The API
5+
may change between releases and is not yet a long-term ABI stability promise.
6+
7+
Cython consumers
8+
----------------
9+
10+
Cython 3.1 or newer can use the installed declarations directly::
11+
12+
from flint cimport fmpz, fmpz_add, fmpz_get_value
13+
14+
``fmpz_add(a, b)`` returns a new ``fmpz`` and operates entirely through the
15+
python-flint capsule. Extensions using only this operation do not need FLINT
16+
headers or a direct FLINT link. See ``examples/cython/capi``.
17+
18+
``fmpz_get_value(value)`` returns a read-only, borrowed pointer to FLINT
19+
storage. It remains valid only while ``value`` is alive. Never clear or mutate
20+
it, retain it independently of its owner, or use it after the owner is
21+
released. Direct consumers must compile and link against the same compatible
22+
FLINT ABI/build as python-flint. In particular, this is not safe with wheels
23+
containing a private, renamed FLINT. See ``examples/cython/flint``.
24+
25+
C consumers
26+
-----------
27+
28+
Include the installed header and import the capsule before using it::
29+
30+
#include <python_flint/fmpz.h>
31+
32+
if (PyFlint_Import() < 0)
33+
return NULL;
34+
result = PyFlint_FMPZ_Add(left, right); /* new reference */
35+
36+
``PyFlint_FMPZ_GetValue`` has the same borrowed, read-only ownership rules as
37+
the Cython wrapper. Both operations return ``NULL`` with a Python exception set
38+
on failure, and reject objects that are not ``flint.fmpz`` instances.
39+
40+
Header location
41+
---------------
42+
43+
Build systems can locate ``python_flint/fmpz.h`` using::
44+
45+
import flint
46+
print(flint.get_include())
47+
48+
The public header and Cython declarations contain no details of python-flint's
49+
private extension-object layout.

doc/source/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ Introduction
2929
:maxdepth: 2
3030

3131
install.rst
32+
c_api.rst
3233
general.rst
3334
build.rst
3435
workflow.rst

examples/cython/capi/README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# Capsule-only Cython example
2+
3+
This extension uses `fmpz_add` through python-flint's capsule API. It neither
4+
includes FLINT headers nor links to FLINT.
5+
6+
```console
7+
python -m pip install .
8+
python test_capi.py
9+
```
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from flint cimport fmpz, fmpz_add
2+
3+
4+
def add(fmpz a, fmpz b):
5+
"""Add through python-flint's capsule; no FLINT linkage is needed."""
6+
return fmpz_add(a, b)

examples/cython/capi/meson.build

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
project('python-flint-capi-example', 'cython', 'c', meson_version: '>=1.3')
2+
py = import('python').find_installation(pure: false)
3+
flint_include = run_command(
4+
py, '-c', 'import flint; print(flint.get_include())', check: true,
5+
).stdout().strip()
6+
py.extension_module(
7+
'_flint_capi_example', '_flint_capi_example.pyx',
8+
include_directories: include_directories(flint_include),
9+
install: true,
10+
)
11+
test('capsule API example', py, args: files('test_capi.py'),
12+
env: {'PYTHONPATH': meson.current_build_dir()})
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
[build-system]
2+
requires = ["meson-python", "cython>=3.1", "python-flint"]
3+
build-backend = "mesonpy"
4+
5+
[project]
6+
name = "python-flint-capi-example"
7+
version = "0.1.0"
8+
dependencies = ["python-flint"]

examples/cython/capi/test_capi.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
from flint import fmpz
2+
from _flint_capi_example import add
3+
4+
a = fmpz(2) ** 300
5+
b = fmpz(3) ** 200
6+
assert add(a, b) == a + b
7+
try:
8+
add(1, b)
9+
except TypeError:
10+
pass
11+
else:
12+
raise AssertionError("wrong argument type did not raise TypeError")

0 commit comments

Comments
 (0)