在 python 中使用 swig 包装的 typedef 结构和枚举转换 string/buffer 数据

Casting string/buffer data using swig wrapped typedef structs and enums in python

我有一些在嵌入式系统上运行的 C 代码,生成一个数据流,我的 python 代码将在 bluetooth/usb 行的另一端读取这些数据。流的协议仍在大量开发中,并且经常更改,但在单个 .h 文件中定义。我想使用 SWIG 使 python 方面的内容保持最新,特别是通过提供对流数据布局(结构)

的访问

这是一个示例 .h 文件,它描述了一些结构和一些常量(如#defines),为了简洁起见,显然是整个协议的一个非常小的子集。

//datalayouts.h

#ifdef SWIG
#define __attribute__(x)
#endif

#define TOKEN_TYPE_SYNC_VALUE 1
#define TOKEN_TYPE_DELTA 2

typedef struct __attribute__((packed))
{
    uint8_t token_type;
    uint32_t timestamp;
    uint32_t value;
} struct_token_type_sync_value;

typedef struct __attribute__((packed))
{
    uint8_t token_type;
    int16_t delta;
} struct_token_type_delta;

加上这个就是基本的界面文件

%module datalayouts
%{
#include "datalayouts.h"
%}
%include "datalayouts.h"

编译和导入都很好。在 python 中,我可以创建一个 token_type_sync_value 类型的变量,但我想要做的是转换我从流中读取的一部分数据(作为字符串),以强加正确的结构就可以了。

例如:

>>> from datalayouts token_type_sync_value
>>> data = stream.read() #returns 100+ bytes
>>> if ord(data[0]) == TOKEN_TYPE_SYNC_VALUE:
...     #here I want to access data[0:9] as a token_type_sync_value

这可能吗,如果可能怎么办?

你可以用 SWIG 做到这一点,最简单的解决方案是使用 %extend 从 Python 中提供一个额外的构造函数,它需要一个 PyObect 作为缓冲区:

%module test

%include <stdint.i>

%inline %{
#ifdef SWIG
#define __attribute__(x)
#endif

#define TOKEN_TYPE_SYNC_VALUE 1
#define TOKEN_TYPE_DELTA 2

typedef struct __attribute__((packed))
{
    uint8_t token_type;
    int16_t delta;
} struct_token_type_delta;
%}

%extend struct_token_type_delta {
  struct_token_type_delta(PyObject *in) {
    assert(PyObject_CheckBuffer(in));
    Py_buffer view;
    const int ret = PyObject_GetBuffer(in, &view, PyBUF_SIMPLE);
    assert(0==ret);
    assert(view.len >= sizeof(struct_token_type_delta));
    struct_token_type_delta *result = new struct_token_type_delta(*static_cast<const struct_token_type_delta*>(view.buf));
    PyBuffer_Release(&view); // Note you could/should retain view.obj for the life of this object to prevent use after free
    return result;
  }
}

您需要为每个要从缓冲区构造的类型执行此操作,但每个构造函数的实际代码保持不变,因此可以包装为宏(使用 %define)很简单。您还希望通过保留对底层缓冲区的引用更长时间来防止出现错误后使用。


就我个人而言,如果是我这样做,虽然我会寻找不同的解决方案,因为有更好的方法来获得相同的结果并编写创建和维护薄 POD/bean 的代码,例如 objects 在任何语言中都是单调乏味的,更不用说 2 种或更多种语言了。假设 protbuf 太重量级而不能在你的嵌入式系统中使用,我会反向解决这个问题,使用 ctypes 作为 Python 然后让你的 Python 代码也为你的 C 生成 header构建工具也是如此。所以像:

import ctypes

class ProtocolStructure(type(ctypes.Structure)):
  def __str__(self):
    s='''
typedef struct __attribute__((packed)) {
\t%s
}'''
    return s % '\n\t'.join(('%s %s;' % (ty.__name__[2:], name) for name,ty in self._fields_))

class struct_token_type_delta(ctypes.Structure, metaclass=ProtocolStructure):
  _fields_ = (('token_type', ctypes.c_uint8),
              ('delta', ctypes.c_int16))

if __name__ == '__main__':
  # when this file is run instead of imported print the header file to stdout

  h='''
#ifndef PROTO_H
#define PROTO_H
%s
#endif
'''

  print(h % ';\n'.join('%s %s;\n' % (ty, name)  for name,ty in globals().items() if issubclass(type(ty), ProtocolStructure)))

然后让你写:

import proto
proto.struct_token_type_delta.from_buffer(bytearray(b'\xff\x11\x22'))