c_ulong的ctypes右移运算重实现

5 投票
1 回答
2400 浏览
提问于 2025-04-17 19:58

我正在通过ctypes访问一个C语言库,但遇到了一个问题:

我正在使用ctypeslib生成一个“包装器”(也就是用ctypes来访问这个库的命令)。这个C语言库里有一些宏,在这个步骤中会被转换成Python函数。为了尽量不依赖库内部的实现,我想在Python中使用这些宏。

其中一个宏看起来是这样的:

# using the ctypes types
myuint16_t = c_ushort
myuint32_t = c_ulong

def mymacro(x): return (myuint16_t)((myuint32_t)(x) >> 16) # macro

我想在一个单独的模块中以以下方式使用生成的函数(在一个函数内部):

return wrapper.mymacro(valueToBeConverted) # valueToBeConverted is an int

但是使用这一行代码时,我遇到了以下错误:

....   
def mymacro(x): return (myuint16_t)((myuint32_t)(x) >> 16) # macro
TypeError: unsupported operand type(s) for >>: 'c_ulong' and 'int'

(我知道通常的方式是用c_ulongvar.value >> x来进行右移,但每次C库有变化时,我都得修改生成的包装器,所以我想避免这样做。)

看起来c_ulong的__rshift__实现不能在这里使用。

print c_ulong.__rshift__
# throws AttributeError: type object 'c_ulong' has no attribute '__rshift__'

嗯,这听起来有点奇怪……所以我决定重新实现c_ulong的__rshift__方法来让它工作:

from ctypes import *
from types import MethodType

def rshift(self, val):
    print self.value >> val

# create an unbound method which applies to all (even existing) instances
c_ulong.__rshift__ = MethodType(rshift, None, c_ulong)

a = c_ulong(1)
a >> 16

但这并没有解决问题。我仍然遇到错误:

a >> 16
TypeError: unsupported operand type(s) for >>: 'c_ulong' and 'int'

难道__rshift__方法只能用于同一个类的两个实例吗?我尝试了以下代码:

def rshift(self, val):
    print self.value >> int(val.value)

a = c_ulong(1)
a >> c_ulong(16) 

结果是可以的。但这也意味着我仍然需要修改生成的包装器。

所以:有没有人知道这里的诀窍是什么?

更新

@eryksun的解决方案有效。我现在使用:

from ctypes import *
# from types import MethodType

def _rshift(self, other):
    if hasattr(other, 'value'):
        other = other.value
    return c_ulong(self.value >> other)

def _lshift(self, other):
    if hasattr(other, 'value'):
        other = other.value
    return c_ulong(self.value << other)

def _coerce(self, other):
    try:
        return self, self.__class__(other)
    except TypeError:
        return NotImplemented

# Add the functions to the type. A method is created when
# accessed as an attribute of an instance.
c_ulong.__lshift__ = _lshift
c_ulong.__rshift__ = _rshift
c_ulong.__coerce__ = _coerce

1 个回答

4

因为 _ctypes._SimpleCData 这个类型没有 Py_TPFLAGS_CHECKTYPES 这个标志,所以在2.x版本中,它的子类会被当作旧式数字来处理,这些旧式数字在进行二元操作时会使用 __coerce__。想了解更多,可以查看Objects/abstract.c,里面有调用方案和 binary_op1 函数的实现。

为了演示,这个标志可以在类型对象上进行切换,你只需要定义(大概用很多 void *)到 tp_flags 字段。

修改 PyTypeObject

from ctypes import *
import _ctypes

Py_TPFLAGS_CHECKTYPES = 1 << 4

class PyTypeObject(Structure):
    _fields_ = (('ob_refcnt', c_ssize_t),
                ('ob_type', c_void_p),
                ('ob_size', c_ssize_t),
                ('tp_name', c_char_p),
                ('tp_basicsize', c_ssize_t),
                ('tp_itemsize', c_ssize_t),
                ('tp_dealloc', c_void_p),
                ('tp_print', c_void_p),
                ('tp_getattr', c_void_p),
                ('tp_setattr', c_void_p),
                ('tp_compare', c_void_p),
                ('tp_repr', c_void_p),
                ('tp_as_number', c_void_p),
                ('tp_as_sequence', c_void_p),
                ('tp_as_mapping', c_void_p),
                ('tp_hash', c_void_p),
                ('tp_call', c_void_p),
                ('tp_str', c_void_p),
                ('tp_getattro', c_void_p),
                ('tp_setattro', c_void_p),
                ('tp_as_buffer', c_void_p),
                ('tp_flags', c_long))

接下来,创建一个 unsigned long 的子类,并使用 from_address 工厂为它创建一个 PyTypeObject。通过内置的 id 获取地址,这个是CPython特有的实现细节:

class c_ulong(_ctypes._SimpleCData):
    _type_ = "L"

    def __rshift__(self, other):
        print '__rshift__', self, other
        if hasattr(other, 'value'):
            other = other.value
        return c_ulong(self.value >> other)

c_ulong_type = PyTypeObject.from_address(id(c_ulong))

演示

>>> a = c_ulong(16)
>>> b = c_ulong(2)

>>> a >> b
__rshift__ c_ulong(16L) c_ulong(2L)
c_ulong(4L)

>>> a >> 2
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unsupported operand type(s) for >>: 'c_ulong' and 'int'

最后一步按预期失败了。现在设置这个标志:

>>> c_ulong_type.tp_flags |= Py_TPFLAGS_CHECKTYPES

>>> a >> 2
__rshift__ c_ulong(16L) 2
c_ulong(4L)

问题解决了吗?但这只是个小技巧。再试一次,看看 __coerce__ 实现后会怎样。


实现 __coerce__

class c_ulong(_ctypes._SimpleCData):
    _type_ = "L"

    def __rshift__(self, other):
        print '__rshift__', self, other
        if hasattr(other, 'value'):
            other = other.value
        return c_ulong(self.value >> other)

    def __coerce__(self, other):
        print '__coerce__', self, other
        try:
            return self, self.__class__(other)
        except TypeError:
            return NotImplemented

演示

>>> a = c_ulong(16)
>>> b = c_ulong(2)

>>> a >> 2
__coerce__ c_ulong(16L) 2
__rshift__ c_ulong(16L) c_ulong(2L)
c_ulong(4L)

>>> 16 >> b
__coerce__ c_ulong(2L) 16
__rshift__ c_ulong(16L) c_ulong(2L)
c_ulong(4L)

当然,如果不能创建 c_ulong,比如对于 float 类型,那肯定会失败:

>>> a >> 2.0
__coerce__ c_ulong(16L) 2.0
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unsupported operand type(s) for >>: 'c_ulong' and 'float'

撰写回答