如何避免回调函数中的RuntimeWarning内存泄漏Python回调将字符串返回给C?

问题描述

C ++库

CallbackTestLib.hpp

#pragma once

using callback_prototype = const char* __cdecl();

extern "C" __declspec(dllexport) int __cdecl do_something(callback_prototype*);

CallbackTestLib.cpp

#include "CallbackTestLib.hpp"
#include <iostream>

using namespace std;

__declspec(dllexport) auto __cdecl do_something(callback_prototype* cb) -> int
{
    if (!cb) { return 5678; }
    const auto* str = cb();
    cout << "Hello " << str << endl;
    return 1234;
}

Python脚本

CallbackTest.py

import os
import sys
from ctypes import CDLL,CFUNCTYPE,c_char_p,c_int32

assert sys.maxsize > 2 ** 32,"Python x64 required"
assert sys.version_info.major == 3 and sys.version_info.minor == 8 and sys.version_info.micro == 4,"Python 3.8.4 required"

callback_prototype = CFUNCTYPE(c_char_p)

@callback_prototype
def python_callback_func() -> bytes:
    return "from Python".encode("utf-8")

dll_path = os.path.join(os.path.dirname(os.path.abspath(__file__)),"CallbackTestLib.dll")
testlib = CDLL(dll_path)
testlib.do_something.restype = c_int32
testlib.do_something.argtypes = [callback_prototype]

null_return_code = testlib.do_something(callback_prototype())
assert null_return_code == 5678,"Null return code Failed"

ok_return_code = testlib.do_something(python_callback_func)
assert ok_return_code == 1234,"Ok return code Failed"

print("Done.")

Python输出

d:/path/to/CallbackTest.py:22:RuntimeWarning:回调函数中的内存泄漏。
ok_return_code = testlib.do_something(python_callback_func)
您好,来自Python
完成。

输出所示,执行python_callback_func时,Python(某种程度上)似乎检测到内存泄漏,该错误 bytes (UTF-8编码的字符串)返回给C ++,字符串正在打印出来。
我的问题全与此有关:此警告正在发生什么,如何避免/解决它?

解决方法

这对我来说仍然有些模糊/不清楚。但这是一个愚蠢修复程序(我对此不满意),它使memleak警告消息消失了:

版本1

CallbackTestLib.hpp

#pragma once

using callback_prototype = void* __cdecl(); // Changed 'const char*' to 'void*'.

extern "C" __declspec(dllexport) int __cdecl do_something(callback_prototype*);

CallbackTestLib.cpp

#include "CallbackTestLib.hpp"
#include <iostream>

using namespace std;

__declspec(dllexport) auto __cdecl do_something(callback_prototype* cb) -> int
{
    if (!cb) { return 5678; }
    const auto* str = cb();
    cout << "Hello " << static_cast<const char*>(str) << endl; // Added 'static_cast'.
    return 1234;
}

CallbackTest.py

import os
import sys
from ctypes import CDLL,CFUNCTYPE,cast,c_void_p,c_int32

assert sys.maxsize > 2 ** 32,"Python x64 required"
assert sys.version_info.major == 3 and sys.version_info.minor == 8 and sys.version_info.micro == 4,"Python 3.8.4 required"

callback_prototype = CFUNCTYPE(c_void_p)  # Changed restype to 'c_void_p'.

@callback_prototype
def python_callback_func():
    return cast("from Python :)".encode("utf-8"),c_void_p).value  # Added casting.

dll_path = os.path.join(os.path.dirname(os.path.abspath(__file__)),"CallbackTestLib.dll")
testlib = CDLL(dll_path)
testlib.do_something.restype = c_int32
testlib.do_something.argtypes = [callback_prototype]

null_return_code = testlib.do_something(callback_prototype())
assert null_return_code == 5678,"Null return code failed"

ok_return_code = testlib.do_something(python_callback_func)
assert ok_return_code == 1234,"Ok return code failed"

print("Done.")

输出

来自Python的Hello :)
完成。

版本2

CallbackTestLib.hpp

#pragma once

using callback_prototype = void __cdecl();

static char* do_something_buffer;

extern "C" __declspec(dllexport) int __cdecl do_something(callback_prototype);

extern "C" __declspec(dllexport) void __cdecl receive_do_something_buffer(const char*);

CallbackTestLib.cpp

#include "CallbackTestLib.hpp"
#include <iostream>

using namespace std;

auto do_something(callback_prototype cb) -> int
{
    if (!cb) { return 5678; }
    cb();
    cout << "Hello " << do_something_buffer << endl;
    cb();
    cout << "Hello again " << do_something_buffer << endl;
    return 1234;
}

void receive_do_something_buffer(const char* str)
{
    // Create a copy of the given string and save it into buffer.
    if (do_something_buffer) { free(do_something_buffer); }
    do_something_buffer = _strdup(str);
}

CallbackTest.py

import os
import sys
from ctypes import CDLL,c_int32,c_char_p

assert sys.maxsize > 2 ** 32,"Python 3.8.4 required"

callback_prototype = CFUNCTYPE(None)

dll_path = os.path.join(os.path.dirname(os.path.abspath(__file__)),"CallbackTestLib.dll")
testlib = CDLL(dll_path)
testlib.do_something.restype = c_int32
testlib.do_something.argtypes = [callback_prototype]

testlib.receive_do_something_buffer.restype = None
testlib.receive_do_something_buffer.argtypes = [c_char_p]

@callback_prototype
def python_callback_func() -> None:
    testlib.receive_do_something_buffer("from Python :D".encode("utf-8"))

null_return_code = testlib.do_something(callback_prototype())
assert null_return_code == 5678,"Ok return code failed"

print("Done.")

输出

来自Python的Hello:D
再次从Python向您问好:D
完成。

,

让Python回调返回char* callback() { return new char[10]; } 类似于C ++函数返回:

#include <iostream>
using namespace std;

typedef void (*CB)(char* buf,size_t len);

extern "C" __declspec(dllexport) int func(CB cb) {
    char buf[80];
    if(cb) {
        cb(buf,sizeof buf);
        cout << "Hello " << buf << endl;
        return 1234;
    }
    return 5678;
}

除非释放内存,否则内存泄漏。由于Python分配了字节对象,因此C ++无法正确释放它,从而导致泄漏。

相反,将C ++管理的缓冲区传递给回调:

test.cpp

from ctypes import *

CALLBACK = CFUNCTYPE(None,POINTER(c_char),c_size_t)

@CALLBACK
def callback(buf,size):
    # Cast the pointer to a single char to a pointer to a sized array
    # so it can be accessed safely and correctly.
    arr = cast(buf,POINTER(c_char * size))
    arr.contents.value = b'world!'

dll = CDLL('./test')
dll.func.argtypes = CALLBACK,dll.func.restype = c_int

print(dll.func(callback))

test.py

Hello world!
1234

输出:

from pygame import mixer
import time
from pynput.keyboard import Key,Listener

def stopsound(evt):
    global running
    mixer.music.stop()  # stop music
    running = False  # exit run loop
    return False  # stop key listener

def music(file) :
    global running
    mixer.init()
    mixer.music.load(file)
    running = True;
    while running :
        try :
           mixer.music.play() # duration of file 2 sec
           time.sleep(3)
        except KeyboardInterrupt :
           mixer.music.stop()
           break
           
with Listener(on_press=stopsound) as listener:
    music('SomeMusic.mp3')
    listener.join()  # wait for listener thread to finish