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.