I recently went down a rabbit hole trying to optimize a Lua script, and hit a snag. I'm pretty sure it's in the realm of "internal stuff that you shouldn't rely on", but I'm curious if anyone knows what's going wrong.
Update: Just a few minutes after posting this, the issue with my code became apparent. It works now! I'll fix my code below.
The script I'm working on does lots of graphics processing, and I found it could go much faster by calling out to C# from Lua, as needed. This isn't a documented feature, but I got it working, and it satisfies my use case.
For example, I can call C#'s
String.Format(String, Object, Object) method like this:
Language: lua
luanet.load_assembly("System")
local String = luanet.import_type("System.String")
console.log(
String.Format("{0} {1}!", "Hello", "World")
)
But what if you want to do something more complicated, like processing a range of bytes from the game's RAM? In Lua, you just call
memory.read_bytes_as_array(), which internally calls the C# method
MemoryApi.ReadByteRange() and converts the byte array into a Lua table of doubles.
Ideally, I'd call that C# method directly so I can use the byte array in other C# methods, but I don't have a reference to the
ApiContainer I'd need in order to call it.
Instead, I call
memory.read_bytes_as_array() in Lua, convert the Lua table back into a C# byte array, and pass that along to the other C# method. It's inefficient, but not too bad.
But another idea would be to have my C# code call the Lua function
memory.read_bytes_as_array() directly, and handle the conversion back to a byte array there.
This is usually a bad idea. I've personally worked on a code base where people wrote scripts that called C++ that called scripts that called C++, and so on. It gets ugly fast. So it's totally reasonable to disallow this in BizHawk.
Still, I feel like I almost got it working, in the script below:
It works now:
Language: lua
luanet.load_assembly("System")
local ArrayList = luanet.import_type("System.Collections.ArrayList")
local Type = luanet.import_type("System.Type")
-- Takes a Lua array and returns a C# Object[]
local function toCSharpArray(lua_array)
local arrayList = ArrayList()
for _, v in ipairs(lua_array) do
arrayList:Add(v)
end
return arrayList:ToArray()
end
-- Does nothing. Just here to test Lua function calls
local function testFunction()
end
-- This calls a Lua function from C#
local function callLuaFunctionInCSharp(lua_function, lua_arg_array)
local t = Type.GetTypeFromHandle(Type.GetTypeHandle(lua_function))
console.log("C# Object for function is of type: ", t)
console.log("lua_function.ToString() returns: " .. t:GetMethod("ToString"):Invoke(lua_function, toCSharpArray({})))
console.log("lua_function.GetHashCode() returns: " .. t:GetMethod("GetHashCode"):Invoke(lua_function, toCSharpArray({})))
-- The bug was here. The Object[] passed into "Invoke" is an array of arguments passed into Call().
-- Call() takes only one argument, an Object[] of the Lua function arguments.
-- So we actually want to send Invoke an Object[][] containing just one Object[]: the array of Lua function arguments.
-- -- Incorrect:
-- local arg_array = toCSharpArray(lua_arg_array)
-- Correct:
local arg_array = toCSharpArray({toCSharpArray(lua_arg_array)})
console.log("arg_array is of type:", arg_array)
local method = t:GetMethod("Call")
console.log("Method is of type:", method)
console.log("Invoking Call method...")
method:Invoke(lua_function, arg_array)
end
callLuaFunctionInCSharp(testFunction, {})
When you pass a Lua function into a C# method call, it gets converted along the way into a
NLua.LuaFunction. I'm able to call
LuaFunction.ToString() and
LuaFunction.GetHashCode() without any trouble.
My issue was that I was passing into
Invoke() an array of the Lua args, instead of an array of size 1 containing an array of the Lua args. I've fixed the code.
But when I try to call LuaFunction.Call(), which should execute the Lua function, it throws an exception.
Here's the script's output on the latest debug build, when it had the bug:
C# Object for function is of type: NLua.LuaFunction
lua_function.ToString() returns: function
lua_function.GetHashCode() returns: 125
arg_array is of type: System.Object[]
Method is of type: System.Object[] Call(System.Object[])
Invoking Call method...
NLua.Exceptions.LuaException: unprotected error in call to Lua API (0)
at NLua.Lua.PanicCallback(IntPtr luaState)
at lua_error(lua_State* )
at NLua.ObjectTranslator.throwError(IntPtr luaState, Object e)
at NLua.Lua.SetPendingException(Exception e)
at NLua.LuaMethodWrapper.call(IntPtr luaState)
at NLua.MetaFunctions.runFunctionDelegate(IntPtr luaState)
at lua_resume(lua_State* , Int32 )
at NLua.Lua.Resume(Int32 narg)
at BizHawk.Client.EmuHawk.Win32LuaLibraries.ResumeScript(LuaFile lf)
at BizHawk.Client.EmuHawk.LuaConsole.<>c__DisplayClass61_1.<ResumeScripts>b__1()
at BizHawk.Client.Common.EnvironmentSandbox.Sandbox(Action callback) in /builds/TASVideos/BizHawk/src/BizHawk.Client.Common/lua/EnvironmentSandbox.cs:line 11
at BizHawk.Client.Common.LuaSandbox.Sandbox(Action callback, Action exceptionCallback) in /builds/TASVideos/BizHawk/src/BizHawk.Client.Common/lua/LuaSandbox.cs:line 57
I assume something somewhere in that stack isn't in the right state to be calling a Lua function. But without building BizHawk myself and setting breakpoints, I'm not sure.
Does anyone have an idea what the problem is?