Skip to content

Commit 6b715c7

Browse files
committed
Replace Cython with nanobind for Python bindings
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 fbc18c1 commit 6b715c7

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

+2309
-3714
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.3.1
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@v6
@@ -122,7 +122,7 @@ jobs:
122122
strategy:
123123
fail-fast: false
124124
matrix:
125-
python_version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.13t", "3.14", "3.14t"]
125+
python_version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.13t", "3.14", "3.14t"]
126126
steps:
127127
- uses: actions/checkout@v6
128128
- name: Set up Python
@@ -157,7 +157,7 @@ jobs:
157157
strategy:
158158
fail-fast: false
159159
matrix:
160-
python_version: ["3.8", "3.13", "3.14"]
160+
python_version: ["3.9", "3.13", "3.14"]
161161
steps:
162162
- uses: actions/checkout@v6
163163
- 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.8 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 ?= .venv/bin/python
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+
$(PYTHON) -m pip install -e . --no-build-isolation
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 --no-build-isolation
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 . --no-build-isolation
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: 42 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,46 @@
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+
metadata.version.provider = "scikit_build_core.metadata.regex"
39+
metadata.version.input = "src/pystack/_version.py"
40+
sdist.include = ["src/pystack/_version.py"]
41+
42+
[tool.scikit-build.cmake.define]
43+
CMAKE_BUILD_TYPE = "Release"
1144

1245
[tool.ruff]
1346
line-length = 95
@@ -43,15 +76,15 @@ type = [
4376
underlines = "-~"
4477

4578
[tool.cibuildwheel]
46-
build = ["cp38-*", "cp39-*", "cp310-*", "cp311-*"]
79+
build = ["cp39-*", "cp310-*", "cp311-*", "cp312-*", "cp313-*", "cp314-*"]
4780
manylinux-x86_64-image = "manylinux2014"
4881
manylinux-i686-image = "manylinux2014"
4982
musllinux-x86_64-image = "musllinux_1_2"
5083
skip = "*-musllinux_aarch64"
5184

5285
[tool.cibuildwheel.linux]
5386
before-all = [
54-
"yum install -y libzstd-devel",
87+
"yum install -y libzstd-devel cmake",
5588
"cd /",
5689
"VERS=0.193",
5790
"curl https://sourceware.org/elfutils/ftp/$VERS/elfutils-$VERS.tar.bz2 > ./elfutils.tar.bz2",
@@ -74,7 +107,7 @@ before-all = [
74107
# set the FNM_EXTMATCH macro to get the build to succeed is seen here:
75108
# https://git.alpinelinux.org/aports/tree/main/elfutils/musl-macros.patch
76109
"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",
110+
"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",
78111
"VERS=0.193",
79112
"curl https://sourceware.org/elfutils/ftp/$VERS/elfutils-$VERS.tar.bz2 > ./elfutils.tar.bz2",
80113
"tar -xf elfutils.tar.bz2",
@@ -88,16 +121,12 @@ before-all = [
88121
]
89122

90123
[tool.coverage.run]
91-
plugins = [
92-
"Cython.Coverage",
93-
]
94124
source = [
95125
"src/pystack",
96126
]
97127
branch = true
98128
parallel = true
99129
omit = [
100-
"stringsource",
101130
"tests/integration/*program*.py",
102131
]
103132

0 commit comments

Comments
 (0)