Error-checking smart pointer
April 27, 2016
As you might know, in the world of Windows programming COM is still actively used, alive and kicking after all these years. It must be usable from plain C so for error handling they use return codes: most functions return HRESULT, an integer that is zero if all went well, negative in case of error and positive if it went "okay but...". Return codes should not be ignored or forgotten, they need to be handled, and in practice it is often the case that all local handling logic reduces to just stopping what we were doing and passing the error code up the stack, to the caller. In languages like Go it looks like this:
err := obj1.Action1(a, b)
if err != nil {
return err
}
err := obj1.Action2(c, d)
if err != nil {
return err
}
err := obj2.Action3(e)
if err != nil {
return err
}
etc. Each meaningful line turns into four, in the best tradition of
In C and C++ dealing with error-returning functions is usually similar, however in these languages we can use macros that can hide error check and return (or even throw an exception in case of C++) inside them. My very old C++ code working with COM looked somewhat like this:
AHRCK(obj1.Action1(a, b));
AHRCK(obj1.Action2(c, d));
AHRCK(obj2.Action3(e));
or, if I needed to add some message to the error,
AHRCK2(obj1.Action1(a, b), "Memory drum fell off and rolled away");
AHRCK2(obj1.Action2(c, d), "Printer is on fire");
AHRCK2(obj2.Action3(e), "This is a user error. Certainly. Not mine.");
These macros threw exception of a dedicated type, and the high-level code near the top of call stack used some additional macros that could catch different errors and exceptions and convert them to a unified form for further handling.
Interestingly, the Rust programming language came to the same solution. It's got a special type for returning "a result or an error", and to perform a sequence of function calls, stopping in case of error and returning it upwards, they write something like this:
try!(obj1.action1(a, b));
try!(obj1.action2(c, d));
try!(obj2.action3(e));
where try! is a macro that expands to if / return . Less verbose than Go but still kind of ugly.
Decently looking are languages with native support for monads (Haskell, Idris) where such code may look like this:
do action1 obj1 a b
action2 obj1 c d
action3 obj2 e
Just as in Rust, here all returned values are checked and in case of an error current computation is aborted and the error is returned up the call stack. However all the boilerplate is hidden inside the monad implementation, and compiler won't let you forget to check the returned value, unchecked call will just have a different type and the code won't compile.
In my recent project (written in D) we needed to actively use COM and I wanted to have something as beautiful as Error Monad, a way to handle return codes that would not clutter the code on one hand and make it hard/impossible to forget to check the returned values. And here's what I got:
auto obj1 = ComPtr!SomeClass(CLSID_SomeClass); //create by GUID
auto obj2 = ComPtr!ISomething(obj1); //use QueryInterface
obj1.action1(a, b);
obj1.action2(c, d);
obj2.action3(e);
So this is a smart pointer, similar to ATL's CComPtr many C++ programmers would recognise. Just as ATL one, my ComPtr contains a pointer to some COM interface, does all the COM reference counting stuff (calls AddRef() / Release() when appropriate) and allows calling methods of the pointed object. However unlike the C++ counterpart it not only calls the methods but also checks what they return and throws exception in case of an error. This is possible thanks to D's opDispatch , it's like Ruby's method_missing only statically checked and compiled.
This smart pointer was initially taken from VisualD sources and then modified a lot. This is how it looks now:
struct ComPtr(Interface)
{
protected Interface ptr; // pointer to a COM interface or object
protected string name;
//... constructors, destructor, copying ...
Interface raw() { return ptr; }
bool opCast(T:bool)() { return ptr !is null; }
I removed implicit casting to the contained type, so if you need the original raw pointer you write obj.raw . For debugging purposes a string name field was added to track evolution and fate of certain named objects. After the usual smart pointer business comes the glorious opDispatch that receives name of the method being called as a compile-time argument together with list of types of the method's parameters, and the actual run-time arguments for the called method:
auto opDispatch(string fn, Ts...)(Ts vs) {
import std.format;
enum vss = unwrapPtrs!Ts;
alias RetType = ReturnType!(mixin("ptr."~fn));
static if (is(RetType == HRESULT)) {
HRESULT hr = mixin("ptr." ~ fn ~ "(" ~ vss ~ ")");
if (hr < 0) {
string msg = format("s::%s returned %X",
name.length > 0 ? name ~ " calling " : "",
Interface.stringof, fn, hr);
throw new COMException(hr, msg);
}
return hr;
} else
return mixin("ptr." ~ fn ~ "(" ~ vss ~ ")");
}
}
Here fn is a compile-time string variable. So, "ptr."~fn is also a string, but when we pass it to mixin it turns into source code inserted at this very place. This lets us query the return type of the called method and if it's HRESULT then not only call the method but also check its returned value. In the error message we can include the name of the interface, name of method and name of the object if it was given when this smart pointer was created.
There is one little difficulty: since I disabled implicit casting of smart pointer ComPtr!T to a raw pointer of the contained type T , we can't just pass such smart pointers to the COM methods, they expect raw COM pointers. So, when such call is made, we need to unwrap the smart pointers before passing the arguments to a COM method. This is what unwrapPtrs function does. It takes a list of types of passed arguments and it knows that actual values are in heterogeneous list named "vs". For each argument type it checks whether it's our smart pointer and uses vs[i].raw instead of vs[i] in that case:
enum isComPtr(T) = is(T == ComPtr!A, A);
string unwrapPtrs(Ts...)() {
import std.string : format;
import std.array : join;
string[] res;
res.length = Ts.length;
foreach(n, T; Ts) {
static if (isComPtr!T) res[n] = format("vs[%d].raw", n);
else res[n] = format("vs[%d]", n);
}
return res.join(", ");
}
It returns a string like "vs[0], vs[1].raw, vs[2].raw, vs[3]" and opDispatch above inserts this string into the method call code.
If the default error handling is not desired at some point, we can just write
auto hr = obj1.raw.action1(a, b); // does not throw
...
And if we need to add some specific message to the error in case it happens, it looks like this:
obj1.action1(a, b).doing("Ensuring printer is on");
obj1.action2(c, d).doing("Checking paper is there");
obj2.action3(e).doing("Producing a spark");
You may wonder: how doing can do anything when obj1.action1 already fired an exception? This became possible with combination of UFCS (universal function call syntax) and lazy evaluation. The first one allows writing f(x,y) as x.f(y) , i.e.
obj1.action1(a, b).doing(z)
turns into
doing( obj1.action1(a, b), z) ,
and the second lets us postpone evaluation of the first argument:
auto doing(lazy HRESULT smth, string desc) {
try {
return smth;
} catch(COMException ex) {
throw new COMException(ex.hr, desc ~ ": " ~ ex.msg);
}
}
It means the call obj1.action1(a, b) happens inside the try-catch block, so if an exception is thrown we catch it, add the error text and throw again.
This is just another example of using D's compile-time instrospection and metaprogramming in our practice. Full source code of the smart pointer module can be found here.
tags: programming
|