Introduction
It is quite common in C APIs to have struct
s 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 tip presents a generic way to eliminate many of the risks inherent in writing code calling such APIs.
Example of API Use
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;
USB_NODE_CONNECTION_NAME connectionName;
PUSB_NODE_CONNECTION_NAME connectionNameP;
connectionName.ConnectionIndex = index;
BOOL success = DeviceIoControl(hDevice,
IOCTL_USB_GET_NODE_CONNECTION_NAME,
&connectionName, sizeof(connectionName),
&connectionName, sizeof(connectionName),
&nBytes,
NULL);
if (!success)
return false;
size_t required = sizeof(connectionName) +
connectionName.ActualLength -
sizeof(WCHAR);
connectionNameP = (PUSB_NODE_CONNECTION_NAME)malloc(required);
connectionNameP->ConnectionIndex = index;
success = DeviceIoControl(hDevice,
IOCTL_USB_GET_NODE_CONNECTION_NAME,
connectionNameP, required,
connectionNameP, required,
&nBytes,
NULL);
if (!success)
return false;
name = std::wstring(connectionNameP->NodeName,
connectionNameP->NodeName +
connectionName.ActualLength / sizeof(WCHAR));
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.
A Generic Struct
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
{
std::vector<char> buffer;
public:
typedef T Type;
dynamic_struct()
: buffer(sizeof(T))
{}
dynamic_struct(std::size_t size)
{
resize(size);
}
void resize(std::size_t size)
{
if (size < sizeof(T))
throw std::invalid_argument("Size too small for struct");
buffer.resize(size, 0);
}
std::size_t size() const
{
return buffer.size();
}
static std::size_t struct_size()
{
return sizeof(T);
}
const T& get() const
{
return *reinterpret_cast<const T*>(&buffer.front());
}
T& get()
{
return *reinterpret_cast<T*>(&buffer.front());
}
};
Example, Simplified
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;
connectionName.get().ConnectionIndex = index;
BOOL success = DeviceIoControl(hDevice,
IOCTL_USB_GET_NODE_CONNECTION_NAME,
&connectionName.get(), connectionName.size(),
&connectionName.get(), connectionName.size(),
&nBytes,
NULL);
if (!success)
return false;
size_t required = sizeof(connectionName) +
connectionName.ActualLength -
sizeof(WCHAR);
connectionName.resize(required);
success = DeviceIoControl(hDevice,
IOCTL_USB_GET_NODE_CONNECTION_NAME,
&connectionName.get(), connectionName.size(),
&connectionName.get(), connectionName.size(),
&nBytes,
NULL);
if (!success)
return false;
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.
History
- 6th November, 2014: Initial version
- 7th November, 2014: Corrected template name from "
t
" to "T
". Put template type in second example (went missing in copy-paste of code)