|
Checked D3D Interfaces
Submitted by |
The Direct3D8 API is based on COM or Component Object Model. Most COM
systems, like the D3D8 API use the old unstructured C-style idiom of
returning an error code from (practically) every function. While this is
obviously better than not reporting the errors, it places a burden on
programmers using the API, because they need to remember to manually check
for errors each time they make a call to the API. If you are like me, you
will forget to check the return value someday, and if you are not lucky,
you may lose lots of debugging time.
C++ exceptions have the advantage over error return values that the client
can use structured error handling techniques. In practice, this
means that the programmer only has to write try {} catch blocks only in
those places that are of interest to the programmer. Instead of writing
hundreds or even thousands of conditional error handling statements
sprinkled everywhere in the application, the programmer can place the try
{} catch blocks in well selected places and effectively separate error
handling code from ordinary program logic.
So, what I would like is to make it so that if an error occures, the D3D
interface would throw an exception, and I could then catch it at the point
in my application where I have the best chances of reporting or working
around the error. Obviously it is not possible to modify the D3D run-time
libraries, but it is possible to use the Decorator pattern [see
Design Patterns: Elements of Reusable Object-Oriented Software] to attach
the responsibility for checking and throwing exceptions in the case of
errors to the D3D interfaces.
In practise this means that we write a Checked forwarding implementation of
each D3D8 interface. The class definition for implementation for a
Checked_IDirect3DSwapChain8 would look something like this:
struct Checked_IDirect3DSwapChain8 : IDirect3DSwapChain8
{
IDirect3DSwapChain8* m_unchecked;
LONG m_ref_cnt;
Checked_IDirect3DSwapChain8(IDirect3DSwapChain8* unchecked);
STDMETHODIMP QueryInterface(REFIID riid, void** ppvObj);
STDMETHODIMP_(ULONG) AddRef();
STDMETHODIMP_(ULONG) Release();
STDMETHODIMP Present(CONST RECT* pSourceRect, CONST RECT* pDestRect,
HWND hDestWindowOverride, CONST RGNDATA* pDirtyRegion);
STDMETHODIMP GetBackBuffer(UINT BackBuffer, D3DBACKBUFFER_TYPE Type,
IDirect3DSurface8** ppBackBuffer);
}; |
And the implementation of a method would look like this:
// ...
STDMETHODIMP Checked_IDirect3DSwapChain8::Present(CONST RECT* pSourceRect,
CONST RECT* pDestRect, HWND hDestWindowOverride, CONST RGNDATA*
pDirtyRegion)
{
HRESULT result = m_unchecked-Present(pSourceRect, pDestRect,
hDestWindowOverride, pDirtyRegion);
Handle_D3D_Call(result);
return result;
}
// ... |
The function Handle_D3D_Call() simply checks if the function failed, and if
so, throws an exception:
static void
Handle_D3D_Call(
HRESULT hr)
{
if (FAILED(hr))
throw D3D_Exception(
As_D3D_Error_Str(hr).c_str(),
hr);
} |
We need to handle a few functions differently. Specifically each function
that takes an interface pointer, that might be a checked object, needs to
detach the checked object from the interface, so that D3D never needs to
know about the decorator. Furthermore, each function that creates new
objects, needs to attach the corresponding checked object to the interface.
Working this way, it is possible to implement a complete decorator layer on
top of the entire D3D8 interface closure and finally implement a function
Checked_Direct3DCreate8() that gives a pointer to the root Direct3D pointer.
IDirect3D8*
Checked_Direct3DCreate8(
UINT SDKVersion)
{
IDirect3D8* result = Direct3DCreate8(SDKVersion);
if (result) result = new Checked_IDirect3D8(result);
return result;
} |
Now after the application first creates the Direct3D object in one place of
the program, all Direct3D calls made by the application will then be
checked and any error will cause an exception to be thrown.
Potential problems
A solution isn't complete before we have considered the disadvantages of
the solution. There are a number of known problems with this approach:
Slower run-time performance caused by extra indirection and run-time
checks
Slower run-time performance is a necessary evil if you want checking. If
you have structured your D3D8 code well, the performance hit is small,
because you are sending large patches of primitives per call. At any rate,
the checking overhead will not be much greater than doing the same checking
manually.
In some cases it is possible to remove the checking completely from the
release build. This can be possible, for instance, while working on the
X-Box, because you know exactly how much memory will be available and which
features are supported by the hardware and don't have to try out various
alternatives in run-time. Errors that might occure in runtime, are then
either programming errors or resource exhaustion errors. Both of those
should be planned for and eliminated before shipping the product.
COM specification related problems
D3D8 will never internally know what is going on. So problems should be
limited to situations when someone will try to use QueryInterface() in
interesting ways or try to use the objects directly as if they were the
internal D3D8 implementation themselves. D3D8 applications have very little
use for QueryInterface(), so the application code should not pose problems.
It leaves the possibility that D3DX8 might use some a priori knowledge of
the D3D8 object. This could be the case on the X-Box, for instance, because
the D3D8 implementation is obviously fixed.
External libraries that do not have exception handling turned on
Another problem is that you might be using a library that doesn't use
exception handling, but you are passing the checked interfaces to such
libraries. In such a case, throwing the exception will obviously fail.
In summary, if you can not use this technique, you shouldn't. Eventhough
you might be unable to use this technique for release builds, you might
benefit from using the technique during development, or debugging, when it
is imperative to be notified of errors as quickly as possible.
About the source code
This article comes with the source code for all checked D3D8 interfaces.
You can customize it to your specific needs. For instance, you might prefer
an assertion or break-point instead of throwing an exception when an error
value is returned by D3D. (You'll lose the ability to respond to run-time
errors though.) In that case you can change the code.
Download the source code package here: Checked_D3D_Interfaces.zip (10k)
You can try the checked interfaces simply by including them in your project
and replacing the call to Direct3DCreate8 with Checked_Direct3DCreate8. You
also need to change the device initialization code to respond to exceptions
instead of error codes:
for (int dev_idx=0; dev_idx < sizeof(s_dev_types)/sizeof(*s_dev_types);
++dev_idx)
{
try {
IDirect3DDevice8* d3d_device=0;
if (SUCCEEDED(m_d3d-CreateDevice(
/* ... */))
{
return;
}
}
catch (const D3D_Exception&)
{
// Ignore and retry with next device...
}
} |
How the code was written
"I would rather write programs to help me write programs than write
programs." -- Dick Sites
The code for the checked interfaces is long and highly repeating. It is
almost possible to make the checked decorators using a regular expression
based search & replace tool, but instead I decided to write a simple
program that reads a header file containing COM interface declarations and
generates the checked interface wrappers.
The architecture of the generator is simple. The most important
architectural feature is that the analysis code is separated from the
synthesis code by using a very simple representation of a COM interface
closure:
struct Argument
{
string m_name;
string m_type_name;
};
struct Method
{
string m_name;
string m_return_type_name;
vector<Argument m_arguments;
};
struct Interface
{
string m_name;
string m_base_name;
vector<Method m_methods;
};
struct COM_Closure
{
vector<Interface m_interfaces;
}; |
The main program of the generator then first parses the header using a very
simple hand-written recursive descent parser that only reads the COM
declarations and creates the COM_Closure representation. Then the
COM_Closure is passed to the interface generator, which outputs the header
and source files for the checked interfaces:
{
COM_Closure com_closure;
Build_COM_Closure(
input_h_file,
&com_closure);
Generate_Checked_Interfaces(
com_closure,
output_h_file,
output_cpp_file);
} |
The length of the code for the generator is about half of the generated
code for D3D8 interfaces. The same generator can be used with small
modifications to generate interfaces for other COM based systems. The code
for the generator is not included here mostly because it makes extensive
use of our libraries.
Thanks must go to my colleagues here at Housemarque for their comments and
support.
Regards,
Vesa Karvonen
Housemarque, Inc.
|
The zip file viewer built into the Developer Toolbox made use
of the zlib library, as well as the zlibdll source additions.
|