github.com/arnodel/golua@v0.0.0-20230215163904-e0b5347eaaa1/runtime/thread.go (about) 1 package runtime 2 3 import ( 4 "errors" 5 "sync" 6 "unsafe" 7 ) 8 9 // ThreadStatus is the type of a thread status 10 type ThreadStatus uint 11 12 // Available statuses for threads. 13 const ( 14 ThreadOK ThreadStatus = 0 // Running thread 15 ThreadSuspended ThreadStatus = 1 // Thread has yielded and is waiting to be resumed 16 ThreadDead ThreadStatus = 3 // Thread has finished and cannot be resumed 17 ) 18 19 // The depth of GoFunction calls in one thread is limited by this number in 20 // order to avoid irrecoverable Go stack overflows. 21 const maxGoFunctionCallDepth = 1000 22 23 // Data passed between Threads via their resume channel (Thread.resumeCh). 24 // 25 // Supported types for exception are ContextTerminationError (which means 26 // execution has run out of resources) and threadClose (which means the thread 27 // should be closed without resuming execution, new in Lua 5.4 via the 28 // coroutine.close() function). Other types will cause a panic. 29 type valuesError struct { 30 args []Value // arguments ot yield or resume 31 err error // execution error 32 exception interface{} // used when the thread should be closed right away 33 } 34 35 // A Thread is a lua thread. 36 // 37 // The mutex guarantees that if status == ThreadRunning, then caller 38 // is not nil. 39 // 40 type Thread struct { 41 *Runtime 42 mux sync.Mutex 43 status ThreadStatus 44 closeErr error // Error that caused the thread to stop 45 currentCont Cont // Currently running continuation 46 resumeCh chan valuesError 47 caller *Thread // Who resumed this thread 48 49 // Depth of GoFunction calls in the thread. This should not exceed 50 // maxGoFunctionCallDepth. The aim is to avoid Go stack overflows that 51 // cannot be recovered from (note that this does not limit recursion for Lua 52 // functions). 53 goFunctionCallDepth int 54 55 DebugHooks 56 57 closeStack // Stack of pending to-be-closed values 58 } 59 60 // NewThread creates a new thread out of a Runtime. Its initial 61 // status is suspended. Call Resume to run it. 62 func NewThread(r *Runtime) *Thread { 63 r.RequireSize(unsafe.Sizeof(Thread{}) + 100) // 100 is my guess at the size of a channel 64 return &Thread{ 65 resumeCh: make(chan valuesError), 66 status: ThreadSuspended, 67 Runtime: r, 68 } 69 } 70 71 // CurrentCont returns the continuation currently running (or suspended) in the 72 // thread. 73 func (t *Thread) CurrentCont() Cont { 74 return t.currentCont 75 } 76 77 // IsMain returns true if the thread is the runtime's main thread. 78 func (t *Thread) IsMain() bool { 79 return t == t.mainThread 80 } 81 82 const maxErrorsInMessageHandler = 10 83 84 var errErrorInMessageHandler = StringValue("error in error handling") 85 86 // RunContinuation runs the continuation c in the thread. It keeps running until 87 // the next continuation is nil or an error occurs, in which case it returns the 88 // error. 89 func (t *Thread) RunContinuation(c Cont) (err error) { 90 var next Cont 91 var errContCount = 0 92 _ = t.triggerCall(t, c) 93 for c != nil { 94 if t != t.gcThread { 95 t.runPendingFinalizers() 96 } 97 t.currentCont = c 98 next, err = c.RunInThread(t) 99 if err != nil { 100 rtErr := ToError(err) 101 if rtErr.Handled() { 102 return rtErr 103 } 104 err = rtErr.AddContext(c, -1) 105 errContCount++ 106 if t.messageHandler != nil { 107 if errContCount > maxErrorsInMessageHandler { 108 return newHandledError(errErrorInMessageHandler) 109 } 110 next = t.messageHandler.Continuation(t, newMessageHandlerCont(c)) 111 } else { 112 next = newMessageHandlerCont(c) 113 } 114 next.Push(t.Runtime, ErrorValue(err)) 115 } 116 c = next 117 } 118 return 119 } 120 121 // This is to be able to close a suspended coroutine without completing it, but 122 // still allow cleaning up the to-be-closed variables. If this is put on the 123 // resume channel of a running thread, yield will cause a panic in the goroutine 124 // and that will be caught in the defer() clause below. 125 type threadClose struct{} 126 127 // 128 // Coroutine management 129 // 130 131 // Start starts the thread in a goroutine, giving it the callable c to run. the 132 // t.Resume() method needs to be called to provide arguments to the callable. 133 func (t *Thread) Start(c Callable) { 134 t.RequireBytes(2 << 10) // A goroutine starts off with 2k stack 135 go func() { 136 var ( 137 args []Value 138 err error 139 ) 140 // If there was a panic due to an exceeded quota, we need to end the 141 // thread and propagate that panic to the calling thread 142 defer func() { 143 r := recover() 144 if r != nil { 145 switch r.(type) { 146 case ContextTerminationError: 147 case threadClose: 148 // This means we want to close the coroutine, so no panic! 149 r = nil 150 default: 151 panic(r) 152 } 153 } 154 t.end(args, err, r) 155 }() 156 args, err = t.getResumeValues() 157 if err == nil { 158 next := NewTerminationWith(t.CurrentCont(), 0, true) 159 err = t.call(c, args, next) 160 args = next.Etc() 161 } 162 }() 163 } 164 165 // Status returns the status of a thread (suspended, running or dead). 166 func (t *Thread) Status() ThreadStatus { 167 return t.status 168 } 169 170 // Resume execution of a suspended thread. Its status switches to 171 // running while its caller's status switches to suspended. 172 func (t *Thread) Resume(caller *Thread, args []Value) ([]Value, error) { 173 t.mux.Lock() 174 if t.status != ThreadSuspended { 175 t.mux.Unlock() 176 switch t.status { 177 case ThreadDead: 178 return nil, errors.New("cannot resume dead thread") 179 default: 180 return nil, errors.New("cannot resume running thread") 181 } 182 } 183 caller.mux.Lock() 184 if caller.status != ThreadOK { 185 panic("Caller of thread to resume is not running") 186 } 187 t.caller = caller 188 t.status = ThreadOK 189 t.mux.Unlock() 190 caller.mux.Unlock() 191 t.sendResumeValues(args, nil, nil) 192 return caller.getResumeValues() 193 } 194 195 // Close a suspended thread. If successful, its status switches to dead. The 196 // boolean returned is true if it was possible to close the thread (i.e. it was 197 // suspended or already dead). The error is non-nil if there was an error in 198 // the cleanup process, or if the thread had already stopped with an error 199 // previously. 200 func (t *Thread) Close(caller *Thread) (bool, error) { 201 t.mux.Lock() 202 if t.status != ThreadSuspended { 203 t.mux.Unlock() 204 switch t.status { 205 case ThreadDead: 206 return true, t.closeErr 207 default: 208 return false, nil 209 } 210 } 211 caller.mux.Lock() 212 if caller.status != ThreadOK { 213 panic("Caller of thread to close is not running") 214 } 215 // The thread needs to go back to running to empty its close stack, before 216 // becoming dead. 217 t.caller = caller 218 t.status = ThreadOK 219 t.mux.Unlock() 220 caller.mux.Unlock() 221 t.sendResumeValues(nil, nil, threadClose{}) 222 _, err := caller.getResumeValues() 223 return true, err 224 } 225 226 // Yield to the caller thread. The yielding thread's status switches to 227 // suspended. The caller's status must be OK. 228 func (t *Thread) Yield(args []Value) ([]Value, error) { 229 t.mux.Lock() 230 if t.status != ThreadOK { 231 panic("Thread to yield is not running") 232 } 233 caller := t.caller 234 if caller == nil { 235 t.mux.Unlock() 236 return nil, errors.New("cannot yield from main thread") 237 } 238 caller.mux.Lock() 239 if caller.status != ThreadOK { 240 panic("Caller of thread to yield is not OK") 241 } 242 t.status = ThreadSuspended 243 t.caller = nil 244 t.mux.Unlock() 245 caller.mux.Unlock() 246 caller.sendResumeValues(args, nil, nil) 247 return t.getResumeValues() 248 } 249 250 // This turns off the thread, cleaning up its close stack. The thread must be 251 // running. 252 func (t *Thread) end(args []Value, err error, exception interface{}) { 253 caller := t.caller 254 t.mux.Lock() 255 caller.mux.Lock() 256 defer t.mux.Unlock() 257 defer caller.mux.Unlock() 258 switch { 259 case t.status != ThreadOK: 260 panic("Called Thread.end on a non-running thread") 261 case caller.status != ThreadOK: 262 panic("Caller thread of ending thread is not OK") 263 } 264 close(t.resumeCh) 265 t.status = ThreadDead 266 t.caller = nil 267 err = t.cleanupCloseStack(nil, 0, err) // TODO: not nil 268 t.closeErr = err 269 caller.sendResumeValues(args, err, exception) 270 t.ReleaseBytes(2 << 10) // The goroutine will terminate after this 271 } 272 273 func (t *Thread) call(c Callable, args []Value, next Cont) error { 274 cont := c.Continuation(t, next) 275 t.Push(cont, args...) 276 return t.RunContinuation(cont) 277 } 278 279 func (t *Thread) getResumeValues() ([]Value, error) { 280 res := <-t.resumeCh 281 if res.exception != nil { 282 panic(res.exception) 283 } 284 return res.args, res.err 285 } 286 287 func (t *Thread) sendResumeValues(args []Value, err error, exception interface{}) { 288 t.resumeCh <- valuesError{args: args, err: err, exception: exception} 289 } 290 291 // 292 // Calling 293 // 294 295 // CallContext pushes a new runtime context on the thread's runtime and attempts 296 // to run f() in the thread. If the context runs out of resources while f() is 297 // running, all operations should abort and the CallContext should return 298 // immediately and not finalizing pending to-be-closed values. 299 // 300 // Otherwise (even if f() returns an error), pending to-be-closed values should 301 // be finalized. 302 // 303 // See quotas.md for details about this API. 304 func (t *Thread) CallContext(def RuntimeContextDef, f func() error) (ctx RuntimeContext, err error) { 305 t.PushContext(def) 306 c, h := t.CurrentCont(), t.closeStack.size() 307 defer func() { 308 ctx = t.PopContext() 309 if r := recover(); r != nil { 310 t.closeStack.truncate(h) // No resources to run that, so just discard it. 311 termErr, ok := r.(ContextTerminationError) 312 if !ok { 313 panic(r) 314 } 315 err = termErr 316 } 317 }() 318 err = t.cleanupCloseStack(c, h, f()) 319 if t.GCPolicy() == IsolateGCPolicy { 320 t.runFinalizers(t.weakRefPool.ExtractAllMarkedFinalize()) 321 } 322 if err != nil { 323 t.setStatus(StatusError) 324 } 325 return 326 } 327 328 // 329 // close stack operations 330 // 331 332 type closeStack struct { 333 stack []Value 334 } 335 336 func (s closeStack) size() int { 337 return len(s.stack) 338 } 339 340 func (s *closeStack) push(v Value) { 341 s.stack = append(s.stack, v) 342 } 343 344 func (s *closeStack) pop() (Value, bool) { 345 sz := len(s.stack) 346 if sz == 0 { 347 return NilValue, false 348 } 349 sz-- 350 v := s.stack[sz] 351 s.stack = s.stack[:sz] 352 return v, true 353 } 354 355 func (s *closeStack) truncate(h int) { 356 sz := len(s.stack) 357 if sz > h { 358 s.stack = s.stack[:h] 359 } 360 } 361 362 // Truncate the close stack to size h, calling the __close metamethods in the 363 // context of the given continuation c and feeding them with the given error. 364 func (t *Thread) cleanupCloseStack(c Cont, h int, err error) error { 365 closeStack := &t.closeStack 366 for closeStack.size() > h { 367 v, _ := closeStack.pop() 368 if Truth(v) { 369 closeErr, ok := Metacall(t, v, "__close", []Value{v, ErrorValue(err)}, NewTerminationWith(c, 0, false)) 370 if !ok { 371 return errors.New("to be closed value missing a __close metamethod") 372 } 373 if closeErr != nil { 374 err = closeErr 375 } 376 } 377 } 378 return err 379 } 380 381 // 382 // messageHandlerCont is a continuation that handles an error message (i.e. 383 // turns it to handled). 384 // 385 type messageHandlerCont struct { 386 c Cont 387 err Value 388 done bool 389 } 390 391 func newMessageHandlerCont(c Cont) *messageHandlerCont { 392 return &messageHandlerCont{c: c} 393 } 394 395 var _ Cont = (*messageHandlerCont)(nil) 396 397 func (c *messageHandlerCont) DebugInfo() *DebugInfo { 398 return c.c.DebugInfo() 399 } 400 401 func (c *messageHandlerCont) Next() Cont { 402 return c.c.Next() 403 } 404 405 func (c *messageHandlerCont) Parent() Cont { 406 return c.Next() 407 } 408 409 func (c *messageHandlerCont) Push(r *Runtime, v Value) { 410 if !c.done { 411 c.done = true 412 c.err = v 413 } 414 } 415 416 func (c *messageHandlerCont) PushEtc(r *Runtime, etc []Value) { 417 if c.done || len(etc) == 0 { 418 return 419 } 420 c.Push(r, etc[0]) 421 } 422 423 func (c *messageHandlerCont) RunInThread(t *Thread) (Cont, error) { 424 return nil, newHandledError(c.err) 425 }