Lua C接口编程(一)

引言

skynet 和 openresty 都是深度使用lua的典范,学习lua不经要学会基本语法,还要学会C语言与Lua交互。lua的一大优点就是能和c/c++无缝连接,而且可以在不需要重复编译c/c++的情况下可以修改lua文件并且起作用,当我们的项目文件很大的时候,使用lua进行项目修改极大的减少了等待时间。

Lua由标准C编写而成,Lua脚本可以很容易的被C/C++ 代码调用,也可以反过来调用C/C++的函数,这使得Lua在应用程序中可以被广泛应用。

Lua C接口编程我会分为两个部分来介绍,第一部分介绍C/C++如何调用Lua,第二部分介绍Lua如何调用C/C++。

一、Lua虚拟栈

  • 栈中只能存放Lua类型的值,如果想保存C类型的值,需要先将C类型转换为Lua类型;
  • Lua每次调用C函数都会生成新的栈,独立于之前的栈
  • C在创建虚拟机是,伴随创建一个主协程和一个虚拟栈
  • 无论何时Lua再调用C时,只保证至少有LUA_MINSTACK大小的堆栈空间可以使用。LUA_MINSTACK一般定义为20,只要你不是一致压栈,通常不用担心堆栈大小。

Lua中的栈有两排索引,正数1索引的位置在栈底,负数索引-1在栈顶,这样做的好处是不需要知道栈的大小,只需要查找正负索引1的位置就能确定栈顶和栈底的位置。
Lua C接口编程(一)_第1张图片
注意:C 在调用lua的时候要注意虚拟堆栈的变化情况,要保持堆栈在函数执行前后不变,养成良好的习惯。

二、注册表

2.1 什么是注册表

有时候,我们需要在程序中使用一些非局部的变量。在C中我们可以使用全局变量或是静态变量来实现,而在为Lua编写C库的过程中,使用以上类型的变量并不是一个好的方式,原因如下:

  1. 这些变量中无法存储Lua的值
  2. 这些变量如果在多个Lua虚拟机中被使用,很可能造成不可预期的结果。

一个替代方案是,将这些值存储在Lua的全局变量中。这种方式解决了上面提到的两个问题,Lua全局变量可以存储任何Lua的值,同时每一个Lua状态机都有自己独立的一套全局变量。但这依旧不是最好的方式,因为是Lua的全局变量,Lua程序可以随意的修改变量的值,这很可能对C库中的函数在使用这些变量时造成影响。

为了进一步避免上述情况,Lua提供了一张特殊的表(table),它可以供C代码随意使用。但是对于Lua代码,访问却是被禁止的。这个特殊的表便是注册表。

2.2 伪索引 pseudo-index

伪索引跟虚拟栈中正常的索引类似,区别在于,虽然使用它也是通过虚拟栈,但其所对应的值并不是存储在虚拟栈中。比如:LUA_REGISTRYINDEX 就是一个伪索引,定义在”lua.h”中。它用于通过虚拟栈访问注册表(但注册表并非实际存储在虚拟栈中),使用时按照虚拟栈中正常索引的使用方式使用。

2.3 小结

用来在多个C库中共享Lua数据,包括userdata 和 lightuserdata等;

  • 一张预定义的表,用来保存任何C代码想保存的Lua值;
  • 使用 LUA_REGISTRYINDEX来索引;
  • 比如:获取skynet_context

三、Lua 常用API介绍

lua_State* L=luaL_newstate(); -- 函数返回一个指向堆栈的指针
luaL_openlibs(L);        -- 打开指定状态机中的所有 Lua 标准库,也就是把所有标准类库加载到指定的虚拟机.
lua_createtable(L,0,0);  -- 新建并压入一张表
lua_pushstring(L,0,0);  -- 压入一个字符串
lua_pushnumber(L,0,0);  -- 压入一个数字
lua_tostring(L,1);  	-- 取出一个字符串
lua_tointeger(L,1); 	-- 取出数字

double b =lua_tonumber();  -- 取出一个double类型的数字
lua_load()函数   			-- 当这个函数返回0时表示加载
luaL_loadfile(filename)    -- 这个函数也是只允许加载lua程序文件,不执行lua文件。它是在内部去用lua_load()去加载指定名为filename的lua程序文件。当返回0表示没有错误。
luaL_dofile()  			   -- 这个函数不仅仅加载了lua程序文件,还执行lua文件。返回0表示没有错误。
lua_push*(L,data)          -- 压栈,
lua_to*(L,index)           -- 只取值, 不出栈
lua_pop(L,count)           -- 出栈。
lua_close(L);              -- 释放lua资源
lua_call(L,0,0,0);         -- 调用对应函数
lua_pcall(L,0,0,0);        -- 安全的调用对应函数,最后一个参数是错误处理函数

除了上面的API,我们着重看下面几个API:

  1. lua_getglobal(L, funcname)
    lua_getglobal是个宏,每次调用这个宏的时候,都会将Lua代码中funcname与之相应的全局变量值压入栈中。
  2. lua_setfield(L, LUA_REGISTRYINDEX, “LUA_NOENV”);
    等价于执行 LUA_REGISTRYINDEX[LUA_NOENV] = stackTopElem 操作, 其中 stackTopElem指当前栈顶元素。
  3. lua_getfield(L, LUA_REGISTRYINDEX, “LUA_NOENV”);
    将LUA_REGISTRYINDEX[LUA_NOENV] 压入栈,返回入栈值的类型
  4. int luaL_ref(lua_State L, int t)
    将栈顶的元素出栈作为value,从虚拟栈的索引 t 处获取到 table ,之后相当于执行"table[key] = value"的操作。函数返回生成的唯一的key,程序可以通过该key获取到对应的value。如果 value 为 nil ,则不会生成key,函数返回 LUA_REFNIL。
  5. void luaL_unref(lua_State L, int t, int ref);
    从虚拟栈的索引 t 处获得 table,之后做等价于执行 table[ref] = nil 操作。如果 ref 是LUA_NOREF或LUA_REFNIL,则函数不做任何操作。
  6. lua_rawgeti(L, LUA_REGISTRYINDEX, ref);
    从注册表通过key(ref)获取到对应的value(foo函数),并将其入栈

使用 luaL_ref 和 luaL_unref 时无需关心如何创建唯一的”key”,便可以在注册表中自由的存取数据。

代码示例

示例1:常用api用法

C源文件test_field.c

#include 
#include 

#include 
#include 
#include 

/*
    Lua的异常捕获主要基于pcall及xpcall函数。
*/

void getStackSize(const char * desc, const int count)
{
    printf("%s stack size = [ %d ]\n",desc, count);
}

void test_api_getfield()
{
    lua_State *L = luaL_newstate();

    // 加载并执行目标lua文件
    if ( LUA_OK != luaL_dofile(L, "test_field.lua") ) {
        const char* err = lua_tostring(L, -1);
        fprintf(stderr, "err:\t%s\n", err);
        return ;
    }

    lua_getglobal(L,"tab");           // 查找 tab 变量压入栈底

    // lua_gettop 获取栈中元素数量
    getStackSize("stage 1", lua_gettop(L));

    // 
    lua_getfield(L, -1, "a");           // 将 tab.a 入栈
    int nTab_a = lua_tointeger(L,-1); // 将 tab.a 取出赋值给变量nTab_a

    lua_getfield(L, -2, "b");           // 将 tab.b 入栈
    int nTab_b = lua_tointeger(L,-1); // 将 tab.b 取出赋值给变量nTab_b
    getStackSize("stage 2",lua_gettop(L));

    lua_pop(L, 3);                  // 清除掉栈中多余的3个变量tab、tab.a、tab.b
    getStackSize("stage 3",lua_gettop(L));

    int nTab_c = 2 * nTab_a + nTab_b;
    lua_pushinteger(L, nTab_c);       // 将 c = 2a + b 计算完成,压入栈顶
    printf("nTab_c = %d \n", nTab_c);  // 输出: 5
    
    getStackSize("stage 4",lua_gettop(L));

    lua_getglobal(L,"lua_func");        // 查找lua_func函数并将其压入栈底
    lua_pushinteger(L, 3);              // 压入函数变量 x=3  
    getStackSize("stage 5",lua_gettop(L));

    // lua_pcall 在保护模式下调用一个函数,防止异常程序直接退出
    lua_pcall(L,1,1,0);             // 执行脚本函数lua_func

    getStackSize("stage 6",lua_gettop(L));

    int result = lua_tointeger(L,-1);   // 从栈中取回返回值 
    getStackSize("stage 7",lua_gettop(L));
    printf("res = %d \n", result);  


    lua_pop(L,1);                       // 弹出返回结果
    getStackSize("stage 8",lua_gettop(L));

    lua_close(L);                       //关闭lua环境  
}

int main(int argc, char** argv)
{
    test_api_getfield();
    return 0;
}

lua文件test_fielf.lua

-- tab 表
tab =
{
    a = 2,
    b = 1
}

-- 全局变量c
c = 100;

function lua_func(x)
    -- print("lua c: ", c)
    return (tab.a * x * x + tab.b * x + c)
end

编译方法:
gcc -g -o test_field test_field.c -llua -ldl -lm

运行结果:
Lua C接口编程(一)_第2张图片

示例2:luaL_ref 、luaL_unref、lua_rawgeti 应用

C源文件test_reg_ref.c

#include 

#include 
#include 
#include 

int main()
{
    lua_State *L = luaL_newstate();  
    luaL_openlibs(L);

    if ( LUA_OK != luaL_dofile(L, "test_reg_ref.lua") ) {
        const char* err = lua_tostring(L, -1);
        fprintf(stderr, "err:\t%s\n", err);
        return 1;
    }

    lua_getglobal(L,"foo");
    printf("stack 1 size : %d, %d\n", lua_gettop(L), lua_type(L,-1));

    // 从堆栈顶弹出foo函数,存放到注册表中并返回引用
    // luaL_ref 返回一个int的值。这个返回值就是对应的foo函数的 key  
    int ref =  luaL_ref(L, LUA_REGISTRYINDEX);
    printf("stack 2 size : %d\n", lua_gettop(L));

   // lua_rawgeti(L,LUA_REGISTRYINDEX,ref); 可以从注册表通过key(ref)获取到对应的value(foo函数)
   // 将该函数放入堆栈
    lua_rawgeti(L, LUA_REGISTRYINDEX, ref);
    printf("stack 3 size : %d, %d\n", lua_gettop(L), lua_type(L,-1));

    // 调用foo函数后 foo出栈
    lua_pcall(L,0,0,0);    
    printf("stack 4 size : %d\n", lua_gettop(L));

    printf("----------------分割线 1------------------\n");
    // 继续将foo压入堆栈
    lua_getglobal(L,"foo");  
    printf("stack 5 size : %d\n", lua_gettop(L));


    lua_setfield(L, LUA_REGISTRYINDEX, "fooKey");  //  相当于:LUA_REGISTRYINDEX[fooKey] = foo;
    printf("stack 6 size : %d\n", lua_gettop(L));

    lua_getfield(L, LUA_REGISTRYINDEX, "fooKey");  // 通过 fooKey 从 LUA_REGISTRYINDEX拿到foo 并入压栈
    printf("stack 7 size : %d, %d\n", lua_gettop(L), lua_type(L,-1));

    lua_pcall(L,0,0,0);        // foo call
    printf("stack 8 size : %d, %d\n", lua_gettop(L), lua_type(L,-1));


    printf("----------------分割线 2------------------\n");
    
    // 一旦ref在注册表的引用解除,就无法继续通过key(ref)这个引用获取到 value(即foo函数)

    luaL_unref(L, LUA_REGISTRYINDEX, ref);


    // 从注册表解除后,lua_rawgeti无法再通过key(ref) 找到foo
    lua_rawgeti(L, LUA_REGISTRYINDEX, ref);
    printf("stack 9 size : %d, %d\n", lua_gettop(L), lua_type(L,-1));

    // 通过 fooKey 还是可以继续从LUA_REGISTRYINDEX拿到foo 并入压栈
    lua_getfield(L,LUA_REGISTRYINDEX,"fooKey");
    printf("stack 10 size : %d\n", lua_gettop(L));

    lua_pcall(L,0,0,0);

    printf("---------------- end -----------------\n");
    lua_close(L);
   return 0;
}

Lua文件test_reg_ref.lua

function foo()
    print("test_reg_ref foo call")
end

编译方法:
gcc -g -o test_reg_ref test_reg_ref.c -llua -ldl -lm
运行结果:
Lua C接口编程(一)_第3张图片
推荐一个零声学院免费教程,个人觉得老师讲得不错,
分享给大家:[Linux,Nginx,ZeroMQ,MySQL,Redis,
fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,
TCP/IP,协程,DPDK等技术内容,点击立即学习:

你可能感兴趣的:(Lua从入门到精通,lua,c语言)