@@ -261,20 +261,53 @@ func (g *pyGen) genFuncBody(sym *symbol, fsym *Func) {
261261 }
262262 }
263263
264+ // Idea 1 (fix for issue #370): C→Go argument conversions such as C.GoString
265+ // call runtime.gostring → mallocgc, which can corrupt Go's GC sweep-generation
266+ // counter when executed inside a CGo callback without the GIL held — causing
267+ // "fatal error: bad sweepgen in refill" on Go ≥1.24 / x86_64.
268+ //
269+ // The fix pre-converts each C argument into a Go variable *before* calling
270+ // PyEval_SaveThread, so that any Go heap allocation happens while the GIL is
271+ // held and the Go runtime is in a consistent state. The GIL is released via
272+ // PyGILState_Ensure/Release (not the SaveThread mechanism) so the two paths
273+ // are balanced independently.
274+ //
275+ // Idea 2 (possible future optimisation): Replace C.GoString with
276+ // unsafe.String((*byte)(unsafe.Pointer(cs)), C.strlen(cs))
277+ // for each string parameter. unsafe.String constructs a Go string header
278+ // directly from the C pointer without any heap allocation, so the mallocgc
279+ // call that triggers the sweep-gen check never happens. This is faster
280+ // (no malloc) and avoids the bug at its source, but requires that the
281+ // resulting string does not escape the CGo callback frame — storing it
282+ // beyond the call would be a use-after-free. Both fixes together provide
283+ // defence in depth.
284+
285+ // Pre-convert any C arguments that need a py2go conversion (e.g. C.GoString)
286+ // while the GIL is still held. Record the variable name to use in the call.
287+ preConvertedArgs := map [int ]string {} // arg index → temp variable name
288+ needsGILForArgs := false
289+ for _ , arg := range args {
290+ if arg .sym .py2go != "" && ! arg .sym .isSignature () {
291+ needsGILForArgs = true
292+ break
293+ }
294+ }
295+ if needsGILForArgs {
296+ g .gofile .Printf ("_gstate := C.PyGILState_Ensure()\n " )
297+ for i , arg := range args {
298+ if arg .sym .py2go != "" && ! arg .sym .isSignature () {
299+ anm := pySafeArg (arg .Name (), i )
300+ tmpVar := fmt .Sprintf ("_arg%d" , i )
301+ g .gofile .Printf ("%s := %s(%s)%s\n " , tmpVar , arg .sym .py2go , anm , arg .sym .py2goParenEx )
302+ preConvertedArgs [i ] = tmpVar
303+ }
304+ }
305+ g .gofile .Printf ("C.PyGILState_Release(_gstate)\n " )
306+ }
307+
264308 // release GIL
265309 g .gofile .Printf ("_saved_thread := C.PyEval_SaveThread()\n " )
266- // Use defer only when there is no go2py return conversion that calls
267- // Python C API (which requires the GIL to already be reacquired).
268- // For go2py returns (excluding handle types) and error/multi-return cases,
269- // we restore explicitly. Handle types use handleFromPtr which is pure Go
270- // and doesn't need the GIL, so they can keep using defer.
271- nresForDefer := nres
272- // hasRetCvt fires when go2py != "" and NOT (hasHandle && !isPtrOrIface).
273- // Suppress defer in exactly those cases so the explicit restore below is the only one.
274- if nres > 0 && res [0 ].sym .go2py != "" && ! (res [0 ].sym .hasHandle () && ! res [0 ].sym .isPtrOrIface ()) {
275- nresForDefer = 2 // treat like nres==2: no defer, explicit restore
276- }
277- if ! rvIsErr && nresForDefer != 2 {
310+ if ! rvIsErr && nres != 2 {
278311 // reacquire GIL after return
279312 g .gofile .Printf ("defer C.PyEval_RestoreThread(_saved_thread)\n " )
280313 }
@@ -317,6 +350,9 @@ if __err != nil {
317350 na = fmt .Sprintf (`gopyh.VarFromHandle((gopyh.CGoHandle)(%s), "interface{}")` , anm )
318351 case arg .sym .isSignature ():
319352 na = fmt .Sprintf ("%s" , arg .sym .py2go )
353+ case preConvertedArgs [i ] != "" :
354+ // Use the pre-converted variable (conversion happened before PyEval_SaveThread).
355+ na = preConvertedArgs [i ]
320356 case arg .sym .py2go != "" :
321357 na = fmt .Sprintf ("%s(%s)%s" , arg .sym .py2go , anm , arg .sym .py2goParenEx )
322358 default :
@@ -368,8 +404,8 @@ if __err != nil {
368404 g .pywrap .Printf ("_%s.%s(" , pkgname , mnm )
369405 }
370406
371- hasAddrOfTmp := false
372407 hasRetCvt := false
408+ hasAddrOfTmp := false
373409 if nres > 0 {
374410 ret := res [0 ]
375411 switch {
@@ -381,10 +417,8 @@ if __err != nil {
381417 hasAddrOfTmp = true
382418 g .gofile .Printf ("cret := " )
383419 case ret .sym .go2py != "" :
384- // Assign to cret first; we'll restore the GIL before calling go2py
385- // so that Python C API functions inside go2py run with the GIL held.
386420 hasRetCvt = true
387- g .gofile .Printf ("cret := " )
421+ g .gofile .Printf ("return %s(" , ret . sym . go2py )
388422 default :
389423 g .gofile .Printf ("return " )
390424 }
@@ -407,6 +441,11 @@ if __err != nil {
407441 } else {
408442 funCall = fmt .Sprintf ("%s(%s)" , fsym .GoFmt (), strings .Join (callArgs , ", " ))
409443 }
444+ if hasRetCvt {
445+ ret := res [0 ]
446+ funCall += fmt .Sprintf (")%s" , ret .sym .go2pyParenEx )
447+ }
448+
410449 if nres == 0 {
411450 g .gofile .Printf ("if boolPyToGo(goRun) {\n " )
412451 g .gofile .Indent ()
@@ -421,13 +460,6 @@ if __err != nil {
421460 g .gofile .Printf ("%s\n " , funCall )
422461 }
423462
424- if hasRetCvt {
425- // Restore GIL before calling go2py, which may call Python C API.
426- ret := res [0 ]
427- g .gofile .Printf ("C.PyEval_RestoreThread(_saved_thread)\n " )
428- g .gofile .Printf ("return %s(cret)%s\n " , ret .sym .go2py , ret .sym .go2pyParenEx )
429- }
430-
431463 if rvIsErr || nres == 2 {
432464 g .gofile .Printf ("\n " )
433465 // reacquire GIL
0 commit comments