const { lua, lauxlib, lualib, to_luastring } = require('fengari')
const binary = require('bops')
const isUtf8 = require('isutf8')
/**
* Interop module to help encoding and decoding data to and from Lua/JavaScript.
*/
const interop = {
/**
* Pushes the given data into the Lua VM, encoding into the appropriate type.
*
* @param {lua_State} vm Lua VM state
* @param {any} val Any data object
*/
push(vm, val) {
switch (typeof val) {
case 'undefined':
lua.lua_pushnil(vm)
break
case 'boolean':
lua.lua_pushboolean(vm, val)
break
case 'number':
lua.lua_pushnumber(vm, val)
break
case 'string':
lua.lua_pushstring(vm, to_luastring(val))
break
case 'symbol':
lua.lua_pushlightuserdata(vm, val)
break
case 'function':
if (val instanceof LuaProxy) {
this.push(vm, this.wrap(val))
} else {
lua.lua_pushjsfunction(vm, val)
}
break
case 'object':
default:
if (val === null) {
lua.lua_pushnil(vm)
break
}
if (binary.is(val)) {
lua.lua_pushstring(vm, val)
break
}
if (val instanceof JSProxy) {
const d = lua.lua_newuserdata(vm)
d.data = val.data
break
}
this.pushTable(vm, val)
break
}
},
/**
* Pushes the given Array, Map or Object into the Lua VM, encoded as a lua Table.
*
* @param {lua_State} vm Lua VM state
* @param {Array|Map|Object} val Data object
*/
pushTable(vm, val) {
lua.lua_createtable(vm, 0, 0)
const base = lua.lua_gettop(vm)
if (Array.isArray(val)) {
val.forEach((v, i) => {
this.pushTableField(vm, i+1, v)
lua.lua_settop(vm, base)
})
}
else if (val instanceof Map) {
for (let [k, v] of val) {
this.pushTableField(vm, k, v)
lua.lua_settop(vm, base)
}
}
else {
for (let k in val) {
this.pushTableField(vm, k, val[k])
lua.lua_settop(vm, base)
}
}
},
/**
* Pushes the given data into the Lua VM as a table field at the given path.
*
* @param {lua_State} vm Lua VM state
* @param {String} key Table field name
* @param {any} val Any data object
*/
pushTableField(vm, key, val) {
this.push(vm, val)
if (Number.isInteger(key)) {
lua.lua_seti(vm, -2, key)
} else {
lua.lua_setfield(vm, -2, to_luastring(key))
}
},
/**
* Gets the data from the Lua VM stack at the given index, and returns a
* decoded JavaScript object.
*
* @param {lua_State} vm Lua VM state
* @param {Integer} i Lua stack index
* @return {any}
*/
tojs(vm, i) {
const type = lua.lua_type(vm, i)
switch(type) {
case lua.LUA_TNONE: // -1
return void 0;
case lua.LUA_TNIL: // 0
return null;
case lua.LUA_TBOOLEAN: // 1
return lua.lua_toboolean(vm, i)
case lua.LUA_TLIGHTUSERDATA: // 2
return lua.lua_touserdata(vm, i)
case lua.LUA_TNUMBER: // 3
return lua.lua_tonumber(vm, i)
case lua.LUA_TSTRING: // 4
let buf = lua.lua_tolstring(vm, i)
if (!binary.is(buf))
buf = binary.from(buf);
return isUtf8(buf) ? binary.to(buf) : buf;
case lua.LUA_TTABLE: // 5
return this.toMap(vm, lua.lua_toproxy(vm, i))
case lua.LUA_TFUNCTION: // 6
return this.toFunction(vm, lua.lua_toproxy(vm, i))
case lua.LUA_TUSERDATA: // 7
let u = lua.lua_touserdata(vm, i)
return u ? u.data : void 0;
case lua.LUA_TTHREAD: // 8
/* fall through */
default:
return lua.lua_toproxy(vm, i)
}
},
/**
* Converts the given Lua proxy into a JavaScript Map.
*
* @param {lua_State} vm Lua VM state
* @param proxy Lua proxy object
* @return {Map}
*/
toMap(vm, proxy) {
// get main thread
lua.lua_rawgeti(vm, lua.LUA_REGISTRYINDEX, lua.LUA_RIDX_MAINTHREAD)
const L = lua.lua_tothread(vm, -1)
lua.lua_pop(vm, 1)
const js = {}
if (typeof Symbol === 'function') {
js[Symbol.iterator] = _ => jsiterator(L, proxy)
}
// Build map
const map = new Map()
for (let [k, v] of js) {
map.set(k, v)
}
// Convert to array if all keys are integers
if ( Array.from(map.keys()).every(Number.isInteger) ) {
return Array.from(map.keys()).sort().map(k => map.get(k))
}
return map
},
/**
* Converts the given Lua proxy into a JavaScript Function.
*
* @param {lua_State} vm Lua VM state
* @param proxy Lua proxy object
* @return {Function}
*/
toFunction(vm, proxy) {
// get main thread
lua.lua_rawgeti(vm, lua.LUA_REGISTRYINDEX, lua.LUA_RIDX_MAINTHREAD)
const L = lua.lua_tothread(vm, -1)
lua.lua_pop(vm, 1)
return new LuaProxy(L, proxy)
},
/**
* Wraps the given data so it will not be encoded for Lua.
*
* @param any data Any data
* @return JSProxy
*/
wrap(data) {
return new JSProxy(data)
}
}
/**
* Wraps any arbirary JavaScript value in a proxy before pushing into the Lua
* state. No encoding or decoding occurs, so if/when the proxy is passed back
* the JavaScript the original value is preserved.
* @ignore
*/
function JSProxy(val) {
this.data = val
}
/**
* Wraps a Lua function in a proxy so it can be invoked from JavaScript.
* @ignore
*/
function LuaProxy(vm, proxy) {
const fn = (...args) => invokeAsync(vm, proxy, args)
Object.setPrototypeOf(fn, LuaProxy.prototype)
fn.invoke = (...args) => invoke(vm, proxy, args)
fn.invokeAsync = (...args) => invokeAsync(vm, proxy, args)
fn.proxy = proxy
return fn
}
/**
* Invokes a Lua function synchronously.
* @ignore
*/
const invoke = function(L, proxy, args) {
const base = lua.lua_gettop(L)
proxy(L)
lauxlib.luaL_checkstack(L, args.length, null)
args.forEach(a => interop.push(L, a))
switch(lua.lua_pcall(L, args.length, lua.LUA_MULTRET, 0)) {
case lua.LUA_OK:
let nres = lua.lua_gettop(L)-base
let res = []
for (let i = 0; i < nres; i++) {
res[i] = interop.tojs(L, base+i+1)
}
lua.lua_settop(L, base)
return nres > 1 ? res : res[0];
default:
let err = interop.tojs(L, -1)
lua.lua_settop(L, base)
throw new Error(`Lua Error: ${ err }`)
}
}
/**
* Invokes a Lua function asynchronously.
* @ignore
*/
const invokeAsync = async function(L, proxy, args) {
const base = lua.lua_gettop(L)
lua.lua_getglobal(L, to_luastring('coroutine'))
lua.lua_getfield(L, -1, to_luastring('create'))
proxy(L)
lua.lua_pcall(L, 1, 1, 0)
const vm = lua.lua_tothread(L, -1)
lua.lua_settop(L, base)
return new Promise((resolve, reject) => {
lauxlib.luaL_checkstack(vm, args.length, null)
args.forEach(a => interop.push(vm, a))
lua.lua_resume(vm, null, args.length)
const pollThread = function(time) {
const status = lua.lua_status(vm)
switch (status) {
case lua.LUA_YIELD:
setTimeout(pollThread)
break;
case lua.LUA_OK:
let nres = lua.lua_gettop(vm)
let res = []
for (let i = 0; i < nres; i++) {
res[i] = interop.tojs(vm, -nres+i)
}
lua.lua_settop(vm, base);
resolve(nres > 1 ? res : res[0])
break;
default:
const err = interop.tojs(vm, -1)
lua.lua_settop(vm, base)
reject(new Error(`Lua Error: ${ err }`))
break;
}
}
setTimeout(pollThread)
})
}
/**
* The following functions are extracted from fengari-interop
* https://github.com/fengari-lua/fengari-interop
* - jsiterator()
* - iter_next()
* @copyright Copyright (c) 2017-2019 Daurnimator
*/
/* make iteration use pairs() */
const jsiterator = function(L, p) {
lauxlib.luaL_checkstack(L, 1, null);
lua.lua_pushcfunction(L, function(L) {
lauxlib.luaL_requiref(L, to_luastring("_G"), lualib.luaopen_base, 0);
lua.lua_getfield(L, -1, to_luastring("pairs"));
p(L);
lua.lua_call(L, 1, 3);
return 3;
});
switch(lua.lua_pcall(L, 0, lua.LUA_MULTRET, 0)) {
case lua.LUA_OK: {
let iter = lua.lua_toproxy(L, -3);
let state = lua.lua_toproxy(L, -2);
let last = lua.lua_toproxy(L, -1);
lua.lua_pop(L, 3);
return {
L: L,
iter: iter,
state: state,
last: last,
next: iter_next
};
}
default: {
let r = interop.tojs(L, -1);
lua.lua_pop(L, 1);
throw r;
}
}
};
/* implements lua's "Generic For" protocol */
const iter_next = function() {
let L = this.L;
lauxlib.luaL_checkstack(L, 3, null);
let top = lua.lua_gettop(L);
this.iter(L);
this.state(L);
this.last(L);
switch(lua.lua_pcall(L, 2, lua.LUA_MULTRET, 0)) {
case lua.LUA_OK: {
this.last = lua.lua_toproxy(L, top+1);
let r;
if (lua.lua_isnil(L, -1)) {
r = {
done: true,
value: void 0
};
} else {
let n_results = lua.lua_gettop(L) - top;
let result = new Array(n_results);
for (let i=0; i<n_results; i++) {
result[i] = interop.tojs(L, top+i+1);
}
r = {
done: false,
value: result
};
}
lua.lua_settop(L, top);
return r;
}
default: {
let e = interop.tojs(L, -1);
lua.lua_pop(L, 1);
throw e;
}
}
};
module.exports = interop
Source