Skip to content

Commit

Permalink
Cython: emit CYTHON_LIMITED_API
Browse files Browse the repository at this point in the history
If the user has specified the `limited_api` keyword arg
on an 'extension_module' invocation, then add `CYTHON_LIMITED_API`
to the C and C++ definitions of the target (along with the existing
`Py_LIMITED_API`).

Cython's support for this functionality was added in version 3.0, so
we issue a warning if the detected Cython compiler is not at least this
version.

Two tests have been added: one based on Cython's limited API test which
aims to test that it works in the case of the user's compiler supporting
this function, and another to test that a warning is issued in the opposite
case.
  • Loading branch information
amcn committed Feb 20, 2024
1 parent c80ece2 commit 2897a40
Show file tree
Hide file tree
Showing 6 changed files with 147 additions and 3 deletions.
22 changes: 22 additions & 0 deletions docs/markdown/Cython.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,25 @@ py.extension_module(
dependencies : dep_py,
)
```

## Limited API support

*(New in 1.4)*

Meson version 1.4 extends the support of the `limited_api` keyword in the Python
module's `extension_module` method to Cython.

```meson
project('my project', 'cython')
py.extension_module(
'foo',
'foo.pyx',
dependencies : dep_py,
limited_api : '3.8'
)
```

Cython's support for Python's Limited API was introduced in Cython version 3.0.
If Meson detects that the user's Cython compiler is too old when attempting to use
this keyword with Cython sources, an error is issued and Meson stops.
4 changes: 4 additions & 0 deletions docs/markdown/snippets/cython-limited-api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
## `limited_api` support for Cython extension modules

The `limited_api` keyword of the Python module's `extension_module`
now supports Cython by defining the `CYTHON_LIMITED_API` symbol.
34 changes: 31 additions & 3 deletions mesonbuild/modules/python.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,14 +173,17 @@ def extension_module_method(self, args: T.Tuple[str, T.List[BuildTargetSource]],
target_suffix = self.limited_api_suffix

limited_api_version_hex = self._convert_api_version_to_py_version_hex(limited_api_version, pydep.version)
limited_api_definition = f'-DPy_LIMITED_API={limited_api_version_hex}'
limited_api_definitions = [
f'-DPy_LIMITED_API={limited_api_version_hex}',
'-DCYTHON_LIMITED_API=1'
]

new_c_args = mesonlib.extract_as_list(kwargs, 'c_args')
new_c_args.append(limited_api_definition)
new_c_args.extend(limited_api_definitions)
kwargs['c_args'] = new_c_args

new_cpp_args = mesonlib.extract_as_list(kwargs, 'cpp_args')
new_cpp_args.append(limited_api_definition)
new_cpp_args.extend(limited_api_definitions)
kwargs['cpp_args'] = new_cpp_args

# When compiled under MSVC, Python's PC/pyconfig.h forcibly inserts pythonMAJOR.MINOR.lib
Expand Down Expand Up @@ -212,6 +215,31 @@ def extension_module_method(self, args: T.Tuple[str, T.List[BuildTargetSource]],

kwargs['link_args'] = new_link_args

cython_compiler = next((c for c in compilers.values() if c.get_id() == 'cython'), None)
if cython_compiler is not None:
# Determine if any of the supplied source files are Cython source.
cython_suffixes = cython_compiler.file_suffixes

def has_cython_files(args: T.List[BuildTargetSource]):
for arg in args:
if isinstance(arg, StructuredSources):
if has_cython_files(arg.as_list()):
return True
continue
if isinstance(arg, GeneratedList):
if has_cython_files(arg.get_outputs()):
return True
continue
if isinstance(arg, mesonlib.File):
arg = arg.fname
suffix = os.path.splitext(arg)[1][1:].lower()
if suffix in cython_suffixes:
return True
return False

if mesonlib.version_compare(cython_compiler.version, '< 3.0.0') and has_cython_files(args[1]):
raise mesonlib.MesonException(f'Python Limited API not supported by Cython versions < 3.0.0 (detected {cython_compiler.version})')

kwargs['dependencies'] = new_deps

# msys2's python3 has "-cpython-36m.dll", we have to be clever
Expand Down
43 changes: 43 additions & 0 deletions test cases/cython/4 limited api/limited.pyx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# This is taken from Cython's limited API tests and
# as a result is under Apache 2.0

import cython

@cython.binding(False)
def fib(int n):
cdef int a, b
a, b = 0, 1
while b < n:
a, b = b, a + b
return b

def lsum(values):
cdef long result = 0
for value in values:
result += value
if type(values) is list:
for value in reversed(<list>values):
result += value
elif type(values) is tuple:
for value in reversed(<tuple>values):
result += value
return result

@cython.binding(False)
def raises():
raise RuntimeError()

def decode(bytes b, bytearray ba):
return b.decode("utf-8") + ba.decode("utf-8")

def cast_float(object o):
return float(o)

class C:
pass

cdef class D:
pass

cdef class E(D):
pass
22 changes: 22 additions & 0 deletions test cases/cython/4 limited api/meson.build
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
project('cython limited api', ['cython', 'c']
# setting limited_api in debug mode on Windows yields errors.
default_options: ['buildtype=release']
)

cythonc = meson.get_compiler('cython')
if cythonc.version() < '3.0'
error('MESON_SKIP_TEST: Cython compiler version does not support limited API')
endif

py3 = import('python').find_installation()

ext_mod = py3.extension_module('limited',
'limited.pyx',
limited_api: '3.7'
)

test('limited_api runner',
py3,
args : files('run.py'),
env : ['PYTHONPATH=' + meson.current_build_dir()]
)
25 changes: 25 additions & 0 deletions test cases/cython/4 limited api/run.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# This is taken from Cython's limited API tests and
# as a result is under Apache 2.0

import limited

limited.fib(11)

assert limited.lsum(list(range(10))) == 90
assert limited.lsum(tuple(range(10))) == 90
assert limited.lsum(iter(range(10))) == 45

try:
limited.raises()
except RuntimeError:
pass

limited.C()
limited.D()
limited.E()

assert limited.decode(b'a', bytearray(b'b')) == "ab"

assert limited.cast_float(1) == 1.0
assert limited.cast_float("2.0") == 2.0
assert limited.cast_float(bytearray(b"3")) == 3.0

0 comments on commit 2897a40

Please sign in to comment.