Search code examples
pythonc++socketsctypes

Access and copy an array of C structs in python ctypes


I have a nested C++ struct that I'm trying to send over to Python using UDP and get the values there.

#define _WINSOCK_DEPRECATED_NO_WARNINGS

#define _CRT_SECURE_NO_WARNINGS
#pragma comment(lib, "Ws2_32.lib")

#include <iostream>
#include <winsock2.h>

struct MyStruct {
    int intValue;
    float floatValue;
    // Add more members as needed

    struct MyInnerStruct {
        int intValue;
        float floatValue;
    };

    MyInnerStruct *InnerStruct;
};

int main() {

    MyStruct testStruct;
    testStruct.floatValue = 3.1;
    testStruct.intValue = 4;
    testStruct.InnerStruct = new MyStruct::MyInnerStruct[4];

    for (int i = 0; i < 4; i++) {
        testStruct.InnerStruct[i].intValue = i + 23;
        testStruct.InnerStruct[i].floatValue = (i/10) + 23;
    
    }

    WSADATA wsaData;
    if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {
        std::cerr << "Failed to initialize winsock." << std::endl;
        return 1;
    }

    char hostname[256];
    char ip[100];

    // Get the hostname
    if (gethostname(hostname, sizeof(hostname)) == 0) {
        std::cout << "Hostname: " << hostname << std::endl;

        // Get the IP address
        struct hostent* host_info = gethostbyname(hostname);
        if (host_info != nullptr) {
            struct in_addr* address = reinterpret_cast<struct in_addr*>(host_info->h_addr);
            strcpy(ip, inet_ntoa(*address));
            std::cout << "IP Address: " << ip << std::endl;
        }
        else {
            std::cerr << "Error getting host information." << std::endl;
        }
    }
    else {
        std::cerr << "Error getting hostname." << std::endl;
    }
    int udpSocket = socket(AF_INET, SOCK_DGRAM, 0);
    if (udpSocket == -1) {
        perror("Error creating socket");
        exit(EXIT_FAILURE);
    }

    // Server address
    sockaddr_in serverAddr;
    serverAddr.sin_family = AF_INET;
    serverAddr.sin_port = htons(12345); // Choose a port
    serverAddr.sin_addr.s_addr = inet_addr(ip); // Replace with the server's IP

    // Serialize the struct into a byte stream
    char buffer[sizeof(MyStruct)];
    memcpy(buffer, &testStruct, sizeof(MyStruct));

    // Send the serialized struct over the UDP socket
    if (sendto(udpSocket, buffer, sizeof(MyStruct), 0, reinterpret_cast<sockaddr*>(&serverAddr), sizeof(serverAddr)) == -1) {
        perror("Error sending data");
        closesocket(udpSocket);
        exit(EXIT_FAILURE);
    }

    std::cout << "Struct sent successfully." << std::endl;

    // Clean up
    closesocket(udpSocket);

    
    WSACleanup();
    return 0;
}

But in Python I can't seem to get the values of the nested C++ struct.

import socket
import struct
from ctypes import *
# IP address and port number for server
HOST = '127.0.0.1'
PORT = 12345

class MyInnerStruct(Structure):
    _fields_ = [
        ('field4', c_int),
        ('field5', c_float),
        
    ] 

class MyStruct(Structure):
    _fields_ = [
        ('field1', c_int),
        ('field2', c_float),
        ('field3', POINTER(MyInnerStruct)),
    ]

sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind((HOST, PORT))

while True:
   
    data, addr = sock.recvfrom(sizeof(MyStruct))
    
    received_struct = cast(data, POINTER(MyStruct)).contents
  
   # Access the fields of the received struct
    field1 = received_struct.field1
    field2 = received_struct.field2
    field3 = received_struct.field3

    received_inner_struct = cast(field3, POINTER(MyInnerStruct)).contents

   

    for i in range(field1):
        array[i].field4 = field3[i].field4
        array[i].field5 = field3[i].field5   //these lines are not working    

# Close the socket
sock.close()

I tried accessing the values of the inner nested struct (in the for loop) by trying to de-reference but I couldn't. How can I access field4 and field5 in the inner struct?


Solution

  • In C, send the equivalent field1-3 followed by field4-5 for each array element. Here's the Python equivalent I used for testing:

    import struct
    import socket
    
    # send field1=4, field2, and 4x the two fields of each array element.
    data = struct.pack('<ififififif', 4, 3.5, 1, 1.25, 2, 2.5, 3, 2.75, 4, 3.00)
    
    with socket.socket(type=socket.SOCK_DGRAM) as s:
        s.sendto(data, ('localhost', 5000))
    

    One way to unpack, not overly efficient. To receive, read the first two fields and initialize field1-2 of MyStruct. Use field1 to allocate an array of MyInnerStruct. Then read the pairs of field4-5 from the buffer and initialize the array. Finally, assign the array to MyStruct.field3:

    import socket
    import struct
    import ctypes as ct
    
    class MyInnerStruct(ct.Structure):
        _fields_ = (('field4', ct.c_int),
                    ('field5', ct.c_float))
        def __repr__(self):  # for display
            return f'({self.field4}, {self.field5})'
    
    class MyStruct(ct.Structure):
        _fields_ = (('field1', ct.c_int),
                    ('field2', ct.c_float),
                    ('field3', ct.POINTER(MyInnerStruct)))
        def __repr__(self):  # for display
            return f'[{self.field1}, {self.field2}, {list(self.field3[:self.field1])})]'
    
    sock = socket.socket(type=socket.SOCK_DGRAM)
    sock.bind(('', 5000))
    
    data, addr = sock.recvfrom(40960)
    field1, field2 = struct.unpack_from('<if', data)
    received_struct = MyStruct(field1, field2)  # pointer is null at this point
    
    inner_array = (MyInnerStruct * field1)()  # allocate inner array
    start_of_inner = struct.calcsize('if')  # index data after MyStruct fields
    size_of_inner = struct.calcsize('if')   # size of inner array element
    index = start_of_inner
    for i in range(field1):
        field4, field5 = struct.unpack_from('<if', data[index:])  # read fields of an array element
        inner_array[i] = MyInnerStruct(field4, field5) # assign to the array element
        index += size_of_inner
    received_struct.field3 = inner_array  # pointer to the initialized inner array
    print(received_struct)  # now print complete received item.
    

    Run the receiver code to wait for a packet, then run the sender code to transmit the packet:

    Output from receiver:

    [4, 3.5, [(1, 1.25), (2, 2.5), (3, 2.75), (4, 3.0)])]
    

    In fact, dropping ctypes may be faster and more simple by building Python class instances parsed from the data stream. No need to deal with Python/C marshaling:

    import socket
    import struct
    
    class MyInnerStruct:
        _format = '<if'  # format and size of serialized data
        _size = struct.calcsize(_format)
    
        def __init__(self, f4, f5):
            self.field4 = f4
            self.field5 = f5
    
        @classmethod
        def from_data(cls, data):
            return cls(*struct.unpack_from(cls._format, data))
    
        @classmethod
        def from_data_array(cls, data, count):
            # construct a list of InnerStruct from data of known size
            return [cls(*struct.unpack_from(cls._format, data[n*cls._size:(n+1)*cls._size])) for n in range(count)]
    
        def __repr__(self):
            return f'[{self.field4}, {self.field5}]'
    
    class MyStruct:
        _format = '<if'  # format and size of serialized data
        _size = struct.calcsize(_format)
    
        def __init__(self, f1, f2):
            self.field1 = f1
            self.field2 = f2
            self.field3 = MyInnerStruct.from_data_array(data[self._size:], f1)
    
        @classmethod
        def from_data(cls, data):
            return cls(*struct.unpack_from(cls._format, data))
                           
        def __repr__(self):
            return f'[{self.field1}, {self.field2}, {self.field3}]'
    
    sock = socket.socket(type=socket.SOCK_DGRAM)
    sock.bind(('', 5000))
    
    data, addr = sock.recvfrom(40960)
    received_struct = MyStruct.from_data(data)
    print(received_struct)  # now print complete received item.
    

    (same output)