1

What is the best way to pass from python 3.6.9 to C-API struct
I have a C library, which I'm trying to create a Python interface for it,
but the library expects to initialize a struct beforehand, the main problem is that one element of a struct is a pointer to another struct.

The part of the C header with the structs:

enum filter_type {
    BLACK,      /* Black list */
    WHITE       /* White list */
};

struct message {
        uint32_t    mid;       /* the message id */
        uint8_t     interface; /* the interface */
} __attribute__ ((__packed__));

struct filtering {
    struct message      *filter;
    uint32_t            arr_len;    /* length of the array
    enum filter_type    mf_type;    /* filter type - black or white list */
} __attribute__ ((__packed__));

Python code:

from ctypes import Structure, c_uint32, c_uint8, c_bool
from myC_Module import test_struct


class Message(Structure):
      _fields_ = [('mid', c_uint32),
                  ('interface', c_uint8)]


def gen_filter(mids, mf_type):
      class Filtering(Structure):
            _fields_ = [('filter', Message*len(mids)),
                        ('arr_len', c_uint32),
                        ('mf_type', c_bool)]

      c_array = Message * len(mids)
      return Filtering(c_array(*mids), len(mids), mf_type)


messages = [  # for testing
      Message(int("1af", 16), 3),
      Message(int("aaaaaaaa", 16), 100),
      Message(int("bbbbbbbb", 16), 200),
]

print(test_struct(gen_filter(messages, True)))

C-API test_struct function code:

static PyObject *test_struct(PyObject *self, PyObject *args) {
    struct filtering *filtering = NULL;
    Py_buffer buffer;
    PyObject *result;

    if (!PyArg_ParseTuple(args, "w*:getargs_w_star", &buffer))
        return NULL;

    printf("buffer.len: %ld\n", buffer.len);
    filtering = buffer.buf;

    printf("recived results: arr_len[%d]  mf_type[%d]\n", filtering->arr_len, filtering->mf_type);
    printf("filter: %d\n", filtering->filter);

    result = PyBytes_FromStringAndSize(buffer.buf, buffer.len);
    PyBuffer_Release(&buffer);
    return result;
}

results:

buffer.len: 32
recived results: arr_len[431]  mf_type[3]
filter: -1442840576
b'\xaf\x01\x00\x00\x03\x00\x00\x00\xaa\xaa\xaa\xaad\x00\x00\x00\xbb\xbb\xbb\xbb\xc8\x00\x00\x00\x03\x00\x00\x00\x01\x00\x00\x00'

With the following approach, in case of a single value of message (not array and by changing the filtering struct accordingly), it works.
Any idea what I'm doing wrong, or What is the right way to this?
Thanks.

EDIT -- Extra Thoughts
I think now I understand why it's happening, yet I don't know how to solve it (yet).
I added additional prints for the buffer.buf to see what I actually have in it:

    filtering = buffer.buf;
    char * buf = (char*)buffer.buf;
    for(int i=0; i<buffer.len; i++){
        if(i==0)
            printf("[%d, ", (uint8_t)buf[i]);
        if(i<buffer.len-1)
            printf("%d, ", (uint8_t)buf[i]);
        else
            printf("%d]\n", (uint8_t)buf[i]);
    }

And I got the following:

[175, 1, 0, 0, 3, 0, 0, 0, 170, 170, 170, 170, 100, 0, 0, 0, 187, 187, 187, 187, 200, 0, 0, 0, 3, 0, 0, 0, 1, 0, 0, 0]

The same results are in python print("[{}]".format(', '.join(map(str, returned))))
For this to work I would expect shorter buffer due to the assigned types (c_uint32, c_uint8)
since I have one gen_filter which have 3 Messages, each Messages should be size 5 and the extra data in gen_filter should be also 5, I would expect total size of 20, but as we can see it's much begger.
I notced that the type c_uint8 is acttualy size 4 insted of 1.
for this to work I expect the following result:

[175, 1, 0, 0, 3, 170, 170, 170, 170, 100, 187, 187, 187, 187, 200, 3, 0, 0, 0, 1]

due to:

{[<c_uint32,c_uint8>,<c_uint32,c_uint8>,<c_uint32,c_uint8>],<c_uint32,c_uint8>}

the buffer have element called format which contain the folloing:
format T{(3)T{<I:mid:<B:interface:}:filter:<I:arr_len:<?:mf_type:}

Rami Hassan
  • 149
  • 12
  • 1
    Is it necessary to construct these weird python structures? I would just pass a string to some custom C or c++ code, and have it construct the binary format for a library call with no language barrier. – Kenny Ostrom Dec 20 '19 at 14:39
  • 1
    In the case that you do need to maintain a collection of C objects, and use them from python, you can keep them entirely in the C extension module, and access them through handles in the python api. I have a module like that. – Kenny Ostrom Dec 20 '19 at 14:44
  • @KennyOstrom I told my self that what I will do in case of a too complicated implementation, But my motivation is in case of future changes of the C library, only minor changes will need to happen (only in python in case I managed to build a generic solution). – Rami Hassan Dec 20 '19 at 14:58
  • @KennyOstrom Moreover, this is only one item that I'm sending to the actual function, but there are more, however, this is the only item that I'm struggling with. – Rami Hassan Dec 20 '19 at 15:07

1 Answers1

1

Your problem is (may be?) that the structure returned by gen_filter isn't the same as the one you define in C. The C equivalent of what you define in gen_filter is:

struct filtering {
    struct message      filter[arr_len]; /* NOT ACTUALLY VALID C SINCE arr_len 
                                            ISN'T A CONSTANT */
    uint32_t            arr_len;    /* length of the array
    enum filter_type    mf_type;    /* filter type - black or white list */
} __attribute__ ((__packed__));

This is a single block of memory, containing space for the message within the structure. However, in C the list of messages is allocated separately to the structure and filter only points to it.

Your Python code should probably be something like this:

class Message(Structure):
      _fields_ = [('mid', c_uint32),
                  ('interface', c_uint8)]
      _pack_ = 1 # matches "packed" - an important addition!

class Filtering(Structure):
    _fields_ = [('filter', POINTER(Message)),
                ('arr_len', c_uint32),
                ('mf_type', c_bool)]
    _pack_ = 1 # matches "packed" - an important addition!
    def __init__(self, messages, mf_type):
        self.filter = (Message*len(messages))(*messages)
        self.arr_len = len(messages)
        self.mf_type = mf_type

Be aware that the lifetime of the separately allocated messages array is tied to the lifetime of the Python Filtering instance.


It's slightly hard to follow your C code since you use the mysterious and unspecified attribute filtering->int_list. Assuming int_list is actually filter, you're just printing the pointer (interpreted as a signed int), not what it points to.

Rami Hassan
  • 149
  • 12
DavidW
  • 29,336
  • 6
  • 55
  • 86
  • You are right, it's a typo, fixed... Thanks, I will check your theory and report. – Rami Hassan Dec 21 '19 at 22:04
  • Can you please elaborate on why is this piece of code necessary `_pack_ = 1 # matches "packed" - an important addition!` – Rami Hassan Dec 21 '19 at 22:31
  • Your C structures have `__attribute(__packed__)__` set, but by default ctypes matches the unpacked memory layout which _may_ be different. https://docs.python.org/3/library/ctypes.html#structure-union-alignment-and-byte-order. If it works without then it's by luck – DavidW Dec 22 '19 at 07:35
  • [DavidW](https://stackoverflow.com/users/4657412/davidw) Many thanks :) – Rami Hassan Dec 22 '19 at 08:11