From f40d8705f2c44efa21b9cfa15d98c99318ed673e Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Tue, 28 Jul 2026 14:55:59 -0600 Subject: [PATCH 1/2] Fix Generated C depends on the host interpreter --- doc/source/whatsnew.rst | 2 ++ src/cffi/_cffi_include.h | 11 +++++++++-- src/cffi/recompiler.py | 13 +++++++------ src/cffi/setuptools_ext.py | 6 +++++- testing/cffi1/test_cffi_gen_src.py | 13 +++++++++++++ testing/cffi1/test_recompiler.py | 13 +++++++++++++ 6 files changed, 49 insertions(+), 9 deletions(-) diff --git a/doc/source/whatsnew.rst b/doc/source/whatsnew.rst index 3b2f48fc..c331f0d5 100644 --- a/doc/source/whatsnew.rst +++ b/doc/source/whatsnew.rst @@ -9,6 +9,8 @@ v2.2.0.dev0 built against a libffi other than the one provided by the macOS SDK — that is, the iOS/tvOS/watchOS wheels, or a macOS build using a Homebrew libffi. (`#265`_). +* The C source generated by ``cffi-gen-src`` and ``ffibuilder.emit_c_code()`` + no longer depends on the interpreter running the generator. .. _`#265`: https://github.com/python-cffi/cffi/pull/265 diff --git a/src/cffi/_cffi_include.h b/src/cffi/_cffi_include.h index c8c6d343..84487185 100644 --- a/src/cffi/_cffi_include.h +++ b/src/cffi/_cffi_include.h @@ -26,11 +26,16 @@ See also 'py_limited_api' in cffi/setuptools_ext.py. */ #if !defined(_CFFI_USE_EMBEDDING) && !defined(Py_LIMITED_API) +# include /* for PY_VERSION_HEX; self-contained */ # ifdef _MSC_VER # if !defined(_DEBUG) && !defined(Py_DEBUG) && !defined(Py_TRACE_REFS) && !defined(Py_REF_DEBUG) && !defined(_CFFI_NO_LIMITED_API) # if !defined(Py_GIL_DISABLED) # define Py_LIMITED_API -# else +# elif PY_VERSION_HEX >= 0x030f0000 + /* the free-threaded build supports the limited API (the abi3t + stable ABI) since 3.15. Note that on Windows pyconfig.h does + not define Py_GIL_DISABLED; build backends targeting the + free-threaded build pass it on the compiler command line. */ # define Py_LIMITED_API 0x030f0000 # endif # endif @@ -56,7 +61,9 @@ # if !defined(Py_DEBUG) && !defined(Py_TRACE_REFS) && !defined(Py_REF_DEBUG) && !defined(_CFFI_NO_LIMITED_API) # if !defined(Py_GIL_DISABLED) # define Py_LIMITED_API -# else +# elif PY_VERSION_HEX >= 0x030f0000 + /* the free-threaded build supports the limited API (the abi3t + stable ABI) since 3.15 */ # define Py_LIMITED_API 0x030f0000 # endif # endif diff --git a/src/cffi/recompiler.py b/src/cffi/recompiler.py index 5e71aed9..49a5129d 100644 --- a/src/cffi/recompiler.py +++ b/src/cffi/recompiler.py @@ -1,4 +1,4 @@ -import io, os, sys, sysconfig +import io, os, sys from . import ffiplatform, model from .error import VerificationError from .cffi_opcode import * @@ -7,11 +7,12 @@ VERSION_EMBEDDED = 0x2701 VERSION_CHAR16CHAR32 = 0x2801 -FREE_THREADED_BUILD = sysconfig.get_config_var("Py_GIL_DISABLED") -USE_LIMITED_API = ((sys.platform != 'win32' or sys.version_info < (3, 0) or - sys.version_info >= (3, 5)) and - # free-threaded build doesn't support the stable ABI until Python 3.15 - (not FREE_THREADED_BUILD or sys.version_info >= (3, 15))) +# Must not depend on the interpreter running the generator: the generated +# source may be compiled for a different one (see cffi-gen-src). Decisions +# about the target, such as abi3t support, are made at compile time in +# _cffi_include.h. +USE_LIMITED_API = (sys.platform != 'win32' or sys.version_info < (3, 0) or + sys.version_info >= (3, 5)) class GlobalExpr: def __init__(self, name, address, type_op, size=0, check_value=0): diff --git a/src/cffi/setuptools_ext.py b/src/cffi/setuptools_ext.py index 946afb45..3b759e31 100644 --- a/src/cffi/setuptools_ext.py +++ b/src/cffi/setuptools_ext.py @@ -92,7 +92,11 @@ def _set_py_limited_api(Extension, kwds): from cffi import recompiler if ('py_limited_api' not in kwds and not hasattr(sys, 'gettotalrefcount') - and recompiler.USE_LIMITED_API): + and recompiler.USE_LIMITED_API + # unlike USE_LIMITED_API, checking the running interpreter is + # correct here: setuptools generates and compiles in one process + and (not sysconfig.get_config_var("Py_GIL_DISABLED") + or sys.version_info >= (3, 15))): import setuptools try: setuptools_major_version = int(setuptools.__version__.partition('.')[0]) diff --git a/testing/cffi1/test_cffi_gen_src.py b/testing/cffi1/test_cffi_gen_src.py index cba18f56..a7c74d6e 100644 --- a/testing/cffi1/test_cffi_gen_src.py +++ b/testing/cffi1/test_cffi_gen_src.py @@ -211,3 +211,16 @@ def test_import_is_inert(): assert proc.returncode == 0, proc.stderr assert proc.stdout == "" assert proc.stderr == "" + + +def test_output_independent_of_generating_interpreter(tmp_path): + # runs on free-threaded and GIL-enabled CI alike; the output must be + # the same on both, with the limited-API decision left to + # _cffi_include.h at compile time + pyfile = tmp_path / "_squared_build.py" + pyfile.write_text(SIMPLE_SCRIPT) + out = tmp_path / "squared.c" + proc = _run([sys.executable, "-m", "cffi.gen_src"], + "exec-python", str(pyfile), str(out)) + assert proc.returncode == 0, proc.stderr + assert "#define _CFFI_NO_LIMITED_API" not in out.read_text().splitlines() diff --git a/testing/cffi1/test_recompiler.py b/testing/cffi1/test_recompiler.py index 0245b8db..691abbdb 100644 --- a/testing/cffi1/test_recompiler.py +++ b/testing/cffi1/test_recompiler.py @@ -2595,3 +2595,16 @@ def test_large_enum(): relements[biglist[i]] = i assert e.elements == elements assert e.relements == relements + +def test_emit_c_code_is_interpreter_independent(): + # the emitted source must be a pure function of the FFI object, with + # no #defines derived from the interpreter running the generator + import io + assert recompiler.USE_LIMITED_API # constant on all supported Pythons + ffi = FFI() + ffi.cdef("int ff_interp_indep(int);") + ffi.set_source("_CFFI_interp_indep", + "int ff_interp_indep(int x) { return x; }") + f = io.StringIO() + ffi.emit_c_code(f) + assert "#define _CFFI_NO_LIMITED_API" not in f.getvalue().splitlines() From 226475514733791478db6826b50ef757ed26688f Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Tue, 28 Jul 2026 15:08:43 -0600 Subject: [PATCH 2/2] Delete historical USE_LIMITED_API helper --- src/cffi/_cffi_include.h | 10 ---------- src/cffi/recompiler.py | 8 -------- src/cffi/setuptools_ext.py | 9 --------- testing/cffi1/test_recompiler.py | 1 - 4 files changed, 28 deletions(-) diff --git a/src/cffi/_cffi_include.h b/src/cffi/_cffi_include.h index 84487185..0fc145d1 100644 --- a/src/cffi/_cffi_include.h +++ b/src/cffi/_cffi_include.h @@ -13,16 +13,6 @@ In that case, we guess what pyconfig.h will do to the macros above, and check our guess after the #include. - Note that on Windows, with CPython 3.x, you need >= 3.5 and virtualenv - version >= 16.0.0. With older versions of either, you don't get a - copy of PYTHON3.DLL in the virtualenv. We can't check the version of - CPython *before* we even include pyconfig.h. ffi.set_source() puts - a ``#define _CFFI_NO_LIMITED_API'' at the start of this file if it is - running on Windows < 3.5, as an attempt at fixing it, but that's - arguably wrong because it may not be the target version of Python. - Still better than nothing I guess. As another workaround, you can - remove the definition of Py_LIMITED_API here. - See also 'py_limited_api' in cffi/setuptools_ext.py. */ #if !defined(_CFFI_USE_EMBEDDING) && !defined(Py_LIMITED_API) diff --git a/src/cffi/recompiler.py b/src/cffi/recompiler.py index 49a5129d..73ba0cec 100644 --- a/src/cffi/recompiler.py +++ b/src/cffi/recompiler.py @@ -7,12 +7,6 @@ VERSION_EMBEDDED = 0x2701 VERSION_CHAR16CHAR32 = 0x2801 -# Must not depend on the interpreter running the generator: the generated -# source may be compiled for a different one (see cffi-gen-src). Decisions -# about the target, such as abi3t support, are made at compile time in -# _cffi_include.h. -USE_LIMITED_API = (sys.platform != 'win32' or sys.version_info < (3, 0) or - sys.version_info >= (3, 5)) class GlobalExpr: def __init__(self, name, address, type_op, size=0, check_value=0): @@ -307,8 +301,6 @@ def write_c_source_to_f(self, f, preamble): prnt = self._prnt if self.ffi._embedding is not None: prnt('#define _CFFI_USE_EMBEDDING') - if not USE_LIMITED_API: - prnt('#define _CFFI_NO_LIMITED_API') # # first the '#include' (actually done by inlining the file's content) lines = self._rel_readlines('_cffi_include.h') diff --git a/src/cffi/setuptools_ext.py b/src/cffi/setuptools_ext.py index 3b759e31..dc94fbdb 100644 --- a/src/cffi/setuptools_ext.py +++ b/src/cffi/setuptools_ext.py @@ -82,19 +82,10 @@ def _set_py_limited_api(Extension, kwds): it doesn't so far, creating troubles. That's why we check for "not hasattr(sys, 'gettotalrefcount')" (the 2.7 compatible equivalent of 'd' not in sys.abiflags). (http://bugs.python.org/issue28401) - - On Windows, with CPython <= 3.4, it's better not to use py_limited_api - because virtualenv *still* doesn't copy PYTHON3.DLL on these versions. - Recently (2020) we started shipping only >= 3.5 wheels, though. So - we'll give it another try and set py_limited_api on Windows >= 3.5. """ from cffi._shimmed_dist_utils import log - from cffi import recompiler if ('py_limited_api' not in kwds and not hasattr(sys, 'gettotalrefcount') - and recompiler.USE_LIMITED_API - # unlike USE_LIMITED_API, checking the running interpreter is - # correct here: setuptools generates and compiles in one process and (not sysconfig.get_config_var("Py_GIL_DISABLED") or sys.version_info >= (3, 15))): import setuptools diff --git a/testing/cffi1/test_recompiler.py b/testing/cffi1/test_recompiler.py index 691abbdb..6d4336b3 100644 --- a/testing/cffi1/test_recompiler.py +++ b/testing/cffi1/test_recompiler.py @@ -2600,7 +2600,6 @@ def test_emit_c_code_is_interpreter_independent(): # the emitted source must be a pure function of the FFI object, with # no #defines derived from the interpreter running the generator import io - assert recompiler.USE_LIMITED_API # constant on all supported Pythons ffi = FFI() ffi.cdef("int ff_interp_indep(int);") ffi.set_source("_CFFI_interp_indep",