Player (12)
Joined: 9/14/2022
Posts: 5
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?
YoshiRulz
Any
Editor, Emulator Coder
Joined: 8/30/2020
Posts: 106
Location: Sydney, Australia
Is there a reason you're not just doing everything in C#?
I contribute to BizHawk as Linux/cross-platform lead, testing and automation lead, and UI designer. This year, I'm experimenting with streaming BizHawk development on Twitch. nope Links to find me elsewhere and to some of my side projects are on my personal site. I will respond on Discord faster than to PMs on this site.
Hey look buddy, I'm an engineer. That means I solve problems. Not problems like "What is software," because that would fall within the purview of your conundrums of philosophy. I solve practical problems. For instance, how am I gonna stop some high-wattage thread-ripping monster of a CPU dead in its tracks? The answer: use code. And if that don't work? Use more code.
Player (12)
Joined: 9/14/2022
Posts: 5
YoshiRulz wrote:
Is there a reason you're not just doing everything in C#?
I'd love to do everything in C#. I thought Lua scripts were the only official way to write tools that ran while the game was running. Is there a C# way to do this?
Language: lua

-- Set up GUI forms, etc. -- ... while true do -- Load from game memory, update forms, log text to console, draw on emulator screen etc. -- ... emu.frameadvance() end
And then would this be a DLL plugin of some kind? I'd prefer something that's simple for others to set up. It's easy enough to tell people "Download this Lua script and run it from the console."
tom_mai78101
He/Him
Player (127)
Joined: 3/16/2015
Posts: 160
calcwatch2 wrote:
YoshiRulz wrote:
Is there a reason you're not just doing everything in C#?
I'd love to do everything in C#. I thought Lua scripts were the only official way to write tools that ran while the game was running. Is there a C# way to do this? And then would this be a DLL plugin of some kind? I'd prefer something that's simple for others to set up. It's easy enough to tell people "Download this Lua script and run it from the console."
It's the Bizhawk API, "ApiHawk". It's a way to write external tools (and is how you would create DLL plugins). https://github.com/TASEmulators/BizHawk-ExternalTools/wiki It's pretty easy for other users as well. Just tell them to drop the DLL inside the Bizhawk/ExternalTools folder, and start up the Emuhawk.