Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
42 changes: 42 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -96,3 +96,45 @@ 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 ./...
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
104 changes: 104 additions & 0 deletions _examples/callbacks/callbacks.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// 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 callbacks has Go functions that take Python callables, and call
// them before returning.
package callbacks

import (
"fmt"
"sync"
"time"
)

// Each calls fun for i in 0..n-1, with a label made from i.
func Each(n int, fun func(i int, label string)) {
for i := 0; i < n; i++ {
fun(i, fmt.Sprintf("item-%d", i))
}
}

// Mixed calls fun with a bool, a float and an unsigned integer.
func Mixed(fun func(on bool, x float64, u uint8)) {
fun(true, 1.5, 200)
fun(false, -2.25, 7)
}

// Twice calls fun, which takes no arguments, two times.
func Twice(fun func()) {
fun()
fun()
}

// Describe calls fun with a string and a fmt.Stringer, as interface{} values.
// They arrive as strings, made by fmt.Sprintf("%s", v).
func Describe(fun func(v interface{})) {
fun("a string")
fun(1500 * time.Millisecond)
}

// Count returns how many of 0..n-1 keep says yes to.
func Count(n int, keep func(i int) bool) int {
total := 0
for i := 0; i < n; i++ {
if keep(i) {
total++
}
}
return total
}

// Sum adds up what val returns for 0..n-1.
func Sum(n int, val func(i int) int) int {
total := 0
for i := 0; i < n; i++ {
total += val(i)
}
return total
}

// Widest returns the largest of what size returns for 0..n-1.
func Widest(n int, size func(i int) uint) uint {
var widest uint
for i := 0; i < n; i++ {
if w := size(i); w > widest {
widest = w
}
}
return widest
}

// Apply returns f(x).
func Apply(x float64, f func(x float64) float64) float64 {
return f(x)
}

// Counter counts how many times it has been visited.
type Counter struct {
N int
}

// Visit calls fun with the counter itself, which arrives as a handle.
func (c *Counter) Visit(times int, fun func(c *Counter, n int)) {
for i := 0; i < times; i++ {
c.N++
fun(c, c.N)
}
}

// Check calls fun with the counter itself, and reports what it answered.
func (c *Counter) Check(fun func(c *Counter, n int) bool) bool {
return fun(c, c.N)
}

// InGoroutine calls fun from another goroutine, and waits for it.
func InGoroutine(fun func(i int)) {
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
fun(7)
}()
wg.Wait()
}
83 changes: 83 additions & 0 deletions _examples/callbacks/test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# 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.

from __future__ import print_function

import io
import sys

import callbacks

print("--- Each: int and string arguments")
callbacks.Each(3, lambda i, label: print("each:", i, label))

print("--- Mixed: bool, float and uint8 arguments")
# bool() because the pybindgen backend passes a bool as 1 or 0
callbacks.Mixed(lambda on, x, u: print("mixed:", bool(on), x, u))

print("--- Twice: no arguments")
calls = []
callbacks.Twice(lambda: calls.append(1))
print("twice:", len(calls))

print("--- Counter.Visit: a Go struct arrives as a handle")
c = callbacks.Counter()

def visit(handle, n):
seen = callbacks.Counter(handle=handle)
print("visit:", n, seen.N)

c.Visit(2, visit)
print("counter:", c.N)

print("--- Describe: an interface{} arrives as a string")
callbacks.Describe(lambda v: print("describe:", repr(v)))

print("--- Count: a bool result")
print("count:", callbacks.Count(10, lambda i: i % 3 == 0))

print("--- Sum: an int result")
print("sum:", callbacks.Sum(5, lambda i: i * i))

print("--- Widest: a uint result")
print("widest:", callbacks.Widest(4, lambda i: i * 10))

print("--- Apply: a float result")
print("apply:", callbacks.Apply(1.5, lambda x: x * 2))

print("--- Counter.Check: a handle argument and a bool result")
print("check:", c.Check(lambda handle, n: callbacks.Counter(handle=handle).N == n))

print("--- a bound method")

class Box(object):
def __init__(self):
self.items = []

def add(self, i, label):
self.items.append((i, label))

box = Box()
callbacks.Each(2, box.add)
print("box:", box.items)

print("--- called from another goroutine")
callbacks.InGoroutine(lambda i: print("goroutine:", i))

print("--- an exception in a callback is reported, and Go carries on")
seen = []

def boom(i, label):
seen.append(i)
raise ValueError("boom %d" % i)

stderr, sys.stderr = sys.stderr, io.StringIO()
try:
callbacks.Each(3, boom)
reported = sys.stderr.getvalue()
finally:
sys.stderr = stderr
print("calls:", len(seen), "reported:", reported.count("ValueError: boom"))

print("OK")
5 changes: 5 additions & 0 deletions _examples/slices/slices.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ type SliceInt32 []int32
type SliceInt64 []int64

type SliceComplex []complex128
type SliceComplex64 []complex64

type SliceIface []interface{}

Expand Down Expand Up @@ -61,6 +62,10 @@ func CmplxSqrt(arr SliceComplex) SliceComplex {
return res
}

func CmplxArray() [3]complex128 {
return [3]complex128{1 + 1i, 2 + 2i, 3 + 3i}
}

func GetEmptyMatrix(xSize int, ySize int) [][]bool {
result := [][]bool{}

Expand Down
13 changes: 13 additions & 0 deletions _examples/slices/test.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,19 @@
assert math.isclose(root_squared.real, orig.real)
assert math.isclose(root_squared.imag, orig.imag)

# complex elements: assignment and append, in both float widths, and reading an array
cmplx[0] = 3 + 4j
assert cmplx[0] == 3 + 4j
cmplx.append(1 - 2j)
assert len(cmplx) == 17 and cmplx[16] == 1 - 2j

cmplx64 = slices.SliceComplex64([1 + 2j, 3.5 - 4.25j])
cmplx64[1] = -0.5 + 8j
cmplx64.append(2j)
assert list(cmplx64) == [1 + 2j, -0.5 + 8j, 2j]

cmplx_arr = slices.CmplxArray()
assert len(cmplx_arr) == 3 and cmplx_arr[2] == 3 + 3j

matrix = slices.GetEmptyMatrix(4,4)
for i in range(4):
Expand Down
65 changes: 65 additions & 0 deletions bind/backend.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// 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 bind

import (
"fmt"
"os"
"strings"
)

// BackendEnvVar is the environment variable that selects which tool is used
// to bind the generated cgo shim to CPython.
const BackendEnvVar = "GOPY_BACKEND"

// Backend names a CPython binding tool.
type Backend string

const (
BackendPyBindGen Backend = "pybindgen" // default
BackendCFFI Backend = "cffi"
BackendPyBind11 Backend = "pybind11"
BackendNanobind Backend = "nanobind"
BackendCAPI Backend = "capi"
BackendCGO Backend = "cgo"
)

// backends lists every known backend and whether gopy can generate it yet.
var backends = []struct {
name Backend
implemented bool
}{
{BackendPyBindGen, true},
{BackendCFFI, true},
{BackendPyBind11, false},
{BackendNanobind, false},
{BackendCAPI, false},
{BackendCGO, false},
}

// BackendFromEnv returns the backend selected by GOPY_BACKEND.
// An unset or empty variable selects pybindgen.
func BackendFromEnv() (Backend, error) {
return parseBackend(os.Getenv(BackendEnvVar))
}

func parseBackend(v string) (Backend, error) {
v = strings.ToLower(strings.TrimSpace(v))
if v == "" {
return BackendPyBindGen, nil
}
names := make([]string, len(backends))
for i, b := range backends {
names[i] = string(b.name)
if string(b.name) != v {
continue
}
if !b.implemented {
return "", fmt.Errorf("gopy: %s=%q is not implemented yet", BackendEnvVar, v)
}
return b.name, nil
}
return "", fmt.Errorf("gopy: unknown %s=%q (valid values: %s)", BackendEnvVar, v, strings.Join(names, ", "))
}
Loading
Loading