19 Modeling I2C Devices 21 Modeling Network Communication
Model Builder User's Guide  /  III Modeling Common Hardware Components  / 

20 Representing Network Packets with frags_t

20.1 Background

When modeling computer systems it is often necessary to send chunks of data around. This is especially common when modeling network devices where one device model may pick up a network packet from target memory, attach a header or a CRC and pass it on to another device that sends it out on the simulated network. When receiving a network packet, the device will get a network packet, examine it, optionally strip parts from it and pass it on to other devices or the target memory.

Models of more advanced devices will need to do additional processing of the packets, adding, removing or updating headers on the way. And a more abstract model of a network node may want to model the whole network stack.

20.2 Fragment lists

The frags_t data type is designed to facilitate the efficient implementation of functions passing these chunks around. It represents the data as a list of fragments, where the individual fragments references pieces of the actual data. The data is not stored in the frags_t structure, nor is it owned by it. Instead, it can be seen as a pointer that points to multiple memory locations in sequence.

The typical use is a function that is passed some block of data. Without frags_t, it might have taken a plain pointer and a size:

void old_receive_data(const uint8 *data, size_t data_len);

But with frags_t it would instead take a pointer to a constant frags_t:

void receive_data(const frags_t *data);

The usage is very similar. Instead of passing the data as a single, contiguous block of memory, it is passed as a list of references to fragments of the data. The only important difference is that the receive_data() function will need another way to access the data itself.

To complete the introduction, this is how a frags_t is created before calling this function:

frags_t data;
frags_init_add(&data, header, header_len);
frags_add(&data, payload, payload_len);
receive_data(&data);

20.3 Getting to the data

A function that receives a const frags_t * argument can use it to read the data it references in two ways.

The first, and most common, way is to extract it to another buffer, using the frags_extract() function, or using the frags_extract_slice() function to only extract parts of it.

void receive_data_extract(const frags_t *data)
{
        uint8 buf[frags_len(data)];
        frags_extract(data, buf);
        // buf now contains all the data
        // ...
}

Or

void receive_data_slice(const frags_t *data)
{
        uint8 buf[HEADER_LEN];
        frags_extract_slice(data, buf, 0, HEADER_LEN);
        // buf now contains the header
        // ...
}

To avoid copies, it is sometimes useful to use the iterator functions. This is an example of a simple checksum function:

int checksum(const frags_t *data)
{
        int chksum = 0;
        for (frags_it_t it = frags_it(data, 0, frags_len(data));
             !frags_it_end(it);
             it = frags_it_next(it))
                chksum = partial_checksum(chksum,
                                          frags_it_data(it),
                                          frags_it_len(it));
        return chksum;
}

The iterator will provide the pointer to the data in one fragment at a time. These iterator functions are usually not very useful to do common things like read the header of a network packet, since there is no guarantee that any fragment contains the entire header.

20.4 Avoiding allocation

To avoid the cost of heap allocation, the preferred way to allocate data to be referenced by a fragment list, and the fragment list itself, is on the stack. Allocating on the stack is extremely cheap, and with variable-sized arrays in C99 (and DML), it is also very simple to use. A typical network device that reads a packet using DMA and attaches a CRC before sending it to the link could look like this:

void send_packet(nic_t *dev)
{
        dma_desc_t desc = get_next_dma_desc(dev);
        uint8 data[desc.len];
        dma_read(dev, desc.addr, desc.len, data);
        uint8 crc[4];
        calculate_crc(data, crc);
                
        frags_t packet;
        frags_init_add(&packet, data, desc.len);
        frags_add(&packet, crc, 4);
        send_to_link(dev, &packet);
}

One advantage of stack allocation is that there is no need for any destructors; the memory is automatically released when the stack frame is removed.

This works since the frags_t type has no external allocation. Adding fragments will not cause any dynamic allocations to be made. This also means that the size of the fragment list fixed, so there is a limit to the number of fragments that can be added. The size of the list is eight, which should be enough for most cases, while still being manageable.

Stack allocation also means that there is never any doubt about the ownership of the data. The pointers to the data can not be taken over by anyone else, so the ownership remains with the function that allocates it.

The references to the data in the fragment list is read-only. It is not possible to use a frags_t reference to modify any data that it points to. There could be other, writeable, references to the same data, such as the local variables data and crc in the example above, but when those are not available to a function it has no way of modifying the data.

20.5 Keeping the data

Since ownership of a fragment list, or of any of its fragments, can not be passed in a function call, there is no way to simply store a fragment list for later use. Instead, the data must be copied if it is going to be needed later.

A network link model that receives a network packet in a frags_t will typically need to hold on to the data for a while before delivering it to all the recipients. This means that it should extract the data into a buffer that it allocates on the heap. And when it sends the packet to one of the recipients, it can simply create a frags_t that references the heap-allocated data and pass that pointer to the receiving device.

Here is some pseudo-code for a link model:

void send_a_packet(link_t *link, const frags_t *packet)
{
        link->packet_buffer = MM_MALLOC(frags_len(packet), uint8);
        link->packet_buffer_len = frags_len(packet);
        frags_extract(packet, link->packet_buffer);
        // ... defer delivery to a later time ...
}

void deliver_a_packet(link_t *link)
{
        frags_t packet;
        frags_init_add(&packet, link->packet_buffer,
                       link->packet_buffer_len);
        for (link_dev_t *dev = link->recipients; dev;
             dev = dev->next)
                deliver_to_dev(link, dev, &packet);
        MM_FREE(link->packet_buffer);
        link->packet_buffer = NULL;
}

As a convenience, there is a function frags_extract_alloc() that does the allocation and extracts to the allocated buffer, so the send function can be written like this instead:

void send_alloc(link_t *link, const frags_t *packet) {
        link->packet_buffer = frags_extract_alloc(packet);
        // ... defer delivery to a later time ...
}

The memory management of the packet buffer in the above code is rather straightforward, but in other cases may be more complex and require reference counting, etc. The frags library does not attempt to solve any such problem; it is only intended to be used for passing data in function calls.

20.6 Multithreading

Since the fragment list and the data it points to are only valid as long as the stack frame they live in is live, it is almost never possible to pass references to them between threads. It is possible to do it and block until the other thread is finished using it before returning, but there are very few occasions where this makes sense. Simply copying the data, as described in the previous section, is usually the best solution.

20.7 Conventions

This is a summary of the rules and conventions that should be adhered to when using this library. Any exception to these rules should be clearly marked in the code.

  1. A frags_t pointer passed to a function is read-only. This means that you should always declare them as const frags_t * in function prototypes.
  2. The data references in a frags_t are read-only. They are declared as const uint8 *, and can not be used to modify the data.
  3. A frags_t pointer passed to a function can not be stored and reused after the function returns. Neither can a copy of the frags_t structure be stored and reused.

20.8 How to use frags_t to...

There are a few common use cases that often occur, and this section outlines some of the more important ones, showing how to best use frags_t to handle them.

20.9 API

The complete API documentation for all frags_t-related functions is available in the API Reference Manual, in the Device API Functions section.

19 Modeling I2C Devices 21 Modeling Network Communication