Background
I am currently using a DLL I generated with golang in python. As a sanity check for debugging I added a function called return_string(), which takes a pointer to a string in C (char*) converts to go, then returns the result. Mostly this is a debugging tool to look for encoding issues between the two languages.
Specific issue
In python I've wrapped the function, and everything works, except if I free after copying the bytes in. Here's the function:
# import library
if platform().lower().startswith("windows"):
lib = cdll.LoadLibrary(os.path.join(os.path.dirname(os.path.realpath(__file__)), "lib.dll"))
else:
lib = cdll.LoadLibrary(os.path.join(os.path.dirname(os.path.realpath(__file__)), "lib.so"))
lib.return_string.argtypes = [c_char_p]
lib.return_string.restype = c_char_p
lib.FreeCString.argtypes = [c_char_p]
def prepare_string(data: str | bytes) -> c_char_p:
"""Takes in a string and returns a C-compatible string"""
if isinstance(data, str):
return c_char_p(data.encode())
return c_char_p(bytes(data))
def return_string(text: str | bytes) -> str:
"""Debugging function that shows you the Go representation of a C string and returns the python string version"""
c_input = prepare_string(text)
result = lib.return_string(c_input) # This is allocated in go using essentially malloc
if not result:
return ""
copied_bytes = string_at(result) # This should be a COPY into new python-managed memory afaik
decoded = copied_bytes.decode(errors="replace") # Gets to here no issues
lib.FreeCString(result) # Program silently fails here
return copied_bytes
If I remove the call to lib.FreeCString(result), everything works, but my understanding is that because the result variable is allocated in go with a malloc() call, it needs to be freed in python. But, when I free it, it's either double-freeing or free-after-use and I'm not sure which, or why.
Do I need to free result? and if not, where does the pointer allocated in go get cleaned up?
Go code
I don't think it's a go-side issue, but just in case, here's the go code as well:
// All code returns `unsafe.Pointer` because this is a package, and CGo breaks your types if you don't
// Used to convert a C-compatible string back to itself, good for debugging encoding issues
//
// Parameters:
// - cString: Pointer to the C string (*C.char).
//
// Returns:
// - Pointer to a new C string with the same content (*C.char).
// Note: The caller is responsible for freeing the allocated memory using FreeCString.
//
//export return_string
func return_string(cString unsafe.Pointer) unsafe.Pointer {
internalRepresentation := C.GoString((*C.char)(cString))
result := StringToCString(internalRepresentation)
return result
}
// Convert a string to a c-compatible C-string (glorified alias for C.CString)
//
// Parameters:
// - input: The Go string to convert.
//
// Returns:
// - A pointer to the newly allocated C string (*C.char).
// Note: The caller is responsible for freeing the allocated memory using FreeCString.
func StringToCString(input string) unsafe.Pointer {
return unsafe.Pointer(C.CString(input))
}
// Free a previously allocated C string from Go.
//
// Parameters:
// - ptr: Pointer to the C string to be freed (*C.char).
//
//export FreeCString
func FreeCString(ptr unsafe.Pointer) {
fmt.Println("FreeCString(): freeing", ptr)
if ptr != nil {
C.free(ptr)
}
}