From 7d38f223fe47b13cd769927da95e3f035e623050 Mon Sep 17 00:00:00 2001 From: Oscar Benjamin Date: Thu, 27 Aug 2026 21:33:40 +0100 Subject: [PATCH 1/2] demo: add a Cython API to access fmpz --- .github/workflows/buildwheel.yml | 23 +++++++++++++ doc/source/cython_api.rst | 41 +++++++++++++++++++++++ doc/source/index.rst | 1 + examples/cython/README.md | 11 ++++++ examples/cython/_flint_cython_example.pyx | 19 +++++++++++ examples/cython/meson.build | 11 ++++++ examples/cython/pyproject.toml | 8 +++++ examples/cython/test_cython_api.py | 16 +++++++++ src/flint/_capi/__init__.py | 1 + src/flint/_capi/cython.pxd | 10 ++++++ src/flint/_capi/cython.pyx | 21 ++++++++++++ src/flint/_capi/meson.build | 17 ++++++++++ src/flint/meson.build | 1 + 13 files changed, 180 insertions(+) create mode 100644 doc/source/cython_api.rst create mode 100644 examples/cython/README.md create mode 100644 examples/cython/_flint_cython_example.pyx create mode 100644 examples/cython/meson.build create mode 100644 examples/cython/pyproject.toml create mode 100644 examples/cython/test_cython_api.py create mode 100644 src/flint/_capi/__init__.py create mode 100644 src/flint/_capi/cython.pxd create mode 100644 src/flint/_capi/cython.pyx create mode 100644 src/flint/_capi/meson.build diff --git a/.github/workflows/buildwheel.yml b/.github/workflows/buildwheel.yml index 7c07eb8a..fed521e7 100644 --- a/.github/workflows/buildwheel.yml +++ b/.github/workflows/buildwheel.yml @@ -329,6 +329,29 @@ jobs: - run: spin run -- pytest --doctest-glob='*.rst' doc/source - run: spin docs + test_cython_api: + name: Test experimental Cython API + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: '3.13' + - run: sudo apt-get update + - run: sudo apt-get install libflint-dev + - run: pip install "cython==3.3.0" meson-python ninja + - run: pip wheel --no-build-isolation --no-deps --wheel-dir=/tmp/python-flint-wheel . + - run: pip install /tmp/python-flint-wheel/python_flint-*.whl + - name: Build and test consumers with supported Cython releases + run: | + for version in 3.0.11 3.1.0 3.2.4 3.3.0; do + pip install "cython==$version" + pip install --no-build-isolation --no-deps --force-reinstall ./examples/cython + (cd /tmp && python "$GITHUB_WORKSPACE/examples/cython/test_cython_api.py") + done + # Test build with minimum Cython and meson-python versions. test_old_build_requires: name: 'Test old Cython/meson-python' diff --git a/doc/source/cython_api.rst b/doc/source/cython_api.rst new file mode 100644 index 00000000..fdfd819b --- /dev/null +++ b/doc/source/cython_api.rst @@ -0,0 +1,41 @@ +Experimental Cython API +======================= + +python-flint provides an experimental Cython API for interoperating with +extension modules written in Cython. The API may grow or change between minor +python-flint releases. Within a minor release series, such as ``0.9.x``, it is +expected to remain ABI compatible. + +The API currently exposes the ``fmpz`` extension type, FLINT's C ``fmpz`` +type as ``flint_fmpz``, and functions for converting in both directions:: + + from flint._capi.cython cimport ( + flint_fmpz, + fmpz, + fmpz_from_value, + fmpz_get_value, + ) + +``fmpz_get_value(value)`` returns a borrowed, read-only pointer to the +underlying FLINT ``fmpz`` value. Its return type is ``const flint_fmpz *``, +which Cython emits as ``const fmpz *`` and which can be passed directly to +read-only FLINT functions accepting an ``fmpz_t`` argument. The pointer is +valid only while ``value`` is alive. Consumers must not clear or mutate the +value through this pointer, keep the pointer after its owner is released, or +use it without holding the GIL. + +``fmpz_from_value(value)`` returns a new Python ``fmpz`` containing a copy of +the supplied FLINT value. The caller retains ownership of the input, which only +needs to remain valid for the duration of the call. + +These functions are intended to allow extensions linked against a separate +FLINT library to exchange values with python-flint, including when +python-flint's wheel contains bundled, renamed FLINT and GMP libraries. The +libraries do not need to be the same shared-library instances, but their FLINT +and GMP data ABIs must be compatible. Each library remains responsible for +allocating, mutating and clearing the values that it owns: borrowed values must +only be inspected, and values crossing into python-flint are copied by +``fmpz_from_value``. + +See ``examples/cython`` for a complete extension that calls ``fmpz_bits`` on +the borrowed value. diff --git a/doc/source/index.rst b/doc/source/index.rst index d8bb93e8..e40eea1a 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -29,6 +29,7 @@ Introduction :maxdepth: 2 install.rst + cython_api.rst general.rst build.rst workflow.rst diff --git a/examples/cython/README.md b/examples/cython/README.md new file mode 100644 index 00000000..b82606f7 --- /dev/null +++ b/examples/cython/README.md @@ -0,0 +1,11 @@ +# Cython API example + +This extension uses python-flint's experimental Cython API to borrow the +underlying value of an `fmpz`, then calls the read-only FLINT function +`fmpz_bits`. The extension may link against a separate FLINT library, provided +its FLINT and GMP data ABIs are compatible with those used by python-flint. + +```console +python -m pip install . +python test_cython_api.py +``` diff --git a/examples/cython/_flint_cython_example.pyx b/examples/cython/_flint_cython_example.pyx new file mode 100644 index 00000000..6c3bed5f --- /dev/null +++ b/examples/cython/_flint_cython_example.pyx @@ -0,0 +1,19 @@ +from flint._capi.cython cimport ( + flint_fmpz, + fmpz, + fmpz_from_value, + fmpz_get_value, +) + +cdef extern from "flint/fmpz.h": + unsigned long fmpz_bits(const flint_fmpz *) + + +def bit_length(fmpz value): + """Call a read-only FLINT operation on borrowed fmpz storage.""" + return fmpz_bits(fmpz_get_value(value)) + + +def copy(fmpz value): + """Create a new Python fmpz by copying a borrowed FLINT value.""" + return fmpz_from_value(fmpz_get_value(value)) diff --git a/examples/cython/meson.build b/examples/cython/meson.build new file mode 100644 index 00000000..759c6a86 --- /dev/null +++ b/examples/cython/meson.build @@ -0,0 +1,11 @@ +project('python-flint-cython-example', 'cython', 'c', meson_version: '>=1.3') + +py = import('python').find_installation(pure: false) +flint_dep = dependency('flint') + +py.extension_module( + '_flint_cython_example', + '_flint_cython_example.pyx', + dependencies: flint_dep, + install: true, +) diff --git a/examples/cython/pyproject.toml b/examples/cython/pyproject.toml new file mode 100644 index 00000000..9da93ddd --- /dev/null +++ b/examples/cython/pyproject.toml @@ -0,0 +1,8 @@ +[build-system] +requires = ["meson-python", "cython>=3.0.11", "python-flint"] +build-backend = "mesonpy" + +[project] +name = "python-flint-cython-example" +version = "0.1.0" +dependencies = ["python-flint"] diff --git a/examples/cython/test_cython_api.py b/examples/cython/test_cython_api.py new file mode 100644 index 00000000..e7a7ec77 --- /dev/null +++ b/examples/cython/test_cython_api.py @@ -0,0 +1,16 @@ +from flint import fmpz +from _flint_cython_example import bit_length, copy + + +value = fmpz(2) ** 300 + 1 +assert bit_length(value) == 301 +value_copy = copy(value) +assert value_copy == value +assert value_copy is not value + +try: + bit_length(1) +except TypeError: + pass +else: + raise AssertionError("wrong argument type did not raise TypeError") diff --git a/src/flint/_capi/__init__.py b/src/flint/_capi/__init__.py new file mode 100644 index 00000000..b2f97baf --- /dev/null +++ b/src/flint/_capi/__init__.py @@ -0,0 +1 @@ +"""Namespace containing python-flint's experimental C APIs.""" diff --git a/src/flint/_capi/cython.pxd b/src/flint/_capi/cython.pxd new file mode 100644 index 00000000..ebba42ae --- /dev/null +++ b/src/flint/_capi/cython.pxd @@ -0,0 +1,10 @@ +"""Experimental Cython API for python-flint consumers.""" + +cdef extern class flint.types.fmpz.fmpz [object PyFlintFMPZObject, check_size ignore]: + pass + +cdef extern from "flint/fmpz.h": + ctypedef long flint_fmpz "fmpz" + +cdef api const flint_fmpz *fmpz_get_value(fmpz value) except NULL +cdef api fmpz fmpz_from_value(const flint_fmpz *value) diff --git a/src/flint/_capi/cython.pyx b/src/flint/_capi/cython.pyx new file mode 100644 index 00000000..b4b205e0 --- /dev/null +++ b/src/flint/_capi/cython.pyx @@ -0,0 +1,21 @@ +"""Implementation of python-flint's experimental Cython API.""" + +from flint.types.fmpz cimport fmpz as fmpz_internal +from flint.flintlib.functions.fmpz cimport fmpz_set + + +cdef api const flint_fmpz *fmpz_get_value(fmpz value) except NULL: + """Return a borrowed, read-only pointer to an fmpz value.""" + return (value).val + + +cdef api fmpz fmpz_from_value(const flint_fmpz *value): + """Return a new fmpz containing a copy of a FLINT fmpz value.""" + cdef fmpz_internal result + + if value == NULL: + raise ValueError("fmpz_from_value does not accept NULL") + + result = fmpz_internal.__new__(fmpz_internal) + fmpz_set(result.val, value) + return result diff --git a/src/flint/_capi/meson.build b/src/flint/_capi/meson.build new file mode 100644 index 00000000..0bcfa54c --- /dev/null +++ b/src/flint/_capi/meson.build @@ -0,0 +1,17 @@ +thisdir = 'flint/_capi' + +py.install_sources( + '__init__.py', + 'cython.pxd', + pure: false, + subdir: thisdir, +) + +py.extension_module( + 'cython', + 'cython.pyx', + dependencies: pyflint_deps, + install: true, + subdir: thisdir, + limited_api: limited_api_version, +) diff --git a/src/flint/meson.build b/src/flint/meson.build index 4feb64ea..51c4de55 100644 --- a/src/flint/meson.build +++ b/src/flint/meson.build @@ -12,6 +12,7 @@ exts = [ ] pkgs = [ + '_capi', 'flint_base', 'types', 'functions', From fedb184fc198fcd04e891ef8ca7e558bdfab3409 Mon Sep 17 00:00:00 2001 From: Oscar Benjamin Date: Thu, 27 Aug 2026 21:45:08 +0100 Subject: [PATCH 2/2] Use FLINT 3.4 --- .github/workflows/buildwheel.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/buildwheel.yml b/.github/workflows/buildwheel.yml index fed521e7..10561ccb 100644 --- a/.github/workflows/buildwheel.yml +++ b/.github/workflows/buildwheel.yml @@ -331,7 +331,7 @@ jobs: test_cython_api: name: Test experimental Cython API - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: