481 lines
12 KiB
Kotlin
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
|
|
}
|
|
}
|