Dynamically sized struct

Code

It is quite common in C APIs to have structs that contain dynamically sized buffers. An initial call is made in which a size parameter is populated, after which a larger chunk of memory is allocated, the pertinent struct parameters are copied across, and a second call is made in which the data is retrieved.

This article presents a generic way to eliminate many of the risks inherent in writing code calling such APIs.

Consider the USB_NODE_CONNECTION_NAME struct, which is used in Windows to retrieve the link name of a connected USB hub.

typedef struct _USB_NODE_CONNECTION_NAME {
  ULONG ConnectionIndex;
  ULONG ActualLength;
  WCHAR NodeName[1];
} USB_NODE_CONNECTION_NAME, *PUSB_NODE_CONNECTION_NAME;

Using only minimal error checking for brevity (don’t try this at home), typical usage would look like this:

bool GetUsbConnectionName(HANDLE hDevice, 
                          ULONG index, 
                          std::wstring& name)
{
    ULONG nBytes;
    
    // One struct of default size to query required size
    USB_NODE_CONNECTION_NAME connectionName;
    
    // And one dynamically created struct
    PUSB_NODE_CONNECTION_NAME connectionNameP;

    // 1. Initialise struct
    connectionName.ConnectionIndex = index;

    // 2. Query actual length
    BOOL success = DeviceIoControl(hDevice,
        IOCTL_USB_GET_NODE_CONNECTION_NAME,
        &connectionName,    // input data (e.g. index)
        sizeof(connectionName),
        &connectionName,    // output data (e.g. length)
        sizeof(connectionName),
        &nBytes,
        NULL);
        
    if (!success)
        return false;

    // 3. Allocate required memory
    size_t required = sizeof(connectionName) + 
                      connectionName.ActualLength -
                      sizeof(WCHAR);
    connectionNameP = (PUSB_NODE_CONNECTION_NAME)malloc(required);
    
    // 4. Initialise struct
    connectionNameP->ConnectionIndex = index;
    
    // 5. Query name
    success = DeviceIoControl(hDevice,
        IOCTL_USB_GET_NODE_CONNECTION_NAME,
        connectionNameP,    // input data (e.g. index)
        required,
        connectionNameP,    // output data (e.g. name)
        required,
        &nBytes,
        NULL);
        
    if (!success)
        return false;

    // 6. Copy data (from the second struct, not the first)
    name = std::wstring(connectionNameP->NodeName, 
                        connectionNameP->NodeName + 
                        connectionName.ActualLength / sizeof(WCHAR));
                    
    // 7. Release memory
    free(connectionNameP);

    return true;
}

There are three problems with this approach: the struct we’re using is initialised twice, we must remember to free the memory, and we have two structures to keep track of.

In C++, we can use the power of templates and managed memory to improve on the code above. We can use a std::vector<char> to take the place of a buffer created dynamically on the heap, and take advantage of the fact that if we make it larger, the existing data is unchanged.

template <typename T>
class dynamic_struct
{
    // Actual memory in which the struct is held
    std::vector<char> buffer;
public:
    // Contained type
    typedef T Type;
    
    // Default constructor ensures minimum buffer size
    dynamic_struct()
    : buffer(sizeof(T))
    {}
    
    // Parameterised constructor for when the size is known
    dynamic_struct(std::size_t size)
    {
        resize(size);
    }

    // Change size of buffer allocated for struct
    void resize(std::size_t size)
    {
        if (size < sizeof(T))
            throw std::invalid_argument("Size too small for struct");
        buffer.resize(size, 0);
    }

    // Get current buffer size (never less than struct_size)
    std::size_t size() const
    {
        return buffer.size();
    }

    // Get struct template type size
    static std::size_t struct_size()
    {
        return sizeof(T);
    }

    // Access struct
    const T& get() const
    {
        return *reinterpret_cast<const T*>(&buffer.front());
    }

    // Access struct
    T& get() 
    {
        return *reinterpret_cast<T*>(&buffer.front());
    }
};

Using this handy class, the function to get the name can be simplified:

bool GetUsbConnectionName(HANDLE hDevice, 
                          ULONG index, 
                          std::wstring& name)
{
    ULONG nBytes;
    dynamic_struct<USB_NODE_CONNECTION_NAME> connectionName;

    // 1. Initialise struct
    connectionName.get().ConnectionIndex = index;

    // 2. Query actual length
    BOOL success = DeviceIoControl(hDevice,
        IOCTL_USB_GET_NODE_CONNECTION_NAME,
        &connectionName.get(), // input data (e.g. index)
        connectionName.size(),
        &connectionName.get(), // output data (e.g. length)
        connectionName.size(),
        &nBytes,
        NULL);
        
    if (!success)
        return false;

    // 3. Allocate required memory
    size_t required = sizeof(connectionName) + 
                      connectionName.get().ActualLength;
    connectionName.resize(required);
    
    // 4. Query name
    success = DeviceIoControl(hDevice,
        IOCTL_USB_GET_NODE_CONNECTION_NAME,
        &connectionName.get(), // input data (e.g. index)
        connectionName.size(),
        &connectionName.get(), // output data (e.g. name)
        connectionName.size(),
        &nBytes,
        NULL);
        
    if (!success)
        return false;

    // 5. Copy data
    name = std::wstring(connectionName.get().NodeName, 
                        connectionName.get().NodeName + 
                        connectionName.get().ActualLength / sizeof(WCHAR));

    return true;
}

In this case, there is no risk of forgetting to initialise the second struct, no risk of getting confused about which struct to copy data from, and no risk of memory leaks, even in the presence of exceptions.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s