Skip to content

Commit 2b38837

Browse files
committed
bind: replace patch-leak fix solution with a built-in PyBindGen code gen
Using the same approach to add exception checking to function calls, this commit replaces the entire patch-leak tool with built-in PyBindGen logic that cleans up the string allocations
1 parent 84915de commit 2b38837

File tree

4 files changed

+36
-161
lines changed

4 files changed

+36
-161
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

bind/gen.go

Lines changed: 15 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -210,23 +210,37 @@ class CheckedFunction(Function):
210210
def __init__(self, *a, **kw):
211211
super(CheckedFunction, self).__init__(*a, **kw)
212212
self._failure_expression = kw.get('failure_expression', '')
213+
self._failure_cleanup = kw.get('failure_cleanup', '')
213214
214215
def set_failure_expression(self, expr):
215216
self._failure_expression = expr
216217
218+
def set_failure_cleanup(self, expr):
219+
self._failure_cleanup = expr
220+
217221
def generate_call(self):
218222
super(CheckedFunction, self).generate_call()
219223
check = "PyErr_Occurred()"
220224
if self._failure_expression:
221225
check = "{} && {}".format(self._failure_expression, check)
222-
self.before_call.write_error_check(check)
226+
failure_cleanup = self._failure_cleanup or None
227+
self.before_call.write_error_check(check, failure_cleanup)
223228
224229
def add_checked_function(mod, name, retval, params, failure_expression='', *a, **kw):
225230
fn = CheckedFunction(name, retval, params, *a, **kw)
226231
fn.set_failure_expression(failure_expression)
227232
mod._add_function_obj(fn)
228233
return fn
229234
235+
def add_checked_string_function(mod, name, retval, params, failure_expression='', *a, **kw):
236+
fn = CheckedFunction(name, retval, params, *a, **kw)
237+
cleanup = 'free(retval);'
238+
fn.set_failure_cleanup(cleanup)
239+
fn.after_call.add_cleanup_code(cleanup)
240+
fn.set_failure_expression(failure_expression)
241+
mod._add_function_obj(fn)
242+
return fn
243+
230244
mod = Module('_%[1]s')
231245
mod.add_include('"%[1]s_go.h"')
232246
mod.add_function('GoPyInit', None, [])
@@ -351,8 +365,6 @@ build:
351365
# use pybindgen to build the %[1]s.c file which are the CPython wrappers to cgo wrappers..
352366
# note: pip install pybindgen to get pybindgen if this fails
353367
$(PYTHON) build.py
354-
# patch storage leaks in pybindgen output
355-
go run patch-leaks.go %[1]s.c
356368
# build the _%[1]s$(LIBEXT) library that contains the cgo and CPython wrappers
357369
# generated %[1]s.py python wrapper imports this c-code package
358370
$(GCC) %[1]s.c %[6]s %[1]s_go$(LIBEXT) -o _%[1]s$(LIBEXT) $(CFLAGS) $(LDFLAGS) -fPIC --shared -w
@@ -394,91 +406,11 @@ build:
394406
# use pybindgen to build the %[1]s.c file which are the CPython wrappers to cgo wrappers..
395407
# note: pip install pybindgen to get pybindgen if this fails
396408
$(PYTHON) build.py
397-
# patch storage leaks in pybindgen output
398-
go run patch-leaks.go %[1]s.c
399409
# build the executable
400410
- rm %[1]s_go$(LIBEXT)
401411
$(GOBUILD) -o py%[1]s
402412
403413
`
404-
405-
patchLeaksPreamble = `// patch-leaks.go for post-processing %[1]s.
406-
// File is generated by gopy. Do not edit.
407-
// %[2]s
408-
// +build ignore
409-
410-
package main
411-
412-
import (
413-
"bufio"
414-
"bytes"
415-
"io/ioutil"
416-
"log"
417-
"os"
418-
"strings"
419-
)
420-
421-
const (
422-
cStringLine = " py_retval = Py_BuildValue((char *) \"s\", retval);"
423-
)
424-
var cstringFunctions = []string{
425-
`
426-
427-
patchLeaksPostamble = `
428-
}
429-
430-
func isCString(line string, names []string) bool {
431-
for _, cfn := range names {
432-
if strings.HasPrefix(line, cfn) {
433-
return true
434-
}
435-
}
436-
return false
437-
}
438-
439-
func patchCString(line string, out *bytes.Buffer) bool {
440-
out.WriteString(line)
441-
out.Write([]byte{'\n'})
442-
switch line {
443-
case "}":
444-
return false
445-
case cStringLine:
446-
out.WriteString(" free(retval);\n")
447-
return false
448-
}
449-
return true
450-
}
451-
452-
func main() {
453-
file := os.Args[1]
454-
buf, err := ioutil.ReadFile(file)
455-
if err != nil {
456-
log.Fatal(err)
457-
}
458-
sc := bufio.NewScanner(bytes.NewBuffer(buf))
459-
obuf := &bytes.Buffer{}
460-
var cstring bool
461-
for sc.Scan() {
462-
line := sc.Text()
463-
if cstring {
464-
cstring = patchCString(line, obuf)
465-
continue
466-
}
467-
cstring = isCString(line, cstringFunctions)
468-
obuf.WriteString(line)
469-
obuf.Write([]byte{'\n'})
470-
}
471-
472-
if err := sc.Err(); err != nil {
473-
log.Fatal(err)
474-
}
475-
476-
err = ioutil.WriteFile(file, obuf.Bytes(), 0644)
477-
if err != nil {
478-
log.Fatal(err)
479-
}
480-
}
481-
`
482414
)
483415

484416
// thePyGen is the current pyGen which is needed in symbols to lookup
@@ -570,7 +502,6 @@ func (g *pyGen) genPre() {
570502
g.makefile = &printer{buf: new(bytes.Buffer), indentEach: []byte("\t")}
571503
}
572504
g.genGoPreamble()
573-
g.genLeaksPreamble()
574505
g.genPyBuildPreamble()
575506
if !NoMake {
576507
g.genMakefile()
@@ -593,9 +524,7 @@ func (g *pyGen) genPrintOut(outfn string, pr *printer) {
593524
func (g *pyGen) genOut() {
594525
g.pybuild.Printf("\nmod.generate(open('%v.c', 'w'))\n\n", g.cfg.Name)
595526
g.gofile.Printf("\n\n")
596-
g.genLeaksPostamble()
597527
g.genPrintOut(g.cfg.Name+".go", g.gofile)
598-
g.genPrintOut("patch-leaks.go", g.leakfile)
599528
g.genPrintOut("build.py", g.pybuild)
600529
if !NoMake {
601530
g.makefile.Printf("\n\n")
@@ -669,14 +598,6 @@ func (g *pyGen) genGoPreamble() {
669598
g.gofile.Printf("\n// --- generated code for package: %[1]s below: ---\n\n", g.cfg.Name)
670599
}
671600

672-
func (g *pyGen) genLeaksPreamble() {
673-
g.leakfile.Printf(patchLeaksPreamble, g.cfg.Name, g.cfg.Cmd)
674-
}
675-
676-
func (g *pyGen) genLeaksPostamble() {
677-
g.leakfile.Printf(patchLeaksPostamble)
678-
}
679-
680601
func (g *pyGen) genPyBuildPreamble() {
681602
g.pybuild.Printf(PyBuildPreamble, g.cfg.Name, g.cfg.Cmd)
682603
}

bind/gen_func.go

Lines changed: 20 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("add_checked_function(mod, '%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("add_checked_function(mod, '%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 {

cmd_build.go

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -84,17 +84,6 @@ func gopyRunCmdBuild(cmdr *commander.Command, args []string) error {
8484
return runBuild("build", cfg)
8585
}
8686

87-
func patchLeaks(cfile string) error {
88-
fmt.Printf("go run patch-leaks.go %v # patch memory leaks in pybindgen output\n", cfile)
89-
cmd := exec.Command("go", "run", "patch-leaks.go", cfile)
90-
cmdout, err := cmd.CombinedOutput()
91-
if err != nil {
92-
fmt.Printf("cmd had error: %v output:\n%v\n", err, string(cmdout))
93-
return err
94-
}
95-
return nil
96-
}
97-
9887
// runBuild calls genPkg and then executes commands to build the resulting files
9988
// exe = executable mode to build an executable instead of a library
10089
// mode = gen, build, pkg, exe
@@ -155,10 +144,6 @@ func runBuild(mode bind.BuildMode, cfg *BuildCfg) error {
155144
return err
156145
}
157146

158-
if err := patchLeaks(cfg.Name + ".c"); err != nil {
159-
return err
160-
}
161-
162147
err = os.Remove(cfg.Name + "_go" + libExt)
163148

164149
fmt.Printf("go build -o py%s\n", cfg.Name)
@@ -212,11 +197,6 @@ func runBuild(mode bind.BuildMode, cfg *BuildCfg) error {
212197
return err
213198
}
214199

215-
// patch c code
216-
if err := patchLeaks(cfg.Name + ".c"); err != nil {
217-
return err
218-
}
219-
220200
cflags := strings.Fields(strings.TrimSpace(pycfg.CFlags))
221201
cflags = append(cflags, "-fPIC", "-Ofast")
222202
if include, exists := os.LookupEnv("GOPY_INCLUDE"); exists {

0 commit comments

Comments
 (0)