1 /**
2 Implements a mixin template that allows throwing Errors from `@nogc` code.
3 */4 5 modulealid.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 (both the object and the
14 buffer it contains are allocated lazily). And throwing is `@nogc` because
15 the thrown object is the single thread-local instance. Each `NogcError`
16 object can carry arbitrary number of data of arbitrary types, which are
17 emplaced inside the single error object. The size of data storage is
18 specified with the `maxDataSize` template parameter.
19 20 The following examples use the `NogcError!"foo"` type and its associated
21 `fooError()` function, which can be defined similarly to the following code:
22 ---
23 // Define NogcError_!"foo", which will be thrown by calling fooError():
24 mixin NogcError!"foo";
25 ---
26 27 Params:
28 29 tag = a differentiating type _tag to allow multiple NogcError types with
30 their associated single instances
31 32 maxDataSize = the size of the buffer to hold additional data accompanying
33 the thrown error object
34 35 Bugs:
36 37 The error data that is emplaced inside the error object are never
38 destroyed. This decision is supported by the realization that the
39 program is about to end due to the thrown NogcError.
40 */41 mixintemplateNogcError(stringtag, size_tmaxDataSize = 1024)
42 {
43 privateclassNogcError_ : Error44 {
45 stringmsg; // Main error message46 ubyte[maxDataSize] data_; // Additional information associated with the error47 size_tdataOffset; // Determines where aligned data starts48 size_tdataSize; // Determines the size of data49 50 // The lambda that knows how to print the type-erased data51 voidfunction(voiddelegate(inchar[]), const(ubyte)*) dataToStr;
52 53 enumname = `NogcError!"` ~ tag ~ '"';
54 55 this() @nogcnothrowpure @safescope56 {
57 super(name);
58 }
59 60 // Where actual data is at after considering alignment offset61 inout(ubyte)* dataStart_() inout @nogcnothrowpure @trustedscope62 {
63 returndata_.ptr + dataOffset;
64 }
65 66 // Adapted from object.Throwable.toString67 override68 voidtoString(scopevoiddelegate(inchar[]) sink) constnothrowscope69 {
70 try71 {
72 importstd.conv : to;
73 74 sink(file); sink(":"); sink(line.to!string); sink(": ");
75 sink(name); sink(": "); sink(msg);
76 77 if (dataSize)
78 {
79 sink("\n Data: ");
80 if (dataToStr)
81 {
82 dataToStr(sink, dataStart_);
83 }
84 }
85 86 if (info)
87 {
88 sink("\n----------------");
89 foreach (t; info)
90 {
91 sink("\n"); sink(t);
92 }
93 }
94 }
95 catch (Throwable)
96 {
97 // ignore more errors98 }
99 }
100 }
101 102 /*
103 Allow access to the per-thread NogcError!tag instance.
104 105 The template constraint is to prevent conflicting mixed-in definitions of
106 unrelated NogcError instantiations.
107 */108 privatestaticreftheError(stringt)() @nogcnothrow @trusted109 if (t == tag)
110 {
111 staticubyte[__traits(classInstanceSize, NogcError_)] mem_;
112 staticNogcError_obj_;
113 114 if (!obj_)
115 {
116 importcore.lifetime : emplace;
117 obj_ = emplace!NogcError_(mem_[]);
118 }
119 120 returnobj_;
121 }
122 123 privatestaticstringthrowNogcError(Data...)(
124 instringmsg, autorefDatadata, instringfile, inintline)
125 @nogcnothrowpure @safe126 {
127 staticthrower(instringmsg, Datadata, instringfile, inintline)
128 @nogcnothrow @trusted129 {
130 importcore.lifetime : emplace;
131 importstd.algorithm : max;
132 importstd.format : format;
133 importstd.typecons : Tuple;
134 135 aliasTD = Tuple!Data;
136 enumsize = (Data.length ? TD.sizeof : 0);
137 enumalignment = max(TD.alignof, (void*).alignof);
138 139 // Although this should never happen, being safe before the140 // subtraction below141 staticassert (alignment > 0);
142 143 // We will consider alignment against the worst case run-time144 // situation by assuming that the modulus operation would produce 1145 // at run time (very unlikely).146 enummaxDataOffset = alignment - 1;
147 148 staticassert((theError!tag.data_.length >= maxDataOffset) &&
149 (size <= (theError!tag.data_.length - maxDataOffset)),
150 format!("Also considering the %s-byte alignment of %s, it is" ~
151 " not possible to fit %s bytes into a %s-byte buffer.")(
152 alignment, Data.stringof, size, maxDataSize));
153 154 theError!tag.msg = msg;
155 theError!tag.file = file;
156 theError!tag.line = line;
157 158 // Ensure correct alignment159 constextra = cast(ulong)theError!tag.data_.ptr % alignment;
160 theError!tag.dataOffset = alignment - extra;
161 theError!tag.dataSize = size;
162 163 emplace(cast(TD*)(theError!tag.dataStart_), TD(data));
164 165 // Save for later printing166 theError!tag.dataToStr = (sink, ptr)
167 {
168 importstd.conv : to;
169 170 autod = cast(TD*)(ptr);
171 staticforeach (i; 0 .. TD.length)
172 {
173 staticif (i != 0)
174 {
175 sink(", ");
176 }
177 sink((*d)[i].to!string);
178 }
179 };
180 181 // We can finally throw the single error object182 throwtheError!tag;
183 }
184 185 // Adapted from std/regex/internal/ir.d186 staticassumePureFunction(T)(inTt) @nogcnothrowpure @trusted187 {
188 importstd.traits :
189 FunctionAttribute, functionAttributes,
190 functionLinkage, SetFunctionAttributes;
191 192 enumattrs = functionAttributes!T | FunctionAttribute.pure_;
193 returncast(SetFunctionAttributes!(T, functionLinkage!T, attrs))t;
194 }
195 196 assumePureFunction(&thrower)(msg, data, file, line);
197 198 assert(false, "A NogcError should have been thrown.");
199 }
200 201 /*
202 Inject the function for <tag>Error() calls like myError(), yourError(),
203 etc. Although the return type of the function is 'string' to satisfy
204 e.g. contracts, the function does not return because throwNogcError()
205 that it calls throws.
206 */207 mixin (`string ` ~ tag ~ `Error(Data...)` ~
208 `(in string msg, in Data data,` ~
209 ` in string file = __FILE__, in int line = __LINE__)` ~
210 ` @nogc nothrow pure @safe` ~
211 `{ return throwNogcError(msg, data, file, line); }`);
212 213 // This version is a workaround for some cases where 'file' and 'line' would214 // become a part of 'data'.215 mixin (`string ` ~ tag ~ `ErrorFileLine(Data...)` ~
216 `(in string file, in int line, in string msg, in Data data)` ~
217 ` @nogc nothrow pure @safe` ~
218 `{ return throwNogcError(msg, data, file, line); }`);
219 }
220 221 ///222 unittest223 {
224 /*
225 Throwing from a pre-condition.
226 227 In this case, the error is thrown while generating the string that the
228 failed pre-condition is expecting. Such a string will never arrive at
229 the pre-condition code.
230 */231 voidtest_1(inti)
232 in (i > 0, fooError("The value must be positive", i, 42))
233 {
234 // ...235 }
236 /*
237 The .msg property of the error contains both the error string and the
238 data that is included in the error.
239 */240 assertErrorStringContains(() => test_1(-1), [ "The value must be positive",
241 "-1, 42" ]);
242 243 // Throwing from the body of a function244 voidtest_2(inti)
245 {
246 stringotherData = "hello world";
247 fooError("Something went wrong", otherData);
248 }
249 assertErrorStringContains(() => test_2(0), [ "Something went wrong",
250 "hello world" ]);
251 252 // Throwing without any data253 voidtest_3()
254 {
255 fooError("Something is bad");
256 }
257 assertErrorStringContains(() => test_3(), [ "Something is bad" ]);
258 }
259 260 version (unittest)
261 {
262 // Define NogcError!"foo", which will be thrown by calling fooError():263 privatemixinNogcError!"foo";
264 265 // Assert that the expression throws an Error object and that its string266 // representation contains all expected strings.267 privatevoidassertErrorStringContains(voiddelegate() expr, string[] expected)
268 {
269 boolthrown = false;
270 271 try272 {
273 expr();
274 }
275 catch (Errorerr)
276 {
277 thrown = true;
278 279 importstd.algorithm : any, canFind, splitter;
280 importstd.conv : to;
281 importstd.format : format;
282 283 autolines = err.to!string.splitter('\n');
284 foreach (exp; expected)
285 {
286 assert(lines.any!(line => line.canFind(exp)),
287 format!"Failed to find \"%s\" in the output: %-(\n |%s%)"(
288 exp, lines));
289 }
290 }
291 292 assert(thrown, "The expression did not throw an Error.");
293 }
294 }
295 296 unittest297 {
298 // Testing assertErrorStringContains itself299 300 importstd.exception : assertNotThrown, assertThrown;
301 importstd.format : format;
302 303 // This test requires that bounds checking is active304 int[] arr;
305 consti = arr.length;
306 autodg = { ++arr[i]; }; // Intentionally buggy307 308 // These should fail because "Does not exist" is not a part of out-of-bound309 // Error output:310 assertThrown!Error(assertErrorStringContains(dg, ["Does not exist",
311 "out of bounds"]));
312 313 assertThrown!Error(assertErrorStringContains(dg, ["out of bounds",
314 "Does not exist"]));
315 316 // This should pass because all provided texts are parts of out-of-bound317 // Error output:318 autoexpected = ["out of bounds",
319 format!"[%s]"(i),
320 "ArrayIndexError",
321 format!"array of length %s"(arr.length) ];
322 assertNotThrown!Error(assertErrorStringContains(dg, expected));
323 }
324 325 unittest326 {
327 // Test that large data is caught at compile time328 329 importstd.format : format;
330 importstd.meta : AliasSeq;
331 332 enumsize = theError!"foo".data_.length;
333 334 // Pairs of static arrays of various sizes and whether data should fit335 enumminAlignment = (void*).alignof;
336 aliascases = AliasSeq!(ubyte[size / 2 - minAlignment], true,
337 ubyte[size - minAlignment], true,
338 ubyte[size + 1], false,
339 );
340 341 staticforeach (i; 0 .. cases.length)
342 {
343 staticif (i % 2 == 0)
344 {
345 staticassert(
346 __traits(compiles, fooError("message", cases[i].init)) == cases[i + 1],
347 format!"Failed for i: %s, type: %s"(i, cases[i].stringof));
348 }
349 }
350 }