Lua C API 研究 —— 基础篇


Lua 提供了和 C 交互的 API,可以在 C 中执行 Lua 代码,也可以在 Lua 中执行 C 代码。两者都通过 Lua C API 实现。本文基于 Lua 5.1,参考 http://www.lua.org/pil/24.html

头文件

在 C/C++ 中使用 Lua C API,需要引入 Lua 的头文件:

  • lua.h

    lua.h 包含了 Lua 的基础函数,这些函数以 lua_ 开头

  • lauxlib.h

    lauxlib.h 包含了 Lua 的辅助函数(称为 auxiliary library 或 auxlib),这些函数以 luaL_ 开头

  • lualib.h

    lualib.h 包含了 Lua 打开内置库的函数,如

    • luaopen_base(L); /* opens the basic library */
    • luaopen_table(L); /* opens the table library */
    • luaopen_io(L); /* opens the I/O library */
    • luaopen_string(L); /* opens the string lib. */
    • luaopen_math(L); /* opens the math lib. */

    在 Lua 5.1 中,可以直接使用 luaL_openlibs 来打开所有库函数,也可以根据需要,只打开需要使用的库

在 C++ 中使用 Lua C API,引用 Lua 头文件时,需要使用 extern “C”:

extern "C" {
#include <lua.h>
}

Lua 栈

Lua 与 C 通过 Lua 栈(lua_State *L)来进行参数传递,Lua 与 C 的互调,就是通过 Lua C API 对 Lua 栈进行操作

在 Lua 代码中,严格遵守 LIFO 的原则,只能操作 Lua 栈的栈顶。在 C 代码中,则可以操作栈中任意元素,甚至可以在栈的任意位置删除和插入元素

在 Lua 栈中可以存放各种类型的变量,如 number,string,可以存放函数,线程等

如果栈中有 4 个元素,如果以正数来表示,栈顶索引为 4,栈底索引为 1;如果以负数来表示,栈顶索引为 -1,栈底索引为 -4。索引 0 为保留槽,不要去对其进行操作

Lua 栈操作很多,后面会单开一篇进行探讨,这里不再进行详述

C 调用 Lua

应用场景

在 C 中调用 Lua,可以实现在用户的 C 程序中集成一个 Lua 解释器,用于执行 Lua 脚本。这样可以实现系统和业务分离,系统层提供底层能力的支持,业务层使用 Lua 进行编写,可以大大提高业务层的开发效率。另外 C 调用 Lua 时提供了保护执行机制,即使 Lua 代码写得有问题,只会影响当前正在执行的 Lua 实例,不会导致整个系统崩溃

目前比较流行的 Redis 和 Nginx Openresty 中都使用了类似的技术:

  • 如 Redis 中加载 Lua 脚本,通过 EVAL 命令可以在 Redis 服务端执行一段 Lua 脚本,以实现类似于存储过程的功能。
  • 在 Nginx Openresty 中,可以将 Lua 脚本加载于 Nginx 的配置中,当外部访问指定 URI 时,Nginx 可以执行相应的 Lua 脚本,实现一些复杂的逻辑,如数据库操作,Redis 操作,甚至向外发送 HTTP 请求等

基本流程

C 调用 Lua 基本流程为:

  1. 引入 Lua 头文件
  2. 创建 Lua 栈
  3. 打开需要使用的 Lua 库
  4. 加载 Lua 代码
  5. 执行 Lua 代码
  6. 获取 Lua 代码执行结果
  7. 关闭 Lua 栈

引入 Lua 头文件

Lua 的头文件为 lua.h,lauxlib.h 和 lualib.h。每个文件包含的内容前面已经介绍过,这里就不再赘述

创建 Lua 栈

lua_open 用于创建一个 Lua 栈,lua_open 实际是一个宏,它调用了 luaL_newstate

#define lua_open()  luaL_newstate()
lua_State *luaL_newstate(void);

lua_State 即为一个 lua 栈

打开需要使用的 Lua 库

打开 Lua 库对应的函数在 lualib.h 中,前面已经说过,这里不再赘述

加载 Lua 代码

Lua 代码可以通过内存的方式进行加载,也可以通过文件的方式进行加载

int luaL_loadbuffer (lua_State *L,const char *buff,size_t sz,const char *name);int luaL_loadfile (lua_State *L, const char *filename);int luaL_loadstring (lua_State *L, const char *s);

这三个函数底层都调用了 lua_load,该函数会对传入的 Lua 代码进行一次预编译,检查代码中的语法错误。如果没有错误,返回 0,并将编译好的代码块作为函数放到 Lua 栈顶。如果错误,则向栈顶推入一条错误信息,错误信息可以通过 lua_tostring(L, -1) 直接获取

注意,这里的函数这个概念很重要,即使是加载一个 Lua 脚本文件(文件并不是以函数方式进行定义的),在 Lua 中都会把整个文件当成一个函数进行处理,即整个文件就是一个 Lua 函数,并且这个函数放到 Lua 栈的栈顶

执行 Lua 代码

上一步中将 Lua 代码作为函数放到了 Lua 栈顶,就可以通过调用 lua_call 或 lua_pcall 来执行 Lua 代码了

void lua_call (lua_State *L, int nargs, int nresults);
int lua_pcall (lua_State *L, int nargs, int nresults, int errfunc);

lua_call

如上,lua_call 中指定了函数的传入参数个数 nargs 和函数返回结果个数 nresults,以下是一个官方文档中,lua 函数调用与 lua_call 操作对应关系。里面涉及一些复杂的 Lua 栈操作,后面会单开一张进行探讨。

Lua 代码

a = f("how", t.x, 14)

Lua 代码在 C 中的执行代码

lua_getfield(L, LUA_GLOBALSINDEX, "f"); /* function to be called */
lua_pushstring(L, "how");                        /* 1st argument */
lua_getfield(L, LUA_GLOBALSINDEX, "t");   /* table to be indexed */
lua_getfield(L, -1, "x");        /* push result of t.x (2nd arg) */
lua_remove(L, -2);                  /* remove 't' from the stack */
lua_pushinteger(L, 14);                          /* 3rd argument */
lua_call(L, 3, 1);     /* call 'f' with 3 arguments and 1 result */
lua_setfield(L, LUA_GLOBALSINDEX, "a");        /* set global 'a' */

由上,lua_call 中指定了几个参数,就需要向 Lua 栈中推入几个参数

lua_pcall

如果 Lua 代码执行过程中没有任何错误,lua_pcall 的行为与 lua_call 是相同的。如果在执行的过程中有错误发生,lua_pcall 会捕捉该错误,并将错误信息推送到 Lua 栈上,并返回一个错误码。

lua_pcall 最后一个参数 errfunc,指定错误处理函数在 Lua 栈中的位置

一般系统嵌入 Lua 代码,都是使用 lua_pcall,调用方法一般都是:

lua_pcall (l, 0, 0, 0)

获取 Lua 代码执行结果

使用 lua_call 或 lua_pcall 执行完一个函数后,会将执行结果放到栈顶,如果有两个返回值,栈索引 -1 和 -2 就是返回值,如果有三个值,栈索引 -1,-2,-3 就是返回值,以此类推

获取这些返回值可以通过栈的操作来实现

关闭 Lua 栈

Lua 栈使用 lua_close 进行关闭

void lua_close (lua_State *L);

示例代码

C 代码:

#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>#include <stdio.h>void load (char *filename) {lua_State *L = lua_open();luaopen_base(L);luaopen_io(L);luaopen_string(L);luaopen_math(L);if (luaL_loadfile(L, filename) || lua_pcall(L, 0, 0, 0))if (luaL_loadfile(L, filename))error(L, "cannot run configuration file: %s",lua_tostring(L, -1));printf("end lua_call\n");lua_close(L);
}int main(int argc, char** argv) {if (argc != 2) {printf("Usage: %s luafile\n", argv[0]);return -1;}load(argv[1]);return 0;
}

lua 代码:

print("Hello World")

编译(我这里装的 luajit,luajit 的链接库放在 /usr/local/lib 下):

$ gcc -g -o aa main.c -I/usr/local/include/luajit-2.1/ -lluajit-5.1

执行:

$ export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/lib
$ ./aa test.lua
Hello World
end lua_call

再来看一下前面提到过的 Lua 的保护机制,我们修改 Lua 代码为:

print("Hello World")
a = 10/0
print(a)

执行:

$ ./aa test.lua
Hello World
inf
end lua_call

Lua 代码除 0 异常退出了,但是,整个 C 代码并没有因此崩溃,而是继续执行,并打印了 end lua_call。这里就点到为止,不再展开了。

Lua 调用 C

Lua 调用 C 的函数,有两种方式,一种是通过 Lua C API,另一种方式是通过 Luajit ffi(个人更喜欢这种方式 ^_^),另外我也单独开过一篇 Luajit ffi 的使用文章 luajit ffi 小结,本文就不再探讨,本文主要探讨的是 Lua C API。

C 函数原型

首先,并不是所有 C 函数都可以使用 Lua C API 进行调用的,能够调用的 C 函数必须遵从 Lua C API 定义的函数原型

typedef int (*lua_CFunction) (lua_State *L);

Lua 每次调用一个 C 函数时,每个 C 函数中传入的 L 都是一个本地栈,这样避免了栈之间的互相干扰。第一个参数在栈中的索引为 1,第二个参数索引为 2,依次类推。C 函数的返回值即为函数返回参数个数。如下面的 Lua 代码

a, b = add(10, 20)

add 对应的 C 函数,函数 Lua 栈索引为 1 的元素为 10,索引为 2 的元素为 20,函数返回值为 2,也就是在函数处理结束前,需要向栈中推入两个元素

C 函数注册

前面一节解决了 Lua 调用 C 函数的方法,但是 Lua 怎么找到 C 函数,这就需要将 C 函数注册到 Lua 的运行栈中,并给它一个 Lua 能够识别的名字

我们使用 lua_pushcfunction 注册函数

void lua_pushcfunction (lua_State *L, lua_CFunction f);

使用 lua_setglobal 指定函数名,该函数实际是弹出栈顶第一个元素,并把该元素设置在全局空间,并给其全局空间的名字

void lua_setglobal (lua_State *L, const char *name);

C 函数注册示例

C 代码:

#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>#include <stdio.h>static int l_add(lua_State *L) {double a = lua_tonumber(L, 1);double b = lua_tonumber(L, 2);lua_pushnumber(L, a+b);return 1;
}void load (char *filename) {lua_State *L = lua_open();luaopen_base(L);luaopen_io(L);luaopen_string(L);luaopen_math(L);lua_pushcfunction(L, l_add);lua_setglobal(L, "add");if (luaL_loadfile(L, filename) || lua_pcall(L, 0, 0, 0))if (luaL_loadfile(L, filename))error(L, "cannot run configuration file: %s",lua_tostring(L, -1));printf("end lua_call\n");lua_close(L);
}int main(int argc, char** argv) {if (argc != 2) {printf("Usage: %s luafile\n", argv[0]);return -1;}load(argv[1]);return 0;
}

Lua 代码:

print("Hello World")
a = add(10.1, 20)
print(a)

执行:

$ ./aa test.lua
Hello World
30.1
end lua_call

C 链接库

在实际应用中,我们新增加一个函数,并不会像上面那样,把函数和 Lua 解释器放在一起,特别是常用的解释器可能直接是 lua 或 luajit,也不可能直接去修改它们的代码。最常用的方式是将自己要使用的函数封装成 C 链接库,然后通过加载 C 链接库的方式,调用链接库中的 C 函数。

在 C 链接库中怎么注册要使用的函数?如同上面所说,我们不可能去修改 Lua 解释器的代码去注册用户 C 链接库中的函数。实际上 Lua 提供了一个入口函数用于注册 C 链接库中的函数,这个函数的命名方式为 luaopen_*,其执行原理为:当用户在 Lua 中执行 require(“mylib”)时,Lua 解释器就会去寻找 mylib.so,并执行 luaopen_mylib。

luaopen_* 与 Lua C 函数的原型相同,为:

typedef int (*lua_CFunction) (lua_State *L);

C 链接库示例

C 链接库代码:

#include <lua.h>static int l_add(lua_State *L) {double a = lua_tonumber(L, 1);double b = lua_tonumber(L, 2);lua_pushnumber(L, a+b);return 1;
}int luaopen_libtest(lua_State *L) {lua_pushcfunction(L, l_add);lua_setglobal(L, "add");return 1;
}

Lua 代码:

require("libtest")print("Hello World")
a = add(10.1, 20)
print(a)

C 链接库编译:

gcc -g -o libtest.so -shared -fPIC test.c -I/usr/local/include/luajit-2.1/ -lluajit-5.1

执行 Lua 代码:

$ lua test.lua
Hello World
30.1

这里需要注意,Lua 代码中 require 的库名和 C 链接库中的 luaopen_* 以及最后编译生成的库名必须保持一致

更高级的函数注册

前面使用的 lua_pushcfunction + lua_setglobal 进行函数注册的方法,对于只有一两个函数时问题不大,如果函数比较多,使用起来就比较繁琐,有没有更好的方法来注册函数?答案是有,这种方法就是 luaL_Reg + luaL_register

typedef struct luaL_Reg {const char *name;lua_CFunction func;
} luaL_Reg;void luaL_register(lua_State *L, const char *libname, const luaL_Reg *l);

更高级的函数注册示例

我们在上面链接库代码的基础上进行修改,修改 luaopen_libtest 函数实现为

int luaopen_libtest(lua_State *L) {luaL_Reg lua_reg[] = {{"add", l_add},{NULL, NULL}};luaL_register(L, "aa", lua_reg);return 1;
}

这里使用了 luaL_* 的库,需要增加

#include <lauxlib.h>

luaL_register 第二个参数会创建一个 table,而对注册的函数,都需要使用该 table 进行引用,因此 Lua 代码修改为:

require("libtest")print("Hello World")
a = aa.add(10.1, 20)
print(a)

执行 Lua 代码:

$ lua test.lua
Hello World
30.1

Lua 和 C 互调模型小结

C 调用 Lua 函数

  1. C 调用 Lua,实际上是先向栈顶压入 Lua 函数
  2. 再向栈顶依次压入参数
  3. 调用 lua_call 或 lua_pcall 执行 lua 函数(这里决定了函数在栈中的位置)
  4. lua 函数执行完成,向栈顶压入返回值
  5. 取出返回值,得到函数执行结果

我们以一个带 2 个参数,返回一个值的函数为例,函数调用方式为:

add(20.1, 10)

前面说过,自己写 Lua 解释器,在载入 Lua 文件时,实际是将整个文件作为一个函数压入到 Lua 栈顶,因为一般情况下没有传入参数,也没有返回参数,所以调用函数的参数个数和返回值个数都为 0

Lua 加载 C 链接库

  1. require 找到对应的 C 链接库(lua 库搜索路径为 package.path,C 库搜索路径为 package.cpath)
  2. 执行 C 链接库中的 luaopen_*,这个 * 与 require 的传入参数相同,如果不同会报以下错误

    ./libtest.so: undefined symbol: luaopen_libtest
    

Lua 执行 C 函数

  1. 通过名字找到 C 函数的函数指针
  2. 创建一个 local Lua 栈
  3. 将传入参数压入 Lua 栈中
  4. 执行 C 函数
  5. C 函数将返回值压入 Lua 栈中
  6. C 函数返回返回值的个数

我们以一个带 2 个参数,返回一个值的函数为例,该函数注册名为 add,注册函数指针为 l_add,函数调用方式为:

add(20.1, 10)

Lua C API 研究 —— 基础篇相关推荐

  1. Lua源码分析 - 基础篇 - Lua源码的结构和架构图(01)

    目录 一.Lua语言简介 二.Lua架构图 三.Lua源码结构 很久很久没有写博客了,一直忙于工作和项目,最近依然想静下来阅读一些好的源码.自从读完了Nginx和Memcache的源码后,对服务器端的 ...

  2. Win32编程API 基础篇 -- 1.入门指南 根据英文教程翻译

    入门指南 本教程是关于什么的 本教程的目的是向你介绍使用win32 API编写程序的基础知识(和通用的写法).使用的语言是C,但大多数C++编译器也能成功编译,事实上,教程中的绝大多数内容都适用于任何 ...

  3. Linux技术研究-基础篇(raid与LVM,配额)

    Linux技术研究-基础篇(raid与LVM,配额) 创建RAID-5 若想建立新的md1设备 只在/dev下建立还不够 重启后会消失 固化的方法是 为了使udev自动产生/dev/md1, /dev ...

  4. ASP.NET Google Maps Javascript API V3 实战基础篇一获取和设置事件处理程序中的属性...

    ASP.NET Google Maps Javascript API V3 实战基础篇一获取和设置事件处理程序中的属性 <%@ Page Language="C#" Auto ...

  5. Linux技术研究-基础篇(启动和自动挂载)

    Linux技术研究-基础篇(启动和自动挂载) 系统启动流程 如果有一天你的服务器启动不了,面对屏幕上的各种各样的提示素手无策. 你不知道服务器出了什么问题,无法判断启动到了哪个环节. 若想排查出问题原 ...

  6. ASP.NET Google Maps Javascript API V3 实战基础篇一检测用户位置

    ASP.NET Google Maps Javascript API V3 实战基础篇一检测用户位置 对于一些基本的东西,google maps JavaScript api v3 文档已经讲解得足够 ...

  7. ArcGIS API for JavaScript之基础篇(二)

    ArcGIS API for JavaScript之基础篇(二) 上一篇文章介绍了Map MapView SceneView的基本知识以及简单的demo.最近几天学习了WebMap WebScene ...

  8. Lua与c++交互实战基础篇-夏曹俊-专题视频课程

    Lua与c++交互实战基础篇-10018人已学习 课程介绍         本课程从实战角度讲解了流行的高性能脚本Lua与c++的联合开发,这套方案已经被大量的对性能由要求的系统使用,成为了高性能脚本 ...

  9. JAVA红石_【Mc我的世界红石研究日记】第四期:红石基础元件(四)——JAVA版基础篇...

    Hello,大家好,欢迎来到Mc元气工作室!本期给大家带来Mc我的世界红石研究日记·第四期!版本:JAVA1.14.3. 第三期答题互动答案 以下哪一个选项被红石比较器检测出的红石信号与其他三项不同? ...

  10. 计算机编程书籍-笨办法学Python 3:基础篇+进阶篇

    编辑推荐: 适读人群 :本书适合所有已经开始使用Python的技术人员,包括初级开发人员和已经升级到Python 3.6版本以上的经验丰富的Python程序员. "笨办法学"系列, ...

最新文章

  1. 为您解析大数据的未来趋势
  2. Python 之 matplotlib (十二) subplot
  3. swift4.0 try 的强大
  4. Elasticsearch 因拷贝多余的jar到lib库导致无法启动的问题
  5. python list存储方式_python list存储
  6. mysql数据库里的表格_mysql数据库中表记录的玩法
  7. postman跨域测试_安装使用Hoppscotch构建API请求访问与测试
  8. arcgis属性表选择两个条件_ARCGIS关联属性表(转)
  9. 体验Vs2005 beta2 测试工具
  10. 揭秘支付宝中的深度学习引擎:xNN
  11. 51CTO与我的大学生活
  12. UE 某图局部 展UV 图标 / CSS 精灵图(sprite) 好像
  13. 2021 12月CSP认证心得
  14. JSON对象转换成Byte(字节)数组
  15. 欧几里得、扩展欧几里得和中国剩余定理
  16. 读书笔记 -- 《瓦尔登湖》
  17. Qt显示PDF之四pdfium封装
  18. 一文快速了解MassGrid网络
  19. Android之近场通信技术
  20. 关于excel表格输入身份证的问题

热门文章

  1. 支持iphone的打印服务器,无需购买WiFi打印机,实现电脑、iPad、iPhone共享无线打印...
  2. 一个月转推荐:LR算法原理
  3. SQL语句之表的创建和使用
  4. 泛微自带第三方短信接口
  5. 7-5 判断上三角矩阵
  6. python编程教学软件-B站最受欢迎的Python教程,免费教学视频可以下载了
  7. stm32f407zg跟ze的区别_STM32F103ZE和STM32F207ZG的芯片区别
  8. 淘宝天猫融合能拉回“出淘”的用户吗?
  9. MBlock开发环境搭建
  10. android内存碎片问题优化梳理