Skip to content

Commit 1c15147

Browse files
authored
Replace Cython with nanobind for Python bindings (#272)
The Cython-based bindings required extensive marshaling between Python and C++ data structures, causing performance overhead and maintenance complexity. Each call crossed the language boundary multiple times, converting maps, thread lists, and frame data back and forth. This migration moves to nanobind with scikit-build-core for the build system. The key architectural change is moving logic that previously lived in Cython or Python into C++: maps parsing, version detection, and thread construction now happen entirely in C++ before returning results to Python. This eliminates round-trips and simplifies the codebase by removing the Cython layer entirely. The Python API remains unchanged. Signed-off-by: Pablo Galindo Salgado <pablogsal@gmail.com>
1 parent 0489b7b commit 1c15147

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+2395
-3810
lines changed

.github/workflows/build_wheels.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ jobs:
6868
- name: Build wheels
6969
uses: pypa/cibuildwheel@v3.4.0
7070
env:
71-
CIBW_BUILD: "cp3{8..14}{t,}-${{ matrix.wheel_type }}"
71+
CIBW_BUILD: "cp3{9..14}{t,}-${{ matrix.wheel_type }}"
7272
CIBW_ARCHS_LINUX: auto
7373
CIBW_ENABLE: cpython-prerelease cpython-freethreading
7474
- uses: actions/upload-artifact@v7
@@ -157,7 +157,7 @@ jobs:
157157
strategy:
158158
fail-fast: false
159159
matrix:
160-
python_version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.13t", "3.14", "3.14t"]
160+
python_version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.13t", "3.14", "3.14t"]
161161
steps:
162162
- uses: actions/checkout@v6
163163
- name: Set up Python
@@ -192,7 +192,7 @@ jobs:
192192
strategy:
193193
fail-fast: false
194194
matrix:
195-
python_version: ["3.8", "3.13", "3.14"]
195+
python_version: ["3.9", "3.13", "3.14"]
196196
steps:
197197
- uses: actions/checkout@v6
198198
- name: Set up Python

.github/workflows/coverage.yml

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -38,29 +38,40 @@ jobs:
3838
sudo apt-get install -qy \
3939
gdb \
4040
lcov \
41+
cmake \
42+
ninja-build \
4143
libdw-dev \
4244
libelf-dev \
4345
python3.10-dev \
4446
python3.10-dbg
4547
- name: Install Python dependencies
4648
run: |
47-
python3 -m pip install --upgrade pip cython pkgconfig
48-
make test-install
49+
python3 -m pip install --upgrade pip scikit-build-core nanobind
50+
python3 -m pip install -e . -r requirements-test.txt
4951
- name: Disable ptrace security restrictions
5052
run: |
5153
echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope
52-
- name: Compute Python + Cython coverage
54+
- name: Compute Python coverage
5355
run: |
54-
make pycoverage
56+
python3 -m pytest -vvv --log-cli-level=info -s --color=yes \
57+
--cov=pystack --cov=tests --cov-config=pyproject.toml --cov-report=term \
58+
--cov-append tests --cov-fail-under=85
59+
python3 -m coverage lcov -i -o pycoverage.lcov
60+
genhtml *coverage.lcov --branch-coverage --output-directory pystack-coverage
5561
- name: Compute C++ coverage
5662
run: |
57-
make ccoverage
58-
- name: Upload {P,C}ython report to Codecov
63+
rm -rf build
64+
CFLAGS="-O0 -pg --coverage" CXXFLAGS="-O0 -pg --coverage" SKBUILD_BUILD_DIR=build pip install -e . --no-build-isolation
65+
python3 -m pytest tests -v
66+
find build -name "*.gcda" -o -name "*.gcno" | head -5
67+
lcov --capture --directory build --output-file cppcoverage.lcov
68+
lcov --extract cppcoverage.lcov '*/src/pystack/_pystack/*' --output-file cppcoverage.lcov
69+
- name: Upload Python report to Codecov
5970
uses: codecov/codecov-action@v5
6071
with:
6172
token: ${{ secrets.CODECOV_TOKEN }}
6273
files: pycoverage.lcov
63-
flags: python_and_cython
74+
flags: python
6475
- name: Upload C++ report to Codecov
6576
uses: codecov/codecov-action@v5
6677
with:

.github/workflows/lint_and_docs.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ jobs:
2222
python3 -m pip install -e .
2323
- name: Lint sources
2424
run: |
25-
make lint
25+
make lint PYTHON=python3
2626
python3 -m pre_commit run --all-files --hook-stage pre-push
2727
- name: Build docs
2828
run: |

CMakeLists.txt

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
cmake_minimum_required(VERSION 3.17...3.27)
2+
3+
project(pystack LANGUAGES CXX)
4+
5+
set(CMAKE_CXX_STANDARD 17)
6+
set(CMAKE_CXX_STANDARD_REQUIRED ON)
7+
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
8+
9+
# Find Python
10+
find_package(Python 3.9 COMPONENTS Interpreter Development.Module REQUIRED)
11+
12+
# Find nanobind
13+
execute_process(
14+
COMMAND "${Python_EXECUTABLE}" -m nanobind --cmake_dir
15+
OUTPUT_STRIP_TRAILING_WHITESPACE OUTPUT_VARIABLE nanobind_ROOT)
16+
find_package(nanobind CONFIG REQUIRED)
17+
18+
# Find libelf and libdw via pkg-config
19+
find_package(PkgConfig REQUIRED)
20+
pkg_check_modules(LIBELF REQUIRED libelf)
21+
pkg_check_modules(LIBDW REQUIRED libdw)
22+
23+
# Add the extension module subdirectory
24+
add_subdirectory(src/pystack/_pystack)

Makefile

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
PYTHON ?= python
1+
PYTHON ?= python3
22
DOCKER_IMAGE ?= pystack
33
DOCKER_SRC_DIR ?= /src
44

@@ -13,19 +13,19 @@ ENV :=
1313

1414
.PHONY: build
1515
build: ## (default) Build package extensions in-place
16-
$(PYTHON) setup.py build_ext --inplace
16+
$(ENV) $(PIP_INSTALL) -e .
1717

1818
.PHONY: dist
1919
dist: ## Generate Python distribution files
20-
$(PYTHON) -m pep517.build .
20+
$(PYTHON) -m build
2121

2222
.PHONY: install-sdist
2323
install-sdist: dist ## Install from source distribution
2424
$(ENV) $(PIP_INSTALL) $(wildcard dist/*.tar.gz)
2525

2626
.PHONY: test-install
2727
test-install: ## Install with test dependencies
28-
$(ENV) CYTHON_TEST_MACROS=1 $(PIP_INSTALL) -e . -r requirements-test.txt
28+
$(ENV) $(PIP_INSTALL) -e . -r requirements-test.txt
2929

3030
.PHONY: docker-build
3131
docker-build: ## Build the Docker image
@@ -59,7 +59,7 @@ check: ## Run the test suite
5959
pycoverage: ## Run the test suite, with Python code coverage
6060
$(PYTHON) -m pytest -vvv --log-cli-level=info -s --color=yes \
6161
--cov=pystack --cov=tests --cov-config=pyproject.toml --cov-report=term \
62-
--cov-append $(PYTEST_ARGS) tests --cov-fail-under=92
62+
--cov-append $(PYTEST_ARGS) tests --cov-fail-under=85
6363
$(PYTHON) -m coverage lcov -i -o pycoverage.lcov
6464
genhtml *coverage.lcov --branch-coverage --output-directory pystack-coverage
6565

@@ -71,10 +71,9 @@ valgrind: ## Run valgrind, with the correct configuration
7171
.PHONY: ccoverage
7272
ccoverage: ## Run the test suite, with C++ code coverage
7373
$(MAKE) clean
74-
CFLAGS="$(CFLAGS) -O0 -pg --coverage" CXXFLAGS="$(CXXFLAGS) -O0 -pg --coverage" $(MAKE) build
74+
CFLAGS="-O0 -pg --coverage" CXXFLAGS="-O0 -pg --coverage" $(PIP_INSTALL) -e .
7575
$(MAKE) check
76-
gcov -i build/*/src/pystack/_pystack -i -d
77-
lcov --capture --directory . --output-file cppcoverage.lcov
76+
lcov --capture --directory . --output-file cppcoverage.lcov
7877
lcov --extract cppcoverage.lcov '*/src/pystack/_pystack/*' --output-file cppcoverage.lcov
7978
genhtml *coverage.lcov --branch-coverage --output-directory pystack-coverage
8079

@@ -116,6 +115,7 @@ clean: ## Clean any built/generated artifacts
116115
find . | grep -E '(\.o|\.gcda|\.gcno|\.gcov\.json\.gz)' | xargs rm -rf
117116
find . | grep -E '(__pycache__|\.pyc|\.pyo)' | xargs rm -rf
118117
rm -rf build
118+
rm -rf _skbuild
119119
rm -f src/pystack/_pystack.*.so
120120
rm -f {cpp,py}coverage.lcov
121121
rm -rf pystack-coverage

news/272.feature.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Replace Cython with nanobind for Python bindings, improving performance by
2+
eliminating round-trips between Python and C++.

pyproject.toml

Lines changed: 53 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,57 @@
11
[build-system]
2+
requires = ["scikit-build-core>=0.4", "nanobind>=1.8"]
3+
build-backend = "scikit_build_core.build"
24

3-
requires = [
4-
"setuptools",
5-
"wheel",
6-
"Cython",
7-
"pkgconfig"
5+
[project]
6+
name = "pystack"
7+
dynamic = ["version"]
8+
description = "Analysis of the stack of remote python processes"
9+
readme = "README.md"
10+
requires-python = ">=3.9"
11+
license = {text = "Apache-2.0"}
12+
authors = [
13+
{name = "Pablo Galindo Salgado"}
814
]
15+
classifiers = [
16+
"Intended Audience :: Developers",
17+
"License :: OSI Approved :: Apache Software License",
18+
"Operating System :: POSIX :: Linux",
19+
"Programming Language :: Python :: 3.9",
20+
"Programming Language :: Python :: 3.10",
21+
"Programming Language :: Python :: 3.11",
22+
"Programming Language :: Python :: 3.12",
23+
"Programming Language :: Python :: 3.13",
24+
"Programming Language :: Python :: 3.14",
25+
"Programming Language :: Python :: Implementation :: CPython",
26+
"Topic :: Software Development :: Debuggers",
27+
]
28+
29+
[project.urls]
30+
Homepage = "https://github.com/bloomberg/pystack"
31+
32+
[project.scripts]
33+
pystack = "pystack.__main__:main"
934

10-
build-backend = 'setuptools.build_meta'
35+
[tool.scikit-build]
36+
wheel.packages = ["src/pystack"]
37+
wheel.install-dir = "pystack"
38+
wheel.exclude = ["pystack/_pystack/**"]
39+
metadata.version.provider = "scikit_build_core.metadata.regex"
40+
metadata.version.input = "src/pystack/_version.py"
41+
sdist.include = ["src/pystack/_version.py"]
42+
sdist.exclude = [
43+
".devcontainer/**",
44+
".github/**",
45+
".vscode/**",
46+
"CONTRIBUTING.md",
47+
"Dockerfile",
48+
"docs/**",
49+
"tests/**",
50+
"uv.lock",
51+
]
52+
53+
[tool.scikit-build.cmake.define]
54+
CMAKE_BUILD_TYPE = "Release"
1155

1256
[tool.ruff]
1357
line-length = 95
@@ -43,15 +87,15 @@ type = [
4387
underlines = "-~"
4488

4589
[tool.cibuildwheel]
46-
build = ["cp38-*", "cp39-*", "cp310-*", "cp311-*"]
90+
build = ["cp39-*", "cp310-*", "cp311-*", "cp312-*", "cp313-*", "cp314-*"]
4791
manylinux-x86_64-image = "manylinux2014"
4892
manylinux-i686-image = "manylinux2014"
4993
musllinux-x86_64-image = "musllinux_1_2"
5094
skip = "*-musllinux_aarch64"
5195

5296
[tool.cibuildwheel.linux]
5397
before-all = [
54-
"yum install -y libzstd-devel",
98+
"yum install -y libzstd-devel cmake",
5599
"cd /",
56100
"VERS=0.193",
57101
"curl https://sourceware.org/elfutils/ftp/$VERS/elfutils-$VERS.tar.bz2 > ./elfutils.tar.bz2",
@@ -74,7 +118,7 @@ before-all = [
74118
# set the FNM_EXTMATCH macro to get the build to succeed is seen here:
75119
# https://git.alpinelinux.org/aports/tree/main/elfutils/musl-macros.patch
76120
"cd /",
77-
"apk add --update argp-standalone bison bsd-compat-headers bzip2-dev flex-dev libtool linux-headers musl-fts-dev musl-libintl musl-obstack-dev xz-dev zlib-dev zstd-dev",
121+
"apk add --update argp-standalone bison bsd-compat-headers bzip2-dev flex-dev libtool linux-headers musl-fts-dev musl-libintl musl-obstack-dev xz-dev zlib-dev zstd-dev cmake",
78122
"VERS=0.193",
79123
"curl https://sourceware.org/elfutils/ftp/$VERS/elfutils-$VERS.tar.bz2 > ./elfutils.tar.bz2",
80124
"tar -xf elfutils.tar.bz2",
@@ -88,16 +132,12 @@ before-all = [
88132
]
89133

90134
[tool.coverage.run]
91-
plugins = [
92-
"Cython.Coverage",
93-
]
94135
source = [
95136
"src/pystack",
96137
]
97138
branch = true
98139
parallel = true
99140
omit = [
100-
"stringsource",
101141
"tests/integration/*program*.py",
102142
]
103143

0 commit comments

Comments
 (0)