Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions .github/workflows/buildwheel.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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-26.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'
Expand Down
41 changes: 41 additions & 0 deletions doc/source/cython_api.rst
Original file line number Diff line number Diff line change
@@ -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.
1 change: 1 addition & 0 deletions doc/source/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ Introduction
:maxdepth: 2

install.rst
cython_api.rst
general.rst
build.rst
workflow.rst
Expand Down
11 changes: 11 additions & 0 deletions examples/cython/README.md
Original file line number Diff line number Diff line change
@@ -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
```
19 changes: 19 additions & 0 deletions examples/cython/_flint_cython_example.pyx
Original file line number Diff line number Diff line change
@@ -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))
11 changes: 11 additions & 0 deletions examples/cython/meson.build
Original file line number Diff line number Diff line change
@@ -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,
)
8 changes: 8 additions & 0 deletions examples/cython/pyproject.toml
Original file line number Diff line number Diff line change
@@ -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"]
16 changes: 16 additions & 0 deletions examples/cython/test_cython_api.py
Original file line number Diff line number Diff line change
@@ -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")
1 change: 1 addition & 0 deletions src/flint/_capi/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Namespace containing python-flint's experimental C APIs."""
10 changes: 10 additions & 0 deletions src/flint/_capi/cython.pxd
Original file line number Diff line number Diff line change
@@ -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)
21 changes: 21 additions & 0 deletions src/flint/_capi/cython.pyx
Original file line number Diff line number Diff line change
@@ -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 (<fmpz_internal>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
17 changes: 17 additions & 0 deletions src/flint/_capi/meson.build
Original file line number Diff line number Diff line change
@@ -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,
)
1 change: 1 addition & 0 deletions src/flint/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ exts = [
]

pkgs = [
'_capi',
'flint_base',
'types',
'functions',
Expand Down
Loading