首页 > 解决方案 > 可以结合 swig 生成的数据类型和 ctypes 函数

问题描述

我正在为带有 ctypes 的 C 库编写 python 包装器,并且我有一个返回结构的 C 函数。但是该结构是在另一个 C 文件中定义的,并且该文件由 swig 包装。

我简化了结构和代码。

这个结构是由 swig 包裹的。

struct point {
  int x;
  int y;
};

这个函数是用 ctypes 包装的。

struct point add_points(struct point a, struct point b) {
  struct point c;
  c.x = a.x + b.x;
  c.y = a.y + b.y;
  return c;
}

Python 包装器。

import swigModule # contains class point generated from c structure point
import ctypes

_libc = ctypes.CDLL('./c_file.so')

def add_points(a, b):
    add_points = _libc.add_points
    add_points.argtypes = [swigModule.point, swigModule.point,]
    add_points.restype = swigModule.point,
    result = add_points(a, b)
    return result

问题是,我不能point在 ctypes 中使用 swig 生成的类作为 restype 和 argtype。但是我不能像这样编写自己的结构包装器。

class Point(ctypes.Structure):
    _fields_ = [("x", ctypes.c_int),
                ("y", ctypes.c_int)]

因为 C 结构的字段是隐藏的,所以我无权访问源代码。我只知道 swig wrapper 的结构名称。

我有两个 C 文件,一个是用 swig 生成的,具有结构数据类型。另一个具有功能并用 ctypes 包装。我想在我的 ctypes 函数中使用来自 swig 的数据类。

如何将从 c 结构生成的 swig 类映射到 ctypes 类,以便可以将其用作返回类型?

标签: pythoncctypesswig

解决方案


让 SWIG 和 ctypes 以各种不同的方式一起工作是完全可能的,我将在下面通过几个示例进行展示。但是,话虽如此,但有一个很大的警告:根据我的经验,它几乎永远不会为您提供可维护、可移植、可扩展等的东西,因此我倾向于在可能的情况下支持替代选项。(这些可能包括使用任何适当的说服方式向原始作者询问来源,从头开始重写整个内容,或者只是使用替代库)。

无论如何,让我们假设您已经为给定的 Python 版本构建了一个 SWIG 包装库。无论出于何种原因,他们都没有包装您最喜欢的功能。因此,您只想使用 ctypes 将某些内容添加到 SWIG 生成的代码中。这实际上很容易,只要:

  1. 您要调用的函数仅按指针获取和返回对象,而不是按值。
  2. 您要调用的函数仅返回您的类型的现有实例,而不是 malloc 新实例。(有一个解决方法,但它又开始让事情变得困难,非常快)。
  3. 您关心的结构实际上包装了您关心的所有成员,并且(如果是 C++ 代码)它们是 POD 类型。

您的案例并不真正符合这些约束条件,但作为热身练习,让我们看一下使用以下“easy_mode”函数执行此操作,因为它确实让我们介绍了一个关键点:

struct point *easy_mode(struct point *a, struct point *b) {
  a->x += b->x;
  a->y += b->y;
  return a;
}

鉴于现有的 SWIG 包装器尚未包装它,但确实包装了结构,此函数非常易于使用。我们可以简单地使用 SWIG 代码来创建结构的实例,并从中提取一个指针以(通过​​ ctypes)给导出但未包装的函数,例如:

from ctypes import *
from test import point

p1 = point()
p2 = point()
p1.x = 123
p1.y = 156
p2.x = 123
p2.y = 156

so = CDLL('./_test.so')

so.easy_mode(int(p1.this), int(p2.this))
print(p1.x)
print(p1.y)

调用这个函数就足够了,而且我们知道返回类型实际上只是在修改p1我们可以使用该知识并使用它。不过,要摆脱这一点的关键是,调用int(p1.this)会为您提供 SWIG 对象正在代理的指针的整数表示形式。这就是 ctypes 需要的指针。

不过,让我们把它推进到我们按值传递和返回结构的情况。这要困难得多,因为调用函数的方式取决于结构的大小和成员。它们的类型和顺序很重要。它因建筑而异。它甚至可以根据各种事物在给定架构内变化。幸运的是 ctypes(通过 libffi,如果您以前从未见过它,它本身就是一件有趣的事情)对我们隐藏了所有这些。

所以现在我们的目标缺失函数可以是这样的:

struct point add_them(struct point a, struct point b) {
  struct point ret = { a.x + b.x, a.y + b.y };
  return ret;
}

问题是,在只有一个不调用它的现有 SWIG 模块的情况下,我们对struct point. 这对于能够按值调用至关重要。当然,我们可以做出一些猜测,如果它足够简单,只需猜测,那么您也可以这样做并为 ctypes 完成它。

幸运的是,结构的可用 SWIG 包装的存在使我们(如果一些假设成立)足以对结构的类型/布局做出足够好的猜测。此外,由于我们知道如何获取指向结构实例使用的底层内存的指针,我们可以构建测试来向我们展示有关布局的信息。如果一切顺利,我们可以使用它来构建Structure兼容的字段的 ctypes 定义。

关键是我们要将memset一个实例设置为 0,然后尝试使用 SWIG 为每个成员生成的设置器代码将每个字节设置为一个类型的标记值。当我们检查我们可以做出我们需要的扣除时。

然而,在我们这样做之前,对结构的大小设置一个上限会很有帮助。我们可以通过调用malloc_useable_size()which 来得到它,它告诉我们堆分配被四舍五入到什么程度。所以我们可以这样做:

useable_size = CDLL(None).malloc_usable_size

upper_size_bound = useable_size(int(p1.this))
buffer_type = POINTER(c_char * upper_size_bound)
print('Upper size bound is %d' % upper_size_bound)

choose_type = dict([(1, c_uint8), (2, c_uint16), (4, c_uint32)]).get

def generate_members(obj):
  for member_name in (x for x in dir(obj) if not x[0] == '_' and x != 'this'):
    print('Looking at member: %s' % member_name)
    def pos(shift):
      test = point()
      memset(int(test.this), 0, upper_size_bound)
      pattern = 0xff << (8 * shift)
      try:
        setattr(test, member_name, pattern)
      except:
        return -1
      return bytes(cast(int(test.this), buffer_type).contents).find(b'\xff')
    a=[pos(x) for x in range(upper_size_bound)]
    offset = min((x for x in a if x >= 0))
    size = 1 + max(a) - offset
    print('%s is a %d byte type at offset %d' % (member_name, size, offset))
    pad = [('pad', c_ubyte * offset)] if (offset > 0) else []
    class Member(Structure):
        _pack_ = 1
        _fields_ = pad + [(member_name, choose_type(size))]

    yield Member

这需要 SWIG 知道给定结构的每个成员,并计算一个数组,计算给定字段的每个字节的偏移量,然后计算该成员的偏移量和大小。(使用 min/max 意味着它应该适用于 BE 和 LE 硬件)。我们可以获取大小并将其映射到类型上。鉴于我们现在拥有的知识,我们可以计算出最符合我们所学的布局。不过我作弊了,并且为每个成员生成了一个结构,该结构在开始时添加了填充,以将成员定位在我们计算的偏移量处。上面的 python 代码是一个生成器,它产生一个 ctypes Structure,它在正确的偏移量处具有正确的大小/类型的成员。

实际上,您需要推断出更多。浮点数可能最好使用已知的可精确表示的值。我们需要考虑有符号和无符号类型。数组、字符串甚至嵌套类型都可以。在试错的基础上,这一切都是可能的,但这留给读者作为练习。

最后我们需要把它拉到一起。因为我欺骗了每个成员的一个结构,所以我们需要做的就是将它们合并到一个联合中:

class YourType(Union):
    _pack_ = 1
    _fields_ = list(zip(string.ascii_lowercase, generate_members()))
    _anonymous_ = string.ascii_lowercase[:len(_fields_)]

有了这个,我们现在有足够的东西来调用我们的add_them函数:

MyType=YourType

y=MyType()
y.x = 1 
y.y = 2

add_them = so.add_them
add_them.argtypes = [MyType, MyType]
add_them.restype = MyType

v=add_them(y,y)
print(v)
print('V: %d,%d' % (v.x, v.y))

仅使用从预先存在的 SWIG 模块派生的信息来调用 ctypes 函数实际上确实有效。

我仍然建议不要在任何真实代码中这样做!


推荐阅读