KStarbound/src/main/kotlin/ru/dbotthepony/kstarbound/lua/LuaState.kt

481 lines
12 KiB
Kotlin

package ru.dbotthepony.kstarbound.lua
import com.google.gson.JsonArray
import com.google.gson.JsonElement
import com.google.gson.JsonNull
import com.google.gson.JsonObject
import com.google.gson.JsonPrimitive
import com.kenai.jffi.CallContext
import com.kenai.jffi.CallingConvention
import com.kenai.jffi.Closure
import com.kenai.jffi.ClosureManager
import com.kenai.jffi.MemoryIO
import com.kenai.jffi.Type
import jnr.ffi.Memory
import jnr.ffi.NativeType
import org.apache.logging.log4j.LogManager
import org.lwjgl.system.MemoryStack
import org.lwjgl.system.MemoryUtil
import ru.dbotthepony.kstarbound.io.json.InternedJsonElementAdapter
import java.io.Closeable
import java.io.PrintWriter
import java.io.StringWriter
import java.lang.ref.Cleaner
import java.nio.ByteBuffer
import java.nio.ByteOrder
import kotlin.system.exitProcess
private fun stringToBuffer(str: String): ByteBuffer {
val bytes = str.toByteArray(charset = Charsets.UTF_8)
val buf = ByteBuffer.allocateDirect(bytes.size)
buf.order(ByteOrder.nativeOrder())
bytes.forEach(buf::put)
buf.position(0)
return buf
}
@Suppress("unused")
class LuaState : Closeable {
private val pointer = LuaJNR.INSTANCE.luaL_newstate() ?: throw OutOfMemoryError("Unable to allocate new LuaState")
private val cleanable: Cleaner.Cleanable
private val sharedStringBufferPtr = MemoryIO.getInstance().allocateMemory(2L shl 16, false)
init {
if (sharedStringBufferPtr == 0L) {
LuaJNR.INSTANCE.lua_close(pointer)
throw OutOfMemoryError("Unable to allocate new string shared buffer")
}
val pointer = pointer
val sharedStringBufferPtr = sharedStringBufferPtr
cleanable = CLEANER.register(this) {
LuaJNR.INSTANCE.lua_close(pointer)
MemoryIO.getInstance().freeMemory(sharedStringBufferPtr)
}
}
private val panicHandler = ClosureManager.getInstance().newClosure(
{
LOGGER.fatal("${this@LuaState} at $pointer has panicked! This should be impossible!")
exitProcess(1)
},
CallContext.getCallContext(Type.SINT, arrayOf(Type.POINTER), CallingConvention.DEFAULT, false)
)
override fun close() {
cleanable.clean()
}
init {
LuaJNR.INSTANCE.lua_atpanic(pointer, panicHandler.address)
LuaJNR.INSTANCE.luaopen_base(pointer)
storeGlobal("_G")
LuaJNR.INSTANCE.luaopen_package(pointer)
storeGlobal("package")
LuaJNR.INSTANCE.luaopen_table(pointer)
storeGlobal("table")
LuaJNR.INSTANCE.luaopen_coroutine(pointer)
storeGlobal("coroutine")
LuaJNR.INSTANCE.luaopen_string(pointer)
storeGlobal("string")
LuaJNR.INSTANCE.luaopen_math(pointer)
storeGlobal("math")
LuaJNR.INSTANCE.luaopen_utf8(pointer)
storeGlobal("utf8")
LuaJNR.INSTANCE.luaopen_debug(pointer)
storeGlobal("debug")
}
val stackTop: Int get() {
val value = LuaJNR.INSTANCE.lua_gettop(pointer)
check(value >= 0) { "Invalid stack top $value" }
return value
}
/**
* Converts the acceptable index idx into an equivalent absolute index (that is, one that does not depend on the stack size).
*/
fun absStackIndex(index: Int): Int {
if (index >= 0)
return index
return LuaJNR.INSTANCE.lua_absindex(pointer, index)
}
fun load(code: String, chunkName: String = "main chunk") {
val buf = stringToBuffer(code)
val closure = ClosureManager.getInstance().newClosure(
object : Closure {
override fun invoke(buffer: Closure.Buffer) {
val remainingSize = LuaJNR.RUNTIME.memoryManager.newPointer(buffer.getAddress(2))
if (buf.remaining() == 0) {
remainingSize.putLong(0L, 0L)
buffer.setAddressReturn(0L)
return
}
remainingSize.putLongLong(0L, buf.remaining().toLong())
val p = MemoryUtil.memAddress(buf)
buf.position(buf.remaining())
buffer.setAddressReturn(p)
}
},
CallContext.getCallContext(Type.POINTER, arrayOf(Type.POINTER, Type.ULONG_LONG, Type.POINTER), CallingConvention.DEFAULT, false)
)
throwLoadError(LuaJNR.INSTANCE.lua_load(pointer, closure.address, 0L, chunkName, "t"))
closure.dispose()
}
fun call(numArgs: Int = 0, numResults: Int = 0): Int {
val status = LuaJNR.INSTANCE.lua_pcallk(pointer, numArgs, numResults, 0, 0L, 0L)
if (status == LUA_ERRRUN) {
throw LuaRuntimeException(getString())
}
return status
}
fun getString(stackIndex: Int = -1, limit: Long = 2 shl 16): String? {
val len = Memory.allocateDirect(LuaJNR.RUNTIME, NativeType.SLONGLONG)
val p = LuaJNR.INSTANCE.lua_tolstring(pointer, absStackIndex(stackIndex), len) ?: return null
if (len.getLong(0L) == 0L) {
return ""
}
if (len.getLong(0L) >= limit) {
throw IllegalStateException("Unreasonably long Lua string: ${len.getLong(0L)}")
}
val readBytes = ByteArray(len.getLong(0L).toInt())
p.get(0L, readBytes, 0, readBytes.size)
return readBytes.toString(charset = Charsets.UTF_8)
}
fun isCFunction(stackIndex: Int = -1): Boolean = LuaJNR.INSTANCE.lua_iscfunction(pointer, absStackIndex(stackIndex)) > 0
fun isFunction(stackIndex: Int = -1): Boolean = typeAt(stackIndex) == LuaType.FUNCTION
fun isInteger(stackIndex: Int = -1): Boolean = LuaJNR.INSTANCE.lua_isinteger(pointer, absStackIndex(stackIndex)) > 0
fun isLightUserdata(stackIndex: Int = -1): Boolean = typeAt(stackIndex) == LuaType.LIGHTUSERDATA
fun isNil(stackIndex: Int = -1): Boolean = typeAt(stackIndex) == LuaType.NIL
fun isNone(stackIndex: Int = -1): Boolean = typeAt(stackIndex) == LuaType.NONE
fun isNoneOrNil(stackIndex: Int = -1): Boolean = typeAt(stackIndex).let { it == LuaType.NIL || it == LuaType.NONE }
fun isNumber(stackIndex: Int = -1): Boolean = LuaJNR.INSTANCE.lua_isnumber(pointer, absStackIndex(stackIndex)) > 0
fun isString(stackIndex: Int = -1): Boolean = LuaJNR.INSTANCE.lua_isstring(pointer, absStackIndex(stackIndex)) > 0
fun isTable(stackIndex: Int = -1): Boolean = typeAt(stackIndex) == LuaType.TABLE
fun isThread(stackIndex: Int = -1): Boolean = typeAt(stackIndex) == LuaType.THREAD
fun isUserdata(stackIndex: Int = -1): Boolean = LuaJNR.INSTANCE.lua_isuserdata(pointer, absStackIndex(stackIndex)) > 0
fun isBoolean(stackIndex: Int = -1): Boolean = typeAt(stackIndex) == LuaType.BOOLEAN
fun getBoolean(stackIndex: Int = -1): Boolean? {
if (!isBoolean(stackIndex))
return null
return LuaJNR.INSTANCE.lua_toboolean(pointer, stackIndex) > 0
}
fun getLong(stackIndex: Int = -1): Long? {
if (!isInteger(stackIndex))
return null
val stack = MemoryStack.stackPush()
val status = stack.mallocInt(1)
val value = LuaJNR.INSTANCE.lua_tointegerx(pointer, stackIndex, MemoryUtil.memAddress(status))
val b = status[0] > 0
stack.close()
if (!b)
return null
return value
}
fun getDouble(stackIndex: Int = -1): Double? {
if (!isNumber(stackIndex))
return null
val stack = MemoryStack.stackPush()
val status = stack.mallocInt(1)
val value = LuaJNR.INSTANCE.lua_tonumberx(pointer, stackIndex, MemoryUtil.memAddress(status))
val b = status[0] > 0
stack.close()
if (!b)
return null
return value
}
fun typeAt(stackIndex: Int = -1): LuaType {
return when (val value = LuaJNR.INSTANCE.lua_type(pointer, stackIndex)) {
LUA_TNONE -> LuaType.NONE
LUA_TNIL -> LuaType.NIL
LUA_TBOOLEAN -> LuaType.BOOLEAN
LUA_TLIGHTUSERDATA -> LuaType.LIGHTUSERDATA
LUA_TNUMBER -> LuaType.NUMBER
LUA_TSTRING -> LuaType.STRING
LUA_TTABLE -> LuaType.TABLE
LUA_TFUNCTION -> LuaType.FUNCTION
LUA_TUSERDATA -> LuaType.USERDATA
LUA_TTHREAD -> LuaType.THREAD
LUA_NUMTYPES -> LuaType.UMTYPES
else -> throw RuntimeException("Invalid Lua type: $value")
}
}
fun getValue(stackIndex: Int = -1): JsonElement? {
val abs = absStackIndex(stackIndex)
if (abs == 0)
return null
return when (typeAt(abs)) {
LuaType.NONE -> null
LuaType.NIL -> JsonNull.INSTANCE
LuaType.BOOLEAN -> InternedJsonElementAdapter.of(getBoolean(abs)!!)
LuaType.LIGHTUSERDATA -> null
LuaType.NUMBER -> JsonPrimitive(if (isInteger(abs)) getLong(abs)!! else getDouble(abs)!!)
LuaType.STRING -> JsonPrimitive(getString(abs))
LuaType.TABLE -> getTable(abs)!!
LuaType.FUNCTION -> null
LuaType.USERDATA -> null
LuaType.THREAD -> null
LuaType.UMTYPES -> null
}
}
fun getTable(stackIndex: Int = -1): JsonObject? {
val abs = absStackIndex(stackIndex)
if (!isTable(abs))
return null
val pairs = JsonObject()
push()
while (LuaJNR.INSTANCE.lua_next(pointer, abs) != 0) {
val key = getValue(abs + 1)
val value = getValue(abs + 2)
if (key is JsonPrimitive && value != null) {
pairs.add(key.asString, value)
}
pop()
}
return pairs
}
fun popBoolean(): Boolean? {
try {
return getBoolean()
} finally {
pop()
}
}
fun popLong(): Long? {
try {
return getLong()
} finally {
pop()
}
}
fun popDouble(): Double? {
try {
return getDouble()
} finally {
pop()
}
}
fun popValue(): JsonElement? {
try {
return getValue()
} finally {
pop()
}
}
fun popTable(): JsonObject? {
try {
return getTable()
} finally {
pop()
}
}
fun pop(amount: Int = 1): Int {
if (amount == 0) return 0
check(amount > 0) { "Invalid amount to pop: $amount" }
val old = stackTop
val new = (old - amount).coerceAtLeast(0)
LuaJNR.INSTANCE.lua_settop(pointer, new)
return old - new
}
fun storeGlobal(name: String) {
LuaJNR.INSTANCE.lua_setglobal(pointer, name)
}
fun loadGlobal(name: String) {
LuaJNR.INSTANCE.lua_getglobal(pointer, name)
}
fun push(closure: (state: LuaState) -> Unit) {
LuaJNI.lua_pushcclosure(pointer.address()) lazy@{
try {
closure.invoke(this@LuaState)
} catch (err: Throwable) {
val builder = StringWriter()
val printWriter = PrintWriter(builder)
err.printStackTrace(printWriter)
push(builder.toString())
return@lazy 1
}
return@lazy 0
}
}
fun push() {
LuaJNR.INSTANCE.lua_pushnil(pointer)
}
fun push(value: Int) {
LuaJNR.INSTANCE.lua_pushinteger(pointer, value.toLong())
}
fun push(value: Long) {
LuaJNR.INSTANCE.lua_pushinteger(pointer, value)
}
fun push(value: Double) {
LuaJNR.INSTANCE.lua_pushnumber(pointer, value)
}
fun push(value: Float) {
LuaJNR.INSTANCE.lua_pushnumber(pointer, value.toDouble())
}
fun push(value: Boolean) {
LuaJNR.INSTANCE.lua_pushboolean(pointer, if (value) 1 else 0)
}
fun push(value: String) {
val bytes = value.toByteArray(Charsets.UTF_8)
if (bytes.size < 2 shl 16) {
MemoryIO.getInstance().putByteArray(sharedStringBufferPtr, bytes, 0, bytes.size)
LuaJNR.INSTANCE.lua_pushlstring(pointer, sharedStringBufferPtr, bytes.size.toLong())
} else {
val mem = MemoryIO.getInstance()
val block = mem.allocateMemory(bytes.size.toLong(), false)
if (block == 0L)
throw OutOfMemoryError("Unable to allocate ${bytes.size} bytes on heap")
try {
mem.putByteArray(block, bytes, 0, bytes.size)
LuaJNR.INSTANCE.lua_pushlstring(pointer, block, bytes.size.toLong())
} finally {
mem.freeMemory(block)
}
}
}
fun pushTable(arraySize: Int = 0, hashSize: Int = 0): Int {
LuaJNR.INSTANCE.lua_createtable(pointer, arraySize, hashSize)
return stackTop
}
fun setTableValue(stackIndex: Int) {
LuaJNR.INSTANCE.lua_settable(pointer, stackIndex)
}
fun push(value: JsonElement) {
when (value) {
JsonNull.INSTANCE -> {
push()
}
is JsonPrimitive -> {
if (value.isNumber) {
val num = value.asNumber
when (num) {
is Int, is Long -> push(num.toLong())
else -> push(num.toDouble())
}
} else if (value.isString) {
push(value.asString)
} else if (value.isBoolean) {
push(value.asBoolean)
} else {
throw IllegalArgumentException(value.toString())
}
}
is JsonArray -> {
val index = pushTable(arraySize = value.size())
for ((i, v) in value.withIndex()) {
push(i + 1L)
push(v)
setTableValue(index)
}
}
is JsonObject -> {
val index = pushTable(hashSize = value.size())
for ((k, v) in value.entrySet()) {
push(k)
push(v)
setTableValue(index)
}
}
else -> {
throw IllegalArgumentException(value.toString())
}
}
}
companion object {
private val LOGGER = LogManager.getLogger()
private val CLEANER = Cleaner.create {
val thread = Thread(it, "LuaState cleaner")
thread.priority = 1
thread
}
const val LUA_TNONE = -1
const val LUA_TNIL = 0
const val LUA_TBOOLEAN = 1
const val LUA_TLIGHTUSERDATA = 2
const val LUA_TNUMBER = 3
const val LUA_TSTRING = 4
const val LUA_TTABLE = 5
const val LUA_TFUNCTION = 6
const val LUA_TUSERDATA = 7
const val LUA_TTHREAD = 8
const val LUA_NUMTYPES = 9
}
}