可以结合 swig 生成的数据类型和 ctypes 函数

Is possible to combine swig generated datatypes and ctypes function

我正在为带有 ctypes 的 C 库编写 python 包装程序,并且我有一个 return 结构的 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

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

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

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

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

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

这当然是个有趣的问题!之前有人问过,见question 7886790。 不过,这个问题没有公认的答案。在 Internet 上进行研究几乎找不到有用的信息。

普遍的共识似乎是“使用 ctypes SWIG”。 所以恐怕你的问题的答案是。 :-(


我从研究这个问题中收集到的信息:

  • 当您必须与多种语言交互时,SWIG 更好。
  • 当你不想编译东西时,
  • ctypes 更好。
  • ctypes 适用于 C 代码,但仅适用于具有 extern C 接口的 C++ 代码。
  • boost.Python 在将 C++ 连接到 Python.
  • 时经常出现

您可能会发现 question 135834 的答案很有帮助。

让 SWIG 和 ctypes 以各种不同的方式协同工作是完全可能的,正如我将在下面的几个示例中展示的那样。然而,话虽如此,但有一个很大的警告:根据我的经验,它几乎永远不会给你带来可维护、可移植、可扩展等的东西,所以我倾向于尽可能选择替代方案。 (这些可能包括使用任何合适的说服方式向原作者询问来源,从头开始重写整个内容,或者只是使用替代库)。

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

  1. 您要调用的函数只能通过指针获取和 return 对象,而不是通过值。
  2. 您只想调用的函数 return 您的类型的现有实例,不要 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)

足以调用此函数,并且我们知道 return 类型实际上只是在修改 p1 我们可以使用该知识并继续使用它。不过,要摆脱这一点的关键是调用 int(p1.this) 可以获得 SWIG 对象代理的指针的整数表示。这就是 ctypes 对指针的全部需求。

不过,让我们将其向前推进到我们按值传递和 return 结构的情况。这要困难得多,因为调用函数的方式取决于结构的大小和成员。它们的类型和顺序很重要。它因架构而异。它甚至可以根据各种事物在给定的体系结构中发生变化。幸运的是 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 包装的存在使我们(如果一些假设成立)足以对结构的 types/layout 做出至少足够好的猜测。此外,由于我们知道如何获取指向底层内存的指针,一个结构实例使用我们可以构建测试,向我们展示有关布局的事情。如果一切顺利,我们可以使用它来构建一个兼容字段的 ctypes Structure 定义。

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

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

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 的生成器,它在正确的偏移量处有一个 size/type 的成员。

实际上,您需要推断更多。浮点数可能最好与已知的可精确表示的值一起使用。我们需要同时考虑有符号和无符号类型。数组、字符串甚至嵌套类型都可以完成。在反复试验的基础上,这一切都是可能的,但这留作 reader.

的练习

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

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 函数确实有效。

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