easy gopher-lua bridge
I have some go code that I’d like to be a little more flexible at runtime. Like a config file, but maybe with some conditional logic based on string matching. If this sounds like a proxy deciding which filtering functions to apply based on URL, that’s a good guess.
The obvious solution would be something like Lua via gopher-lua. The stack based Lua API is easy enough to work with, but rather tedious and boring. Exporting a dozen functions requires a dozen wrappers which I don’t want to write. All of my Lua code recently has been Lua first, using luajit FFI to call into C, avoiding this problem, but there’s nothing quite like that for gopher-lua and I’m not sure go has the required reflection abilities. (Maybe someday the plugin facility will get there?)
Fortunately, go does have better runtime type information available than C does, so we can generate wrapper functions on the fly. This way I can take any existing go function and export it to Lua with a single line. It’s not very much code at all, but gave me a chance to play with the go reflect
package. No promises that it works in all cases; some types are missing, but the missing parts are probably just an extra line or two away.
And so, here’s func2lua
, a function which converts a go function into one that’s Lua API compatible.
func func2lua(gofunc interface{}) lua.LGFunction {
return func (L *lua.LState) int {
We’re going to return a function with the required API. Inside that function, we first unpack the arguments provided by the Lua side.
rf := reflect.ValueOf(gofunc)
ft := reflect.TypeOf(gofunc)
numargs := ft.NumIn()
var args []reflect.Value
for i := 0; i < numargs; i++ {
var gv interface{}
lt := L.Get(i + 1).Type()
switch lt {
case lua.LTString:
gv = L.ToString(i + 1)
case lua.LTNumber:
gv = L.ToNumber(i + 1)
case lua.LTBool:
gv = L.ToBool(i + 1)
case lua.LTUserData:
gv = L.ToUserData(i + 1).Value
default:
panic("banana")
}
args = append(args, reflect.ValueOf(gv))
}
I’m mixing and matching my reflection here. The number of expected arguments come from the go function, but then Lua tells me what types I actually received. Could reverse that, or do it a few different ways. In the end, I don’t think it matters except to the extent you want control over the error message in case of mistakes. It should be approximately as safe no matter which way you go about it, but one could add some extra type checks at this point for more forgiving error messages.
Next we call the function.
rvs := rf.Call(args)
So that was easy. Now to unpack the return values.
for i := 0; i < len(rvs); i++ {
rv := rvs[i]
switch rv.Kind() {
case reflect.Int:
L.Push(lua.LNumber(rv.Int()))
case reflect.String:
L.Push(lua.LString(rv.String()))
default:
ud := L.NewUserData()
ud.Value = rv.Interface()
L.Push(ud)
}
}
return len(rvs)
}
}
We don’t know what types Lua is expecting (and it’s rather flexible, to be honest) so we inspect the go types this time around.
Finally a bit of test code. On the go side, a simple function that verifies we can inspect a passed request object and interface with Lua via basic types.
func myfunc(s string, r *Request) (string, int) {
fmt.Printf("lua called me with %s\n", s)
fmt.Printf("the request url is %s\n", r.url)
return "ok", 88
}
func main() {
var r Request
r.url = "google.com"
L := lua.NewState()
L.SetGlobal("lfunc", L.NewFunction(func2lua(myfunc)))
ud := L.NewUserData()
ud.Value = &r
L.SetGlobal("req", ud)
err := L.DoFile("test.lua")
if err != nil {
fmt.Printf("error running file %s\n", err)
}
fmt.Println("woot")
}
One thing to be mindful of is the difference between pointers. For better or worse, go lets you mix and match pointers and values in many cases, but when converting to interface we need to get things right. On the Lua side, we just print some stuff and the return.
print(req)
print(lfunc("this string came from lua", req, "more"))
Here’s the answer:
lua called me with this string came from lua
the request url is google.com
ok 88
woot
Boom.
Is it slow? Probably. Is it too slow? Probably not.