Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
44cf245
Establish `BackendFromEnv` machinery
b-long Sep 19, 2026
9cd2233
Use experimental cffi backend by GOPY_BACKEND=cffi
b-long Sep 19, 2026
3bbe911
Create a separate cffi job for Github Actions
b-long Sep 19, 2026
f7da474
Prevent cffi from unloading Go shared library
b-long Sep 19, 2026
2d8f4e2
Add to/from bytes & `pkg` mode, start Complex nums
b-long Sep 19, 2026
93188fb
Complete complex64/128 number support
b-long Sep 19, 2026
36c319c
Support Python callbacks in cffi backend
b-long Sep 20, 2026
d1206d0
Add interface{} args & results in cffi callbacks
b-long Sep 20, 2026
a65a870
Complete cffi support for full test suite
b-long Sep 20, 2026
363ee92
Isolate cffi errors per OS thread
b-long Sep 24, 2026
af1fe2d
Fix cffi DLL load failure on Windows
b-long Sep 24, 2026
6454e06
Generalize cffi impl, start pybind11 GOPY_BACKEND
b-long Sep 25, 2026
d385fff
Add pybind11 backend to CI matrix
b-long Sep 25, 2026
b7474fd
Fix pybind11 DLL load failure on Windows
b-long Sep 25, 2026
3460138
fix: static-link libstdc++/libgcc on Windows
b-long Sep 25, 2026
9325cee
Add []byte support, shrink pybind11 CI skips
b-long Sep 26, 2026
faed63e
Raise pybind11 CI test timeout to 30m
b-long Sep 26, 2026
9fb3276
Support complex numbers in pybind11 backend
b-long Sep 26, 2026
4af5643
Add pybind11 callbacks; fix GIL deadlock and crash
b-long Sep 26, 2026
325a6a5
Add benchmark job for cffi, pybind11, & pybindgen
b-long Sep 26, 2026
cfffb88
Add nanobind backend (GOPY_BACKEND=nanobind)
b-long Sep 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/requirements-cffi.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Python packages for the GOPY_BACKEND=cffi job in workflows/ci.yml.
# pybindgen is deliberately absent: the cffi backend must not need it.
cffi
# used by the memory-leak checks on Windows, where the resource module is missing
psutil
5 changes: 5 additions & 0 deletions .github/requirements-nanobind.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Python packages for the GOPY_BACKEND=nanobind job in workflows/ci.yml.
# pybindgen is deliberately absent: the nanobind backend must not need it.
nanobind
# used by the memory-leak checks on Windows, where the resource module is missing
psutil
5 changes: 5 additions & 0 deletions .github/requirements-pybind11.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Python packages for the GOPY_BACKEND=pybind11 job in workflows/ci.yml.
# pybindgen is deliberately absent: the pybind11 backend must not need it.
pybind11
# used by the memory-leak checks on Windows, where the resource module is missing
psutil
184 changes: 184 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -96,3 +96,187 @@ jobs:
- name: Upload-Coverage
if: matrix.platform == 'ubuntu-latest'
uses: codecov/codecov-action@v4

# Builds and tests the opt-in cffi backend (GOPY_BACKEND=cffi). Runs beside
# the main matrix, on one Go version, and stops at the first failure.
cffi:
name: cffi backend (${{ matrix.platform }}, Python ${{ matrix.python-version }})
strategy:
fail-fast: true
matrix:
platform: [ubuntu-latest, windows-latest, macos-15]
python-version: ['3.11', '3.12']
runs-on: ${{ matrix.platform }}
env:
GOPY_BACKEND: cffi
# print the python stack if the process crashes, e.g. at exit
PYTHONFAULTHANDLER: 1
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
cache: pip
cache-dependency-path: .github/requirements-cffi.txt

- name: Install Go
uses: actions/setup-go@v5
with:
go-version: 1.25.x
cache: true

- name: Install packages
run: |
python -m pip install -r .github/requirements-cffi.txt
go install golang.org/x/tools/cmd/goimports@v0.29.0

- name: Build
run: go build -v ./...

- name: Test
run: go test -v ./...

# Builds and tests the opt-in pybind11 backend (GOPY_BACKEND=pybind11).
# Runs beside the main matrix, on one Go version, and stops at the first
# failure. Shares its cgo shim with cffi (see noAPIShim in bind/cffi.go);
# what's new here is compiling a C++ file against that shim with a real
# C++ compiler, so unlike the cffi job this needs no extra system packages
# (a C++ compiler is already required for CGO_ENABLED=1 on every platform).
pybind11:
name: pybind11 backend (${{ matrix.platform }}, Python ${{ matrix.python-version }})
strategy:
fail-fast: true
matrix:
platform: [ubuntu-latest, windows-latest, macos-15]
python-version: ['3.11', '3.12']
runs-on: ${{ matrix.platform }}
env:
GOPY_BACKEND: pybind11
# CXX defaults to "c++" in buildCXXModule (cmd_build.go), which isn't a
# recognized command on windows-latest's MinGW toolchain (the same one
# CGO_ENABLED=1 already needs there, so no extra install is needed).
CXX: g++
PYTHONFAULTHANDLER: 1
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
cache: pip
cache-dependency-path: .github/requirements-pybind11.txt

- name: Install Go
uses: actions/setup-go@v5
with:
go-version: 1.25.x
cache: true

- name: Install packages
run: |
python -m pip install -r .github/requirements-pybind11.txt
go install golang.org/x/tools/cmd/goimports@v0.29.0

- name: Build
run: go build -v ./...

# The skipped test is pybind11's own auto-generated TypeError wording,
# which differs from pybindgen's for one case in TestHi (Person's
# setters given the wrong argument type) -- a message-text difference,
# not a functional one.
#
# -timeout raises go test's own default (10m): each test here compiles
# twice (cgo, then a separate C++ step), against cffi's one, and once
# measured at ~18.5s/test average on windows-latest, 32 tests alone used
# 590s of the default budget.
- name: Test
run: go test -v -timeout=30m -skip '^TestHi$' ./...

# Builds and tests the opt-in nanobind backend (GOPY_BACKEND=nanobind).
# Same shape as the pybind11 job above (the two share a cgo shim and a
# build step, see buildCXXModule in cmd_build.go); nanobind additionally
# compiles its own runtime (nb_combined.cpp) into every module, which
# needs no extra system packages either.
nanobind:
name: nanobind backend (${{ matrix.platform }}, Python ${{ matrix.python-version }})
strategy:
fail-fast: true
matrix:
platform: [ubuntu-latest, windows-latest, macos-15]
python-version: ['3.11', '3.12']
runs-on: ${{ matrix.platform }}
env:
GOPY_BACKEND: nanobind
# see the pybind11 job
CXX: g++
PYTHONFAULTHANDLER: 1
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
cache: pip
cache-dependency-path: .github/requirements-nanobind.txt

- name: Install Go
uses: actions/setup-go@v5
with:
go-version: 1.25.x
cache: true

- name: Install packages
run: |
python -m pip install -r .github/requirements-nanobind.txt
go install golang.org/x/tools/cmd/goimports@v0.29.0

- name: Build
run: go build -v ./...

# Skipped for the same reason as in the pybind11 job: nanobind words its
# own auto-generated TypeError the same way pybind11 does, not the way
# pybindgen does.
- name: Test
run: go test -v -timeout=30m -skip '^TestHi$' ./...

# Compares per-call overhead across backends (see _examples/bench/run.sh):
# not a pass/fail check, just a table uploaded as a build artifact. Runs
# on ubuntu-latest only -- the comparison is between backends, not OSes.
benchmark:
name: benchmark backends
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'

- name: Install Go
uses: actions/setup-go@v5
with:
go-version: 1.25.x
cache: true

- name: Install packages
run: |
python -m pip install pybindgen cffi pybind11 nanobind
go install golang.org/x/tools/cmd/goimports@v0.29.0

- name: Run benchmark
run: bash _examples/bench/run.sh | tee benchmark.txt

- name: Upload benchmark table
uses: actions/upload-artifact@v4
with:
name: benchmark
path: benchmark.txt
1 change: 1 addition & 0 deletions SUPPORT_MATRIX.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ don't modify manually.
Feature |py3
--- | ---
_examples/arrays | yes
_examples/callbacks | yes
_examples/cgo | yes
_examples/consts | yes
_examples/cstrings | yes
Expand Down
23 changes: 23 additions & 0 deletions _examples/bench/bench.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// Copyright 2026 The go-python Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// Package bench has a few trivial functions used to compare the per-call
// overhead of gopy's backends (GOPY_BACKEND); see bench.py and the
// "benchmark" job in .github/workflows/ci.yml. Not part of the test suite
// itself: no _examples/bench entry in main_test.go's features map.
package bench

// Add returns the sum of its arguments: the cheapest possible call, to
// isolate per-call FFI overhead from any argument-marshaling cost.
func Add(i, j int) int {
return i + j
}

// Concat concatenates two strings: a second data point, since string
// arguments/returns cross the boundary very differently from an int
// (a managed buffer + length or a null-terminated copy, depending on the
// backend) and might not share the int case's relative cost.
func Concat(a, b string) string {
return a + b
}
40 changes: 40 additions & 0 deletions _examples/bench/run.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
#!/bin/bash
# Copyright 2026 The go-python Authors. All rights reserved.
# Use of this source code is governed by a BSD-style
# license that can be found in the LICENSE file.

# Builds _examples/bench under each of gopy's backends and prints a table
# comparing run_bench.py's timing/memory numbers across them. Run from the repo
# root; needs pybindgen, cffi, pybind11 and nanobind all installed for the python
# interpreter named by $PYTHON (defaults to python3), and a C++ compiler.
set -eu

PYTHON="${PYTHON:-python3}"
REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
WORK="$(mktemp -d)"
trap 'rm -rf "$WORK"' EXIT

echo "building gopy..."
GOPY="$WORK/gopy"
(cd "$REPO" && go build -o "$GOPY" .)

printf '%-10s %8s %14s %14s %10s\n' backend calls "add (s)" "concat (s)" "peak (KB)"

for backend in pybindgen cffi pybind11 nanobind; do
out="$WORK/$backend"
mkdir -p "$out"
# gopy build cds into -output and runs go build there; give it a module
# that resolves back to this checkout, same as go.mod already does for
# anyone building _examples/* in place.
printf 'module dummy\n\nrequire github.com/go-python/gopy v0.0.0\nreplace github.com/go-python/gopy => %s\n' "$REPO" >"$out/go.mod"
GOPY_BACKEND="$backend" "$GOPY" build -vm="$PYTHON" -output="$out" -no-make -package-prefix= \
"$REPO/_examples/bench" >"$out/build.log" 2>&1 || {
echo "$backend: build failed, see $out/build.log" >&2
tail -n 20 "$out/build.log" >&2
continue
}
cp "$REPO/_examples/bench/run_bench.py" "$out/"
row="$(cd "$out" && "$PYTHON" run_bench.py)"
IFS=, read -r calls add_s concat_s peak_kb <<<"$row"
printf '%-10s %8s %14s %14s %10s\n' "$backend" "$calls" "$add_s" "$concat_s" "$peak_kb"
done
47 changes: 47 additions & 0 deletions _examples/bench/run_bench.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Copyright 2026 The go-python Authors. All rights reserved.
# Use of this source code is governed by a BSD-style
# license that can be found in the LICENSE file.

# Times N calls each of bench.Add and bench.Concat, and the peak memory
# after them, printing one CSV line: iterations,add_seconds,concat_seconds,peak_kb
# (see run.sh, which builds this example under each backend and prints the
# resulting rows as a table -- this script itself doesn't know which
# backend it was built with).

from __future__ import print_function

import sys
import time

import bench

if sys.platform == "win32":
import psutil

def peak_kb():
return psutil.Process().memory_info().rss // 1024
else:
import resource

def peak_kb():
return resource.getrusage(resource.RUSAGE_SELF).ru_maxrss


N = 1000
WARMUP = 100

for i in range(WARMUP):
bench.Add(i, i)
bench.Concat("a", "b")

start = time.perf_counter()
for i in range(N):
bench.Add(i, i)
add_seconds = time.perf_counter() - start

start = time.perf_counter()
for i in range(N):
bench.Concat("a", "b")
concat_seconds = time.perf_counter() - start

print("%d,%f,%f,%d" % (N, add_seconds, concat_seconds, peak_kb()))
Loading
Loading