Skip to content

Commit 5e6692e

Browse files
authored
Merge pull request #264 from harfbuzz/py-limited-api
enable Py_LIMITED_API to strip down build matrix to one abi3 wheel per platform
2 parents cab5247 + 26c63e2 commit 5e6692e

File tree

5 files changed

+73
-50
lines changed

5 files changed

+73
-50
lines changed

pyproject.toml

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,20 @@
11
[build-system]
22
requires = [
3-
# pin setuptools on pypy to workaround this bug: https://github.com/pypa/distutils/issues/283
4-
"setuptools >= 36.4, < 72.2; platform_python_implementation == 'PyPy'",
5-
"setuptools >= 36.4; platform_python_implementation != 'PyPy'",
3+
"setuptools >= 36.4",
64
"wheel",
75
"setuptools_scm >= 2.1",
8-
"cython >= 0.28.1",
6+
"cython >= 3.1",
97
"pkgconfig"
108
]
119
build-backend = "setuptools.build_meta"
1210

1311
[tool.setuptools_scm]
1412

1513
[tool.cibuildwheel]
16-
# Skip building PyPy 3.8 wheels, the build currently fails
1714
# Also skip wheels for free-threaded CPython 3.14 (e.g. 'cp314t') until we
1815
# actually support them. We currently aren't thread-safe as we define HB_NO_MT=1.
19-
skip = ["pp38-*", "cp3??t-*"]
16+
# CPython abi3 wheels only need universal2 on macOS, skip the single-arch builds.
17+
skip = ["cp38-*", "cp39-*", "cp3??t-*", "cp*-macosx_x86_64", "cp*-macosx_arm64"]
2018
enable = ["pypy"]
2119
test-requires = "pytest"
2220
test-command = "pytest {project}/tests"
@@ -27,6 +25,21 @@ archs = ["x86_64", "universal2", "arm64"]
2725
[tool.cibuildwheel.linux]
2826
archs = ["native"]
2927

28+
# Run abi3audit after the default repair commands to scan for abi3 violations
29+
# https://github.com/pypa/abi3audit
30+
# Only on Unix platforms (Linux/macOS) since Windows has no default repair command
31+
[[tool.cibuildwheel.overrides]]
32+
select = "cp*-{linux,macosx}*"
33+
inherit.repair-wheel-command = "append"
34+
repair-wheel-command = "pipx run abi3audit --strict --report {wheel}"
35+
36+
# Disable Limited API for PyPy as the build currently fails, see:
37+
# https://github.com/harfbuzz/uharfbuzz/issues/262#issuecomment-3415144557
38+
# https://cibuildwheel.pypa.io/en/stable/configuration/#overrides
39+
[[tool.cibuildwheel.overrides]]
40+
select = "pp*"
41+
environment = { USE_PY_LIMITED_API = "0" }
42+
3043

3144
[tool.black]
3245
extend-exclude = "harfbuzz"

requirements-dev.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
cython>=0.29.1
1+
cython>=3.1
22

33
# we need wheel >= 0.31.0 to support Markdown long_description
44
wheel>=0.31

setup.cfg

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,2 @@
1-
[sdist]
2-
formats = zip
3-
41
[metadata]
52
license_file = LICENSE

setup.py

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,10 @@
1010
from setuptools import Extension, setup
1111

1212

13-
def bool_from_environ(key: str):
13+
def bool_from_environ(key: str, default: bool = False):
1414
value = os.environ.get(key)
1515
if not value:
16-
return False
16+
return default
1717
if value == "1":
1818
return True
1919
if value == "0":
@@ -32,6 +32,12 @@ def bool_from_environ(key: str):
3232
use_system_libraries = bool_from_environ("USE_SYSTEM_LIBS")
3333
use_cython_linetrace = bool_from_environ("CYTHON_LINETRACE")
3434
use_cython_annotate = bool_from_environ("CYTHON_ANNOTATE")
35+
# Python Limited API for stable ABI support is enabled by default.
36+
# Set USE_PY_LIMITED_API=0 to turn it off.
37+
# https://docs.python.org/3.14/c-api/stable.html#limited-c-api
38+
use_py_limited_api = bool_from_environ("USE_PY_LIMITED_API", default=True)
39+
# NOTE: this must be kept in sync with python_requires='>=3.10' below
40+
limited_api_min_version = "0x030A0000"
3541

3642

3743
def _configure_extensions_with_system_libs() -> List[Extension]:
@@ -51,6 +57,9 @@ def _configure_extensions_with_system_libs() -> List[Extension]:
5157
if use_cython_linetrace:
5258
define_macros.append(("CYTHON_TRACE_NOGIL", "1"))
5359

60+
if use_py_limited_api:
61+
define_macros.append(("Py_LIMITED_API", limited_api_min_version))
62+
5463
extension = Extension(
5564
"uharfbuzz._harfbuzz",
5665
define_macros=define_macros,
@@ -61,6 +70,7 @@ def _configure_extensions_with_system_libs() -> List[Extension]:
6170
language="c++",
6271
libraries=libraries,
6372
library_dirs=library_dirs,
73+
py_limited_api=use_py_limited_api,
6474
)
6575

6676
extension_test = Extension(
@@ -74,6 +84,7 @@ def _configure_extensions_with_system_libs() -> List[Extension]:
7484
language="c++",
7585
libraries=libraries,
7686
library_dirs=library_dirs,
87+
py_limited_api=use_py_limited_api,
7788
)
7889

7990
return [extension, extension_test]
@@ -87,6 +98,9 @@ def _configure_extensions_with_vendored_libs() -> List[Extension]:
8798
if use_cython_linetrace:
8899
define_macros.append(("CYTHON_TRACE_NOGIL", "1"))
89100

101+
if use_py_limited_api:
102+
define_macros.append(("Py_LIMITED_API", limited_api_min_version))
103+
90104
extra_compile_args = []
91105
extra_link_args = []
92106
libraries = []
@@ -124,6 +138,7 @@ def _configure_extensions_with_vendored_libs() -> List[Extension]:
124138
libraries=libraries,
125139
extra_compile_args=extra_compile_args,
126140
extra_link_args=extra_link_args,
141+
py_limited_api=use_py_limited_api,
127142
)
128143

129144
extension_test = Extension(
@@ -138,6 +153,7 @@ def _configure_extensions_with_vendored_libs() -> List[Extension]:
138153
libraries=libraries,
139154
extra_compile_args=extra_compile_args,
140155
extra_link_args=extra_link_args,
156+
py_limited_api=use_py_limited_api,
141157
)
142158

143159
return [extension, extension_test]
@@ -164,10 +180,11 @@ def configure_extensions() -> List[Extension]:
164180
packages=["uharfbuzz"],
165181
zip_safe=False,
166182
setup_requires=["setuptools_scm"],
167-
python_requires=">=3.8",
183+
python_requires=">=3.10",
168184
ext_modules=cythonize(
169185
configure_extensions(),
170186
annotate=use_cython_annotate,
171187
compiler_directives={"linetrace": use_cython_linetrace},
172188
),
189+
options={"bdist_wheel": {"py_limited_api": "cp310"}} if use_py_limited_api else {},
173190
)

src/uharfbuzz/_harfbuzz.pyx

Lines changed: 33 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,16 @@ from .charfbuzz cimport *
77
from libc.stdlib cimport free, malloc, calloc
88
from libc.string cimport const_char
99
from cpython.pycapsule cimport PyCapsule_GetPointer, PyCapsule_IsValid
10-
from cpython.unicode cimport (
11-
PyUnicode_1BYTE_DATA, PyUnicode_2BYTE_DATA, PyUnicode_4BYTE_DATA,
12-
PyUnicode_1BYTE_KIND, PyUnicode_2BYTE_KIND, PyUnicode_4BYTE_KIND,
13-
PyUnicode_KIND, PyUnicode_GET_LENGTH, PyUnicode_FromKindAndData
14-
)
10+
from cpython.unicode cimport PyUnicode_GetLength, PyUnicode_AsUCS4Copy
11+
from cpython.mem cimport PyMem_Free
1512
from typing import Callable, Dict, List, Sequence, Tuple, Union, NamedTuple
1613
from pathlib import Path
1714
from functools import wraps
1815

16+
# Declare Limited API types and functions (Python 3.3+)
17+
cdef extern from "Python.h":
18+
ctypedef uint32_t Py_UCS4
19+
1920

2021
DEF STATIC_ARRAY_SIZE = 128
2122

@@ -341,38 +342,27 @@ cdef class Buffer:
341342

342343
def add_str(self, text: str,
343344
item_offset: int = 0, item_length: int = -1) -> None:
345+
cdef Py_UCS4* ucs4_buffer
346+
cdef Py_ssize_t text_length
344347

345-
cdef Py_ssize_t length = PyUnicode_GET_LENGTH(text)
346-
cdef int kind = PyUnicode_KIND(text)
347-
348-
if kind == PyUnicode_1BYTE_KIND:
349-
hb_buffer_add_latin1(
350-
self._hb_buffer,
351-
<uint8_t*>PyUnicode_1BYTE_DATA(text),
352-
length,
353-
item_offset,
354-
item_length,
355-
)
356-
elif kind == PyUnicode_2BYTE_KIND:
357-
hb_buffer_add_utf16(
358-
self._hb_buffer,
359-
<uint16_t*>PyUnicode_2BYTE_DATA(text),
360-
length,
361-
item_offset,
362-
item_length,
363-
)
364-
elif kind == PyUnicode_4BYTE_KIND:
348+
ucs4_buffer = PyUnicode_AsUCS4Copy(text)
349+
if ucs4_buffer == NULL:
350+
raise MemoryError()
351+
try:
352+
text_length = PyUnicode_GetLength(text)
353+
if text_length == -1:
354+
raise ValueError("Invalid Unicode string")
365355
hb_buffer_add_utf32(
366356
self._hb_buffer,
367-
<uint32_t*>PyUnicode_4BYTE_DATA(text),
368-
length,
357+
<uint32_t*>ucs4_buffer,
358+
text_length,
369359
item_offset,
370-
item_length,
360+
item_length
371361
)
372-
else:
373-
raise AssertionError(kind)
374-
if not hb_buffer_allocation_successful(self._hb_buffer):
375-
raise MemoryError()
362+
if not hb_buffer_allocation_successful(self._hb_buffer):
363+
raise MemoryError()
364+
finally:
365+
PyMem_Free(ucs4_buffer)
376366

377367
def guess_segment_properties(self) -> None:
378368
hb_buffer_guess_segment_properties(self._hb_buffer)
@@ -952,7 +942,7 @@ cdef class Face:
952942
def get_name(self, name_id: OTNameIdPredefined | int, language: str | None = None) -> str | None:
953943
cdef bytes packed
954944
cdef hb_language_t lang
955-
cdef uint32_t *text
945+
cdef char *text
956946
cdef unsigned int length
957947

958948
if language is None:
@@ -961,12 +951,18 @@ cdef class Face:
961951
packed = language.encode()
962952
lang = hb_language_from_string(<char*>packed, -1)
963953

964-
length = hb_ot_name_get_utf32(self._hb_face, name_id, lang, NULL, NULL)
954+
length = hb_ot_name_get_utf8(self._hb_face, name_id, lang, NULL, NULL)
965955
if length:
966956
length += 1 # for the null terminator
967-
text = <uint32_t*>malloc(length * sizeof(uint32_t))
968-
hb_ot_name_get_utf32(self._hb_face, name_id, lang, &length, text)
969-
return PyUnicode_FromKindAndData(PyUnicode_4BYTE_KIND, text, length)
957+
text = <char*>malloc(length * sizeof(char))
958+
if text == NULL:
959+
raise MemoryError()
960+
try:
961+
hb_ot_name_get_utf8(self._hb_face, name_id, lang, &length, text)
962+
result = text[:length].decode("utf-8")
963+
return result
964+
finally:
965+
free(text)
970966
return None
971967

972968

0 commit comments

Comments
 (0)