diff --git a/AGENTS.md b/AGENTS.md index 5da74d5..eaeb295 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -9,10 +9,10 @@ A collection of small, self-contained example packages under `projects/`, each d Projects fall into distinct families; know which one you're editing before changing build config: - **Legacy `scikit-build`** (`build-backend = "setuptools.build_meta"`, has `setup.py`): `hello-pure`, `hello-cpp`, `hello-cython`, `hello-pybind11`, `pen2-cython`, `tower-of-babel`. Build requires include `scikit-build`, `cmake`, `ninja` in `[build-system].requires`. Metadata lives in `setup.py`. -- **Modern `scikit-build-core`** (`build-backend = "scikit_build_core.build"`, pyproject-only, no `setup.py`): `core-c-hello`, `core-cython-hello`, `core-pybind11-hello`, `core-nanobind-shared`, `hello-free-threading`, `pi-fortran`, `hello-cmake-package`. Metadata lives in `[project]`; configure via `[tool.scikit-build]`. +- **Modern `scikit-build-core`** (`build-backend = "scikit_build_core.build"`, pyproject-only, no `setup.py`): `core-c-hello`, `core-cffi-hello`, `core-cython-hello`, `core-pybind11-hello`, `core-nanobind-shared`, `hello-free-threading`, `pi-fortran`, `hello-cmake-package`. Metadata lives in `[project]`; configure via `[tool.scikit-build]`. - **Hatchling + scikit-build-core plugin** (`build-backend = "hatchling.build"`): `hatchling-pybind11-hello`, using `[tool.hatch.build.targets.wheel.hooks.scikit-build]`. -Language bindings vary per project: pure Python, raw C/C++, pybind11, nanobind (`core-nanobind-shared`, which also demonstrates linking to a shared library shipped in the wheel), Cython, Fortran (`pi-fortran`). `tower-of-babel` is the broadest, exercising Boost + Cython static/shared linkage. +Language bindings vary per project: pure Python, raw C/C++, pybind11, nanobind (`core-nanobind-shared`, which also demonstrates linking to a shared library shipped in the wheel), cffi (`core-cffi-hello`, generating the extension source with `cffi-gen-src`), Cython, Fortran (`pi-fortran`). `tower-of-babel` is the broadest, exercising Boost + Cython static/shared linkage. ## Commands @@ -30,7 +30,7 @@ Development is driven by nox (`noxfile.py`). Use `uvx nox` (uv is the assumed ru `noxfile.py` defines which projects participate, with platform/version gates that must be respected when adding or moving a project: -- `hello_list` (tested by `test`): the four `hello-*` setuptools projects, plus `hello-cmake-package` and `pi-fortran` (**non-Windows only**), plus `hello-free-threading` (**Python 3.13+ only**). +- `hello_list` (tested by `test`): the four `hello-*` setuptools projects, plus `hello-cmake-package` and `pi-fortran` (**non-Windows only**), plus `core-cffi-hello` (**Python 3.10+ only**, also in `long_hello_list`) and `hello-free-threading` (**Python 3.13+ only**). - `long_hello_list` (built by `dist`): `hello_list` plus `pen2-cython`, `core-c-hello`, `core-pybind11-hello`, `hatchling-pybind11-hello`. - `hello-free-threading` and `core-nanobind-shared` are also wheel-built and tested via cibuildwheel in CI (`cibuildwheel` job); `hello-free-threading` only through that job, not nox. diff --git a/README.md b/README.md index 61082c2..1a66ca8 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ configured via `[tool.scikit-build]`. | Project | Demonstrates | | --- | --- | | [`core-c-hello`](projects/core-c-hello) | Raw C extension | +| [`core-cffi-hello`](projects/core-cffi-hello) | [cffi](https://cffi.readthedocs.io) via the `cffi-gen-src` source generator; requires Python 3.10+ | | [`core-cython-hello`](projects/core-cython-hello) | Cython via [cython-cmake](https://github.com/scikit-build/cython-cmake) | | [`core-pybind11-hello`](projects/core-pybind11-hello) | pybind11 binding | | [`core-nanobind-shared`](projects/core-nanobind-shared) | [nanobind](https://nanobind.readthedocs.io) binding linking a shared library shipped in the wheel | diff --git a/noxfile.py b/noxfile.py index 3efe108..bec6a97 100644 --- a/noxfile.py +++ b/noxfile.py @@ -22,6 +22,10 @@ "core-pybind11-hello", "hatchling-pybind11-hello", ] +if sys.version_info >= (3, 10): + # cffi 2.1 (first release with cffi-gen-src) requires Python 3.10 + hello_list.append("core-cffi-hello") + long_hello_list.append("core-cffi-hello") if sys.version_info >= (3, 13): hello_list.append("hello-free-threading") diff --git a/projects/core-cffi-hello/CMakeLists.txt b/projects/core-cffi-hello/CMakeLists.txt new file mode 100644 index 0000000..45063fc --- /dev/null +++ b/projects/core-cffi-hello/CMakeLists.txt @@ -0,0 +1,28 @@ +cmake_minimum_required(VERSION 3.15...4.3) + +project(${SKBUILD_PROJECT_NAME} VERSION ${SKBUILD_PROJECT_VERSION} LANGUAGES C) + +find_package(Python REQUIRED COMPONENTS Interpreter Development.Module + ${SKBUILD_SABI_COMPONENT}) + +set(HELLO_CDEF "${CMAKE_CURRENT_SOURCE_DIR}/src/hello/_hello.cdef.txt") +set(HELLO_CSRC "${CMAKE_CURRENT_SOURCE_DIR}/src/hello/_hello.csrc.c") +add_custom_command( + OUTPUT _hello.c + COMMAND Python::Interpreter -m cffi.gen_src read-sources hello._hello + "${HELLO_CDEF}" "${HELLO_CSRC}" _hello.c + DEPENDS "${HELLO_CDEF}" "${HELLO_CSRC}" + VERBATIM) + +# cffi's generated code targets the limited API unless +# _CFFI_NO_LIMITED_API is defined (or unavailable, like on 3.14t) +if(NOT "${SKBUILD_SABI_COMPONENT}" STREQUAL "") + python_add_library(_hello MODULE USE_SABI "${SKBUILD_SABI_VERSION}" WITH_SOABI + "${CMAKE_CURRENT_BINARY_DIR}/_hello.c" csrc/square.c) +else() + python_add_library(_hello MODULE WITH_SOABI + "${CMAKE_CURRENT_BINARY_DIR}/_hello.c" csrc/square.c) +endif() +target_include_directories(_hello PRIVATE csrc) + +install(TARGETS _hello DESTINATION hello) diff --git a/projects/core-cffi-hello/README.md b/projects/core-cffi-hello/README.md new file mode 100644 index 0000000..081d39e --- /dev/null +++ b/projects/core-cffi-hello/README.md @@ -0,0 +1,49 @@ +# core-cffi-hello + +A minimal [scikit-build-core](https://scikit-build-core.readthedocs.io) + +[cffi](https://cffi.readthedocs.io) project that generates the extension +source at build time with the +[`cffi-gen-src`](https://cffi.readthedocs.io/en/stable/cffi-gen-src.html) CLI +(new in cffi 2.1). It is the scikit-build-core analog of the meson-python +example in the cffi documentation. + +## Layout + +- `csrc/`: a plain C library (`square.c`/`square.h`) with no Python or cffi + knowledge. +- `src/hello/_hello.cdef.txt` and `src/hello/_hello.csrc.c`: the FFI + definitions and the C source prelude, read by `cffi-gen-src read-sources` + and turned into `_hello.c`. This replaces the classic builder script (an + `FFI` object with `cdef()` and `set_source()`); if you already have one, + the `exec-python` subcommand runs it directly instead. +- `src/hello/`: the pure Python package, picked up automatically because it + matches the project name. `wheel.exclude` keeps the cdef/csrc build inputs + out of the wheel. + +## The parts that trip people up + +- **Run the generator from CMake.** An `add_custom_command` invokes + `python -m cffi.gen_src read-sources hello._hello _hello.c` + (the module form of the `cffi-gen-src` script), so cffi from + `[build-system].requires` is used. The generated file lands in the build + tree and is compiled together with `csrc/square.c` by `python_add_library`. + The module name (`hello._hello`) is a command-line argument here, not + something read from a file — it must match where the extension is + installed. +- **Compile/link flags move to CMake.** `cffi-gen-src` ignores the + `libraries=`, `include_dirs=`, `library_dirs=`, and `extra_compile_args=` + arguments of `set_source()`; use `target_include_directories`, + `target_link_libraries`, etc. instead. +- **The limited API / abi3.** cffi's generated code targets the limited API; + on MSVC release builds it defines `Py_LIMITED_API` on its own (unless + `CFFI_NO_LIMITED_API` is defined), which makes `pyconfig.h` auto-link + `python3.lib` — and the link fails if CMake linked the version-specific + library instead. This sample leans in: `wheel.py-api = "cp310"` plus + `python_add_library(... USE_SABI 3.10 ...)` produce a single abi3 wheel + for all CPython >= 3.10. If you want per-version wheels instead, define + `CFFI_NO_LIMITED_API` when compiling on MSVC. +- **cffi is a runtime dependency.** The compiled module imports + `_cffi_backend` when loaded, so `cffi` stays in `[project].dependencies`. +- **Python 3.10+.** cffi 2.1 is the first release shipping `cffi-gen-src` + and requires Python 3.10, so `requires-python` (and the gate in the + top-level `noxfile.py`) reflect that. diff --git a/projects/core-cffi-hello/csrc/square.c b/projects/core-cffi-hello/csrc/square.c new file mode 100644 index 0000000..e499420 --- /dev/null +++ b/projects/core-cffi-hello/csrc/square.c @@ -0,0 +1,3 @@ +#include "square.h" + +int square(int n) { return n * n; } diff --git a/projects/core-cffi-hello/csrc/square.h b/projects/core-cffi-hello/csrc/square.h new file mode 100644 index 0000000..ea6b4fc --- /dev/null +++ b/projects/core-cffi-hello/csrc/square.h @@ -0,0 +1,3 @@ +#pragma once + +int square(int n); diff --git a/projects/core-cffi-hello/pyproject.toml b/projects/core-cffi-hello/pyproject.toml new file mode 100644 index 0000000..b6f7464 --- /dev/null +++ b/projects/core-cffi-hello/pyproject.toml @@ -0,0 +1,22 @@ +[build-system] +requires = ["scikit-build-core", "cffi>=2.1"] +build-backend = "scikit_build_core.build" + +[project] +name = "hello" +version = "1.2.3" +authors = [ + { name = "The scikit-build team" }, +] +description = "a minimal example package (cffi cffi-gen-src version)" +# cffi 2.1, the first release with cffi-gen-src, requires Python 3.10 +requires-python = ">=3.10" +license = "MIT" +# The generated extension imports _cffi_backend at runtime +dependencies = ["cffi"] + +[tool.scikit-build] +# The cdef/csrc files are build-time inputs, not part of the package +wheel.exclude = ["hello/_hello.cdef.txt", "hello/_hello.csrc.c"] +# cffi's generated code is limited-API compatible; ship one abi3 wheel +wheel.py-api = "cp310.cp315t" diff --git a/projects/core-cffi-hello/src/hello/__init__.py b/projects/core-cffi-hello/src/hello/__init__.py new file mode 100644 index 0000000..585df36 --- /dev/null +++ b/projects/core-cffi-hello/src/hello/__init__.py @@ -0,0 +1,7 @@ +from ._hello import lib + +__all__ = ["square"] + + +def square(n: int) -> int: + return lib.square(n) diff --git a/projects/core-cffi-hello/src/hello/_hello.cdef.txt b/projects/core-cffi-hello/src/hello/_hello.cdef.txt new file mode 100644 index 0000000..771c081 --- /dev/null +++ b/projects/core-cffi-hello/src/hello/_hello.cdef.txt @@ -0,0 +1 @@ +int square(int n); diff --git a/projects/core-cffi-hello/src/hello/_hello.csrc.c b/projects/core-cffi-hello/src/hello/_hello.csrc.c new file mode 100644 index 0000000..930ea7e --- /dev/null +++ b/projects/core-cffi-hello/src/hello/_hello.csrc.c @@ -0,0 +1 @@ +#include "square.h" diff --git a/projects/core-cffi-hello/tests/test_hello.py b/projects/core-cffi-hello/tests/test_hello.py new file mode 100644 index 0000000..8f500a1 --- /dev/null +++ b/projects/core-cffi-hello/tests/test_hello.py @@ -0,0 +1,5 @@ +import hello + + +def test_square(): + assert hello.square(4) == 16