1 module libd.async.task; 2 3 import libd.async.coroutine; 4 import libd.datastructures : TypedPtr, makeTyped; 5 import libd.memory : move; 6 import libd.util : BcError, raise, displayError; 7 8 enum TaskState 9 { 10 uninit, 11 running, 12 yielded, 13 errored, 14 done 15 } 16 17 private struct TaskContext 18 { 19 TypedPtr userContext; 20 TypedPtr taskYieldValue; 21 CoroutineFunc entryPoint; 22 BcError error; 23 bool yieldedWithValue; 24 } 25 26 struct Task 27 { 28 private Coroutine* _coroutine; 29 private TaskState _state; 30 private TaskContext _context; 31 private CoroutineStack _stack; 32 33 @disable this(this){} 34 35 @nogc nothrow: 36 37 this(CoroutineFunc func) 38 { 39 this(func, null); 40 } 41 42 this(T)(CoroutineFunc func, auto ref T context) 43 { 44 static if(!is(T == typeof(null))) 45 this._context.userContext = context.makeTyped; 46 47 // TODO: Stack customisation. New PageAllocator means memory is managed a lot better now. 48 this._stack = coroutineCreateStandaloneStack(); 49 this._context.entryPoint = func; 50 this._coroutine = coroutineCreate(&coroutine, this._stack, &this._context); 51 } 52 53 this(ref return scope Task rhs) 54 { 55 this._coroutine = rhs._coroutine; 56 this._state = rhs._state; 57 move(rhs._context, this._context); 58 this._stack = rhs._stack; 59 if(this._coroutine !is null) 60 this._coroutine.context = &this._context; 61 } 62 63 private static void coroutine() 64 { 65 auto ctx = cast(TaskContext*)coroutineGetContext(); 66 assert(ctx !is null, "This was not called during a task. Tasks are a more focused layer placed on top of coroutines."); 67 assert(ctx.entryPoint !is null, "No/null entrypoint was given."); 68 version(D_BetterC) 69 ctx.entryPoint(); 70 else // So unittests don't completely crash on failure (most of the time). 71 { 72 try ctx.entryPoint(); 73 catch(Error e) 74 { 75 taskYieldRaise(e.msg); 76 } 77 } 78 coroutineExit(); 79 } 80 81 ~this() 82 { 83 if(this.isValid) 84 this.dispose(); 85 } 86 87 void resume() 88 { 89 assert(this.isValid, "This task is in an invalid state."); 90 assert(!this.hasError, "This task has errored. Used `.error` to see what went wrong."); 91 this._state = TaskState.running; 92 this._context.yieldedWithValue = false; 93 final switch(this._coroutine.state) with(CoroutineState) 94 { 95 case start: coroutineStart(this._coroutine); break; 96 case running: assert(false, "This task is already running."); 97 case suspended: coroutineResume(this._coroutine); break; 98 case end: assert(false, "This task has finished."); 99 } 100 101 if(this._context.error != BcError.init) 102 { 103 // displayError(this._context.error); // Not ideal, but it's fine until there's a logging system up 104 this._state = TaskState.errored; 105 } 106 else if(this._coroutine.state == CoroutineState.suspended) 107 this._state = TaskState.yielded; 108 else if(this._coroutine.state == CoroutineState.end) 109 this._state = TaskState.done; 110 } 111 112 void dispose() 113 { 114 assert(this.isValid, "This task is in an invalid state."); 115 this._state = TaskState.uninit; 116 coroutineDestroy(this._coroutine); 117 coroutineDestroyStack(this._stack); 118 this._coroutine = null; 119 } 120 121 ref T valueAs(alias T)() 122 { 123 assert(this.hasValue); 124 return *this._context.taskYieldValue.ptrUnsafeAs!T; 125 } 126 127 @property @safe 128 TaskState state() const 129 { 130 return this._state; 131 } 132 133 @property @safe 134 bool isValid() const pure 135 { 136 return this._coroutine !is null; 137 } 138 139 @property 140 BcError error() 141 { 142 assert(this.hasError, "This task hasn't errored, there's no reason for this to be called."); 143 return this._context.error; 144 } 145 146 @property @safe 147 bool hasError() const pure 148 { 149 assert(this.isValid); 150 return this._state == TaskState.errored; 151 } 152 153 @property @safe 154 bool hasYielded() const pure 155 { 156 assert(this.isValid); 157 return this._state == TaskState.yielded; 158 } 159 160 @property @safe 161 bool hasEnded() const pure 162 { 163 assert(this.isValid); 164 return this._state == TaskState.done || this.hasError; 165 } 166 167 @property @safe 168 bool hasValue() const pure 169 { 170 assert(this.isValid); 171 return this._state == TaskState.yielded && this._context.yieldedWithValue; 172 } 173 } 174 175 void taskRun(T)(ref return Task task, CoroutineFunc func, auto ref T context = null) 176 { 177 import libd.memory : emplaceCtor; 178 emplaceCtor(task, func, context); 179 task.resume(); 180 } 181 182 @nogc nothrow 183 void taskYield() 184 { 185 coroutineYield(); 186 } 187 188 @nogc nothrow 189 void taskYieldRaise(BcError error) 190 { 191 auto ctx = cast(TaskContext*)coroutineGetContext(); 192 assert(ctx !is null, "This was not called during a task. Tasks are a more focused layer placed on top of coroutines."); 193 ctx.error = error; 194 taskYield(); 195 } 196 197 void taskYieldRaise(string File = __FILE_FULL_PATH__, string Function = __PRETTY_FUNCTION__, string Module = __MODULE__, size_t Line = __LINE__)( 198 bcstring message, 199 int errorCode = 0 200 ) 201 { 202 taskYieldRaise(raise!(File, Function, Module, Line)(message, errorCode)); 203 } 204 205 void taskYieldValue(T)(auto ref T value) 206 { 207 auto ctx = cast(TaskContext*)coroutineGetContext(); 208 assert(ctx !is null, "This was not called during a task. Tasks are a more focused layer placed on top of coroutines."); 209 ctx.taskYieldValue.setByForce(value); 210 ctx.yieldedWithValue = true; 211 taskYield(); 212 } 213 214 void taskAccessContext(alias T, alias Func)() 215 { 216 auto ctx = cast(TaskContext*)coroutineGetContext(); 217 assert(ctx !is null, "This was not called during a task. Tasks are a more focused layer placed on top of coroutines."); 218 ctx.userContext.access!T((scope ref T value) { Func(value); }); 219 } 220 221 @("task - basic") 222 unittest 223 { 224 Task task; 225 taskRun(task, (){ 226 taskYield(); 227 }); 228 229 assert(task.isValid); 230 assert(task.hasYielded); 231 task.resume(); 232 assert(task.hasEnded); 233 } 234 235 @("task - context") 236 unittest 237 { 238 Task task; 239 int num; 240 taskRun(task, (){ 241 taskAccessContext!(int*, (scope ref ptr){ 242 (*ptr)++; 243 }); 244 taskYield(); 245 taskAccessContext!(int*, (scope ref ptr){ 246 (*ptr)++; 247 }); 248 }, &num); 249 250 assert(num == 1); 251 task.resume(); 252 assert(num == 2); 253 } 254 255 @("task - error") 256 unittest 257 { 258 Task task; 259 taskRun(task, (){ taskYieldRaise("error"); }); 260 assert(task.hasEnded && task.hasError); 261 assert(task.error.message == "error"); 262 } 263 264 @("task - value") 265 unittest 266 { 267 Task task; 268 taskRun(task, (){ 269 taskYieldValue(1); 270 taskYieldValue("string"); 271 }); 272 assert(task.hasYielded && task.hasValue); 273 assert(task.valueAs!int == 1); 274 task.resume(); 275 assert(task.hasYielded && task.hasValue); 276 assert(task.valueAs!string == "string"); 277 task.resume(); 278 assert(task.hasEnded); 279 } 280 281 @("task - move support") 282 unittest 283 { 284 import libd.memory : move; 285 286 Task task; 287 Task moved; 288 int num = 200; 289 taskRun(task, (){ 290 taskYield(); 291 taskAccessContext!(int*, (scope ref ptr){ 292 assert(*ptr == 200); 293 *ptr *= 2; 294 }); 295 }, &num); 296 297 move(task, moved); 298 moved.resume(); 299 assert(moved.hasEnded); 300 assert(num == 400); 301 }