带回调的ctypes:退出时访问冲突

时间:2018-04-20 00:30:37

标签: python dll callback ctypes

之前我没有回复过这个问题。我再次问它,这次更加简化了。

我有一个由Python ctypes调用的dll,带有一个回调函数。回调一直正常工作(如果我在Visual Studio中逐步执行该程序,我可以在操作中看到它),但在退出时,Visual Studio会抛出“访问冲突”异常。但是如果我从dll中删除对回调的调用,则它会在没有访问冲突的情况下正常退出。

我必须做些什么来退出带回调的dll吗?我已经研究了几个小时了,我还没有在网上找到解决这个问题的东西。

这是ctypes代码。我省略了dll代码来保持这个简短(它是用NASM编写的)但是如果需要的话我也可以发布它。

def SimpleTestFunction_asm(X):

    Input_Length_Array = []
    Input_Length_Array.append(len(X)*8)

    CA_X = (ctypes.c_double * len(X))(*X)

    length_array_out = (ctypes.c_double * len(Input_Length_Array))(*Input_Length_Array)

    hDLL = ctypes.WinDLL("C:/Test_Projects/SimpleTestFunction/SimpleTestFunction.dll")
    CallName = hDLL.Main_Entry_fn
    CallName.argtypes = [ctypes.POINTER(ctypes.c_double),ctypes.POINTER(ctypes.c_double),ctypes.POINTER(ctypes.c_longlong)]
    CallName.restype = ctypes.POINTER(ctypes.c_int64)
    #__________
    #The callback function

    LibraryCB = ctypes.WINFUNCTYPE(ctypes.c_double, ctypes.c_double)

    def LibraryCall(ax):
        bx = math.ceil(ax)
        return (bx)

    lib_call = LibraryCB(LibraryCall)
    lib_call = ctypes.cast(lib_call,ctypes.POINTER(ctypes.c_longlong))

    #__________

    ret_ptr = CallName(CA_X,length_array_out,lib_call)

我真的非常感谢有关如何解决这个问题的任何想法。我希望这篇简化的帖子会有所帮助。

非常感谢。

2 个答案:

答案 0 :(得分:3)

我对你的代码进行了一些小改动,以实际运行(导入)并添加了一个打印,以查看传递的对象的地址和返回值,并创建了一个等效的C DLL,以确保指针正确传递和回调的工作原理。

的Python:

import ctypes
import math

def SimpleTestFunction_asm(X):
    Input_Length_Array = []
    Input_Length_Array.append(len(X)*8)

    CA_X = (ctypes.c_double * len(X))(*X)

    length_array_out = (ctypes.c_double * len(Input_Length_Array))(*Input_Length_Array)

    hDLL = ctypes.WinDLL('test')
    CallName = hDLL.Main_Entry_fn
    CallName.argtypes = [ctypes.POINTER(ctypes.c_double),ctypes.POINTER(ctypes.c_double),ctypes.POINTER(ctypes.c_longlong)]
    CallName.restype = ctypes.POINTER(ctypes.c_int64)

    LibraryCB = ctypes.WINFUNCTYPE(ctypes.c_double, ctypes.c_double)

    def LibraryCall(ax):
        bx = math.ceil(ax)
        return (bx)

    lib_call = LibraryCB(LibraryCall)
    lib_call = ctypes.cast(lib_call,ctypes.POINTER(ctypes.c_longlong))

    ret_ptr = CallName(CA_X,length_array_out,lib_call)
    print('{:016X} {:016X} {:016X} {}'.format(ctypes.addressof(CA_X),ctypes.addressof(length_array_out),ctypes.addressof(lib_call.contents),ret_ptr.contents))

SimpleTestFunction_asm([1.1,2.2,3.3])

Test.DLL来源:

#include <inttypes.h>
#include <stdio.h>

typedef double (*CB)(double);

__declspec(dllexport) int64_t* __stdcall Main_Entry_fn(double* p1, double* p2, long long* p3)
{
    static int64_t x = 123;
    double out = ((CB)p3)(1.1);
    printf("%p %p %p %lf\n",p1,p2,p3,out);
    return &x;
}

输出:

0000021CC99B23A8 0000021CCBADAC10 0000021CCBC90FC0 2.000000
0000021CC99B23A8 0000021CCBADAC10 0000021CCBC90FC0 c_longlong(123)

您可以看到指针是相同的,并且回调返回值和函数返回值是正确的。

您的NASM代码可能没有正确实现调用约定或破坏访问数组的堆栈。我只是尽力使你的Python代码工作。我确实认为length_array_out始终是长度为1的双数组,其值为输入数组X长度的8倍。 NASM代码如何知道数组有多长?

您可以更加确定类型并声明以下内容,而不是将回调转换为long long *

CALLBACK = ctypes.WINFUNCTYPE(ctypes.c_double, ctypes.c_double)

CallName.argtypes = [ctypes.POINTER(ctypes.c_double),ctypes.POINTER(ctypes.c_double),CALLBACK]
CallName.restype = ctypes.POINTER(ctypes.c_int64)


@CALLBACK
def LibraryCall(ax):
    bx = math.ceil(ax)
    return (bx)

ret_ptr = CallName(CA_X,length_array_out,LibraryCall)

答案 1 :(得分:0)

@Mark Tolonen,非常感谢您的详细分析。我发布这个作为答案,因为代码的格式不会在评论中正确出现 - 但我选择你的答案作为最佳答案。

我怀疑堆栈对齐可能是问题所在,并且你消除了ctypes作为源代码,所以我专注于堆栈。这就是我做的工作。

在NASM代码中,我在输入时按下rbp和rdi,然后在退出时恢复它们。这里,在调用之前,我通过从堆栈中弹出rbp和rdi来设置堆栈状态。然后我从rsp中减去32个字节(不是40个)。调用完成后,我恢复堆栈状态:

pop rbp
pop rdi
sub rsp,32
call [CB_Pointer] ; The call to the callback function
add rsp,32
push rdi
push rbp

对于外部函数调用(比如C库函数),我必须减去40个字节,但对于这个回调,我只需要32个字节。在你的回答之前我尝试了40个字节并且它不起作用。我想原因是因为它没有调用外部库,它是对ctypes代码的回调,它首先调用了dll。

另一件事。调用发送一个浮点值(xmm0)并返回一个整数值,但整数值在xmm0寄存器中返回,而不是rax。将ctypes中的原型设置为整数返回不会这样做。它必须保持这样:

LibraryCB = ctypes.WINFUNCTYPE(ctypes.c_double, ctypes.c_double)

再次感谢您的回复。你告诉我在哪里看。

P.S。 length_array_out将输入数组的长度传递给NASM。如果我传递多个数组,则length_array_out将更长,每个长度只有一个qword;目前我在输入时将qword转换为整数。