用Lua或c调用go编写的库或函数
- 背景
- 思路
- 操作
- go
- lua
- 代码汇总
- 参考文章
又好久没有更新了,这次肯定又是遇上了什么问题,但又解决了的,才跑过来更新的。我也是翻遍了全网都没找到lua去调go代码的,于是干脆自己写一个
背景
作为提供统一接口的一方,由于历史原因我们是基于openresty+lua开发的。本身我们服务对上游数据要求都是明文的,一直以来都是如此。
可就有一方上游,非要传密文。因为合规原因,端上只能拿到加密数据不可避免。
但是,加密是你后端来的,那解密也过一下你后端,你后端解密透传给我明文不就好了吗?这样你想控制解密哪个字段就解密哪个字段,以后你每个端或增或减,我都不关心,专心做我的引擎不好吗??
什么?不行?上升一级去聊还是要我们做?行吧,那给SDK吧。
什么?只有go和php的?让我自己实现?拒绝🙅🏻♀️!实在不行你提供接口吧。
什么?没有接口,不同意支持?这日子需求 没法做了!!!
思路
不做是不可能不做的,这辈子老板都不会让我不做的。
老板说,只能我想办法自己做了。要么你用lua再实现一遍,要么你用别的方法。我他喵的我怎么用lua实现,go的库都是有引用第三方库的,我lua怎么找到对应的第三方库,我难道把第三方库也自己实现一遍??
这时候一个同事抛出来一个方案:当我们把go编译成一个动态链接库(.so),这样c/cpp就可以引用这个库;另外lua可以无缝引入c/cpp。那么我们可以把go编译成一个动态链接库,然后让lua去调用这个动态链接库,那不就可以做到了吗。网上有写c/cpp调用go的,但是对我而言还少一步lua。
下面讲一些操作,原理我就不讲了
操作
go
首先,起一个go项目,如果有外部库引入,建议用go mod
go mod init
创建一个go文件,并且一定要是main包的,并在其中引入C包,这个包并不真实存在,也不会被Go的compile组件见到,它会在编译前被CGO工具捕捉到,并做一些代码的改写和桩文件的生成。
package main
// #include <stdlib.h>
import "C"
里面封装好你想实现的函数,输入和输出如果是要给lua用的,那么请求用C包下的类型输出,go的string对应*C.char,其他类型可以自己找找。
注:函数名的上一行一定要写“//export Func”,否则一会链接时会找不到接口,他表示输出这个函数(别问我怎么知道的,我在这个上面被坑了半小时)
//export Func
func Func(s *C.char) *C.char {
…………
return C.CString(goString)
}
因为c/cpp是没有垃圾回收的,所以最好加一个Free函数去手动释放结果的内存,不然久而久之会内存泄漏。同样Free函数也是需要export的
//export Free
func Free(p unsafe.Pointer) {
C.free(p)
}
然后写一个空的main函数,这个也是必须的。
func main() {}
接下来,让我们执行命令
go build -o 你的so.so -buildmode=c-shared 你的go文件.go
就会生成对应的.h头文件,这个你不用管。还会生成一个.so的动态链接库,这个是我们要用的,可以拷贝下来
lua
将刚刚生成的so拷贝在lua的项目目录里,让你的lua能找到就行
lua想要引用动态链接库,需要ffi包,openresty自带这个包,luajit好像也是。
local ffi = require("ffi")
然后我们定义这个go所export的函数。
注:c中是没有string的,所以形参和返回想用string的需要用char*代替
ffi.cdef [[
char* Func1(const char* s);
char* Func2(const char* s);
void Free(void* p);
]]
让ffi加载我们的动态链接库
local libconvert = ffi.load("你的so.so")
将go的函数在lua中封装一次,其中调用go的函数并返回后,要将返回值再赋值给一个其他变量,因为原来的返回值我们需要去free掉
local function func1(str)
if str == "" or str == nil then
return str
end
local tmp = so.Func1(str)
local a = ffi.string(tmp)--返回的是char*,想变成lua中string的话,需要ffi转一下
libconvert.Free(tmp) --释放c内存
return a
end
代码汇总
local ffi = require("ffi")
ffi.cdef [[
char* Func1(const char* s);
char* Func2(const char* s);
void Free(void* p);
]]
local libconvert = ffi.load(".so")
local function func1(str)
if str == "" or str == nil then
return str
end
local tmp = so.Func1(str)
local a = ffi.string(tmp)
libconvert.Free(tmp) --释放c内存
return a
end
local codec = {
func1 = func1,
}
return codec
package main
// #include <stdlib.h>
import "C"
import (
"******/utils"
"strconv"
"unsafe"
)
//编译命令:go build -o 你的so.so -buildmode=c-shared 你的go.go
//export Func1
func Func1(s *C.char) *C.char {
//fmt.Printf("input uid:%s\n", C.GoString(s))
n, _ := strconv.Atoi(C.GoString(s))
e, _ := utils.Encode(n)
return C.CString(e)
}
//export Func2
func Func2(s *C.char) *C.char {
d, _ := utils.Decode(C.GoString(s))
//fmt.Printf("uid.so decode uid:%v\n", uid)
ds := strconv.Itoa(d)
//fmt.Printf("itoa :%s\n", uidStr)
return C.CString(ds)
}
//export Free
func Free(p unsafe.Pointer) {
C.free(p)
}
func main() {}
好了到此,功能上的问题就解决了。我们测试了一下,是可以实现实现的。但是性能呢?这个so我看了下,没几行go代码却有12M这么大。
经过我们压测后发现,相同输入,不加这个功能和加这个功能,单pod减少了400qps。我认为是不小的损耗了,唉就是说为啥要来我这里做呢?有懂的小伙伴可以评论区交流一下。
如果你觉得这篇博客对你有帮助,那就还请给我一个大大的赞👍🏻吧。
参考文章
C/C++代码中调用golang的接口
通过 C 代码调用 Go