从ctypes回调返回至NASM 64位DLL的指针类型及处理方法
如何在NASM 64位DLL中处理Python回调返回的指针?
我现在遇到一个棘手的问题:用ctypes调用NASM编写的64位DLL,其中涉及一个回调函数——这个回调要调用SciPy的integrate.dblquad,接收指向两个double数组的指针,再返回指向两个double数组的指针。回调在Python里执行完全正常,结果也对,但把指针返回给NASM的时候就出问题了。
之前我在Stack Overflow上问过相关的ctypes问题,得到了基于C语言的解决方案,但没法直接用到NASM里,所以想找适配NASM的解决办法。
调用DLL的ctypes代码
CA_data1 = (ctypes.c_double * len(data1))(*data1) CA_data2 = (ctypes.c_double * len(data2))(*data2) hDLL = ctypes.WinDLL("C:/NASM_Test_Projects/SciPy_Test/SciPy_Test.dll") CallName = hDLL.Main_Entry_fn CallName.argtypes = [ctypes.POINTER(ctypes.c_double),ctypes.POINTER(ctypes.c_double),ctypes.POINTER(ctypes.c_double),ctypes.POINTER(ctypes.c_longlong)] CallName.restype = ctypes.c_double ret_ptr = CallName(CA_data1,CA_data2,length_array_out,lib_call)
Python回调函数代码
from scipy.integrate import dblquad import ctypes def LibraryCall(ptr): n = ctypes.cast(ptr,ctypes.POINTER(ctypes.c_double)) x = n[0] y = n[1] area = dblquad(lambda x, y: x*y, 0, 0.5, lambda x: 0, lambda x: 1-2*x) return_val = area[0], area[1] r_val = (ctypes.c_double * len(return_val))(*return_val) rv = ctypes.cast(r_val,ctypes.POINTER(ctypes.c_double)) # 下面三种返回方式得到的数据一致,但返回给DLL都不对 # return (r_val) # return (rv) # return (return_val) LibraryCB = ctypes.WINFUNCTYPE(None, ctypes.POINTER(ctypes.c_double)) # 也试过这个声明,没区别 # LibraryCB = ctypes.PYFUNCTYPE(None,ctypes.POINTER(ctypes.c_double)) lib_call = LibraryCB(LibraryCall) lib_call = ctypes.cast(lib_call,ctypes.POINTER(ctypes.c_longlong))
NASM中调用回调并接收指针的代码片段
pop rbp pop rdi sub rsp,32 call [scipy.integrate_dblquad_Pointer] add rsp,32 push rdi push rbp mov [dblquad_Pointer],rax ; 把返回的指针存到这里 ; 检查返回值 lea rdi,[rel dblquad_Pointer] mov rbp,qword [rdi] ; 获取返回的指针 movsd xmm0,qword[rbp] ret
我尝试过的方法
- 三种返回指针的方式:
return (r_val)、return (rv)、return (return_val),在Python里结果正确,但返回给DLL后数据都不对 - 试过修改回调函数的声明(
WINFUNCTYPE和PYFUNCTYPE),没有区别 - 之前得到的C语言方案是用输入/输出参数,但不知道NASM里怎么实现
适配NASM的解决方案:改用输出参数传递结果
问题核心在于:Python回调返回的指针指向的是Python管理的临时内存(比如r_val是Python数组对象),回调结束后可能被Python的垃圾回收机制回收,导致NASM读取到无效数据。同时64位Windows的fastcall调用约定对返回指针的处理有严格要求,最稳妥的方式是让NASM提前分配好内存,把指针传给Python回调,回调直接把结果写入这块内存,而非返回新指针。
步骤1:修改Python回调函数,改为写入传入的输出指针
def LibraryCall(input_ptr, output_ptr): # 读取输入的两个double值 n = ctypes.cast(input_ptr,ctypes.POINTER(ctypes.c_double)) x = n[0] y = n[1] # 计算积分 area = dblquad(lambda x, y: x*y, 0, 0.5, lambda x: 0, lambda x: 1-2*x) # 把结果写入NASM传入的输出内存 out_arr = ctypes.cast(output_ptr, ctypes.POINTER(ctypes.c_double)) out_arr[0] = area[0] out_arr[1] = area[1] # 修改回调类型:接收两个指针,无返回值 LibraryCB = ctypes.WINFUNCTYPE(None, ctypes.POINTER(ctypes.c_double), ctypes.POINTER(ctypes.c_double)) lib_call = LibraryCB(LibraryCall) lib_call = ctypes.cast(lib_call, ctypes.POINTER(ctypes.c_longlong))
步骤2:修改NASM代码,提前分配内存并传递给回调
; 提前分配两个double的内存(16字节),这里用栈示例(也可以用全局变量) sub rsp, 16 ; 分配16字节栈空间,用于存储输出结果 mov rdx, rsp ; rdx作为第二个参数,传递输出指针 ; 假设第一个参数input_ptr已经在rcx寄存器中 sub rsp, 32 ; 预留shadow space,符合fastcall栈对齐要求 call [scipy.integrate_dblquad_Pointer] add rsp, 32 ; 释放shadow space ; 读取输出结果 movsd xmm0, qword [rsp] ; 第一个结果:积分值 movsd xmm1, qword [rsp+8] ; 第二个结果:误差值 add rsp, 16 ; 释放栈上的输出内存 ; 可选:把结果存到全局变量 mov [dblquad_Result_Value], xmm0 mov [dblquad_Result_Error], xmm1 ret
步骤3:同步修改ctypes的函数参数声明
确保DLL的Main_Entry_fn参数能正确传递输入/输出指针的信息(根据你的实际逻辑调整,核心是让DLL能把输入指针和输出指针传给回调)。
为什么这个方法可行?
- 内存生命周期可控:内存由NASM分配(栈或全局内存),Python仅负责写入数据,不会涉及内存回收问题
- 符合调用约定:64位Windows的
fastcall约定中,前4个参数用rcx、rdx、r8、r9传递,这里将输入指针放rcx、输出指针放rdx,完全符合规范 - 避免指针返回的坑:绕开了Python返回指针可能被GC回收的问题,从根源上解决了无效内存访问的问题
内容的提问来源于stack exchange,提问作者RTC222
相关产品推荐
相关产品推荐

