Search code examples
pythoncgocgo

How to free unsafe.Pointer using a wrapper in cgo?


How would I go about creating a wrapper that frees the unsafe.Pointer in my code? Here's my cgo code:

//export clientpy
func clientpy(url *C.char, headersfrompy *C.char, proxy *C.char) unsafe.Pointer {
    s := C.GoString(url)
    headers := C.GoString(headersfrompy)
    p := C.GoString(proxy)

    request := UrlGet(s, headers, p)

    length := make([]byte, 8)

    binary.LittleEndian.PutUint64(length, uint64(len(request)))
    return C.CBytes(append(length, request...))
}

//export FreeCByte
func FreeCByte(b *unsafe.Pointer) {
    C.free(unsafe.Pointer(b))
}

It seems like I cannot free the memory in my python code. I am creating a wrapper so that I can free the memory in python instead of inside of go so that I won't have a dangling pointer.

Here's my python code:

from ctypes import cdll
import ctypes, cchardet, json
from bs4 import BeautifulSoup

lib = cdll.LoadLibrary("./test.so")
lib.cclientpy.argtypes = [ctypes.c_char_p, ctypes.c_char_p, ctypes.c_char_p]
lib.cclientpy.restype = ctypes.POINTER(ctypes.c_ubyte * 8)
""" THERE IS A BIG MEMORY LEAK, BEWARE """

free = lib.free
free.argtypes = [ctypes.POINTER(ctypes.c_ubyte * 8)]

def newrequest(path, lister={}, proxy=[]):
    try:
        print(f"proxy: {proxy}")
        ptr = lib.cclientpy(path.encode("utf-8"), str(lister).encode("utf-8"), str(proxy).encode("utf-8"))
        length = int.from_bytes(ptr.contents, byteorder="little")
        data = bytes(ctypes.cast(ptr,
                                 ctypes.POINTER(ctypes.c_ubyte * (8 + length))
                                 ).contents[8:])
        #free(ptr)
        lib.FreeCByte(ptr)
        print(f'bytes: {bytes(ctypes.cast(ptr,ctypes.POINTER(ctypes.c_ubyte * (8 + length))).contents[8:])}')

        return data
    except:
        pass

How can I free the unsafe pointer in go?


Solution

  • Free the unsafe.Pointer in Go, not a pointer to the unsafe.Pointer.

    //export FreeCByte
    func FreeCByte(b unsafe.Pointer) {
        C.free(b)
    }
    

    Comment: I am still getting memory leaks. – JJ Cauton

    The code in your question does not compile and run. I fixed your code to compile and run, and added some debugging code. Clearly, memory is being freed.

    $ go version
    go version devel go1.18-8214257347 Wed Sep 8 14:51:40 2021 +0000 linux/amd64
    $ go build -o test.so -buildmode=c-shared test.go
    $ python3 --version
    Python 3.9.5
    $ python3 test.py
    proxy: []
    Go: cclientpy: C.CBytes: 0x1a7e420
    Go: FreeCByte: 0x1a7e420
    Go: FreeCByte: double free 0x1a7e420
    free(): double free detected in tcache 2
    Aborted (core dumped)
    $ 
    

    $ cat test.go
    
    package main
    
    import (
        "encoding/binary"
        "fmt"
        "os"
        "unsafe"
    )
    
    /*
    #include <stdlib.h>
    */
    import "C"
    
    func UrlGet(s, headers, p string) []byte {
        return nil
    }
    
    const debug = true
    
    //export cclientpy
    func cclientpy(url *C.char, headersfrompy *C.char, proxy *C.char) unsafe.Pointer {
        s := C.GoString(url)
        headers := C.GoString(headersfrompy)
        p := C.GoString(proxy)
    
        request := UrlGet(s, headers, p)
    
        length := make([]byte, 8)
    
        binary.LittleEndian.PutUint64(length, uint64(len(request)))
        cbytes := C.CBytes(append(length, request...))
    
        if debug {
            fmt.Fprintln(os.Stderr, "Go: cclientpy: C.CBytes:", cbytes)
        }
    
        return cbytes
    }
    
    //export FreeCByte
    func FreeCByte(b unsafe.Pointer) {
        if debug {
            fmt.Fprintln(os.Stderr, "Go: FreeCByte:", b)
        }
    
        C.free(b)
    
        if debug {
            // tests to see if already freed, should fail
            // free(): double free detected in tcache 2
            // Aborted (core dumped)
            fmt.Fprintln(os.Stderr, "Go: FreeCByte: double free", b)
            C.free(b)
        }
    }
    
    func main() {}
    
    $ 
    

    $ cat test.py
    
    from ctypes import cdll
    import ctypes, chardet as cchardet, json
    from bs4 import BeautifulSoup
    
    lib = cdll.LoadLibrary("./test.so")
    lib.cclientpy.argtypes = [ctypes.c_char_p, ctypes.c_char_p, ctypes.c_char_p]
    lib.cclientpy.restype = ctypes.POINTER(ctypes.c_ubyte * 8)
    """ THERE IS A BIG MEMORY LEAK, BEWARE """
    
    free = lib.free
    free.argtypes = [ctypes.POINTER(ctypes.c_ubyte * 8)]
    
    def newrequest(path, lister={}, proxy=[]):
        try:
            print(f"proxy: {proxy}")
            ptr = lib.cclientpy(path.encode("utf-8"), str(lister).encode("utf-8"), str(proxy).encode("utf-8"))
            length = int.from_bytes(ptr.contents, byteorder="little")
            data = bytes(ctypes.cast(ptr,
                                     ctypes.POINTER(ctypes.c_ubyte * (8 + length))
                                     ).contents[8:])
            #free(ptr)
            lib.FreeCByte(ptr)
            print(f'bytes: {bytes(ctypes.cast(ptr,ctypes.POINTER(ctypes.c_ubyte * (8 + length))).contents[8:])}')
    
            return data
        except:
            pass
        
    newrequest(path='argpath', lister={}, proxy=[])
    
    $