Skip to content

Commit 8569a8d

Browse files
authored
Merge pull request #255 from justinfx/254_err_check
bind: Check for exceptions and propagate errors back to python caller
2 parents ac5c077 + 57a66d6 commit 8569a8d

File tree

9 files changed

+120
-171
lines changed

9 files changed

+120
-171
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ package hi exposes a few Go functions to be wrapped and used from Python.
179179
```sh
180180
$ gopy build -output=out -vm=`which python3` github.com/go-python/gopy/_examples/hi
181181
$ ls out
182-
Makefile __init__.py __pycache__/ _hi.so* build.py go.py hi.c hi.go hi.py hi_go.h hi_go.so patch-leaks.go
182+
Makefile __init__.py __pycache__/ _hi.so* build.py go.py hi.c hi.go hi.py hi_go.h hi_go.so
183183
```
184184
185185

_examples/pyerrors/pyerrors.go

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@
55
// Package pyerrors holds functions returning an error.
66
package pyerrors
77

8-
import "errors"
8+
import (
9+
"errors"
10+
"fmt"
11+
)
912

1013
// Div is a function for detecting errors.
1114
func Div(i, j int) (int, error) {
@@ -14,3 +17,18 @@ func Div(i, j int) (int, error) {
1417
}
1518
return i / j, nil
1619
}
20+
21+
type Stringer fmt.Stringer
22+
type MyString string
23+
24+
func (t MyString) String() string { return string(t) }
25+
26+
// NewMyString converts a string to a custom MyString type.
27+
// It is an error to pass an empty string value.
28+
func NewMyString(val string) (stringer Stringer, err error) {
29+
if val == "" {
30+
err = errors.New("Empty string value.")
31+
return
32+
}
33+
return MyString(val), nil
34+
}

_examples/pyerrors/test.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,19 @@ def div(a, b):
1414
except Exception as e:
1515
print(e)
1616

17-
div(5,0)
17+
18+
def new_mystring(s):
19+
try:
20+
ms = pyerrors.NewMyString(s)
21+
print('pyerrors.NewMyString("%s") = "%s"'% (s, ms.String()))
22+
except Exception as e:
23+
print(e)
24+
25+
26+
div(5,0) # error
1827
div(5,2)
1928

29+
new_mystring("") # error
30+
new_mystring("hello")
31+
2032
print("OK")

bind/gen.go

Lines changed: 48 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -203,9 +203,43 @@ func GoPyMainRun() {
203203
# File is generated by gopy. Do not edit.
204204
# %[2]s
205205
206-
from pybindgen import retval, param, Module
206+
from pybindgen import retval, param, Function, Module
207207
import sys
208208
209+
class CheckedFunction(Function):
210+
def __init__(self, *a, **kw):
211+
super(CheckedFunction, self).__init__(*a, **kw)
212+
self._failure_expression = kw.get('failure_expression', '')
213+
self._failure_cleanup = kw.get('failure_cleanup', '')
214+
215+
def set_failure_expression(self, expr):
216+
self._failure_expression = expr
217+
218+
def set_failure_cleanup(self, expr):
219+
self._failure_cleanup = expr
220+
221+
def generate_call(self):
222+
super(CheckedFunction, self).generate_call()
223+
check = "PyErr_Occurred()"
224+
if self._failure_expression:
225+
check = "{} && {}".format(self._failure_expression, check)
226+
failure_cleanup = self._failure_cleanup or None
227+
self.before_call.write_error_check(check, failure_cleanup)
228+
229+
def add_checked_function(mod, name, retval, params, failure_expression='', *a, **kw):
230+
fn = CheckedFunction(name, retval, params, *a, **kw)
231+
fn.set_failure_expression(failure_expression)
232+
mod._add_function_obj(fn)
233+
return fn
234+
235+
def add_checked_string_function(mod, name, retval, params, failure_expression='', *a, **kw):
236+
fn = CheckedFunction(name, retval, params, *a, **kw)
237+
fn.set_failure_cleanup('if (retval != NULL) free(retval);')
238+
fn.after_call.add_cleanup_code('free(retval);')
239+
fn.set_failure_expression(failure_expression)
240+
mod._add_function_obj(fn)
241+
return fn
242+
209243
mod = Module('_%[1]s')
210244
mod.add_include('"%[1]s_go.h"')
211245
mod.add_function('GoPyInit', None, [])
@@ -226,6 +260,11 @@ mod.add_function('NumHandles', retval('int'), [])
226260
227261
# the following is required to enable dlopen to open the _go.so file
228262
import os,sys,inspect,collections
263+
try:
264+
import collections.abc as _collections_abc
265+
except ImportError:
266+
_collections_abc = collections
267+
229268
cwd = os.getcwd()
230269
currentdir = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe())))
231270
os.chdir(currentdir)
@@ -250,6 +289,10 @@ os.chdir(cwd)
250289
# %[2]s
251290
252291
import collections
292+
try:
293+
import collections.abc as _collections_abc
294+
except ImportError:
295+
_collections_abc = collections
253296
%[6]s
254297
255298
# to use this code in your end-user python file, import it as follows:
@@ -263,6 +306,10 @@ import collections
263306

264307
GoPkgDefs = `
265308
import collections
309+
try:
310+
import collections.abc as _collections_abc
311+
except ImportError:
312+
_collections_abc = collections
266313
267314
class GoClass(object):
268315
"""GoClass is the base class for all GoPy wrapper classes"""
@@ -317,8 +364,6 @@ build:
317364
# use pybindgen to build the %[1]s.c file which are the CPython wrappers to cgo wrappers..
318365
# note: pip install pybindgen to get pybindgen if this fails
319366
$(PYTHON) build.py
320-
# patch storage leaks in pybindgen output
321-
go run patch-leaks.go %[1]s.c
322367
# build the _%[1]s$(LIBEXT) library that contains the cgo and CPython wrappers
323368
# generated %[1]s.py python wrapper imports this c-code package
324369
$(GCC) %[1]s.c %[6]s %[1]s_go$(LIBEXT) -o _%[1]s$(LIBEXT) $(CFLAGS) $(LDFLAGS) -fPIC --shared -w
@@ -360,91 +405,11 @@ build:
360405
# use pybindgen to build the %[1]s.c file which are the CPython wrappers to cgo wrappers..
361406
# note: pip install pybindgen to get pybindgen if this fails
362407
$(PYTHON) build.py
363-
# patch storage leaks in pybindgen output
364-
go run patch-leaks.go %[1]s.c
365408
# build the executable
366409
- rm %[1]s_go$(LIBEXT)
367410
$(GOBUILD) -o py%[1]s
368411
369412
`
370-
371-
patchLeaksPreamble = `// patch-leaks.go for post-processing %[1]s.
372-
// File is generated by gopy. Do not edit.
373-
// %[2]s
374-
// +build ignore
375-
376-
package main
377-
378-
import (
379-
"bufio"
380-
"bytes"
381-
"io/ioutil"
382-
"log"
383-
"os"
384-
"strings"
385-
)
386-
387-
const (
388-
cStringLine = " py_retval = Py_BuildValue((char *) \"s\", retval);"
389-
)
390-
var cstringFunctions = []string{
391-
`
392-
393-
patchLeaksPostamble = `
394-
}
395-
396-
func isCString(line string, names []string) bool {
397-
for _, cfn := range names {
398-
if strings.HasPrefix(line, cfn) {
399-
return true
400-
}
401-
}
402-
return false
403-
}
404-
405-
func patchCString(line string, out *bytes.Buffer) bool {
406-
out.WriteString(line)
407-
out.Write([]byte{'\n'})
408-
switch line {
409-
case "}":
410-
return false
411-
case cStringLine:
412-
out.WriteString(" free(retval);\n")
413-
return false
414-
}
415-
return true
416-
}
417-
418-
func main() {
419-
file := os.Args[1]
420-
buf, err := ioutil.ReadFile(file)
421-
if err != nil {
422-
log.Fatal(err)
423-
}
424-
sc := bufio.NewScanner(bytes.NewBuffer(buf))
425-
obuf := &bytes.Buffer{}
426-
var cstring bool
427-
for sc.Scan() {
428-
line := sc.Text()
429-
if cstring {
430-
cstring = patchCString(line, obuf)
431-
continue
432-
}
433-
cstring = isCString(line, cstringFunctions)
434-
obuf.WriteString(line)
435-
obuf.Write([]byte{'\n'})
436-
}
437-
438-
if err := sc.Err(); err != nil {
439-
log.Fatal(err)
440-
}
441-
442-
err = ioutil.WriteFile(file, obuf.Bytes(), 0644)
443-
if err != nil {
444-
log.Fatal(err)
445-
}
446-
}
447-
`
448413
)
449414

450415
// thePyGen is the current pyGen which is needed in symbols to lookup
@@ -536,7 +501,6 @@ func (g *pyGen) genPre() {
536501
g.makefile = &printer{buf: new(bytes.Buffer), indentEach: []byte("\t")}
537502
}
538503
g.genGoPreamble()
539-
g.genLeaksPreamble()
540504
g.genPyBuildPreamble()
541505
if !NoMake {
542506
g.genMakefile()
@@ -559,9 +523,7 @@ func (g *pyGen) genPrintOut(outfn string, pr *printer) {
559523
func (g *pyGen) genOut() {
560524
g.pybuild.Printf("\nmod.generate(open('%v.c', 'w'))\n\n", g.cfg.Name)
561525
g.gofile.Printf("\n\n")
562-
g.genLeaksPostamble()
563526
g.genPrintOut(g.cfg.Name+".go", g.gofile)
564-
g.genPrintOut("patch-leaks.go", g.leakfile)
565527
g.genPrintOut("build.py", g.pybuild)
566528
if !NoMake {
567529
g.makefile.Printf("\n\n")
@@ -635,14 +597,6 @@ func (g *pyGen) genGoPreamble() {
635597
g.gofile.Printf("\n// --- generated code for package: %[1]s below: ---\n\n", g.cfg.Name)
636598
}
637599

638-
func (g *pyGen) genLeaksPreamble() {
639-
g.leakfile.Printf(patchLeaksPreamble, g.cfg.Name, g.cfg.Cmd)
640-
}
641-
642-
func (g *pyGen) genLeaksPostamble() {
643-
g.leakfile.Printf(patchLeaksPostamble)
644-
}
645-
646600
func (g *pyGen) genPyBuildPreamble() {
647601
g.pybuild.Printf(PyBuildPreamble, g.cfg.Name, g.cfg.Cmd)
648602
}

bind/gen_func.go

Lines changed: 29 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -10,51 +10,10 @@ import (
1010
"strings"
1111
)
1212

13-
// genPatchLeakFuncName outputs the name of either the function or the class
14-
// 'getter' represented by its arguments that need to have a memory
15-
// leak induced by the interaction of pybindgen and gopy.
16-
// These leaks occur because the cgo bindings use C.CString
17-
// to allocate a string on the C heap, as in:
18-
//
19-
// return C.CString(cstrings.StringValue(C.GoString(s), int(n)))
20-
//
21-
// but the calling pybindgen code then takes a copy of this
22-
// newly allocated string indirectly by calling Py_BuildValue (see below)
23-
// without freeing the copy returned by the function or getter method.
24-
// Arguably pybindgen should provide a means of directly requesting
25-
// this behaviour (simularly to the transfer_ownership options it provides),
26-
// but as of now there is no means of doing so. As a work around the
27-
// output of pybindgen is modified to insert the free in the appropriate
28-
// functions identified by genPatchLeakFuncName.
29-
//
30-
// The patch-leaks.go (see gen.go) file patches these code fragments
31-
// to insert free(retval) immediately after the call to PyBuildValue.
32-
// The patch-leaks.go generated file will perform this patch for
33-
// functions whose names are embedded in it by genPatchLeakFuncName.
34-
//
35-
// .....
36-
// retval = cstrings_StringValue(s, n);
37-
// py_retval = Py_BuildValue((char *) "s", retval);
38-
// .....
39-
//
40-
// This patching is required for top level functions that return
41-
// strings directly and also for getters that return strings that
42-
// are embedded in maps, structs etc.
43-
// NOTE that it's ok to output the same function name multiple times (eg.
44-
// for a shared structs).
45-
func (g *pyGen) genPatchLeakFuncName(ret *Var, fsym *Func) {
46-
if ret == nil || fsym == nil || ret.GoType() == nil {
47-
return
48-
}
49-
g.recurse(ret.GoType(), "_wrap__"+fsym.pkg.Name()+"_", fsym.ID())
50-
}
51-
5213
func (g *pyGen) recurse(gotype types.Type, prefix, name string) {
5314
switch t := gotype.(type) {
5415
case *types.Basic:
55-
if t.Kind() == types.String {
56-
g.leakfile.Printf(" %q,\n", prefix+name)
57-
}
16+
// pass
5817
case *types.Map:
5918
g.recurse(t.Elem(), prefix, "Map_"+t.Key().String()+"_string_elem")
6019
case *types.Named:
@@ -149,21 +108,38 @@ func (g *pyGen) genFuncSig(sym *symbol, fsym *Func) bool {
149108
wpArgs = append(wpArgs, "goRun=False")
150109
}
151110

111+
// When building the pybindgen builder code, we start with
112+
// a function that adds function calls with exception checking.
113+
// But given specific return types, we may want to add more
114+
// behavior to the wrapped function code gen.
115+
addFuncName := "add_checked_function"
116+
if len(res) > 0 {
117+
ret := res[0]
118+
switch t := ret.GoType().(type) {
119+
case *types.Basic:
120+
// string return types need special memory leak patches
121+
// to free the allocated char*
122+
if t.Kind() == types.String {
123+
addFuncName = "add_checked_string_function"
124+
}
125+
}
126+
}
127+
152128
switch {
153129
case isMethod:
154130
mnm := sym.id + "_" + fsym.GoName()
155131

156132
g.gofile.Printf("\n//export %s\n", mnm)
157133
g.gofile.Printf("func %s(", mnm)
158134

159-
g.pybuild.Printf("mod.add_function('%s', ", mnm)
135+
g.pybuild.Printf("%s(mod, '%s', ", addFuncName, mnm)
160136

161137
g.pywrap.Printf("def %s(", gname)
162138
default:
163139
g.gofile.Printf("\n//export %s\n", fsym.ID())
164140
g.gofile.Printf("func %s(", fsym.ID())
165141

166-
g.pybuild.Printf("mod.add_function('%s', ", fsym.ID())
142+
g.pybuild.Printf("%s(mod, '%s', ", addFuncName, fsym.ID())
167143

168144
g.pywrap.Printf("def %s(", gname)
169145
}
@@ -180,8 +156,6 @@ func (g *pyGen) genFuncSig(sym *symbol, fsym *Func) bool {
180156
))
181157
}
182158

183-
g.genPatchLeakFuncName(ret, fsym)
184-
185159
if sret.cpyname == "PyObject*" {
186160
g.pybuild.Printf("retval('%s', caller_owns_return=True)", sret.cpyname)
187161
} else {
@@ -420,6 +394,15 @@ if __err != nil {
420394
g.gofile.Printf("return estr\n") // NOTE: leaked string
421395
} else {
422396
g.gofile.Printf("C.free(unsafe.Pointer(estr))\n") // python should have converted, safe
397+
ret := res[0]
398+
if ret.sym.zval == "" {
399+
fmt.Printf("gopy: programmer error: empty zval zero value in symbol: %v\n", ret.sym)
400+
}
401+
if ret.sym.go2py != "" {
402+
g.gofile.Printf("return %s(%s)%s\n", ret.sym.go2py, ret.sym.zval, ret.sym.go2pyParenEx)
403+
} else {
404+
g.gofile.Printf("return %s\n", ret.sym.zval)
405+
}
423406
}
424407
g.gofile.Outdent()
425408
g.gofile.Printf("}\n")

0 commit comments

Comments
 (0)