OWL provides a mechanism called transfer buffers to help the programmer get data in and out of dialog boxes. Unfortunately, transfer buffers are known to be error prone due to a lack of type safety. Consequently, transfer buffers have been widely criticized, and users have been advised to use alternative solutions [1]. Here we look closer at the problem the transfer buffer mechanism was meant to solve, the flaws in its design and implementation, and the changes made in OWLNext to make transfer buffers safe.
OWLNext provides a solid set of control encapsulation classes to simplify the manipulation and querying of the interface elements of a dialog. While these control classes are powerful they are limited when it comes to getting data into and out of a dialog box. The problem is that the control classes are only proxies for the GUI elements. Hence, they are only useful for data transfer during the lifetime of the dialog box on the screen. In particular, the control classes do not buffer the interface element's data. The following code snippet illustrates the problem:
class TSearchDialog
: public TDialog
{
public:
TSearchDialog(TWindow* parent)
: TDialog(parent, IDD_SEARCH_DIALOG),
SearchStringEdit(this, IDC_SEARCH_STRING),
SearchUpCheckBox(this, IDC_SEARCH_UP)
{}
TEdit SearchStringEdit;
TCheckBox SearchUpCheckBox;
};
struct TSearchArguments
{
tstring SearchString;
uint Flags;
enum TFlags {CaseSensitive = 1, WholeWord = 2, SearchUp = 4};
};
void TTransferDemo::CmSearch(TSearchArguments& a)
{
TSearchDialog dlg(GetMainWindow());
// ...too early to transfer data...
if (dlg.Execute() == IDOK)
{
// ... too late to transfer data...
Search(a);
}
}
Although SearchStringEdit and SearchUpCheckBox are public, they are of no use to us for transferring data to and from the dialog. While the instances of the C++ encapsulation classes exist after construction of the TSearchDialog object, the underlying interface elements do not come into being until Execute is called. The interface elements stay alive until the dialog box is closed. Then they are dead and gone. Since the OWLNext encapsulation classes do not buffer the contents of the interface elements, it is now too late to get data out.
The brute-force solution is to further specialize the dialog class by overriding setup (SetupWindow) and shutdown (CmOk) and handle the data transfer explicitly. The transfer buffer mechanism was invented to save you from this chore.
The basic idea of transfer buffers is that every control class has a particular buffer field type.
Control type | Field type |
---|---|
TCheckBox | WORD |
TCheckList | TCheckListData |
TComboBox | TComboBoxData |
TComboBoxEx | TComboBoxExData |
TDateTimePicker | TDateTimePickerData |
TEdit | owl::tchar [] |
THotKey | owl::uint16 |
TIPAddress | TIPAddressBits |
TListBox | TListBoxData |
TMemComboBox | owl::tchar [] |
TMonthCalendar | TMonthCalendarData |
TRadioButton | WORD |
TScrollBar | TScrollBarData |
TSlider | TScrollBarData |
TStatic | owl::tchar [] |
Given a pointer to such a field the control class instance can transfer data to or from the interface element it encapsulates. The dialog class then simply takes a pointer to a buffer and calls each control to transfer data to or from the buffer. The dialog performs this transfer in one direction at window setup and in the other direction just before the window closes after the OK button is pressed.
// Traditional OWL transfer buffer
// NB! Byte packing is required!
#include <pshpack1.h>
struct TTransferBuffer
{
enum {SearchStringSize = 255};
char SearchString[SearchStringSize]; // TEdit data
WORD SearchUp; // TCheckBox data
};
#include <poppack.h>
class TSearchDialog
: public TDialog
{
public:
TSearchDialog(TWindow* parent, TTransferBuffer& b)
: TDialog(parent, IDD_SEARCH_DIALOG)
{
new TEdit(this, IDC_SEARCH_STRING, TTransferBuffer::SearchStringSize);
new TCheckBox(this, IDC_SEARCH_UP);
SetTransferBuffer(&b);
}
};
You can then use the transfer buffer with the dialog as follows. Notice how data is transferred into the transfer buffer before Execute is called and out of the transfer buffer afterwards. In particular, we have to be very careful when copying string buffers.
void TTransferDemo::CmSearch(TSearchArguments& a)
{
TTransferBuffer b;
const size_t n = TTransferBuffer::SearchStringSize - 1;
b.SearchString[0] = '\0';
strncat(b.SearchString, a.SearchString.c_str(), n); // tstring -> char[]
WARN(n < a.SearchString.size(), "The search string was truncated!");
b.SearchUp = (a.Flags & TSearchArguments::SearchUp) ?
BST_CHECKED : BST_UNCHECKED; // TFlags -> WORD
TSearchDialog dlg(GetMainWindow(), b);
if (dlg.Execute() == IDOK)
{
a.SearchString = b.SearchString; // char[] -> tstring
a.Flags = (b.SearchUp == BST_CHECKED) ?
TSearchArguments::SearchUp : 0; // WORD -> TFlags
Search(a);
}
}
After decades of C, a standard way to safely copy a null-terminated string is still controversial. Some use strncpy. But, strncpy will not null-terminate the result if the source string is too big. The reason is that it was designed to fill a fixed-length buffer [2]. For the same reason strncpy will unnecessarily pad the buffer with zeroes if the source is shorter than the size of the buffer. An alternative is to use strncat after initializing the buffer with an empty string, as we do here. The strncat function will always null-terminate the result, and it does not do any padding. Just note that we pass the maximum number of characters to copy, excluding the null-terminator, and not the size of the buffer.
Proper handling of string buffer truncation is awkward. In most cases truncation means that the application is in an invalid state. Here we just produce a diagnostic warning, but the proper way to handle truncation may be to alert the user, halt the operation, and/or terminate execution, all depending on the circumstance. Truncation is rarely insignificant.
The transfer buffer mechanism imposes the type of the data source on the client. The transfer buffer that serves as the data source is defined in terms of types and layout dictated by the GUI. In a well designed application, this requires a further transfer between the buffer and application data to separate the GUI front-end from the application back-end (logic). We saw that above; the search arguments; defined by the application; had to be transferred into the dialog transfer buffer; defined by the GUI; and back out again after the dialog was committed. This transfer between application data and transfer buffer is error-prone. Text fields (char arrays) need particular attention to avoid buffer overruns.
To avoid the extra transfer, a novice programmer may be tempted to use transfer buffers as part of the application logic, thus coupling the application logic with the GUI, leading to poor application design. For example, a change from a check box to a drop-down list in a dialog box would affect a change in the application data structures. This is bad. Never use a transfer buffer to implement application logic.
As this flaw is inherent in the transfer buffers design, there is no way to fix this problem other than using a better dialog data transfer mechanism. OWLNext 6.32 introduces such a mechanism, Dialog Data Transfer [3], and new code should use that mechanism in preference to transfer buffers. But, OWLNext is mainly used to maintain old OWL applications, so let us look closer at the implementation of transfer buffers and see how we can improve the safety of existing code.
The flaws and pit-falls in the implementation and usage of traditional OWL transfer buffers are grave and numerous. This stems from very low-level usage of void pointers and unchecked assumptions by the implementation about memory layout.
The implementation assumes that a field of the correct type has been reserved in the transfer buffer for the corresponding control. If there is a type mismatch the buffer will be corrupted by the transfer.
The associations between fields and controls are not enforced. The implementation relies on the assumption that the order of the child control list; which depends on creation order; matches the order of the corresponding fields in the buffer. It also assumes that any control that should not participate in data transfer, i.e. has no reserved field in the buffer, has had its data transfer disabled by a call to DisableTransfer.
The assumption that field order matches child order implies that the buffer type must be a standard layout type [4]. The C++ standard has strict rules about standard layout. If a type does not satisify all these rules, the compiler is free to rearrange fields and add hidden fields. While you can get away with breaking these rules with a particular compiler, it is bad practice to rely on non-standard behavior.
The mechanism assumes that the size of one field determines the offset to the next, i.e. that there are no gaps between fields in the buffer. This is not guaranteed by the C++ standard; not even when the buffer has standard layout. In fact, it is unlikely to be the case, since modern compilers use the preferred alignment of data for modern CPUs. This means that you must ensure that transfer buffers are byte-packed by using compiler pragma statements around the buffer definition.
The mechanism trusts the child control to return the correct size of its data. Buffer underflows and overflows are possible, even likely, to happen. In particular, this can easily happen with text fields. If there is a mismatch between the size of the text field in the buffer and the setting of the text limit of the corresponding edit control, the transfer will underflow or overflow the field. This then leads to errors in the offset calculation for the following fields. The result is a corrupted buffer.
There is another complication in the transfer buffer mechanism specific to edit controls. If TEdit is assigned a validator (TValidator derivative), and that validator has the "voTransfer" option activated, then TEdit::Transfer will delegate the transfer job to the validator. This means that calls to TEdit::SetValidator and TValidator::SetOption (voTransfer) may change the expected type of the corresponding field in the transfer buffer.
TRangeValidator is currently the only validator in OWLNext that uses this feature. When activated, TRangeValidator::Transfer will try to transfer a long int to and from the buffer. This assumes that you provide a buffer that has a long int field for that particular edit control. Otherwise, the transfer corrupts the buffer.
OWLNext 6.32 and later versions incorporate a buffer size check that will detect any potential transfer buffer underflow or overflow. If a size mismatch is detected, an exception is thrown. This ensures that the program does not continue in a corrupt state. In most cases, the check requires no change to user code, but it can only detect a limited number of problems. For example, it will not detect errors in the ordering of fields.
You can make your transfer buffers fully type-safe by using the TTransferBufferWindow template, a new mix-in class template that enforces a mapping between controls and transfer buffer fields of the correct type. TTransferBufferWindow has the following features:
You have to make some minimal changes to your transfer buffer dialogs. First, your dialog class must inherit from TTransferBufferWindow instantiated with your buffer type. Second, you have to bind each control that participates in data transfer to a field in your buffer. For example,
// Safe transfer buffer
// Any alignment and packing is supported.
struct TTransferBuffer
{
char SearchString[255]; // TEdit data
WORD SearchUp; // TCheckBox data
};
class TSearchDialog
: public TDialog,
virtual public TTransferBufferWindow<TTransferBuffer>
{
public:
TSearchDialog(TWindow* parent, TTransferBuffer& b)
: TDialog(parent, IDD_SEARCH_DIALOG)
{
Bind<TEdit>(&TTransferBuffer::SearchString, IDC_SEARCH_STRING);
Bind<TCheckBox>(&TTransferBuffer::SearchUp, IDC_SEARCH_UP);
SetTransferBuffer(&b); // type-safe overload
}
};
That is it! Your transfer buffer dialog is now fully type-safe.
Note that we didn't have to specify the packing of the transfer buffer. Since each control is bound directly to its corresponding field in the buffer, the order and alignment of fields no longer matter.
Also note that we no longer need to worry about the size of the TTransferBuffer::SearchString array. The size is automatically inferred by the Bind function. It ensures that the text limit of the control class is set to the size of the field.
If you forget to bind a child control, or you forget to disable transfer for an intentionally unbound control, then TTransferBufferWindow will throw an exception at run-time. Unless handled, it will cause a helpful error message that sounds something like this:
ObjectWindows Exception: Transfer requested by unbound control #1002 (class owl::TCheckBox) in window class TSearchDialog. Use TTransferBufferWindow::Bind to bind the control to a field in the window's transfer buffer, or disable transfer for this control using TWindow::DisableTransfer.
This should be enough information to track down the dialog and control that caused the problem. You will get similar messages if a control tries to bind to more than one field, if more than one control tries to bind to the same field, and if a validator tries to meddle in the transfer procedure.
The usage of the dialog is exactly as before. So although the transfer between the dialog and the buffer is now guaranteed to be safe, any transfer in and out of the buffer has exactly the same issues as before. Therefore, you should consider replacing all character array fields in your transfer buffers by string class fields. When you bind a control to a string field, the bind function automatically figures out the correct transfer implementation to use and configures the control accordingly.
// Safe transfer buffer
// Note that we use a string class instead of an array.
struct TTransferBuffer
{
tstring SearchString; // TEdit data
WORD SearchUp; // TCheckBox data
};
class TSearchDialog
: public TDialog,
virtual public TTransferBufferWindow<TTransferBuffer>
{
public:
// TTransferBuffer is a typedef provided by the base class.
TSearchDialog(TWindow* parent, TTransferBuffer& b)
: TDialog(parent, IDD_SEARCH_DIALOG)
{
Bind<TEdit>(&TTransferBuffer::SearchString, IDC_SEARCH_STRING); // string-aware
Bind<TCheckBox>(&TTransferBuffer::SearchUp, IDC_SEARCH_UP);
SetTransferBuffer(&b); // type-safe overload
}
};
void TTransferDemo::CmSearch(TSearchArguments& a)
{
TSearchDialog::TTransferBuffer b;
b.SearchString = a.SearchString; // tstring -> tstring, nice!
b.SearchUp = (a.Flags & TSearchArguments::SearchUp) ?
BST_CHECKED : BST_UNCHECKED; // TFlags -> WORD
TSearchDialog dlg(GetMainWindow(), b);
if (dlg.Execute() == IDOK)
{
a.SearchString = b.SearchString;
a.Flags = (b.SearchUp == BST_CHECKED) ? TSearchArguments::SearchUp : 0;
Search(a);
}
}
Now we are freed from the chore and danger of manual character array handling. For the same reason, you should consider using string class fields for TStatic and TMemComboBox. These are the only other control classes in OWLNext that use string fields.
In the examples above we have created the controls anonymously. What if you want to bind a control that has already been created in a member variable? Well, you just pass the identifier to Bind instead of the constructor arguments:
TStringEdit SearchStringEdit;
TCheckBox SearchUpCheckBox;
TSearchDialog(TWindow* parent, TTransferBuffer& b)
: TDialog(parent, IDD_SEARCH_DIALOG),
SearchStringEdit(this, IDC_SEARCH_STRING),
SearchUpCheckBox(this, IDC_SEARCH_UP)
{
Bind(&TTransferBuffer::SearchString, SearchStringEdit);
Bind(&TTransferBuffer::Up, SearchUpCheckBox);
SetTransferBuffer(&b); // type-safe overload
}
Note that you do not have to specify a template argument to Bind here; it is inferred from the type of the control argument.
If your dialogs are using pointers for the child controls, which is the common OWL idiom, you can take the address of the return value of the constructor version of Bind, or you can Bind the control after assignment of the pointer. Here is an example of both for the controls above:
TStringEdit* SearchStringEdit;
TCheckBox* SearchUpCheckBox;
TSearchDialog(TWindow* parent, TTransferBuffer& b)
: TDialog(parent, IDD_SEARCH_DIALOG),
SearchStringEdit(&Bind<TStringEdit>(&TTransferBuffer::SearchString, IDC_SEARCH_STRING))
{
SearchUpCheckBox = new TCheckBox(this, IDC_SEARCH_UP);
Bind(&TTransferBuffer::Up, *SearchUpCheckBox);
SetTransferBuffer(&b); // type-safe overload
}
To add support for your custom control classes in your transfer buffers, you must add a specialization of TTransferBufferBinder for each control type. To do that you add the following after your control class, or in some header that you include before your dialog class:
namespace owl {
//
// Binder for TMyControl; restricts the field type to TMyControlData.
//
template <>
class TTransferBufferBinder<TMyControl>
{
public:
template <class TBuffer>
static void Restrict(TMyControlData TBuffer::* field, TMyControl& c)
{
/*... Add checks and enforcements here, if any ...*/
}
template <class TBuffer>
static TMyControl& Create(TMyControlData TBuffer::* field,
TWindow* p, int id, /*... constructor args ...*/)
{
return *new TMyControl(p, id, /*... constructor args ...*/);
}
};
} // OWL namespace
Note that you will not use Restrict or Create directly. They are called by TTransferBufferWindow as needed. The essential effect of the code is that the first argument of Restrict and Create is restricted to the correct field type for your control type. If you need to do extra checks and enforcements, e.g. tell the control how much data to transfer, then you can do so in the function body of Restrict. For example, the specialization for TEdit restricts the text limit here.
If you do not have any extra checks and enforcements for your control type, then you can simplify the code above by inheriting a default implementation instead. The base class TTransferBufferBinderImplementation defines Restrict and Create as above for you.
namespace owl {
//
// Binder for TMyControl; restricts the field type to TMyControlData.
//
template <>
class TTransferBufferBinder<TMyControl>
: public TTransferBufferBinderImplementation<TMyControl, TMyControlData>
{};
} // OWL namespace
That is all you need to do in most cases!
While the TTransferBufferWindow template handles most of the safety issues for you, there are still a couple of issues you need to pay attention to:
The OWL transfer buffer mechanism is inherently brittle and unsafe. OWLNext can now detect most usage mistakes by the buffer size check it performs. In most cases no changes to your legacy code is required to benefit from this check. With a little more effort you can make your transfer buffers fully type-safe by using the new TTransferBufferWindow mix-in. But for new code you should look at better alternatives, such as the new Dialog Data Transfer framework.
There are two examples in OWLNext accompanying this article:
Ready-made projects for C++Builder and Visual Studio are provided for both examples.
Bugs: #445
Discussion: Can I replace pDialog->Execuite with pDialog->Create + pDialog->DoExecute() ?
Discussion: OWL 6 SafeTransferBuffer
Discussion: Using C++ Builder VCL Style
Discussion: Experimenting OWL6.30 to OWL6.42
Discussion: SetTransferBuffer/Safe transfer buffers question
Wiki: Dialog_Data_Transfer
Wiki: Examples
Wiki: Features
Wiki: Frequently_Asked_Questions
Wiki: History
Wiki: Knowledge_Base
Wiki: Upgrading_from_OWL
Wiki: Vidar_Hasfjord