import { Decoration } from './decoration' ; import { LUA_MULTRET , LuaEventMasks , LuaResumeResult , LuaReturn , LuaState , LuaThreadRunOptions , LuaTimeoutError , LuaType , PointerSize , } from './types' ; import { Pointer } from './pointer' ; import LuaTypeExtension from './type-extension' ; import MultiReturn from './multireturn' ; import type LuaWasm from './luawasm' ; export interface OrderedExtension { priority : number ; extension : LuaTypeExtension < unknown > ; } const INSTRUCTION_HOOK_COUNT = 1000 ; export default class Thread { public readonly address : LuaState ; public readonly lua : LuaWasm ; protected readonly typeExtensions : OrderedExtension [ ] ; private closed = false ; private hookFunctionPointer : number | undefined ; private timeout ? : number ; private readonly parent ? : Thread ; public constructor ( lua , typeExtensions , address , parent ? ) { this . lua = lua ; this . typeExtensions = typeExtensions ; this . address = address ; this . parent = parent ; } public newThread ( ) { const address = this . lua . lua_newthread ( this . address ) ; if ( ! address ) { throw new Error ( 'lua_newthread returned a null pointer' ) ; } return new Thread ( this . lua , this . typeExtensions , address ) ; } public resetThread ( ) { this . assertOk ( this . lua . lua_resetthread ( this . address ) ) ; } public loadString ( luaCode ) { this . assertOk ( this . lua . luaL_loadstring ( this . address , luaCode ) ) ; } public loadFile ( filename ) { this . assertOk ( this . lua . luaL_loadfilex ( this . address , filename , null ) ) ; } public resume ( argCount = 0 ) { const dataPointer = this . lua . module . _malloc ( PointerSize ) ; try { this . lua . module . setValue ( dataPointer , 0 , 'i32' ) ; const luaResult = this . lua . lua_resume ( this . address , null , argCount , dataPointer ) ; return { result : luaResult , resultCount : this . lua . module . getValue ( dataPointer , 'i32' ) , } ; } finally { this . lua . module . _free ( dataPointer ) ; } } public getTop ( ) { return this . lua . lua_gettop ( this . address ) ; } public setTop ( index ) { this . lua . lua_settop ( this . address , index ) ; } public remove ( index ) { return this . lua . lua_remove ( this . address , index ) ; } public setField ( index , name , value ) { index = this . lua . lua_absindex ( this . address , index ) ; this . pushValue ( value ) ; this . lua . lua_setfield ( this . address , index , name ) ; } public async run ( argCount = 0 , options ? ) { const originalTimeout = this . timeout ; try { if ( options ?. timeout !== undefined ) { this . setTimeout ( Date . now ( ) + options . timeout ) ; } let resumeResult = this . resume ( argCount ) ; while ( resumeResult . result === LuaReturn . Yield ) { if ( this . timeout && Date . now ( ) > this . timeout ) { if ( resumeResult . resultCount > 0 ) { this . pop ( resumeResult . resultCount ) ; } throw new LuaTimeoutError ( `thread timeout exceeded` ) ; } if ( resumeResult . resultCount > 0 ) { const lastValue = this . getValue ( - 1 ) ; this . pop ( resumeResult . resultCount ) ; if ( lastValue === Promise . resolve ( lastValue ) ) { await lastValue ; } else { await new Promise ( ( resolve ) => setImmediate ( resolve ) ) ; } } else { await new Promise ( ( resolve ) => setImmediate ( resolve ) ) ; } resumeResult = this . resume ( 0 ) ; } this . assertOk ( resumeResult . result ) ; return this . getStackValues ( ) ; } finally { if ( options ?. timeout !== undefined ) { this . setTimeout ( originalTimeout ) ; } } } public runSync ( argCount = 0 ) { this . assertOk ( this . lua . lua_pcallk ( this . address , argCount , LUA_MULTRET , 0 , 0 , null ) as LuaReturn ) ; return this . getStackValues ( ) ; } public pop ( count = 1 ) { this . lua . lua_pop ( this . address , count ) ; } public call ( name , ... args ) { const type = this . lua . lua_getglobal ( this . address , name ) ; if ( type !== LuaType . Function ) { throw new Error ( `A function of type '${ type }' was pushed, expected is ${ LuaType . Function }` ) ; } for ( const arg of args ) { this . pushValue ( arg ) ; } this . lua . lua_callk ( this . address , args . length , LUA_MULTRET , 0 , null ) ; return this . getStackValues ( ) ; } public getStackValues ( ) { const returns = this . getTop ( ) ; const returnValues = new MultiReturn ( returns ) ; for ( let i = 0 ; i < returns ; i ++ ) { returnValues [ i ] = this . getValue ( i + 1 ) ; } return returnValues ; } public stateToThread ( L ) { return L === this . parent ?. address ? this . parent : new Thread ( this . lua , this . typeExtensions , L , this . parent || this ) ; } public pushValue ( rawValue , userdata ? ) { const decoratedValue = this . getValueDecorations ( rawValue ) ; const target = decoratedValue . target ?? undefined ; if ( target instanceof Thread ) { const isMain = this . lua . lua_pushthread ( target . address ) === 1 ; if ( ! isMain ) { this . lua . lua_xmove ( target . address , this . address , 1 ) ; } return ; } const startTop = this . getTop ( ) ; switch ( typeof target ) { case 'undefined' : this . lua . lua_pushnil ( this . address ) ; break ; case 'number' : if ( Number . isInteger ( target ) ) { this . lua . lua_pushinteger ( this . address , target ) ; } else { this . lua . lua_pushnumber ( this . address , target ) ; } break ; case 'string' : this . lua . lua_pushstring ( this . address , target ) ; break ; case 'boolean' : this . lua . lua_pushboolean ( this . address , target ? 1 : 0 ) ; break ; default : if ( ! this . typeExtensions . find ( ( wrapper ) => wrapper . extension . pushValue ( this , decoratedValue , userdata ) ) ) { throw new Error ( `The type '${ typeof target }' is not supported by Lua` ) ; } } if ( decoratedValue . options . metatable ) { this . setMetatable ( decoratedValue . options . metatable , - 1 ) ; } if ( this . getTop ( ) !== startTop + 1 ) { throw new Error ( `pushValue expected stack size ${ startTop + 1 }, got ${ this . getTop ( ) }` ) ; } } public setMetatable ( metatable , index ) { index = this . lua . lua_absindex ( this . address , index ) ; if ( this . lua . lua_getmetatable ( this . address , index ) ) { this . pop ( 1 ) ; const name = this . getMetatableName ( index ) ; throw new Error ( `data already has associated metatable: ${ name || 'unknown name' }` ) ; } this . pushValue ( metatable ) ; this . lua . lua_setmetatable ( this . address , index ) ; } public getMetatableName ( index ) { const metatableNameType = this . lua . luaL_getmetafield ( this . address , index , '__name' ) ; if ( metatableNameType === LuaType . Nil ) { return undefined ; } if ( metatableNameType !== LuaType . String ) { this . pop ( 1 ) ; return undefined ; } const name = this . lua . lua_tolstring ( this . address , - 1 , null ) ; this . pop ( 1 ) ; return name ; } public getValue ( idx , inputType ? , userdata ? ) { idx = this . lua . lua_absindex ( this . address , idx ) ; const type = inputType ?? this . lua . lua_type ( this . address , idx ) ; switch ( type ) { case LuaType . None : return undefined ; case LuaType . Nil : return null ; case LuaType . Number : return this . lua . lua_tonumberx ( this . address , idx , null ) ; case LuaType . String : return this . lua . lua_tolstring ( this . address , idx , null ) ; case LuaType . Boolean : return Boolean ( this . lua . lua_toboolean ( this . address , idx ) ) ; case LuaType . Thread : return this . stateToThread ( this . lua . lua_tothread ( this . address , idx ) ) ; default : { let metatableName ; if ( type === LuaType . Table || type === LuaType . Userdata ) { metatableName = this . getMetatableName ( idx ) ; } const typeExtensionWrapper = this . typeExtensions . find ( ( wrapper ) => wrapper . extension . isType ( this , idx , type , metatableName ) ) ; if ( typeExtensionWrapper ) { return typeExtensionWrapper . extension . getValue ( this , idx , userdata ) ; } console . warn ( `The type '${ this . lua . lua_typename ( this . address , type ) }' returned is not supported on JS` ) ; return new Pointer ( this . lua . lua_topointer ( this . address , idx ) ) ; } } } public close ( ) { if ( this . isClosed ( ) ) { return ; } if ( this . hookFunctionPointer ) { this . lua . module . removeFunction ( this . hookFunctionPointer ) ; } this . closed = true ; } public setTimeout ( timeout ) { if ( timeout && timeout > 0 ) { if ( ! this . hookFunctionPointer ) { this . hookFunctionPointer = this . lua . module . addFunction ( ( ) : void => { if ( Date . now ( ) > timeout ) { this . pushValue ( new LuaTimeoutError ( `thread timeout exceeded` ) ) ; this . lua . lua_error ( this . address ) ; } } , 'vii' ) ; } this . lua . lua_sethook ( this . address , this . hookFunctionPointer , LuaEventMasks . Count , INSTRUCTION_HOOK_COUNT ) ; this . timeout = timeout ; } else { this . timeout = undefined ; this . lua . lua_sethook ( this . address , null , 0 , 0 ) ; } } public getTimeout ( ) { return this . timeout ; } public getPointer ( index ) { return new Pointer ( this . lua . lua_topointer ( this . address , index ) ) ; } public isClosed ( ) { return ! this . address || this . closed || Boolean ( this . parent ?. isClosed ( ) ) ; } public indexToString ( index ) { const str = this . lua . luaL_tolstring ( this . address , index , null ) ; this . pop ( ) ; return str ; } public dumpStack ( log = console . log ) { const top = this . getTop ( ) ; for ( let i = 1 ; i <= top ; i ++ ) { const type = this . lua . lua_type ( this . address , i ) ; const typename = this . lua . lua_typename ( this . address , type ) ; const pointer = this . getPointer ( i ) ; const name = this . indexToString ( i ) ; const value = this . getValue ( i , type ) ; log ( i , typename , pointer , name , value ) ; } } public assertOk ( result ) { if ( result !== LuaReturn . Ok && result !== LuaReturn . Yield ) { const resultString = LuaReturn [ result ] ; const error = new Error ( `Lua Error(${ resultString }/${ result })` ) ; if ( this . getTop ( ) > 0 ) { if ( result === LuaReturn . ErrorMem ) { error . message = this . lua . lua_tolstring ( this . address , - 1 , null ) ; } else { const luaError = this . getValue ( - 1 ) ; if ( luaError instanceof Error ) { error . stack = luaError . stack ; } error . message = this . indexToString ( - 1 ) ; } } throw error ; } } private getValueDecorations ( value ) { return value instanceof Decoration ? value : new Decoration ( value , { } ) ; } }