1 /++ 2 A draft of a better way to do exceptions 3 4 History: 5 Originally written in May 2015 as a demo, but I never used it inside arsd. 6 7 Deprecated in March 2023 (dub v11.0), with the successful parts moved to [arsd.core]. It is unlikely to get any future updates. 8 +/ 9 deprecated("This was just a proof of concept demo, the actual concepts are now implemented inside arsd.core") 10 module arsd.exception; 11 /* 12 Exceptions 2.0 13 */ 14 15 interface ThrowableBase { 16 void fly(string file = __FILE__, size_t line = __LINE__); // should be built into the compiler's throw statement 17 18 // override these as needed 19 void printMembers(scope void delegate(in char[]) sink) const; // Tip: use mixin PrintMembers; instead of doing it yourself 20 void getHumanReadableMessage(scope void delegate(in char[]) sink) const; // the exception name should give this generally but it is nice if you have an error code that needs translation or something else that isn't obvious from the name 21 void printName(scope void delegate(in char[]) sink) const; // only need to override this if you aren't happy with RTTI's name field 22 23 // just call this when you are ready 24 void toString(scope void delegate(in char[]) sink) const; 25 } 26 27 mixin template ThrowableBaseImplementation() { 28 // This sets file and line at the throw point instead of in the ctor 29 // thereby separating allocation from error information - call this and 30 // file+line will be set then allowing you to reuse exception objects easier 31 void fly(string file = __FILE__, size_t line = __LINE__) { 32 this.file = file; 33 this.line = line; 34 throw this; 35 } 36 37 // You don't really need this - the class name and members should give all the 38 // necessary info, but it can be nice in cases like a Windows or errno exception 39 // where the code isn't necessarily as at-a-glance easy as the string from GetLastError. 40 /* virtual */ void getHumanReadableMessage(scope void delegate(in char[]) sink) const { 41 sink(msg); // for backward compatibility 42 } 43 44 // This prints the really useful info to the user, the members' values. 45 // You don't have to write this typically, instead use the mixin below. 46 /* virtual */ void printMembers(scope void delegate(in char[]) sink) const { 47 // this is done with the mixin from derived classes 48 } 49 50 /* virtual */ void printName(scope void delegate(in char[]) sink) const { 51 sink(typeid(this).name); // FIXME: would be nice if eponymous templates didn't spew the name twice 52 } 53 54 override void toString(scope void delegate(in char[]) sink) const { 55 char[32] tmpBuff = void; 56 printName(sink); 57 sink("@"); sink(file); 58 sink("("); sink(line.sizeToTempString(tmpBuff[])); sink(")"); 59 sink(": "); getHumanReadableMessage(sink); 60 sink("\n"); 61 printMembers(sink); 62 if (info) { 63 try { 64 sink("----------------"); 65 foreach (t; info) { 66 sink("\n"); sink(t); 67 } 68 } 69 catch (Throwable) { 70 // ignore more errors 71 } 72 } 73 } 74 75 } 76 77 class ExceptionBase : Exception, ThrowableBase { 78 // Hugely simplified ctor - nothing is even needed 79 this() { 80 super(""); 81 } 82 83 mixin ThrowableBaseImplementation; 84 } 85 86 class ErrorBase : Error, ThrowableBase { 87 this() { super(""); } 88 mixin ThrowableBaseImplementation; 89 } 90 91 // Mix this into your derived class to print all its members automatically for easier debugging! 92 mixin template PrintMembers() { 93 override void printMembers(scope void delegate(in char[]) sink) const { 94 foreach(memberName; __traits(derivedMembers, typeof(this))) { 95 static if(is(typeof(__traits(getMember, this, memberName))) && !is(typeof(__traits(getMember, typeof(this), memberName)) == function)) { 96 sink("\t"); 97 sink(memberName); 98 sink(" = "); 99 static if(is(typeof(__traits(getMember, this, memberName)) : const(char)[])) 100 sink(__traits(getMember, this, memberName)); 101 else static if(is(typeof(__traits(getMember, this, memberName)) : long)) { 102 char[32] tmpBuff = void; 103 sink(sizeToTempString(__traits(getMember, this, memberName), tmpBuff)); 104 } // else pragma(msg, typeof(__traits(getMember, this, memberName))); 105 sink("\n"); 106 } 107 } 108 109 super.printMembers(sink); 110 } 111 } 112 113 // The class name SHOULD obviate this but you can also add another message if you like. 114 // You can also just override the getHumanReadableMessage yourself in cases like calling strerror 115 mixin template StaticHumanReadableMessage(string s) { 116 override void getHumanReadableMessage(scope void delegate(in char[]) sink) const { 117 sink(s); 118 } 119 } 120 121 122 123 124 /* 125 Enforce 2.0 126 */ 127 128 interface DynamicException { 129 /* 130 TypeInfo getArgumentType(size_t idx); 131 void* getArgumentData(size_t idx); 132 string getArgumentAsString(size_t idx); 133 */ 134 } 135 136 template enforceBase(ExceptionBaseClass, string failureCondition = "ret is null") { 137 auto enforceBase(alias func, string file = __FILE__, size_t line = __LINE__, T...)(T args) { 138 auto ret = func(args); 139 if(mixin(failureCondition)) { 140 class C : ExceptionBaseClass, DynamicException { 141 T args; 142 this(T args) { 143 this.args = args; 144 } 145 146 override void printMembers(scope void delegate(in char[]) sink) const { 147 import std.traits; 148 import std.conv; 149 foreach(idx, arg; args) { 150 sink("\t"); 151 sink(ParameterIdentifierTuple!func[idx]); 152 sink(" = "); 153 sink(to!string(arg)); 154 sink("\n"); 155 } 156 sink("\treturn value = "); 157 sink(to!string(ret)); 158 sink("\n"); 159 } 160 161 override void printName(scope void delegate(in char[]) sink) const { 162 sink(__traits(identifier, ExceptionBaseClass)); 163 } 164 165 override void getHumanReadableMessage(scope void delegate(in char[]) sink) const { 166 sink(__traits(identifier, func)); 167 sink(" call failed"); 168 } 169 } 170 171 auto exception = new C(args); 172 exception.file = file; 173 exception.line = line; 174 throw exception; 175 } 176 177 return ret; 178 } 179 } 180 181 /// Raises an exception given a set of local variables to print out 182 void raise(ExceptionBaseClass, T...)(string file = __FILE__, size_t line = __LINE__) { 183 class C : ExceptionBaseClass, DynamicException { 184 override void printMembers(scope void delegate(in char[]) sink) const { 185 import std.conv; 186 foreach(idx, arg; T) { 187 sink("\t"); 188 sink(__traits(identifier, T[idx])); 189 sink(" = "); 190 sink(to!string(arg)); 191 sink("\n"); 192 } 193 } 194 195 override void printName(scope void delegate(in char[]) sink) const { 196 sink(__traits(identifier, ExceptionBaseClass)); 197 } 198 } 199 200 auto exception = new C(); 201 exception.file = file; 202 exception.line = line; 203 throw exception; 204 } 205 206 const(char)[] sizeToTempString(long size, char[] buffer) { 207 size_t pos = buffer.length - 1; 208 bool negative = size < 0; 209 if(size < 0) 210 size = -size; 211 while(size) { 212 buffer[pos] = size % 10 + '0'; 213 size /= 10; 214 pos--; 215 } 216 if(negative) { 217 buffer[pos] = '-'; 218 pos--; 219 } 220 return buffer[pos + 1 .. $]; 221 } 222 223 ///////////////////////////// 224 /* USAGE EXAMPLE FOLLOWS */ 225 ///////////////////////////// 226 227 228 // Make sure there's sane base classes for things that take 229 // various types. For example, RangeError might be thrown for 230 // any type of index, but we might just catch any kind of range error. 231 // 232 // The base class gives us an easy catch point for the category. 233 class MyRangeError : ErrorBase { 234 // unnecessary but kinda nice to have static error message 235 mixin StaticHumanReadableMessage!"Index out of bounds"; 236 } 237 238 // Now, we do a new class for each error condition that can happen 239 // inheriting from a convenient catch-all base class for our error type 240 // (which might be ExceptionBase itself btw) 241 class TypedRangeError(T) : MyRangeError { 242 // Error details are stored as DATA MEMBERS 243 // do NOT convert them to a string yourself 244 this(T index) { 245 this.index = index; 246 } 247 248 mixin StaticHumanReadableMessage!(T.stringof ~ " index out of bounds"); 249 250 // The data members can be easily inspected to learn more 251 // about the error, perhaps even to retry it programmatically 252 // and this also avoids the need to do something like call to!string 253 // and string concatenation functions at the construction point. 254 // 255 // Yea, this gives more info AND is allocation-free. What's not to love? 256 // 257 // Templated ones can be a pain just because of the need to specify it to 258 // catch or cast, but it will always at least be available in the printed string. 259 T index; 260 261 // Then, mixin PrintMembers uses D's reflection to do all the messy toString 262 // data sink nonsense for you. Do this in each subclass where you add more 263 // data members (which out to be generally all of them, more info is good. 264 mixin PrintMembers; 265 } 266 267 version(exception_2_example) { 268 269 // We can pass pre-constructed exceptions to functions and get good file/line and stacktrace info! 270 void stackExample(ThrowableBase exception) { 271 // throw it now (custom function cuz I change the behavior a wee bit) 272 exception.fly(); // ideally, I'd change the throw statement to call this function for you to set up line and file 273 } 274 275 void main() { 276 int a = 230; 277 string file = "lol"; 278 static class BadValues : ExceptionBase {} 279 //raise!(BadValues, a, file); 280 281 alias enforce = enforceBase!ExceptionBase; 282 283 import core.stdc.stdio; 284 auto fp = enforce!fopen("nofile.txt".ptr, "rb".ptr); 285 286 287 // construct, passing it error details as data, not strings. 288 auto exception = new TypedRangeError!int(4); // exception construction is separated from file/line setting 289 stackExample(exception); // so you can allocate/construct in one place, then set and throw somewhere else 290 } 291 }