Python将32位指针地址传递给C函数

9
我希望能够从Python脚本中调用共享库内的C函数,但在传递指针时会出现问题,被调用函数内的64位地址似乎被截断为32位地址。Python和我的库都是64位的。
以下示例代码演示了这个问题。Python脚本打印传递给C函数的数据的地址。接着,在被调用的C函数内打印接收到的地址。此外,C函数通过打印局部创建内存的大小和地址证明它是64位的。如果以任何其他方式使用指针,则结果将是段错误。
CMakeLists.txt:
cmake_minimum_required (VERSION 2.6) 
add_library(plate MODULE plate.c)

plate.c

#include <stdio.h>
#include <stdlib.h>

void plate(float *in, float *out, int cnt)
{
    void *ptr = malloc(1024);
    fprintf(stderr, "passed address: %p\n", in);
    fprintf(stderr, "local pointer size: %lu\n local pointer address: %p\n", sizeof(void *), ptr);
    free(ptr);
}

test_plate.py

import numpy
import scipy
import ctypes

N = 3
x = numpy.ones(N, dtype=numpy.float32)
y = numpy.ones(N, dtype=numpy.float32)
plate = ctypes.cdll.LoadLibrary('libplate.so')

print 'passing address: %0x' % x.ctypes.data
plate.plate(x.ctypes.data, y.ctypes.data, ctypes.c_int(N))

python-2.7输出结果

在[1]处:运行../test_plate.py

传递地址:7f9a09b02320

传递的地址:0x9b02320

本地指针大小:8

本地指针地址:0x7f9a0949a400

4个回答

9
问题在于ctypes模块不会检查你试图调用的函数的函数签名。相反,它基于Python类型而不是C类型,因此该行代码...
plate.plate(x.ctypes.data, y.ctypes.data, ctypes.c_int(N))

...正在将前两个参数作为整数传递。请参阅eryksun的答案,了解它们被截断为32位的原因。

为避免截断,您需要告诉ctypes这些参数实际上是指针,例如使用以下方法...

plate.plate(ctypes.c_void_p(x.ctypes.data),
            ctypes.c_void_p(y.ctypes.data),
            ctypes.c_int(N))

尽管它们实际上是指针另一个问题,但它们可能不像您的C代码所假设的那样是指向float的指针。


更新

eryksun已经发布了更完整的numpy特定示例的答案,但是我将保留这个答案,因为对于使用其他东西而不是numpy的程序员来说,在指针截断的一般情况下可能会有用。


@eryksun 在64位的Windows上,Windows的“long”类型仍然只有32位吗? - Aya
2
在64位Windows上,long是32位,而long long是64位。 - Eryk Sun
@eryksun 很奇怪,但我想它有一致性的优点。 - Aya
谢谢!你的建议解决了指针地址截断问题。虽然正如你所暗示的那样,这些指针并没有指向预期的浮点数据。 - papahabla
我发现,如果使用dtype = numpy.float32创建数组,则示例将有效。 - papahabla

6
Python的PyIntObject在内部使用C long,这在大多数64位平台上是64位的(不包括64位Windows)。然而,ctypes将转换结果分配给pa->value.i,其中value是一个union,i字段是32位int。详情请参见Modules/_ctypes/callproc.c中的ConvParam,第588-607行和645-664行。ctypes是在Windows上开发的,因此long始终为32位,但我不知道为什么没有改为使用long字段,即pa->value.l。可能,在大多数情况下,创建C int比使用long的完整范围更方便。

总之,这意味着您不能简单地传递Python int以创建64位指针。您必须明确创建ctypes指针。有许多选项可供选择。如果您不关心类型安全性,则NumPy数组的最简单选项是使用其ctypes属性。这定义了钩子_as_parameter_,允许Python对象设置它们在ctypes函数调用中如何转换(请参见前面链接中的第707-719行)。在这种情况下,它创建一个void *。例如,您可以像这样调用plate

plate.plate(x.ctypes, y.ctypes, N)

然而,这样并不能提供任何类型安全性,以防止使用错误类型的数组调用函数,这将导致无意义、错误或分段错误。 np.ctypeslib.ndpointer 解决了这个问题。它创建了一个自定义类型,可用于设置 ctypes 函数指针的 argtypesrestype。该类型可以验证数组的数据类型、维数、形状和标志。例如:
import numpy as np
import ctypes

c_npfloat32_1 = np.ctypeslib.ndpointer(
    dtype=np.float32, 
    ndim=1, 
    flags=['C', 'W'])

plate = ctypes.CDLL('libplate.so')

plate.plate.argtypes = [
    c_npfloat32_1,
    c_npfloat32_1,
    ctypes.c_int,
]

N = 3
x = np.ones(N, dtype=np.float32)
y = np.ones(N, dtype=np.float32)

plate.plate(x, y, N)  # the parameter is the array itself

1
如果你不告诉ctypes参数的类型,它会尝试从你传递给函数的值中推断出类型。但是这种推断并不总是按照你的需求工作。
处理此问题的推荐方法是设置函数的argtypes属性,明确告诉ctypes参数的类型。
plate.plate.argtypes = [
    ctypes.POINTER(ctypes.c_float), 
    ctypes.POINTER(ctypes.c_float), 
    ctypes.c_int
]

然后你可以像这样调用函数:
plate.plate(x.ctypes.data, y.ctypes.data, N)

@eryksun,我个人认为这些信息值得在答案中包含。我认为argtypes是比当前被接受的答案更好的解决方案。但显然我不够了解细节,特别是对于明显有些特殊的numpy。您能否添加一个答案?那么我会删除这个问题并点赞您准确的答案。 - David Heffernan
我有一个看起来类似的问题,但与numpy无关。在我的情况下,所有函数原型都已定义,但这并没有帮助。我的64位指针仍然被截断为32位 :-( - kriss
好的,我的问题:我找到了为什么在我的情况下指针被截断的原因。接收指针的函数的原型没问题,但我与从 ctype 调用的另一个 C 函数存在困难,这个函数分配该指针。该函数的返回类型必须使用 restype = c_void_p 或其他指针或返回类型默认为 int 来定义。在我的情况下,我确实提供了 restype 但是我加了一个错别字(restype 后面跟了一个 "s")结果它被忽略了。让人恼火的是,有时会被忽视,因为只要库中使用低内存地址就可以工作。 - kriss

1

实际上,您应该设置plate.argstype = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_int],然后就可以从Python中接受C函数中的地址了。
我遇到了这个问题,但我按照自己说的方法解决了它。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接