You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

从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能把输入指针和输出指针传给回调)。


为什么这个方法可行?

  1. 内存生命周期可控:内存由NASM分配(栈或全局内存),Python仅负责写入数据,不会涉及内存回收问题
  2. 符合调用约定:64位Windows的fastcall约定中,前4个参数用rcx、rdx、r8、r9传递,这里将输入指针放rcx、输出指针放rdx,完全符合规范
  3. 避免指针返回的坑:绕开了Python返回指针可能被GC回收的问题,从根源上解决了无效内存访问的问题

内容的提问来源于stack exchange,提问作者RTC222

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.28 07:31:41