1 /** 2 Implements a mixin template that allows throwing Errors from `@nogc` code. 3 */ 4 5 module alid.errornogc; 6 7 /** 8 Mixes in the definition of a subclass of `Error` as well as a convenience 9 function (e.g. `myError()`) to throw the single thread-local instance of 10 that class. 11 12 The creation of the single instance is `@nogc` because the object that is 13 thrown is emplaced on a thread-local memory buffer. And throwing is `@nogc` 14 because the thrown object is the single thread-local instance. Each 15 `NogcError` object can carry arbitrary number of data of arbitrary types, 16 which are emplaced inside the single error object. The size of data storage 17 is specified with the `maxDataSize` template parameter. 18 19 The following examples use the `NogcError!"foo"` type and its associated 20 `fooError()` function, which can be defined similarly to the following code: 21 --- 22 // Define NogcError_!"foo", which will be thrown by calling fooError(): 23 mixin NogcError!"foo"; 24 --- 25 26 Params: 27 28 tag = a differentiating type _tag to allow multiple NogcError types with 29 their associated single instances 30 31 maxDataSize = the size of the buffer to hold additional data accompanying 32 the thrown error object 33 34 Bugs: 35 36 The error data that is emplaced inside the error object are never 37 destroyed. This decision is supported by the realization that the 38 program is about to end due to the thrown NogcError. 39 */ 40 mixin template NogcError(string tag, size_t maxDataSize = 1024) 41 { 42 private class NogcError_ : Error 43 { 44 string msg; // Main error message 45 ubyte[maxDataSize] data_; // Additional information associated with the error 46 size_t dataOffset; // Determines where aligned data starts 47 size_t dataSize; // Determines the size of data 48 49 // The lambda that knows how to print the type-erased data 50 void function(void delegate(in char[]), const(ubyte)*) dataToStr; 51 52 enum name = `NogcError!"` ~ tag ~ '"'; 53 54 this() @nogc nothrow pure @safe scope 55 { 56 super(name); 57 } 58 59 // Where actual data is at after considering alignment offset 60 inout(ubyte)* dataStart_() inout @nogc nothrow pure @trusted scope 61 { 62 return data_.ptr + dataOffset; 63 } 64 65 // Adapted from object.Throwable.toString 66 override 67 void toString(scope void delegate(in char[]) sink) const nothrow scope 68 { 69 try 70 { 71 import std.conv : to; 72 73 sink(file); sink(":"); sink(line.to!string); sink(": "); 74 sink(name); sink(": "); sink(msg); 75 76 if (dataSize) 77 { 78 sink("\n Data: "); 79 if (dataToStr) 80 { 81 dataToStr(sink, dataStart_); 82 } 83 } 84 85 if (info) 86 { 87 sink("\n----------------"); 88 foreach (t; info) 89 { 90 sink("\n"); sink(t); 91 } 92 } 93 } 94 catch (Throwable) 95 { 96 // ignore more errors 97 } 98 } 99 } 100 101 /* 102 Allow access to the per-thread NogcError!tag instance. 103 104 The template constraint is to prevent conflicting mixed-in definitions of 105 unrelated NogcError instantiations. 106 */ 107 private static ref theError(string t)() @trusted 108 if (t == tag) 109 { 110 static ubyte[__traits(classInstanceSize, NogcError_)] mem_; 111 static NogcError_ obj_; 112 113 if (!obj_) 114 { 115 import core.lifetime : emplace; 116 obj_ = emplace!NogcError_(mem_[]); 117 } 118 119 return obj_; 120 } 121 122 private static string throwNogcError(Data...)( 123 string msg, auto ref Data data, string file, in int line) @trusted 124 { 125 import core.lifetime : emplace; 126 import std.algorithm : max; 127 import std.format : format; 128 import std.typecons : Tuple; 129 130 alias TD = Tuple!Data; 131 enum size = (Data.length ? TD.sizeof : 0); 132 enum alignment = max(TD.alignof, (void*).alignof); 133 134 // Although this should never happen, being safe before the 135 // subtraction below 136 static assert (alignment > 0); 137 138 // We will consider alignment against the worst case run-time 139 // situation by assuming that the modulus operation would produce 1 140 // at run time (very unlikely). 141 enum maxDataOffset = alignment - 1; 142 143 static assert((theError!tag.data_.length >= maxDataOffset) && 144 (size <= (theError!tag.data_.length - maxDataOffset)), 145 format!("Also considering the %s-byte alignment of %s, it is" ~ 146 " not possible to fit %s bytes into a %s-byte buffer.")( 147 alignment, Data.stringof, size, maxDataSize)); 148 149 theError!tag.msg = msg; 150 theError!tag.file = file; 151 theError!tag.line = line; 152 153 // Ensure correct alignment 154 const extra = cast(ulong)theError!tag.data_.ptr % alignment; 155 theError!tag.dataOffset = alignment - extra; 156 theError!tag.dataSize = size; 157 158 emplace(cast(TD*)(theError!tag.dataStart_), TD(data)); 159 160 // Save for later printing 161 theError!tag.dataToStr = (sink, ptr) 162 { 163 import std.conv : to; 164 165 auto d = cast(TD*)(ptr); 166 static foreach (i; 0 .. TD.length) 167 { 168 static if (i != 0) 169 { 170 sink(", "); 171 } 172 sink((*d)[i].to!string); 173 } 174 }; 175 176 // We can finally throw the single error object 177 throw theError!tag; 178 } 179 180 /* 181 Inject the function for <tag>Error() calls like myError(), yourError(), 182 etc. Although the return type of the function is 'string' to satisfy 183 e.g. contracts, the function does not return because throwNogcError() 184 that it calls throws. 185 */ 186 mixin (`string ` ~ tag ~ `Error(Data...)` ~ 187 `(in string msg, in Data data,` ~ 188 ` in string file = __FILE__, in int line = __LINE__)` ~ 189 `{ return throwNogcError(msg, data, file, line); }`); 190 191 // This version is a workaround for some cases where 'file' and 'line' would 192 // become a part of 'data'. 193 mixin (`string ` ~ tag ~ `ErrorFileLine(Data...)` ~ 194 `(in string file, in int line, in string msg, in Data data)` ~ 195 `{ return throwNogcError(msg, data, file, line); }`); 196 } 197 198 /// 199 unittest 200 { 201 /* 202 Throwing from a pre-condition. 203 204 In this case, the error is thrown while generating the string that the 205 failed pre-condition is expecting. Such a string will never arrive at 206 the pre-condition code. 207 */ 208 void test_1(int i) @nogc nothrow @safe 209 in (i > 0, fooError("The value must be positive", i, 42)) 210 { 211 // ... 212 } 213 /* 214 The .msg property of the error contains both the error string and the 215 data that is included in the error. 216 */ 217 assertErrorStringContains(() => test_1(-1), [ "The value must be positive", 218 "-1, 42" ]); 219 220 // Throwing from the body of a function 221 void test_2() @nogc nothrow @safe 222 { 223 string otherData = "hello world"; 224 fooError("Something went wrong", otherData); 225 } 226 assertErrorStringContains(() => test_2(), [ "Something went wrong", 227 "hello world" ]); 228 229 // Throwing without any data 230 void test_3() @nogc nothrow @safe 231 { 232 fooError("Something is bad"); 233 } 234 assertErrorStringContains(() => test_3(), [ "Something is bad" ]); 235 } 236 237 version (unittest) 238 { 239 // Define NogcError!"foo", which will be thrown by calling fooError(): 240 private mixin NogcError!"foo"; 241 242 // Assert that the expression throws an Error object and that its string 243 // representation contains all expected strings. 244 private void assertErrorStringContains( 245 void delegate() @nogc nothrow @safe expr, string[] expected) 246 { 247 bool thrown = false; 248 249 try 250 { 251 expr(); 252 } 253 catch (Error err) 254 { 255 thrown = true; 256 257 import std.algorithm : any, canFind, splitter; 258 import std.conv : to; 259 import std.format : format; 260 261 auto lines = err.to!string.splitter('\n'); 262 foreach (exp; expected) 263 { 264 assert(lines.any!(line => line.canFind(exp)), 265 format!"Failed to find \"%s\" in the output: %-(\n |%s%)"( 266 exp, lines)); 267 } 268 } 269 270 assert(thrown, "The expression did not throw an Error."); 271 } 272 } 273 274 unittest 275 { 276 // Testing assertErrorStringContains itself 277 278 import std.exception : assertNotThrown, assertThrown; 279 import std.format : format; 280 281 // This test requires that bounds checking is active 282 int[] arr; 283 const i = arr.length; 284 auto dg = { ++arr[i]; }; // Intentionally buggy 285 286 // These should fail because "Does not exist" is not a part of out-of-bound 287 // Error output: 288 assertThrown!Error(assertErrorStringContains(dg, ["Does not exist", 289 "out of bounds"])); 290 291 assertThrown!Error(assertErrorStringContains(dg, ["out of bounds", 292 "Does not exist"])); 293 294 // This should pass because all provided texts are parts of out-of-bound 295 // Error output: 296 auto expected = ["out of bounds", 297 format!"[%s]"(i), 298 "ArrayIndexError", 299 format!"array of length %s"(arr.length) ]; 300 assertNotThrown!Error(assertErrorStringContains(dg, expected)); 301 } 302 303 @nogc nothrow @safe 304 unittest 305 { 306 // Test that large data is caught at compile time 307 308 import std.format : format; 309 import std.meta : AliasSeq; 310 311 enum size = theError!"foo".data_.length; 312 313 // Pairs of static arrays of various sizes and whether data should fit 314 enum minAlignment = (void*).alignof; 315 alias cases = AliasSeq!(ubyte[size / 2 - minAlignment], true, 316 ubyte[size - minAlignment], true, 317 ubyte[size + 1], false, 318 ); 319 320 static foreach (i; 0 .. cases.length) 321 { 322 static if (i % 2 == 0) 323 { 324 static assert( 325 __traits(compiles, fooError("message", cases[i].init)) == cases[i + 1], 326 format!"Failed for i: %s, type: %s"(i, cases[i].stringof)); 327 } 328 } 329 }