From 44cf245cee4e9d0c97062f058a2b026ba57fa809 Mon Sep 17 00:00:00 2001 From: b-long Date: Fri, 18 Sep 2026 21:25:35 -0400 Subject: [PATCH 1/9] Establish `BackendFromEnv` machinery --- bind/backend.go | 65 ++++++++++++++++++++++++++++++++++++++++++++ bind/backend_test.go | 46 +++++++++++++++++++++++++++++++ gen.go | 3 ++ 3 files changed, 114 insertions(+) create mode 100644 bind/backend.go create mode 100644 bind/backend_test.go diff --git a/bind/backend.go b/bind/backend.go new file mode 100644 index 00000000..cb2cc4d0 --- /dev/null +++ b/bind/backend.go @@ -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, false}, + {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, ", ")) +} diff --git a/bind/backend_test.go b/bind/backend_test.go new file mode 100644 index 00000000..5c734356 --- /dev/null +++ b/bind/backend_test.go @@ -0,0 +1,46 @@ +// 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 ( + "strings" + "testing" +) + +func TestParseBackend(t *testing.T) { + for _, tc := range []struct { + in string + want Backend + errPart string + }{ + {in: "", want: BackendPyBindGen}, + {in: "pybindgen", want: BackendPyBindGen}, + {in: " PyBindGen ", want: BackendPyBindGen}, + {in: "cffi", errPart: "not implemented yet"}, + {in: "bogus", errPart: "unknown GOPY_BACKEND"}, + } { + got, err := parseBackend(tc.in) + if tc.errPart != "" { + if err == nil || !strings.Contains(err.Error(), tc.errPart) { + t.Errorf("parseBackend(%q): got err=%v, want error containing %q", tc.in, err, tc.errPart) + } + continue + } + if err != nil || got != tc.want { + t.Errorf("parseBackend(%q) = %q, %v; want %q", tc.in, got, err, tc.want) + } + } +} + +func TestBackendFromEnv(t *testing.T) { + t.Setenv(BackendEnvVar, "") + if got, err := BackendFromEnv(); err != nil || got != BackendPyBindGen { + t.Errorf("unset: got %q, %v", got, err) + } + t.Setenv(BackendEnvVar, "bogus") + if _, err := BackendFromEnv(); err == nil { + t.Error("bogus value: want error") + } +} diff --git a/gen.go b/gen.go index 549ee2ad..c43ffd62 100644 --- a/gen.go +++ b/gen.go @@ -63,6 +63,9 @@ func genOutDir(odir string) (string, error) { // mode = gen, build, pkg, exe func genPkg(mode bind.BuildMode, cfg *BuildCfg) error { var err error + if _, err = bind.BackendFromEnv(); err != nil { + return err + } cfg.OutputDir, err = genOutDir(cfg.OutputDir) if err != nil { return err From 9cd22337b779f30f12778fec50a238bfb8c9e77b Mon Sep 17 00:00:00 2001 From: b-long Date: Fri, 18 Sep 2026 21:47:04 -0400 Subject: [PATCH 2/9] Use experimental cffi backend by GOPY_BACKEND=cffi --- bind/backend.go | 2 +- bind/backend_test.go | 3 +- bind/bind.go | 2 + bind/cffi.go | 171 +++++++++++++++++++++++++++++++++++++++++++ bind/cffi_build.py | 165 +++++++++++++++++++++++++++++++++++++++++ bind/gen.go | 28 ++++++- bind/gen_func.go | 19 +++-- bind/gen_map.go | 6 +- bind/gen_slice.go | 51 +++++++------ cmd_build.go | 31 ++++++++ gen.go | 5 +- 11 files changed, 446 insertions(+), 37 deletions(-) create mode 100644 bind/cffi.go create mode 100644 bind/cffi_build.py diff --git a/bind/backend.go b/bind/backend.go index cb2cc4d0..e19a6d9c 100644 --- a/bind/backend.go +++ b/bind/backend.go @@ -32,7 +32,7 @@ var backends = []struct { implemented bool }{ {BackendPyBindGen, true}, - {BackendCFFI, false}, + {BackendCFFI, true}, {BackendPyBind11, false}, {BackendNanobind, false}, {BackendCAPI, false}, diff --git a/bind/backend_test.go b/bind/backend_test.go index 5c734356..cbbe40ac 100644 --- a/bind/backend_test.go +++ b/bind/backend_test.go @@ -18,7 +18,8 @@ func TestParseBackend(t *testing.T) { {in: "", want: BackendPyBindGen}, {in: "pybindgen", want: BackendPyBindGen}, {in: " PyBindGen ", want: BackendPyBindGen}, - {in: "cffi", errPart: "not implemented yet"}, + {in: "cffi", want: BackendCFFI}, + {in: "pybind11", errPart: "not implemented yet"}, {in: "bogus", errPart: "unknown GOPY_BACKEND"}, } { got, err := parseBackend(tc.in) diff --git a/bind/bind.go b/bind/bind.go index 93e78af8..f60acf66 100644 --- a/bind/bind.go +++ b/bind/bind.go @@ -31,6 +31,8 @@ type BindCfg struct { // gopy version string embedded in this binary, stamped into generated // file headers so output can be traced back to the release that produced it Version string + // tool used to bind the cgo shim to CPython, see BackendFromEnv + Backend Backend } // ErrorList is a list of errors diff --git a/bind/cffi.go b/bind/cffi.go new file mode 100644 index 00000000..04dfabcd --- /dev/null +++ b/bind/cffi.go @@ -0,0 +1,171 @@ +// 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 ( + _ "embed" + "strings" +) + +// The cffi backend (GOPY_BACKEND=cffi) generates the same cgo shim as the +// default backend, minus every call into the CPython C API: the shim is a +// plain C shared library that Python loads with cffi. Errors are recorded +// for the Python side to raise, instead of being set with PyErr_SetString. + +//go:embed cffi_build.py +var cffiBuildPy string + +func (g *pyGen) isCFFI() bool { + return g.cfg.Backend == BackendCFFI +} + +// cffiBuildPreamble returns the start of build.py: the cffi recorder. +func (g *pyGen) cffiBuildPreamble() string { + return strings.NewReplacer( + "@NAME@", g.cfg.Name, + "@CMD@", g.cfg.Cmd, + "@VERSION@", g.cfg.Version, + "@LIBEXT@", g.libext, + ).Replace(cffiBuildPy) +} + +// goSetError returns Go code that records an error for Python to raise. +// kind is the name of a Python builtin exception, msg a Go string expression. +func (g *pyGen) goSetError(kind, msg string) string { + return "gopySetError(\"" + kind + "\", " + msg + ")\n" +} + +// same argument positions as goPreamble: 1 = name of package, 2 = cmdstr, +// 4 = GoHandle, 5 = CGoHandle, 6 = all imports, 7 = mainstr, 10 = gopy version. +const goPreambleCFFI = `/* +cgo stubs for package %[1]s, for use with cffi. +File is generated by gopy version %[10]s. Do not edit. +%[2]s +*/ + +package main + +/* +#include +#include +#if !defined(__STDC_VERSION__) || (__STDC_VERSION__ < 202311L) +typedef uint8_t bool; +#endif +*/ +import "C" +import ( + "runtime" + "sync" + "unsafe" + "github.com/go-python/gopy/gopyh" // handler + %[6]s +) + +func main() { + %[7]s +} + +//export GoPyInit +func GoPyInit() { + %[7]s +} + +// type for the handle -- int64 for speed (can switch to string) +type GoHandle %[4]s +type CGoHandle %[5]s + +// DecRef decrements the reference count for the specified handle +// and deletes it it goes to zero. +//export DecRef +func DecRef(handle CGoHandle) { + gopyh.DecRef(gopyh.CGoHandle(handle)) +} + +// IncRef increments the reference count for the specified handle. +//export IncRef +func IncRef(handle CGoHandle) { + gopyh.IncRef(gopyh.CGoHandle(handle)) +} + +// NumHandles returns the number of handles currently in use. +//export NumHandles +func NumHandles() int { + return gopyh.NumHandles() +} + +// RequestGC runs Go's garbage collector on a dedicated goroutine, and waits. +//export RequestGC +func RequestGC() { + done := make(chan struct{}) + _gcReq <- done + <-done +} + +var _gcReq = make(chan chan struct{}) + +func init() { + go func() { + for done := range _gcReq { + runtime.GC() + close(done) + } + }() +} + +// The error of the last call, for the python side to raise. There is one +// slot for the whole process, so calls from several python threads at once +// can be given each other's errors. +var ( + gopyErrMu sync.Mutex + gopyErrMsg string + gopyHasErr bool +) + +func gopySetError(kind, msg string) { + gopyErrMu.Lock() + gopyErrMsg, gopyHasErr = kind+":"+msg, true + gopyErrMu.Unlock() +} + +// GopyTakeError returns "Kind:message" for the error recorded by the last +// call and clears it, or NULL if there is none. Free with GopyFreeString. +//export GopyTakeError +func GopyTakeError() *C.char { + gopyErrMu.Lock() + defer gopyErrMu.Unlock() + if !gopyHasErr { + return nil + } + gopyHasErr = false + return C.CString(gopyErrMsg) +} + +// GopyFreeString frees a string returned by this library. +//export GopyFreeString +func GopyFreeString(s *C.char) { + C.free(unsafe.Pointer(s)) +} + +// boolGoToPy converts a Go bool to python-compatible C.char +func boolGoToPy(b bool) C.char { + if b { + return 1 + } + return 0 +} + +// boolPyToGo converts a python-compatible C.Char to Go bool +func boolPyToGo(b C.char) bool { + return b != 0 +} + +// errorGoToPy converts a Go error to python-compatible C.CString +func errorGoToPy(e error) *C.char { + if e != nil { + return C.CString(e.Error()) + } + return C.CString("") +} +` diff --git a/bind/cffi_build.py b/bind/cffi_build.py new file mode 100644 index 00000000..d22a3950 --- /dev/null +++ b/bind/cffi_build.py @@ -0,0 +1,165 @@ +# python build stubs for package @NAME@ (cffi backend) +# File is generated by gopy version @VERSION@. Do not edit. +# @CMD@ +# +# The generated build code below is written against the pybindgen API. Here +# the same calls are only recorded, and Module.generate() then writes a cffi +# (ABI mode) module, _@NAME@.py, that loads @NAME@_go@LIBEXT@ directly. The +# exact C types come from the extern declarations in cgo's @NAME@_go.h. + +import os +import re +import sys + +import cffi + + +def retval(ctype, *a, **kw): + return ctype + + +def param(ctype, name, *a, **kw): + return (ctype, name) + + +class Module(object): + def __init__(self, name): + self.name = name + self.header = None + self.funcs = [] + + def add_include(self, inc): + self.header = inc.strip('"') + + def add_function(self, name, ret, params, *a, **kw): + self.funcs.append((name, ret, params)) + + def generate(self): + here = os.path.dirname(os.path.abspath(__file__)) + with open(os.path.join(here, self.header)) as f: + typedefs, cdefs = go_decls(f.read()) + out = [MODULE_HEAD.replace("@CDEFS@", repr("\n".join(typedefs + list(cdefs.values()))))] + for name, ret, params in self.funcs: + out.append(wrapper(name, ret, params, name in cdefs)) + with open(os.path.join(here, self.name + ".py"), "w") as f: + f.write("\n".join(out)) + + +def add_checked_function(mod, name, retval, params, failure_expression="", *a, **kw): + mod.add_function(name, retval, params) + + +add_checked_string_function = add_checked_function + + +def go_decls(header): + """Returns the Go typedefs, and {function name: cdef line} for the functions cgo exports.""" + typedefs = [] + decls = {} + ffi = cffi.FFI() + for line in header.split("\n"): + if re.match(r"typedef [\w ]+ Go\w+;$", line): + try: + ffi.cdef(line) + typedefs.append(line) + except Exception: + pass # e.g. GoComplex64: not used by exports + continue + m = re.match(r"extern (.*?(\w+)\(.*\));$", line.replace("__declspec(dllexport) ", "")) + if not m or "_GoString_" in line: + continue + # cffi takes a plain char as a byte string only; the integer kinds + # (bool, int8, byte) are passed as ints, with the same C ABI. + decl = re.sub(r"\b(? Date: Fri, 18 Sep 2026 22:08:46 -0400 Subject: [PATCH 3/9] Create a separate cffi job for Github Actions --- .github/requirements-cffi.txt | 5 +++++ .github/workflows/ci.yml | 42 +++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+) create mode 100644 .github/requirements-cffi.txt diff --git a/.github/requirements-cffi.txt b/.github/requirements-cffi.txt new file mode 100644 index 00000000..aff0e708 --- /dev/null +++ b/.github/requirements-cffi.txt @@ -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 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index af0017d8..31cb4807 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 + 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 ./... + + # The skipped tests use features the cffi backend does not support yet: + # callbacks into Python, complex numbers, python bytes, and the Makefile. + - name: Test + run: go test -v -skip '^(TestBytes|TestBindFuncs|TestBindSimple|TestBuiltinSlices|TestGilString|TestGenHeaderHasVersion)$' ./... From f7da474247f94c5caaf6e75ff917bd4854635bc9 Mon Sep 17 00:00:00 2001 From: b-long Date: Fri, 18 Sep 2026 22:25:07 -0400 Subject: [PATCH 4/9] Prevent cffi from unloading Go shared library --- .github/workflows/ci.yml | 2 ++ bind/cffi_build.py | 8 +++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 31cb4807..2a89afa3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -109,6 +109,8 @@ jobs: 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 diff --git a/bind/cffi_build.py b/bind/cffi_build.py index d22a3950..a1c44937 100644 --- a/bind/cffi_build.py +++ b/bind/cffi_build.py @@ -116,6 +116,7 @@ def wrapper(name, ret, params, exported): MODULE_HEAD = '''# python bindings for package @NAME@ using cffi. # File is generated by gopy version @VERSION@. Do not edit. import builtins +import ctypes import os from operator import index as _index @@ -123,7 +124,12 @@ def wrapper(name, ret, params, exported): _ffi = cffi.FFI() _ffi.cdef(@CDEFS@) -_lib = _ffi.dlopen(os.path.join(os.path.dirname(os.path.abspath(__file__)), "@NAME@_go@LIBEXT@")) +_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "@NAME@_go@LIBEXT@") +_lib = _ffi.dlopen(_path) +# Go cannot unload its runtime. cffi unloads _lib at interpreter shutdown but +# ctypes never unloads, so this second handle keeps the library loaded until +# the process exits, as for an extension module. +_pin = ctypes.CDLL(_path) def _enc(s, argn): From 2d8f4e234c583e81af983a438dc1072631888d09 Mon Sep 17 00:00:00 2001 From: b-long Date: Sat, 19 Sep 2026 08:26:21 -0400 Subject: [PATCH 5/9] Add to/from bytes & `pkg` mode, start Complex nums --- .github/workflows/ci.yml | 5 +-- bind/cffi_build.py | 28 ++++++++++++++++ bind/gen.go | 36 +++++++++++++++++++-- bind/gen_func.go | 17 ++++++++++ bind/gen_slice.go | 70 +++++++++++++++++++++++++++++++++++++--- gen.go | 7 ++-- 6 files changed, 151 insertions(+), 12 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2a89afa3..d849ba36 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -137,6 +137,7 @@ jobs: run: go build -v ./... # The skipped tests use features the cffi backend does not support yet: - # callbacks into Python, complex numbers, python bytes, and the Makefile. + # Python callback arguments (TestBindFuncs), complex numbers as a plain + # function arg/return (TestBindSimple) or slice element (TestBuiltinSlices). - name: Test - run: go test -v -skip '^(TestBytes|TestBindFuncs|TestBindSimple|TestBuiltinSlices|TestGilString|TestGenHeaderHasVersion)$' ./... + run: go test -v -skip '^(TestBindFuncs|TestBindSimple|TestBuiltinSlices)$' ./... diff --git a/bind/cffi_build.py b/bind/cffi_build.py index a1c44937..cb459fda 100644 --- a/bind/cffi_build.py +++ b/bind/cffi_build.py @@ -41,6 +41,12 @@ def generate(self): out = [MODULE_HEAD.replace("@CDEFS@", repr("\n".join(typedefs + list(cdefs.values()))))] for name, ret, params in self.funcs: out.append(wrapper(name, ret, params, name in cdefs)) + # Slice_byte's converters exchange a raw pointer+length instead of a + # PyObject* (see gen_slice.go); they are recognized by name here + # rather than recorded via add_function, since their python bodies + # aren't derived from a plain C signature. + if "Slice_byte_from_bytes" in cdefs and "Slice_byte_to_bytes_ptr" in cdefs: + out.append(BYTES_FUNCS) with open(os.path.join(here, self.name + ".py"), "w") as f: f.write("\n".join(out)) @@ -161,6 +167,28 @@ def _check(): ''' +BYTES_FUNCS = ''' +def Slice_byte_from_bytes(b): + if not isinstance(b, (bytes, bytearray)): + raise TypeError("argument 1 must be bytes, not %s" % type(b).__name__) + _r = _lib.Slice_byte_from_bytes(_ffi.from_buffer(b), len(b)) + _check() + return _r + + +def Slice_byte_to_bytes(handle): + n = _lib.Slice_byte_to_bytes_len(handle) + _check() + if n == 0: + return b"" + ptr = _lib.Slice_byte_to_bytes_ptr(handle) + _check() + try: + return bytes(_ffi.buffer(ptr, n)) + finally: + _lib.Slice_byte_free_ptr(ptr) +''' + mod = Module('_@NAME@') mod.add_include('"@NAME@_go.h"') mod.add_function('GoPyInit', None, []) diff --git a/bind/gen.go b/bind/gen.go index 64687fc0..85db4741 100644 --- a/bind/gen.go +++ b/bind/gen.go @@ -469,6 +469,35 @@ build: %[9]s $(GCC) %[1]s.c %[6]s %[1]s_go$(LIBEXT) -o _%[1]s$(LIBEXT) $(CFLAGS) $(LDFLAGS) -fPIC --shared -w +` + + // same argument positions as MakefileTemplate, though only 1-5 and 10 are used: + // cffi needs none of the CPython CFLAGS/LDFLAGS that building %[1]s.c would. + MakefileTemplateCFFI = `# Makefile for python interface for package %[1]s, using cffi. +# File is generated by gopy version %[10]s. Do not edit. +# %[2]s + +GOCMD=go +GOBUILD=$(GOCMD) build -mod=mod +GOIMPORTS=goimports +PYTHON=%[4]s +LIBEXT=%[5]s + +all: gen build + +gen: + %[3]s + +build: + # goimports is needed to ensure that the imports list is valid + $(GOIMPORTS) -w %[1]s.go + # generate %[1]s_go$(LIBEXT) from %[1]s.go -- the cgo wrappers to go functions. + # unlike the default (pybindgen) backend, this is the only library gopy + # builds: cffi loads it directly, with no Python.h/libpython involved. + $(GOBUILD) -buildmode=c-shared -o %[1]s_go$(LIBEXT) %[1]s.go + # writes _%[1]s.py, the cffi module %[1]s.py imports + $(PYTHON) build.py + ` // exe version of template: 3 = gencmd, 4 = vm, 5 = libext, 8 = gopy version @@ -685,10 +714,9 @@ func (g *pyGen) genPkg(p *Package) { g.pkg = nil } -// wantMakefile reports whether to write a Makefile, which only knows how to -// build the default backend. +// wantMakefile reports whether to write a Makefile. func (g *pyGen) wantMakefile() bool { - return !NoMake && !g.isCFFI() + return !NoMake } func (g *pyGen) genGoPreamble() { @@ -838,6 +866,8 @@ func (g *pyGen) genMakefile() { if g.mode == ModeExe { g.makefile.Printf(MakefileExeTemplate, g.cfg.Name, g.cfg.Cmd, gencmd, g.cfg.VM, g.libext, pycfg.CFlags, pycfg.LdFlags, g.cfg.Version) + } else if g.isCFFI() { + g.makefile.Printf(MakefileTemplateCFFI, g.cfg.Name, g.cfg.Cmd, gencmd, g.cfg.VM, g.libext, "", "", "", "", g.cfg.Version) } else { winhack := "" if WindowsOS { diff --git a/bind/gen_func.go b/bind/gen_func.go index 57749b1b..efa24bd6 100644 --- a/bind/gen_func.go +++ b/bind/gen_func.go @@ -67,6 +67,23 @@ func (g *pyGen) genFuncSig(sym *symbol, fsym *Func) bool { return false } + // cffi has no way to cross a raw PyObject* (complex64/128, and Python + // callback arguments -- see isSignature() below): skip these functions + // rather than emit a signature that references the CPython C API, which + // would fail to even compile under the cffi preamble. + if g.isCFFI() { + for _, arg := range args { + if sarg := current.symtype(arg.GoType()); sarg != nil && sarg.cpyname == "PyObject*" { + return false + } + } + for _, ret := range res { + if sret := current.symtype(ret.GoType()); sret != nil && sret.cpyname == "PyObject*" { + return false + } + } + } + var ( goArgs []string pyArgs []string diff --git a/bind/gen_slice.go b/bind/gen_slice.go index 2eef8f0a..7f277e7c 100644 --- a/bind/gen_slice.go +++ b/bind/gen_slice.go @@ -70,6 +70,14 @@ func (g *pyGen) genSliceInit(slc *symbol, extTypes, pyWrapOnly bool, slob *Slice esym = current.symtype(typ.Elem()) } + // element access (elem/set/append) below would reference *C.PyObject, + // which cffi's preamble doesn't declare (see genFuncSig for the same + // restriction on plain function args/returns); skip the whole wrapper + // rather than emit code that fails to compile. + if g.isCFFI() && esym != nil && esym.cpyname == "PyObject*" { + return + } + gocl := "go." if g.pkg == goPackage { gocl = "" @@ -395,8 +403,60 @@ otherwise parameter is a python list that we copy from } if slNm == "Slice_byte" { - // these take and return python bytes objects, which the cffi backend does not support yet - if !g.isCFFI() { + if g.isCFFI() { + // PyBytes_* is off-limits for cffi (no CPython headers), so these + // exchange a raw pointer+length instead of a PyObject*; the cffi + // build script (cffi_build.py) recognizes them by name and writes + // the bytes<->buffer conversion into the generated python module. + g.gofile.Printf("//export Slice_byte_from_bytes\n") + g.gofile.Printf("func Slice_byte_from_bytes(ptr unsafe.Pointer, size C.longlong) CGoHandle {\n") + g.gofile.Indent() + g.gofile.Printf("data := make([]byte, size)\n") + g.gofile.Printf("if size > 0 {\n") + g.gofile.Indent() + g.gofile.Printf("tmp := unsafe.Slice((*byte)(ptr), size)\n") + g.gofile.Printf("copy(data, tmp)\n") + g.gofile.Outdent() + g.gofile.Printf("}\n") + g.gofile.Printf("return handleFromPtr_Slice_byte(&data)\n") + g.gofile.Outdent() + g.gofile.Printf("}\n\n") + + g.gofile.Printf("//export Slice_byte_to_bytes_len\n") + g.gofile.Printf("func Slice_byte_to_bytes_len(handle CGoHandle) C.longlong {\n") + g.gofile.Indent() + g.gofile.Printf("s := deptrFromHandle_Slice_byte(handle)\n") + g.gofile.Printf("return C.longlong(len(s))\n") + g.gofile.Outdent() + g.gofile.Printf("}\n\n") + + // Returning &s[0] directly would hand cgo a pointer into the Go + // heap, which cgo's pointer checks reject once it crosses back + // to the caller; copy into a C-owned buffer instead, freed by + // the python side (Slice_byte_free_ptr) once it has read it. + g.gofile.Printf("//export Slice_byte_to_bytes_ptr\n") + g.gofile.Printf("func Slice_byte_to_bytes_ptr(handle CGoHandle) unsafe.Pointer {\n") + g.gofile.Indent() + g.gofile.Printf("s := deptrFromHandle_Slice_byte(handle)\n") + g.gofile.Printf("n := len(s)\n") + g.gofile.Printf("if n == 0 {\n") + g.gofile.Indent() + g.gofile.Printf("return nil\n") + g.gofile.Outdent() + g.gofile.Printf("}\n") + g.gofile.Printf("buf := C.malloc(C.size_t(n))\n") + g.gofile.Printf("copy(unsafe.Slice((*byte)(buf), n), s)\n") + g.gofile.Printf("return buf\n") + g.gofile.Outdent() + g.gofile.Printf("}\n\n") + + g.gofile.Printf("//export Slice_byte_free_ptr\n") + g.gofile.Printf("func Slice_byte_free_ptr(ptr unsafe.Pointer) {\n") + g.gofile.Indent() + g.gofile.Printf("C.free(ptr)\n") + g.gofile.Outdent() + g.gofile.Printf("}\n\n") + } else { g.gofile.Printf("//export Slice_byte_from_bytes\n") g.gofile.Printf("func Slice_byte_from_bytes(o *C.PyObject) CGoHandle {\n") g.gofile.Indent() @@ -422,10 +482,10 @@ otherwise parameter is a python list that we copy from } g.gofile.Outdent() g.gofile.Printf("}\n\n") - } - g.pybuild.Printf("mod.add_function('Slice_byte_from_bytes', retval('%s'%s), [param('PyObject*', 'o', transfer_ownership=False)])\n", PyHandle, caller_owns_ret) - g.pybuild.Printf("mod.add_function('Slice_byte_to_bytes', retval('PyObject*', caller_owns_return=True), [param('%s', 'handle')])\n", PyHandle) + g.pybuild.Printf("mod.add_function('Slice_byte_from_bytes', retval('%s'%s), [param('PyObject*', 'o', transfer_ownership=False)])\n", PyHandle, caller_owns_ret) + g.pybuild.Printf("mod.add_function('Slice_byte_to_bytes', retval('PyObject*', caller_owns_return=True), [param('%s', 'handle')])\n", PyHandle) + } } } } diff --git a/gen.go b/gen.go index b7591b0e..d0cd0419 100644 --- a/gen.go +++ b/gen.go @@ -66,8 +66,11 @@ func genPkg(mode bind.BuildMode, cfg *BuildCfg) error { if cfg.Backend, err = bind.BackendFromEnv(); err != nil { return err } - if cfg.Backend == bind.BackendCFFI && (mode == bind.ModePkg || mode == bind.ModeExe) { - return fmt.Errorf("gopy: %s=%s only supports gopy gen and gopy build", bind.BackendEnvVar, cfg.Backend) + if cfg.Backend == bind.BackendCFFI && mode == bind.ModeExe { + // exe mode embeds the Python interpreter into the Go binary via the + // CPython C API (see goExePreambleC/Go in bind/gen.go), unrelated to + // how the bindings themselves are generated; cffi doesn't support it. + return fmt.Errorf("gopy: %s=%s does not support gopy exe", bind.BackendEnvVar, cfg.Backend) } cfg.OutputDir, err = genOutDir(cfg.OutputDir) if err != nil { From 93188fb1b8ce967228499263612a6ed2a1284a9d Mon Sep 17 00:00:00 2001 From: b-long Date: Sat, 19 Sep 2026 08:47:43 -0400 Subject: [PATCH 6/9] Complete complex64/128 number support --- .github/workflows/ci.yml | 7 ++-- bind/cffi_build.py | 67 +++++++++++++++++++++++++++++--- bind/gen_func.go | 83 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 149 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d849ba36..3524e3b7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -137,7 +137,8 @@ jobs: run: go build -v ./... # The skipped tests use features the cffi backend does not support yet: - # Python callback arguments (TestBindFuncs), complex numbers as a plain - # function arg/return (TestBindSimple) or slice element (TestBuiltinSlices). + # Python callback arguments (TestBindFuncs), and complex numbers as a + # slice element rather than a plain function arg/return (TestBuiltinSlices; + # see genFuncComplexCFFI in gen_func.go for the arg/return case, which works). - name: Test - run: go test -v -skip '^(TestBindFuncs|TestBindSimple|TestBuiltinSlices)$' ./... + run: go test -v -skip '^(TestBindFuncs|TestBuiltinSlices)$' ./... diff --git a/bind/cffi_build.py b/bind/cffi_build.py index cb459fda..dc4648c5 100644 --- a/bind/cffi_build.py +++ b/bind/cffi_build.py @@ -32,15 +32,24 @@ def add_include(self, inc): self.header = inc.strip('"') def add_function(self, name, ret, params, *a, **kw): - self.funcs.append((name, ret, params)) + self.funcs.append(("plain", name, ret, params)) + + def add_complex_function(self, name, nargs): + # see genFuncComplexCFFI (gen_func.go) for the calling convention. + self.funcs.append(("complex", name, nargs)) def generate(self): here = os.path.dirname(os.path.abspath(__file__)) with open(os.path.join(here, self.header)) as f: typedefs, cdefs = go_decls(f.read()) out = [MODULE_HEAD.replace("@CDEFS@", repr("\n".join(typedefs + list(cdefs.values()))))] - for name, ret, params in self.funcs: - out.append(wrapper(name, ret, params, name in cdefs)) + for entry in self.funcs: + if entry[0] == "complex": + _, name, nargs = entry + out.append(complex_wrapper(name, nargs, name in cdefs)) + else: + _, name, ret, params = entry + out.append(wrapper(name, ret, params, name in cdefs)) # Slice_byte's converters exchange a raw pointer+length instead of a # PyObject* (see gen_slice.go); they are recognized by name here # rather than recorded via add_function, since their python bodies @@ -59,20 +68,44 @@ def add_checked_function(mod, name, retval, params, failure_expression="", *a, * def go_decls(header): - """Returns the Go typedefs, and {function name: cdef line} for the functions cgo exports.""" + """Returns the Go typedefs (plus any "struct X { ... };" body -- cgo emits + one ahead of a multi-value-returning export, see genFuncComplexCFFI in + gen_func.go), and {function name: cdef line} for the functions cgo exports. + """ typedefs = [] decls = {} ffi = cffi.FFI() - for line in header.split("\n"): + lines = header.split("\n") + i = 0 + while i < len(lines): + line = lines[i] + m = re.match(r"struct (\w+) \{$", line) + if m: + block = [line] + i += 1 + while i < len(lines) and lines[i] != "};": + block.append(lines[i]) + i += 1 + block.append("};") + i += 1 + decl = "\n".join(block) + try: + ffi.cdef(decl) + typedefs.append(decl) + except Exception as err: + print("gopy: cffi cannot declare struct %s: %s" % (m.group(1), err), file=sys.stderr) + continue if re.match(r"typedef [\w ]+ Go\w+;$", line): try: ffi.cdef(line) typedefs.append(line) except Exception: pass # e.g. GoComplex64: not used by exports + i += 1 continue m = re.match(r"extern (.*?(\w+)\(.*\));$", line.replace("__declspec(dllexport) ", "")) if not m or "_GoString_" in line: + i += 1 continue # cffi takes a plain char as a byte string only; the integer kinds # (bool, int8, byte) are passed as ints, with the same C ABI. @@ -81,8 +114,10 @@ def go_decls(header): ffi.cdef(decl) except Exception as err: print("gopy: cffi cannot declare %s: %s" % (m.group(2), err), file=sys.stderr) + i += 1 continue decls[m.group(2)] = decl + i += 1 return typedefs, decls @@ -119,6 +154,28 @@ def wrapper(name, ret, params, exported): return "\n".join(body) + "\n" +def complex_wrapper(name, nargs, exported): + """A plain function whose every argument and return value is + complex64/128 (see genFuncComplexCFFI, gen_func.go): each argument + crosses as two floats (.real, .imag), and the return value comes back + as the {r0, r1} struct cgo generates for a two-value Go return. + """ + if not exported: + return ( + "def %s(*args):\n" + " raise NotImplementedError('%s is not available with the cffi backend')\n" % (name, name) + ) + names = ["c%d" % i for i in range(nargs)] + sig = ", ".join(names) + callargs = ", ".join("%s.real, %s.imag" % (n, n) for n in names) + return ( + "def %s(%s):\n" + " _r = _lib.%s(%s)\n" + " _check()\n" + " return complex(_r.r0, _r.r1)\n" % (name, sig, name, callargs) + ) + + MODULE_HEAD = '''# python bindings for package @NAME@ using cffi. # File is generated by gopy version @VERSION@. Do not edit. import builtins diff --git a/bind/gen_func.go b/bind/gen_func.go index efa24bd6..595ac986 100644 --- a/bind/gen_func.go +++ b/bind/gen_func.go @@ -213,11 +213,94 @@ func (g *pyGen) genFuncSig(sym *symbol, fsym *Func) bool { } func (g *pyGen) genFunc(o *Func) { + if g.isCFFI() && g.genFuncComplexCFFI(o) { + return + } if g.genFuncSig(nil, o) { g.genFuncBody(nil, o) } } +func isComplexSym(sym *symbol) bool { + return sym != nil && (sym.goname == "complex64" || sym.goname == "complex128") +} + +// genFuncComplexCFFI generates a plain (non-method) function whose every +// argument and its one return value are complex64/128. The normal path +// (genFuncSig/genFuncBody) represents complex64/128 as a *C.PyObject, which +// the cffi preamble doesn't declare -- fine for most types, but complex +// values need actual marshaling here, not just a skip. Instead, an +// argument crosses as two plain floats (real, imag), and the return value +// uses Go's native multi-value return, which cgo exports as a small C +// struct {r0; r1;} that cffi can declare (see go_decls/complex_wrapper in +// cffi_build.py). It returns false, writing nothing, if the signature +// doesn't fit that narrow shape (methods, a mix of complex and other +// argument types, or an error return) -- genFuncSig's PyObject* check then +// skips the function instead of emitting code that fails to compile. +func (g *pyGen) genFuncComplexCFFI(fsym *Func) bool { + sig := fsym.sig + if sig == nil || fsym.isVariadic || fsym.err { + return false + } + args := sig.Params() + res := sig.Results() + if len(res) != 1 || !isComplexSym(current.symtype(res[0].GoType())) { + return false + } + for _, arg := range args { + if !isComplexSym(current.symtype(arg.GoType())) { + return false + } + } + + gname := fsym.GoName() + if g.cfg.RenameCase { + gname = toSnakeCase(gname) + } + gname, gdoc, err := extractPythonName(gname, fsym.Doc()) + if err != nil { + return false + } + + cfloatOf := func(sym *symbol) (cgo, gotyp string) { + if sym.goname == "complex64" { + return "C.float", "float32" + } + return "C.double", "float64" + } + + retFloat, _ := cfloatOf(current.symtype(res[0].GoType())) + + var goArgs, callArgs, wpArgs []string + for i, arg := range args { + anm := pySafeArg(arg.Name(), i) + cfloat, gofloat := cfloatOf(current.symtype(arg.GoType())) + reNm, imNm := anm+"_re", anm+"_im" + goArgs = append(goArgs, fmt.Sprintf("%s %s, %s %s", reNm, cfloat, imNm, cfloat)) + callArgs = append(callArgs, fmt.Sprintf("complex(%s(%s), %s(%s))", gofloat, reNm, gofloat, imNm)) + wpArgs = append(wpArgs, anm) + } + + g.gofile.Printf("\n//export %s\n", fsym.ID()) + g.gofile.Printf("func %s(%s) (%s, %s) {\n", fsym.ID(), strings.Join(goArgs, ", "), retFloat, retFloat) + g.gofile.Indent() + g.gofile.Printf("_r := %s(%s)\n", fsym.GoFmt(), strings.Join(callArgs, ", ")) + g.gofile.Printf("return %s(real(_r)), %s(imag(_r))\n", retFloat, retFloat) + g.gofile.Outdent() + g.gofile.Printf("}\n\n") + + g.pybuild.Printf("mod.add_complex_function('%s', %d)\n", fsym.ID(), len(args)) + + g.pywrap.Printf("def %s(%s):\n", gname, strings.Join(wpArgs, ", ")) + g.pywrap.Indent() + g.pywrap.Printf(`"""%s"""`, gdoc) + g.pywrap.Printf("\n") + g.pywrap.Printf("return _%s.%s(%s)\n", g.cfg.Name, fsym.ID(), strings.Join(wpArgs, ", ")) + g.pywrap.Outdent() + + return true +} + func (g *pyGen) genMethod(s *symbol, o *Func) { if g.genFuncSig(s, o) { g.genFuncBody(s, o) From 36c319c839bde3331f64f145738bcde77fc5e8a4 Mon Sep 17 00:00:00 2001 From: b-long Date: Sat, 19 Sep 2026 20:57:54 -0400 Subject: [PATCH 7/9] Support Python callbacks in cffi backend --- .github/workflows/ci.yml | 7 +- SUPPORT_MATRIX.md | 1 + _examples/callbacks/callbacks.go | 55 ++++++++++ _examples/callbacks/test.py | 65 ++++++++++++ bind/cffi.go | 30 +++++- bind/cffi_build.py | 36 ++++++- bind/cffi_callback.go | 175 +++++++++++++++++++++++++++++++ bind/gen.go | 4 +- bind/gen_func.go | 28 +++-- main_test.go | 33 ++++++ 10 files changed, 421 insertions(+), 13 deletions(-) create mode 100644 _examples/callbacks/callbacks.go create mode 100644 _examples/callbacks/test.py create mode 100644 bind/cffi_callback.go diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3524e3b7..903861b5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -137,8 +137,9 @@ jobs: run: go build -v ./... # The skipped tests use features the cffi backend does not support yet: - # Python callback arguments (TestBindFuncs), and complex numbers as a - # slice element rather than a plain function arg/return (TestBuiltinSlices; - # see genFuncComplexCFFI in gen_func.go for the arg/return case, which works). + # Python callbacks that take an interface{} or return a value + # (TestBindFuncs; see cffi_callback.go for the ones that work), and complex + # numbers as a slice element rather than a plain function arg/return + # (TestBuiltinSlices; see genFuncComplexCFFI in gen_func.go for the case that works). - name: Test run: go test -v -skip '^(TestBindFuncs|TestBuiltinSlices)$' ./... diff --git a/SUPPORT_MATRIX.md b/SUPPORT_MATRIX.md index 8f30be77..afaae308 100644 --- a/SUPPORT_MATRIX.md +++ b/SUPPORT_MATRIX.md @@ -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 diff --git a/_examples/callbacks/callbacks.go b/_examples/callbacks/callbacks.go new file mode 100644 index 00000000..ad0add37 --- /dev/null +++ b/_examples/callbacks/callbacks.go @@ -0,0 +1,55 @@ +// 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" +) + +// 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() +} + +// 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) + } +} + +// 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() +} diff --git a/_examples/callbacks/test.py b/_examples/callbacks/test.py new file mode 100644 index 00000000..3e652e19 --- /dev/null +++ b/_examples/callbacks/test.py @@ -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. + +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("--- 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") diff --git a/bind/cffi.go b/bind/cffi.go index 04dfabcd..8a880da2 100644 --- a/bind/cffi.go +++ b/bind/cffi.go @@ -38,7 +38,8 @@ func (g *pyGen) goSetError(kind, msg string) string { } // same argument positions as goPreamble: 1 = name of package, 2 = cmdstr, -// 4 = GoHandle, 5 = CGoHandle, 6 = all imports, 7 = mainstr, 10 = gopy version. +// 4 = GoHandle, 5 = CGoHandle, 6 = all imports, 7 = mainstr, 8 = C trampolines for +// callbacks (see cffi_callback.go), 10 = gopy version. const goPreambleCFFI = `/* cgo stubs for package %[1]s, for use with cffi. File is generated by gopy version %[10]s. Do not edit. @@ -53,6 +54,7 @@ package main #if !defined(__STDC_VERSION__) || (__STDC_VERSION__ < 202311L) typedef uint8_t bool; #endif +%[8]s */ import "C" import ( @@ -148,6 +150,32 @@ func GopyFreeString(s *C.char) { C.free(unsafe.Pointer(s)) } +// gopyCallbackScope guards a python callback passed to Go, which only exists +// while the python call it was passed to is running (see cffi_callback.go): +// close waits for callbacks that are running, and then refuses new ones. +type gopyCallbackScope struct { + mu sync.RWMutex + done bool +} + +func (s *gopyCallbackScope) enter() bool { + s.mu.RLock() + if s.done { + s.mu.RUnlock() + println("gopy: callback called after the python call it was passed to returned") + return false + } + return true +} + +func (s *gopyCallbackScope) leave() { s.mu.RUnlock() } + +func (s *gopyCallbackScope) close() { + s.mu.Lock() + s.done = true + s.mu.Unlock() +} + // boolGoToPy converts a Go bool to python-compatible C.char func boolGoToPy(b bool) C.char { if b { diff --git a/bind/cffi_build.py b/bind/cffi_build.py index dc4648c5..df76e687 100644 --- a/bind/cffi_build.py +++ b/bind/cffi_build.py @@ -130,8 +130,12 @@ def wrapper(name, ret, params, exported): " raise NotImplementedError('%s is not available with the cffi backend')\n" % (name, name) ) args = [] + setup = [] for i, (ctype, pname) in enumerate(params): - if ctype == "char*": + if ctype.startswith("callback:"): + setup.append(callback_setup(pname, ctype, i + 1)) + args.append("_ffi.cast('void*', _cb_%s)" % pname) + elif ctype == "char*": args.append("_enc(%s, %d)" % (pname, i + 1)) elif ctype == "bool": args.append("(1 if %s else 0)" % pname) @@ -141,7 +145,7 @@ def wrapper(name, ret, params, exported): args.append("_index(%s)" % pname) else: args.append(pname) - body = ["def %s(%s):" % (name, sig), " _r = _lib.%s(%s)" % (name, ", ".join(args))] + body = ["def %s(%s):" % (name, sig)] + setup + [" _r = _lib.%s(%s)" % (name, ", ".join(args))] if ret == "char*": body.append(" _r = _dec(_r)") elif ret == "bool": @@ -154,6 +158,34 @@ def wrapper(name, ret, params, exported): return "\n".join(body) + "\n" +def callback_setup(pname, ctype, argn): + """Returns the python source that wraps the callable pname in an + ffi.callback, for the C signature in ctype ("callback:void(int64_t,char*)", + see cffi_callback.go). The wrapper passes it on to Go as _cb_, + which is kept referenced by this local variable until the Go call + returns: cffi frees a callback as soon as nothing refers to it. + """ + cargs = ctype[len("callback:void("):-1] + ctypes = cargs.split(",") if cargs else [] + names = ["a%d" % i for i in range(len(ctypes))] + conv = [] + for n, t in zip(names, ctypes): + if t == "char*": + conv.append('_ffi.string(%s).decode("utf-8")' % n) + elif t == "bool": + conv.append("bool(%s)" % n) + else: + conv.append(n) + cdecl = "void(%s)" % ", ".join("unsigned char" if t == "bool" else t for t in ctypes) + return "\n".join([ + " if not callable(%s):" % pname, + " raise TypeError('argument %d must be callable, not %%s' %% type(%s).__name__)" % (argn, pname), + " def _cbfn_%s(%s):" % (pname, ", ".join(names)), + " %s(%s)" % (pname, ", ".join(conv)), + " _cb_%s = _ffi.callback(%r, _cbfn_%s)" % (pname, cdecl, pname), + ]) + + def complex_wrapper(name, nargs, exported): """A plain function whose every argument and return value is complex64/128 (see genFuncComplexCFFI, gen_func.go): each argument diff --git a/bind/cffi_callback.go b/bind/cffi_callback.go new file mode 100644 index 00000000..1c357745 --- /dev/null +++ b/bind/cffi_callback.go @@ -0,0 +1,175 @@ +// 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 ( + "bytes" + "fmt" + "go/types" + "strings" +) + +// A Python callable passed to Go as a func-typed argument crosses the cffi +// boundary as a C function pointer: the python side wraps the callable in an +// ffi.callback (see callback_setup in cffi_build.py), and the Go closure +// built here calls that pointer through a small static C trampoline, since +// cgo cannot call a C function pointer directly. +// +// The callback only lives as long as the Go call it was passed to, so the +// closure goes through a gopyCallbackScope that is closed when that call +// returns, instead of jumping to freed memory if Go kept it any longer. +// +// Only what is needed so far is supported: no results, and parameters +// limited to pointer/interface handles, numbers, bool and string. Any other +// callback type makes the function be skipped, see genFuncSig. + +// cffiTrampolinesKey stands in for the trampolines in the cgo preamble, +// which is written before the callback types that need them are known. +const cffiTrampolinesKey = "@@GOPY_CFFI_TRAMPOLINES@@" + +// cffiCBParam is one parameter of a callback. +type cffiCBParam struct { + name string // name of the parameter in the func literal + gotyp string // its Go type + ctype string // how it crosses: int64_t, uint64_t, double, char* or bool + pre string // Go statements to run before the call, if any + conv string // Go expression converting it to ctype +} + +type cffiCallback struct { + params []cffiCBParam +} + +// cffiCallback returns how to pass sym, a func-typed argument, to Go +// as a callback, or nil if its type isn't supported. +func (g *pyGen) cffiCallback(sym *symbol) *cffiCallback { + sig, ok := sym.GoType().Underlying().(*types.Signature) + if !ok || sig.Results().Len() != 0 || sig.Variadic() { + return nil + } + cb := &cffiCallback{} + for i := 0; i < sig.Params().Len(); i++ { + p, ok := cffiCallbackParam(sig.Params().At(i), i) + if !ok { + return nil + } + cb.params = append(cb.params, p) + } + return cb +} + +func cffiCallbackParam(v *types.Var, i int) (cffiCBParam, bool) { + typ := v.Type() + vsym := current.symtype(typ) + if vsym == nil { + return cffiCBParam{}, false + } + nm := pySafeArg(v.Name(), i) + p := cffiCBParam{name: nm, gotyp: current.typeGoName(typ)} + + if vsym.hasHandle() && vsym.isPtrOrIface() { + p.ctype = "int64_t" + p.conv = fmt.Sprintf("C.int64_t(%s(%s)%s)", vsym.go2py, nm, vsym.go2pyParenEx) + return p, true + } + bt, ok := typ.Underlying().(*types.Basic) + if !ok { + return p, false + } + switch k := bt.Kind(); { + case types.Int <= k && k <= types.Int64: + p.ctype, p.conv = "int64_t", fmt.Sprintf("C.int64_t(%s)", nm) + case types.Uint <= k && k <= types.Uintptr: + p.ctype, p.conv = "uint64_t", fmt.Sprintf("C.uint64_t(%s)", nm) + case k == types.Float32 || k == types.Float64: + p.ctype, p.conv = "double", fmt.Sprintf("C.double(%s)", nm) + case k == types.Bool: + p.ctype, p.conv = "bool", fmt.Sprintf("C.uint8_t(boolGoToPy(bool(%s)))", nm) + case k == types.String: + // the python side copies it, so it can be freed once the call returns + p.ctype, p.conv = "char*", "_c"+nm + p.pre = fmt.Sprintf("_c%[1]s := C.CString(string(%[1]s))\ndefer C.free(unsafe.Pointer(_c%[1]s))\n", nm) + default: + return p, false + } + return p, true +} + +func (cb *cffiCallback) ctypes() []string { + ts := make([]string, len(cb.params)) + for i, p := range cb.params { + ts[i] = p.ctype + } + return ts +} + +// pyType is the type given to the callback's parameter in build.py, which +// callback_setup in cffi_build.py takes apart. +func (cb *cffiCallback) pyType() string { + return "callback:void(" + strings.Join(cb.ctypes(), ",") + ")" +} + +// cffiCallbackPrologue returns the Go statements that set up the callback +// argument named anm, ahead of cffiCallbackLit. +func cffiCallbackPrologue(anm string) string { + return fmt.Sprintf("_cbfp_%[1]s := %[1]s\n_cbs_%[1]s := new(gopyCallbackScope)\ndefer _cbs_%[1]s.close()\n", anm) +} + +// cffiCallbackLit returns a Go func literal that calls the Python callable +// passed as the argument named anm. +func (g *pyGen) cffiCallbackLit(cb *cffiCallback, anm string) string { + var decl, pre, args []string + args = append(args, "_cbfp_"+anm) + for _, p := range cb.params { + decl = append(decl, p.name+" "+p.gotyp) + if p.pre != "" { + pre = append(pre, p.pre) + } + args = append(args, p.conv) + } + return fmt.Sprintf("func(%s) {\nif !_cbs_%s.enter() {\nreturn\n}\ndefer _cbs_%[2]s.leave()\n%sC.gopy_cb_%d(%s)\n}", + strings.Join(decl, ", "), anm, strings.Join(pre, ""), g.cffiTrampoline(cb), strings.Join(args, ", ")) +} + +// cffiTrampoline returns the number of the C trampoline that calls a callback +// like cb, adding it if it is the first. +func (g *pyGen) cffiTrampoline(cb *cffiCallback) int { + key := strings.Join(cb.ctypes(), ",") + for i, k := range g.cbSigs { + if k == key { + return i + } + } + g.cbSigs = append(g.cbSigs, key) + return len(g.cbSigs) - 1 +} + +// spliceCFFITrampolines writes the trampolines into the cgo preamble. +func (g *pyGen) spliceCFFITrampolines() { + if !g.isCFFI() { + return + } + var c strings.Builder + for i, key := range g.cbSigs { + params := []string{"void* f"} + var args, ptypes []string + if key != "" { + for j, t := range strings.Split(key, ",") { + if t == "bool" { + t = "uint8_t" + } + params = append(params, fmt.Sprintf("%s a%d", t, j)) + args = append(args, fmt.Sprintf("a%d", j)) + ptypes = append(ptypes, t) + } + } else { + ptypes = append(ptypes, "void") + } + fmt.Fprintf(&c, "static inline void gopy_cb_%d(%s) { ((void (*)(%s))f)(%s); }\n", + i, strings.Join(params, ", "), strings.Join(ptypes, ", "), strings.Join(args, ", ")) + } + b := bytes.Replace(g.gofile.buf.Bytes(), []byte(cffiTrampolinesKey), []byte(c.String()), 1) + g.gofile.buf = bytes.NewBuffer(b) +} diff --git a/bind/gen.go b/bind/gen.go index 85db4741..f4552375 100644 --- a/bind/gen.go +++ b/bind/gen.go @@ -608,6 +608,7 @@ type pyGen struct { extraGccArgs string lang int // c-python api version (2,3) dynamicLink bool + cbSigs []string // cffi: the callback types that have a C trampoline, see cffi_callback.go } func (g *pyGen) gen() error { @@ -670,6 +671,7 @@ func (g *pyGen) genOut() { g.pybuild.Printf("\nmod.generate(open('%v.c', 'w'))\n\n", g.cfg.Name) } g.gofile.Printf("\n\n") + g.spliceCFFITrampolines() g.genPrintOut(g.cfg.Name+".go", g.gofile) g.genPrintOut("build.py", g.pybuild) if g.wantMakefile() { @@ -731,7 +733,7 @@ func (g *pyGen) genGoPreamble() { } if g.isCFFI() { g.gofile.Printf(goPreambleCFFI, g.cfg.Name, g.cfg.Cmd, "", GoHandle, CGoHandle, - pkgimport, g.cfg.Main, "", "", g.cfg.Version) + pkgimport, g.cfg.Main, cffiTrampolinesKey, "", g.cfg.Version) g.gofile.Printf("\n// --- generated code for package: %[1]s below: ---\n\n", g.cfg.Name) return } diff --git a/bind/gen_func.go b/bind/gen_func.go index 595ac986..0b8b7548 100644 --- a/bind/gen_func.go +++ b/bind/gen_func.go @@ -67,13 +67,20 @@ func (g *pyGen) genFuncSig(sym *symbol, fsym *Func) bool { return false } - // cffi has no way to cross a raw PyObject* (complex64/128, and Python - // callback arguments -- see isSignature() below): skip these functions + // cffi has no way to cross a raw PyObject* (complex64/128, and callback + // arguments of a type cffiCallback doesn't support): skip these functions // rather than emit a signature that references the CPython C API, which // would fail to even compile under the cffi preamble. if g.isCFFI() { for _, arg := range args { - if sarg := current.symtype(arg.GoType()); sarg != nil && sarg.cpyname == "PyObject*" { + sarg := current.symtype(arg.GoType()) + switch { + case sarg == nil: + case sarg.isSignature(): + if g.cffiCallback(sarg) == nil { + return false + } + case sarg.cpyname == "PyObject*": return false } } @@ -105,10 +112,14 @@ func (g *pyGen) genFuncSig(sym *symbol, fsym *Func) bool { } anm := pySafeArg(arg.Name(), i) - if ifchandle && arg.sym.goname == "interface{}" { + switch { + case g.isCFFI() && sarg.isSignature(): + goArgs = append(goArgs, fmt.Sprintf("%s unsafe.Pointer", anm)) + pyArgs = append(pyArgs, fmt.Sprintf("param('%s', '%s')", g.cffiCallback(sarg).pyType(), anm)) + case ifchandle && arg.sym.goname == "interface{}": goArgs = append(goArgs, fmt.Sprintf("%s %s", anm, CGoHandle)) pyArgs = append(pyArgs, fmt.Sprintf("param('%s', '%s')", PyHandle, anm)) - } else { + default: goArgs = append(goArgs, fmt.Sprintf("%s %s", anm, sarg.cgoname)) if sarg.cpyname == "PyObject*" { pyArgs = append(pyArgs, fmt.Sprintf("param('%s', '%s', transfer_ownership=False)", sarg.cpyname, anm)) @@ -355,7 +366,10 @@ func (g *pyGen) genFuncBody(sym *symbol, fsym *Func) { g.gofile.Indent() if fsym.hasfun { for i, arg := range args { - if arg.sym.isSignature() { + switch { + case arg.sym.isSignature() && g.isCFFI(): + g.gofile.Printf("%s", cffiCallbackPrologue(pySafeArg(arg.Name(), i))) + case arg.sym.isSignature(): g.gofile.Printf("_fun_arg := %s\n", pySafeArg(arg.Name(), i)) } } @@ -405,6 +419,8 @@ if __err != nil { switch { case ifchandle && arg.sym.goname == "interface{}": na = fmt.Sprintf(`gopyh.VarFromHandle((gopyh.CGoHandle)(%s), "interface{}")`, anm) + case arg.sym.isSignature() && g.isCFFI(): + na = g.cffiCallbackLit(g.cffiCallback(arg.sym), anm) case arg.sym.isSignature(): na = fmt.Sprintf("%s", arg.sym.py2go) case arg.sym.py2go != "": diff --git a/main_test.go b/main_test.go index 0e0f7391..a1815a99 100644 --- a/main_test.go +++ b/main_test.go @@ -51,6 +51,7 @@ var ( "_examples/pkgconflict": []string{"py3"}, "_examples/variadic": []string{"py3"}, "_examples/gilstring": []string{"py3"}, + "_examples/callbacks": []string{"py3"}, } testEnvironment = os.Environ() @@ -392,6 +393,38 @@ OK }) } +func TestBindCallbacks(t *testing.T) { + // t.Parallel() + path := "_examples/callbacks" + testPkg(t, pkg{ + path: path, + lang: features[path], + cmd: "build", + extras: nil, + want: []byte(`--- Each: int and string arguments +each: 0 item-0 +each: 1 item-1 +each: 2 item-2 +--- Mixed: bool, float and uint8 arguments +mixed: True 1.5 200 +mixed: False -2.25 7 +--- Twice: no arguments +twice: 2 +--- Counter.Visit: a Go struct arrives as a handle +visit: 1 1 +visit: 2 2 +counter: 2 +--- a bound method +box: [(0, 'item-0'), (1, 'item-1')] +--- called from another goroutine +goroutine: 7 +--- an exception in a callback is reported, and Go carries on +calls: 3 reported: 3 +OK +`), + }) +} + func TestBindSimple(t *testing.T) { // t.Parallel() path := "_examples/simple" From d1206d0d06123504faf6e2382cd53099d23adcb7 Mon Sep 17 00:00:00 2001 From: b-long Date: Sat, 19 Sep 2026 21:22:22 -0400 Subject: [PATCH 8/9] Add interface{} args & results in cffi callbacks --- .github/workflows/ci.yml | 10 +- _examples/callbacks/callbacks.go | 49 +++++++++ _examples/callbacks/test.py | 18 ++++ bind/cffi_build.py | 30 ++++-- bind/cffi_callback.go | 172 ++++++++++++++++++++++--------- bind/gen.go | 2 +- main_test.go | 13 +++ 7 files changed, 228 insertions(+), 66 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 903861b5..441d93e8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -136,10 +136,8 @@ jobs: - name: Build run: go build -v ./... - # The skipped tests use features the cffi backend does not support yet: - # Python callbacks that take an interface{} or return a value - # (TestBindFuncs; see cffi_callback.go for the ones that work), and complex - # numbers as a slice element rather than a plain function arg/return - # (TestBuiltinSlices; see genFuncComplexCFFI in gen_func.go for the case that works). + # The skipped test uses a feature the cffi backend does not support yet: + # complex numbers as a slice element rather than a plain function + # arg/return (see genFuncComplexCFFI in gen_func.go for the case that works). - name: Test - run: go test -v -skip '^(TestBindFuncs|TestBuiltinSlices)$' ./... + run: go test -v -skip '^TestBuiltinSlices$' ./... diff --git a/_examples/callbacks/callbacks.go b/_examples/callbacks/callbacks.go index ad0add37..578fd5ba 100644 --- a/_examples/callbacks/callbacks.go +++ b/_examples/callbacks/callbacks.go @@ -9,6 +9,7 @@ package callbacks import ( "fmt" "sync" + "time" ) // Each calls fun for i in 0..n-1, with a label made from i. @@ -30,6 +31,49 @@ func Twice(fun func()) { 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 @@ -43,6 +87,11 @@ func (c *Counter) Visit(times int, fun func(c *Counter, n int)) { } } +// 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 diff --git a/_examples/callbacks/test.py b/_examples/callbacks/test.py index 3e652e19..d24aec27 100644 --- a/_examples/callbacks/test.py +++ b/_examples/callbacks/test.py @@ -31,6 +31,24 @@ def visit(handle, 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): diff --git a/bind/cffi_build.py b/bind/cffi_build.py index df76e687..36b93ff7 100644 --- a/bind/cffi_build.py +++ b/bind/cffi_build.py @@ -160,13 +160,16 @@ def wrapper(name, ret, params, exported): def callback_setup(pname, ctype, argn): """Returns the python source that wraps the callable pname in an - ffi.callback, for the C signature in ctype ("callback:void(int64_t,char*)", - see cffi_callback.go). The wrapper passes it on to Go as _cb_, - which is kept referenced by this local variable until the Go call - returns: cffi frees a callback as soon as nothing refers to it. + ffi.callback, for the C signature in ctype + ("callback:()", see cffi_callback.go). + The wrapper passes it on to Go as _cb_, which is kept referenced by + this local variable until the Go call returns: cffi frees a callback as + soon as nothing refers to it. + + If the callable raises, cffi prints the traceback and returns 0 to Go. """ - cargs = ctype[len("callback:void("):-1] - ctypes = cargs.split(",") if cargs else [] + ret, _, rest = ctype[len("callback:"):].partition("(") + ctypes = [t for t in rest[:-1].split(",") if t] names = ["a%d" % i for i in range(len(ctypes))] conv = [] for n, t in zip(names, ctypes): @@ -176,12 +179,23 @@ def callback_setup(pname, ctype, argn): conv.append("bool(%s)" % n) else: conv.append(n) - cdecl = "void(%s)" % ", ".join("unsigned char" if t == "bool" else t for t in ctypes) + call = "%s(%s)" % (pname, ", ".join(conv)) + if ret == "void": + body = call + elif ret == "bool": + body = "return 1 if %s else 0" % call + else: + body = "return %s" % call + + def cdecl_type(t): + return "unsigned char" if t == "bool" else t + + cdecl = "%s(%s)" % (cdecl_type(ret), ", ".join(cdecl_type(t) for t in ctypes)) return "\n".join([ " if not callable(%s):" % pname, " raise TypeError('argument %d must be callable, not %%s' %% type(%s).__name__)" % (argn, pname), " def _cbfn_%s(%s):" % (pname, ", ".join(names)), - " %s(%s)" % (pname, ", ".join(conv)), + " %s" % body, " _cb_%s = _ffi.callback(%r, _cbfn_%s)" % (pname, cdecl, pname), ]) diff --git a/bind/cffi_callback.go b/bind/cffi_callback.go index 1c357745..284b6b11 100644 --- a/bind/cffi_callback.go +++ b/bind/cffi_callback.go @@ -21,9 +21,10 @@ import ( // closure goes through a gopyCallbackScope that is closed when that call // returns, instead of jumping to freed memory if Go kept it any longer. // -// Only what is needed so far is supported: no results, and parameters -// limited to pointer/interface handles, numbers, bool and string. Any other -// callback type makes the function be skipped, see genFuncSig. +// Supported: parameters that are pointer/interface handles, numbers, bool, +// string or interface{} (which arrives as a string, as with pybindgen), and a +// result that is a number or bool. Any other callback type makes the +// function be skipped, see genFuncSig. // cffiTrampolinesKey stands in for the trampolines in the cgo preamble, // which is written before the callback types that need them are known. @@ -38,15 +39,48 @@ type cffiCBParam struct { conv string // Go expression converting it to ctype } +// cffiCBResult is the result of a callback. +type cffiCBResult struct { + gotyp string // its Go type + ctype string // how it crosses: int64_t, uint64_t, double or bool +} + type cffiCallback struct { params []cffiCBParam + ret *cffiCBResult // nil if the callback has no result +} + +// cffiBasicCType returns how a value of a basic Go type crosses, or "" if it +// can't. +func cffiBasicCType(k types.BasicKind) string { + switch { + case types.Int <= k && k <= types.Int64: + return "int64_t" + case types.Uint <= k && k <= types.Uintptr: + return "uint64_t" + case k == types.Float32 || k == types.Float64: + return "double" + case k == types.Bool: + return "bool" + case k == types.String: + return "char*" + } + return "" +} + +// cffiCType returns the C type used for ctype in C code: bool is a byte. +func cffiCType(ctype string) string { + if ctype == "bool" { + return "uint8_t" + } + return ctype } // cffiCallback returns how to pass sym, a func-typed argument, to Go // as a callback, or nil if its type isn't supported. func (g *pyGen) cffiCallback(sym *symbol) *cffiCallback { sig, ok := sym.GoType().Underlying().(*types.Signature) - if !ok || sig.Results().Len() != 0 || sig.Variadic() { + if !ok || sig.Results().Len() > 1 || sig.Variadic() { return nil } cb := &cffiCallback{} @@ -57,9 +91,22 @@ func (g *pyGen) cffiCallback(sym *symbol) *cffiCallback { } cb.params = append(cb.params, p) } + if sig.Results().Len() == 1 { + r, ok := cffiCallbackResult(sig.Results().At(0).Type()) + if !ok { + return nil + } + cb.ret = r + } return cb } +// cStringPre returns the Go statements that make the C string _c from +// expr, for the duration of the call. The python side copies it. +func cStringPre(nm, expr string) string { + return fmt.Sprintf("_c%[1]s := %[2]s\ndefer C.free(unsafe.Pointer(_c%[1]s))\n", nm, expr) +} + func cffiCallbackParam(v *types.Var, i int) (cffiCBParam, bool) { typ := v.Type() vsym := current.symtype(typ) @@ -74,41 +121,55 @@ func cffiCallbackParam(v *types.Var, i int) (cffiCBParam, bool) { p.conv = fmt.Sprintf("C.int64_t(%s(%s)%s)", vsym.go2py, nm, vsym.go2pyParenEx) return p, true } + if vsym.goname == "interface{}" { + p.ctype, p.conv = "char*", "_c"+nm + p.pre = cStringPre(nm, fmt.Sprintf("%s(%s)%s", vsym.go2py, nm, vsym.go2pyParenEx)) + return p, true + } bt, ok := typ.Underlying().(*types.Basic) if !ok { return p, false } - switch k := bt.Kind(); { - case types.Int <= k && k <= types.Int64: - p.ctype, p.conv = "int64_t", fmt.Sprintf("C.int64_t(%s)", nm) - case types.Uint <= k && k <= types.Uintptr: - p.ctype, p.conv = "uint64_t", fmt.Sprintf("C.uint64_t(%s)", nm) - case k == types.Float32 || k == types.Float64: - p.ctype, p.conv = "double", fmt.Sprintf("C.double(%s)", nm) - case k == types.Bool: - p.ctype, p.conv = "bool", fmt.Sprintf("C.uint8_t(boolGoToPy(bool(%s)))", nm) - case k == types.String: - // the python side copies it, so it can be freed once the call returns - p.ctype, p.conv = "char*", "_c"+nm - p.pre = fmt.Sprintf("_c%[1]s := C.CString(string(%[1]s))\ndefer C.free(unsafe.Pointer(_c%[1]s))\n", nm) - default: + switch p.ctype = cffiBasicCType(bt.Kind()); p.ctype { + case "": return p, false + case "bool": + p.conv = fmt.Sprintf("C.uint8_t(boolGoToPy(bool(%s)))", nm) + case "char*": + p.conv = "_c" + nm + p.pre = cStringPre(nm, fmt.Sprintf("C.CString(string(%s))", nm)) + default: + p.conv = fmt.Sprintf("C.%s(%s)", p.ctype, nm) } return p, true } -func (cb *cffiCallback) ctypes() []string { - ts := make([]string, len(cb.params)) - for i, p := range cb.params { - ts[i] = p.ctype +func cffiCallbackResult(typ types.Type) (*cffiCBResult, bool) { + bt, ok := typ.Underlying().(*types.Basic) + if !ok { + return nil, false + } + switch ctype := cffiBasicCType(bt.Kind()); ctype { + case "", "char*": // the python side has no way to give Go a string it owns + return nil, false + default: + return &cffiCBResult{gotyp: current.typeGoName(typ), ctype: ctype}, true } - return ts } // pyType is the type given to the callback's parameter in build.py, which -// callback_setup in cffi_build.py takes apart. +// callback_setup in cffi_build.py takes apart: +// callback:() func (cb *cffiCallback) pyType() string { - return "callback:void(" + strings.Join(cb.ctypes(), ",") + ")" + ret := "void" + if cb.ret != nil { + ret = cb.ret.ctype + } + ts := make([]string, len(cb.params)) + for i, p := range cb.params { + ts[i] = p.ctype + } + return "callback:" + ret + "(" + strings.Join(ts, ",") + ")" } // cffiCallbackPrologue returns the Go statements that set up the callback @@ -120,30 +181,37 @@ func cffiCallbackPrologue(anm string) string { // cffiCallbackLit returns a Go func literal that calls the Python callable // passed as the argument named anm. func (g *pyGen) cffiCallbackLit(cb *cffiCallback, anm string) string { - var decl, pre, args []string - args = append(args, "_cbfp_"+anm) + var decl, pre []string + args := []string{"_cbfp_" + anm} for _, p := range cb.params { decl = append(decl, p.name+" "+p.gotyp) - if p.pre != "" { - pre = append(pre, p.pre) - } + pre = append(pre, p.pre) args = append(args, p.conv) } - return fmt.Sprintf("func(%s) {\nif !_cbs_%s.enter() {\nreturn\n}\ndefer _cbs_%[2]s.leave()\n%sC.gopy_cb_%d(%s)\n}", - strings.Join(decl, ", "), anm, strings.Join(pre, ""), g.cffiTrampoline(cb), strings.Join(args, ", ")) + call := fmt.Sprintf("C.gopy_cb_%d(%s)", g.cffiTrampoline(cb), strings.Join(args, ", ")) + result := "" + if cb.ret != nil { + // named, so that a refused call returns its zero value + result = " (_r " + cb.ret.gotyp + ")" + if cb.ret.ctype == "bool" { + call += " != 0" + } + call = "return " + cb.ret.gotyp + "(" + call + ")" + } + return fmt.Sprintf("func(%[1]s)%[2]s {\nif !_cbs_%[3]s.enter() {\nreturn\n}\ndefer _cbs_%[3]s.leave()\n%[4]s%[5]s\n}", + strings.Join(decl, ", "), result, anm, strings.Join(pre, ""), call) } // cffiTrampoline returns the number of the C trampoline that calls a callback // like cb, adding it if it is the first. func (g *pyGen) cffiTrampoline(cb *cffiCallback) int { - key := strings.Join(cb.ctypes(), ",") - for i, k := range g.cbSigs { - if k == key { + for i, c := range g.cbs { + if c.pyType() == cb.pyType() { return i } } - g.cbSigs = append(g.cbSigs, key) - return len(g.cbSigs) - 1 + g.cbs = append(g.cbs, cb) + return len(g.cbs) - 1 } // spliceCFFITrampolines writes the trampolines into the cgo preamble. @@ -152,23 +220,25 @@ func (g *pyGen) spliceCFFITrampolines() { return } var c strings.Builder - for i, key := range g.cbSigs { + for i, cb := range g.cbs { params := []string{"void* f"} - var args, ptypes []string - if key != "" { - for j, t := range strings.Split(key, ",") { - if t == "bool" { - t = "uint8_t" - } - params = append(params, fmt.Sprintf("%s a%d", t, j)) - args = append(args, fmt.Sprintf("a%d", j)) - ptypes = append(ptypes, t) - } - } else { + ptypes := []string{} + args := []string{} + for j, p := range cb.params { + t := cffiCType(p.ctype) + params = append(params, fmt.Sprintf("%s a%d", t, j)) + ptypes = append(ptypes, t) + args = append(args, fmt.Sprintf("a%d", j)) + } + if len(ptypes) == 0 { ptypes = append(ptypes, "void") } - fmt.Fprintf(&c, "static inline void gopy_cb_%d(%s) { ((void (*)(%s))f)(%s); }\n", - i, strings.Join(params, ", "), strings.Join(ptypes, ", "), strings.Join(args, ", ")) + ret, retStmt := "void", "" + if cb.ret != nil { + ret, retStmt = cffiCType(cb.ret.ctype), "return " + } + fmt.Fprintf(&c, "static inline %s gopy_cb_%d(%s) { %s((%s (*)(%s))f)(%s); }\n", + ret, i, strings.Join(params, ", "), retStmt, ret, strings.Join(ptypes, ", "), strings.Join(args, ", ")) } b := bytes.Replace(g.gofile.buf.Bytes(), []byte(cffiTrampolinesKey), []byte(c.String()), 1) g.gofile.buf = bytes.NewBuffer(b) diff --git a/bind/gen.go b/bind/gen.go index f4552375..52f694af 100644 --- a/bind/gen.go +++ b/bind/gen.go @@ -608,7 +608,7 @@ type pyGen struct { extraGccArgs string lang int // c-python api version (2,3) dynamicLink bool - cbSigs []string // cffi: the callback types that have a C trampoline, see cffi_callback.go + cbs []*cffiCallback // cffi: the callback types that have a C trampoline, see cffi_callback.go } func (g *pyGen) gen() error { diff --git a/main_test.go b/main_test.go index a1815a99..9a5a6c4d 100644 --- a/main_test.go +++ b/main_test.go @@ -414,6 +414,19 @@ twice: 2 visit: 1 1 visit: 2 2 counter: 2 +--- Describe: an interface{} arrives as a string +describe: 'a string' +describe: '1.5s' +--- Count: a bool result +count: 4 +--- Sum: an int result +sum: 30 +--- Widest: a uint result +widest: 30 +--- Apply: a float result +apply: 3.0 +--- Counter.Check: a handle argument and a bool result +check: True --- a bound method box: [(0, 'item-0'), (1, 'item-1')] --- called from another goroutine From a65a870f7055b75f547e54cb1ed09ae247749ba9 Mon Sep 17 00:00:00 2001 From: b-long Date: Sat, 19 Sep 2026 21:47:12 -0400 Subject: [PATCH 9/9] Complete cffi support for full test suite --- .github/workflows/ci.yml | 5 +- _examples/slices/slices.go | 5 ++ _examples/slices/test.py | 13 ++++++ bind/cffi.go | 96 ++++++++++++++++++++++++++++++++++++++ bind/cffi_build.py | 46 ++++-------------- bind/gen_func.go | 49 +++++++------------ bind/gen_slice.go | 42 ++++++----------- 7 files changed, 155 insertions(+), 101 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 441d93e8..f0c7b441 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -136,8 +136,5 @@ jobs: - name: Build run: go build -v ./... - # The skipped test uses a feature the cffi backend does not support yet: - # complex numbers as a slice element rather than a plain function - # arg/return (see genFuncComplexCFFI in gen_func.go for the case that works). - name: Test - run: go test -v -skip '^TestBuiltinSlices$' ./... + run: go test -v ./... diff --git a/_examples/slices/slices.go b/_examples/slices/slices.go index baa5d7e6..82d6777b 100644 --- a/_examples/slices/slices.go +++ b/_examples/slices/slices.go @@ -32,6 +32,7 @@ type SliceInt32 []int32 type SliceInt64 []int64 type SliceComplex []complex128 +type SliceComplex64 []complex64 type SliceIface []interface{} @@ -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{} diff --git a/_examples/slices/test.py b/_examples/slices/test.py index 143f4533..b503909a 100644 --- a/_examples/slices/test.py +++ b/_examples/slices/test.py @@ -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): diff --git a/bind/cffi.go b/bind/cffi.go index 8a880da2..2798babb 100644 --- a/bind/cffi.go +++ b/bind/cffi.go @@ -6,6 +6,7 @@ package bind import ( _ "embed" + "fmt" "strings" ) @@ -37,6 +38,84 @@ func (g *pyGen) goSetError(kind, msg string) string { return "gopySetError(\"" + kind + "\", " + msg + ")\n" } +func isComplexSym(sym *symbol) bool { + return sym != nil && (sym.goname == "complex64" || sym.goname == "complex128") +} + +// A complex64/complex128 value has no single C type that cffi can declare +// (cgo's is _Complex), and cgo won't export a struct, so under cffi it crosses +// as two floats: as two parameters (_re, _im), and as a result in +// cgo's two-value return, which it exports as a plain C struct {r0; r1;}. +// The methods below say how a value of a given symbol crosses, so that the +// generators only differ from the default backend here. + +// isCFFIComplex reports whether sym crosses as two floats. +func (g *pyGen) isCFFIComplex(sym *symbol) bool { + return g.isCFFI() && isComplexSym(sym) +} + +// cffiComplexFloat returns the cgo and the Go float type of the parts of a +// complex64 or complex128 symbol. +func cffiComplexFloat(sym *symbol) (cfloat, gofloat string) { + if sym.goname == "complex64" { + return "C.float", "float32" + } + return "C.double", "float64" +} + +// cgoParam returns the declaration of the parameter of an exported function +// that carries a value of sym. +func (g *pyGen) cgoParam(name string, sym *symbol) string { + if g.isCFFIComplex(sym) { + cf, _ := cffiComplexFloat(sym) + return fmt.Sprintf("%[1]s_re %[2]s, %[1]s_im %[2]s", name, cf) + } + return name + " " + sym.cgoname +} + +// cgoResult returns the result type of an exported function that returns a +// value of sym. +func (g *pyGen) cgoResult(sym *symbol) string { + if g.isCFFIComplex(sym) { + cf, _ := cffiComplexFloat(sym) + return "(" + cf + ", " + cf + ")" + } + return sym.cgoname +} + +// cpyName returns the type that build.py records for a value of sym. +// wrapper in cffi_build.py expands complex64 and complex128. +func (g *pyGen) cpyName(sym *symbol) string { + if g.isCFFIComplex(sym) { + return sym.goname + } + return sym.cpyname +} + +// goToCgo returns the Go expression that converts expr, a value of sym, to +// what an exported function returns. +func (g *pyGen) goToCgo(sym *symbol, expr string) string { + switch { + case g.isCFFIComplex(sym): + return sym.goname + "GoToPyCFFI(" + expr + ")" + case sym.go2py != "": + return sym.go2py + "(" + expr + ")" + sym.go2pyParenEx + } + return expr +} + +// cgoToGo returns the Go expression that converts the parameter name, as +// declared by cgoParam, to a value of sym. +func (g *pyGen) cgoToGo(sym *symbol, name string) string { + switch { + case g.isCFFIComplex(sym): + return sym.goname + "PyToGoCFFI(" + name + "_re, " + name + "_im)" + case sym.py2go != "": + return sym.py2go + "(" + name + ")" + sym.py2goParenEx + } + return name +} + // same argument positions as goPreamble: 1 = name of package, 2 = cmdstr, // 4 = GoHandle, 5 = CGoHandle, 6 = all imports, 7 = mainstr, 8 = C trampolines for // callbacks (see cffi_callback.go), 10 = gopy version. @@ -196,4 +275,21 @@ func errorGoToPy(e error) *C.char { } return C.CString("") } + +// complex values cross as two floats, see isCFFIComplex in cffi.go +func complex64GoToPyCFFI(c complex64) (C.float, C.float) { + return C.float(real(c)), C.float(imag(c)) +} + +func complex64PyToGoCFFI(re, im C.float) complex64 { + return complex(float32(re), float32(im)) +} + +func complex128GoToPyCFFI(c complex128) (C.double, C.double) { + return C.double(real(c)), C.double(imag(c)) +} + +func complex128PyToGoCFFI(re, im C.double) complex128 { + return complex(float64(re), float64(im)) +} ` diff --git a/bind/cffi_build.py b/bind/cffi_build.py index 36b93ff7..c4ff530b 100644 --- a/bind/cffi_build.py +++ b/bind/cffi_build.py @@ -32,24 +32,15 @@ def add_include(self, inc): self.header = inc.strip('"') def add_function(self, name, ret, params, *a, **kw): - self.funcs.append(("plain", name, ret, params)) - - def add_complex_function(self, name, nargs): - # see genFuncComplexCFFI (gen_func.go) for the calling convention. - self.funcs.append(("complex", name, nargs)) + self.funcs.append((name, ret, params)) def generate(self): here = os.path.dirname(os.path.abspath(__file__)) with open(os.path.join(here, self.header)) as f: typedefs, cdefs = go_decls(f.read()) out = [MODULE_HEAD.replace("@CDEFS@", repr("\n".join(typedefs + list(cdefs.values()))))] - for entry in self.funcs: - if entry[0] == "complex": - _, name, nargs = entry - out.append(complex_wrapper(name, nargs, name in cdefs)) - else: - _, name, ret, params = entry - out.append(wrapper(name, ret, params, name in cdefs)) + for name, ret, params in self.funcs: + out.append(wrapper(name, ret, params, name in cdefs)) # Slice_byte's converters exchange a raw pointer+length instead of a # PyObject* (see gen_slice.go); they are recognized by name here # rather than recorded via add_function, since their python bodies @@ -69,8 +60,9 @@ def add_checked_function(mod, name, retval, params, failure_expression="", *a, * def go_decls(header): """Returns the Go typedefs (plus any "struct X { ... };" body -- cgo emits - one ahead of a multi-value-returning export, see genFuncComplexCFFI in - gen_func.go), and {function name: cdef line} for the functions cgo exports. + one ahead of an export that returns two values, which is how a complex + number is returned, see isCFFIComplex in cffi.go), and {function name: + cdef line} for the functions cgo exports. """ typedefs = [] decls = {} @@ -137,6 +129,8 @@ def wrapper(name, ret, params, exported): args.append("_ffi.cast('void*', _cb_%s)" % pname) elif ctype == "char*": args.append("_enc(%s, %d)" % (pname, i + 1)) + elif ctype in ("complex64", "complex128"): + args.append("%s.real, %s.imag" % (pname, pname)) elif ctype == "bool": args.append("(1 if %s else 0)" % pname) elif ctype == "uint8_t": @@ -152,6 +146,8 @@ def wrapper(name, ret, params, exported): body.append(" _r = bool(_r)") elif ret == "uint8_t": body.append(" _r = _r & 0xFF") + elif ret in ("complex64", "complex128"): + body.append(" _r = complex(_r.r0, _r.r1)") body.append(" _check()") if ret is not None: body.append(" return _r") @@ -200,28 +196,6 @@ def cdecl_type(t): ]) -def complex_wrapper(name, nargs, exported): - """A plain function whose every argument and return value is - complex64/128 (see genFuncComplexCFFI, gen_func.go): each argument - crosses as two floats (.real, .imag), and the return value comes back - as the {r0, r1} struct cgo generates for a two-value Go return. - """ - if not exported: - return ( - "def %s(*args):\n" - " raise NotImplementedError('%s is not available with the cffi backend')\n" % (name, name) - ) - names = ["c%d" % i for i in range(nargs)] - sig = ", ".join(names) - callargs = ", ".join("%s.real, %s.imag" % (n, n) for n in names) - return ( - "def %s(%s):\n" - " _r = _lib.%s(%s)\n" - " _check()\n" - " return complex(_r.r0, _r.r1)\n" % (name, sig, name, callargs) - ) - - MODULE_HEAD = '''# python bindings for package @NAME@ using cffi. # File is generated by gopy version @VERSION@. Do not edit. import builtins diff --git a/bind/gen_func.go b/bind/gen_func.go index 0b8b7548..d46e3fc9 100644 --- a/bind/gen_func.go +++ b/bind/gen_func.go @@ -232,22 +232,14 @@ func (g *pyGen) genFunc(o *Func) { } } -func isComplexSym(sym *symbol) bool { - return sym != nil && (sym.goname == "complex64" || sym.goname == "complex128") -} - // genFuncComplexCFFI generates a plain (non-method) function whose every -// argument and its one return value are complex64/128. The normal path -// (genFuncSig/genFuncBody) represents complex64/128 as a *C.PyObject, which -// the cffi preamble doesn't declare -- fine for most types, but complex -// values need actual marshaling here, not just a skip. Instead, an -// argument crosses as two plain floats (real, imag), and the return value -// uses Go's native multi-value return, which cgo exports as a small C -// struct {r0; r1;} that cffi can declare (see go_decls/complex_wrapper in -// cffi_build.py). It returns false, writing nothing, if the signature -// doesn't fit that narrow shape (methods, a mix of complex and other -// argument types, or an error return) -- genFuncSig's PyObject* check then -// skips the function instead of emitting code that fails to compile. +// argument and its one return value are complex64/128, which the normal path +// (genFuncSig/genFuncBody) can't do under cffi, where a complex value crosses +// as two floats (see isCFFIComplex in cffi.go). It returns false, writing +// nothing, if the signature doesn't fit that narrow shape (methods, a mix of +// complex and other argument types, or an error return): genFuncSig's +// PyObject* check then skips the function instead of emitting code that +// fails to compile. func (g *pyGen) genFuncComplexCFFI(fsym *Func) bool { sig := fsym.sig if sig == nil || fsym.isVariadic || fsym.err { @@ -273,34 +265,25 @@ func (g *pyGen) genFuncComplexCFFI(fsym *Func) bool { return false } - cfloatOf := func(sym *symbol) (cgo, gotyp string) { - if sym.goname == "complex64" { - return "C.float", "float32" - } - return "C.double", "float64" - } - - retFloat, _ := cfloatOf(current.symtype(res[0].GoType())) - - var goArgs, callArgs, wpArgs []string + ret := current.symtype(res[0].GoType()) + var goArgs, pyArgs, callArgs, wpArgs []string for i, arg := range args { anm := pySafeArg(arg.Name(), i) - cfloat, gofloat := cfloatOf(current.symtype(arg.GoType())) - reNm, imNm := anm+"_re", anm+"_im" - goArgs = append(goArgs, fmt.Sprintf("%s %s, %s %s", reNm, cfloat, imNm, cfloat)) - callArgs = append(callArgs, fmt.Sprintf("complex(%s(%s), %s(%s))", gofloat, reNm, gofloat, imNm)) + sarg := current.symtype(arg.GoType()) + goArgs = append(goArgs, g.cgoParam(anm, sarg)) + pyArgs = append(pyArgs, fmt.Sprintf("param('%s', '%s')", g.cpyName(sarg), anm)) + callArgs = append(callArgs, g.cgoToGo(sarg, anm)) wpArgs = append(wpArgs, anm) } g.gofile.Printf("\n//export %s\n", fsym.ID()) - g.gofile.Printf("func %s(%s) (%s, %s) {\n", fsym.ID(), strings.Join(goArgs, ", "), retFloat, retFloat) + g.gofile.Printf("func %s(%s) %s {\n", fsym.ID(), strings.Join(goArgs, ", "), g.cgoResult(ret)) g.gofile.Indent() - g.gofile.Printf("_r := %s(%s)\n", fsym.GoFmt(), strings.Join(callArgs, ", ")) - g.gofile.Printf("return %s(real(_r)), %s(imag(_r))\n", retFloat, retFloat) + g.gofile.Printf("return %s\n", g.goToCgo(ret, fmt.Sprintf("%s(%s)", fsym.GoFmt(), strings.Join(callArgs, ", ")))) g.gofile.Outdent() g.gofile.Printf("}\n\n") - g.pybuild.Printf("mod.add_complex_function('%s', %d)\n", fsym.ID(), len(args)) + g.pybuild.Printf("mod.add_function('%s', retval('%s'), [%s])\n", fsym.ID(), g.cpyName(ret), strings.Join(pyArgs, ", ")) g.pywrap.Printf("def %s(%s):\n", gname, strings.Join(wpArgs, ", ")) g.pywrap.Indent() diff --git a/bind/gen_slice.go b/bind/gen_slice.go index 7f277e7c..96f05b42 100644 --- a/bind/gen_slice.go +++ b/bind/gen_slice.go @@ -74,7 +74,7 @@ func (g *pyGen) genSliceInit(slc *symbol, extTypes, pyWrapOnly bool, slob *Slice // which cffi's preamble doesn't declare (see genFuncSig for the same // restriction on plain function args/returns); skip the whole wrapper // rather than emit code that fails to compile. - if g.isCFFI() && esym != nil && esym.cpyname == "PyObject*" { + if g.isCFFI() && esym != nil && esym.cpyname == "PyObject*" && !isComplexSym(esym) { return } @@ -329,21 +329,15 @@ otherwise parameter is a python list that we copy from g.pybuild.Printf("mod.add_function('%s_len', retval('int'), [param('%s', 'handle')])\n", slNm, PyHandle) g.gofile.Printf("//export %s_elem\n", slNm) - g.gofile.Printf("func %s_elem(handle CGoHandle, _idx int) %s {\n", slNm, esym.cgoname) + g.gofile.Printf("func %s_elem(handle CGoHandle, _idx int) %s {\n", slNm, g.cgoResult(esym)) g.gofile.Indent() g.gofile.Printf("s := deptrFromHandle_%s(handle)\n", slNm) - if esym.go2py != "" { - // If the go2py starts with handleFromPtr_, use reference &, otherwise just return the value - val_str := "" - if strings.HasPrefix(esym.go2py, "handleFromPtr_") { - val_str = "&(s[_idx])" - } else { - val_str = "s[_idx]" - } - g.gofile.Printf("return %s(%s)%s\n", esym.go2py, val_str, esym.go2pyParenEx) - } else { - g.gofile.Printf("return s[_idx]\n") + // If the go2py starts with handleFromPtr_, use reference &, otherwise just return the value + val_str := "s[_idx]" + if strings.HasPrefix(esym.go2py, "handleFromPtr_") { + val_str = "&(s[_idx])" } + g.gofile.Printf("return %s\n", g.goToCgo(esym, val_str)) g.gofile.Outdent() g.gofile.Printf("}\n\n") @@ -356,7 +350,7 @@ otherwise parameter is a python list that we copy from if esym.cpyname == "char*" { g.pybuild.Printf("add_checked_string_function(mod, '%s_elem', retval('%s'), [param('%s', 'handle'), param('int', 'idx')])\n", slNm, esym.cpyname, PyHandle) } else { - g.pybuild.Printf("mod.add_function('%s_elem', retval('%s'%s), [param('%s', 'handle'), param('int', 'idx')])\n", slNm, esym.cpyname, caller_owns_ret, PyHandle) + g.pybuild.Printf("mod.add_function('%s_elem', retval('%s'%s), [param('%s', 'handle'), param('int', 'idx')])\n", slNm, g.cpyName(esym), caller_owns_ret, PyHandle) } if slc.isSlice() { @@ -373,33 +367,25 @@ otherwise parameter is a python list that we copy from } g.gofile.Printf("//export %s_set\n", slNm) - g.gofile.Printf("func %s_set(handle CGoHandle, _idx int, _vl %s) {\n", slNm, esym.cgoname) + g.gofile.Printf("func %s_set(handle CGoHandle, _idx int, %s) {\n", slNm, g.cgoParam("_vl", esym)) g.gofile.Indent() g.gofile.Printf("s := deptrFromHandle_%s(handle)\n", slNm) - if esym.py2go != "" { - g.gofile.Printf("s[_idx] = %s(_vl)%s\n", esym.py2go, esym.py2goParenEx) - } else { - g.gofile.Printf("s[_idx] = _vl\n") - } + g.gofile.Printf("s[_idx] = %s\n", g.cgoToGo(esym, "_vl")) g.gofile.Outdent() g.gofile.Printf("}\n\n") - g.pybuild.Printf("mod.add_function('%s_set', None, [param('%s', 'handle'), param('int', 'idx'), param('%v', 'value'%s)])\n", slNm, PyHandle, esym.cpyname, transfer_ownership) + g.pybuild.Printf("mod.add_function('%s_set', None, [param('%s', 'handle'), param('int', 'idx'), param('%v', 'value'%s)])\n", slNm, PyHandle, g.cpyName(esym), transfer_ownership) if slc.isSlice() { g.gofile.Printf("//export %s_append\n", slNm) - g.gofile.Printf("func %s_append(handle CGoHandle, _vl %s) {\n", slNm, esym.cgoname) + g.gofile.Printf("func %s_append(handle CGoHandle, %s) {\n", slNm, g.cgoParam("_vl", esym)) g.gofile.Indent() g.gofile.Printf("s := ptrFromHandle_%s(handle)\n", slNm) - if esym.py2go != "" { - g.gofile.Printf("*s = append(*s, %s(_vl)%s)\n", esym.py2go, esym.py2goParenEx) - } else { - g.gofile.Printf("*s = append(*s, _vl)\n") - } + g.gofile.Printf("*s = append(*s, %s)\n", g.cgoToGo(esym, "_vl")) g.gofile.Outdent() g.gofile.Printf("}\n\n") - g.pybuild.Printf("mod.add_function('%s_append', None, [param('%s', 'handle'), param('%s', 'value'%s)])\n", slNm, PyHandle, esym.cpyname, transfer_ownership) + g.pybuild.Printf("mod.add_function('%s_append', None, [param('%s', 'handle'), param('%s', 'value'%s)])\n", slNm, PyHandle, g.cpyName(esym), transfer_ownership) } if slNm == "Slice_byte" {