• Redis源码学习:Lua脚本


    Redis源码学习:Lua脚本

    1.Sublime Text配置

    我是在Win7下,用Sublime Text + Cygwin开发的,配置方法请参考《Sublime Text 3下C/C++开发环境搭建》

    要注意的是:在Cygwin中安装Lua解析器后,SublimeClang插件就能识别出可饮用的Lua头文件了,因为Build System中我们已经配置过"-I", "D:\cygwin64\usr\include",而新安装的Lua头文件会添加到这里。但是,编译时却无法链接到头文件对应的动态链接库。此时,还需要添加一个链接选项lua5.1,修改后的完整Build System配置文件如下:

    {
        "path": "D:\cygwin64\bin",
        "cmd": ["gcc", "-I", "D:\cygwin64\usr\include", "${file}", "-o", "${file_path}/${file_base_name}", "-lm", "-llua5.1", "-Wall", "&", "start", "${file_path}/${file_base_name}.exe"],
        "file_regex": "^(..[^:]*):([0-9]+):?([0-9]+)?:? (.*)$",
        "working_dir": "${file_path}",
        "selector": "source.c, source.c++",
        "shell": true,
        "variants":
        [
           {
                "name": "Run::Cygwin",
                "cmd": [ "start", "${file_path}/${file_base_name}.exe"]
           }
        ]
    }

    2.Lua基础

    2.1 执行脚本

    首先创建一个最简单的helloworld脚本hello.lua:

    print("helloworld!")

    下面详细解释一下从C代码中如何执行Lua脚本文件。不管是如何执行,Lua脚本的执行过程都分为以下五步。以下面一段代码框架适用于后面所有示例程序:

    • 初始化解释器:lua_open是一个宏定义,等同于luaL_newstate()。创建出的lua_state也暗示了,Lua解释器不使用C全局变量,而是将所有状态都保存到lua_state这个数据结构中。
    • 加载类库:luaL_openLibs()加载常用类库,如core、table、string、math等等。
    • 加载并编译代码/脚本文件:通常由luaL_loadfile()或luaL_loadbuffer()来完成,注意这只会将Lua代码编译好,并不会真正执行。下面例子中lua_dofile等同于luaL_loadfile(L, fn) || lua_pcall(L, 0, LUA_MULTRET, 0),两步合并为一步了。
    • 执行代码/脚本文件:由lua_pcall()完成,会根据当前栈上的函数名、参数执行。当错误时处理方式与上一步加载雷同,都是打印异常日志,然后从栈上弹出错误处理器,最后直接返回或退出。
    • 清理释放:lua_close()清理释放解释器占用的资源。
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <lua.h>
    #include <lualib.h>
    #include <lauxlib.h>
    
    void execute_from_script(char *filename);
    
    int main(int argc, char const *argv[])
    {
        execute_from_script("hello.lua");
        return 0;
    }
    
    /**
     * Execute from Lua script.
     * @param filename  script file name
     */
    void execute_from_script(char *filename) 
    {
        /* Lua interpreter */
        lua_State *lua = lua_open();
    
        /* Open Lua standard lib: io, string... */
        luaL_openlibs(lua);
    
        /* Execute code in script */
        if (luaL_dofile(lua, filename)) {
            fprintf(stderr, "Error when executing script: %s, %s
    ", 
                        filename, lua_tostring(lua, -1));
            /* Remove error handler */
            lua_pop(lua, 1);
            return;
        }
    
        /* Release all resource used */
        lua_close(lua);
    }

    2.2 执行代码

    为了简化后面的示例代码,对错误处理统一封装成bail()函数:

    void bail(lua_State *lua, char *msg, char *arg) 
    {
        fprintf(stderr, "%s %s: %s
    ", msg, arg, lua_tostring(lua, -1));
        exit(-1);
    }

    这一次我们不单独创建一个Lua脚本文件,而是将Lua代码嵌入到C代码中直接执行!

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <lua.h>
    #include <lualib.h>
    #include <lauxlib.h>
    
    void execute_from_code(char *code);
    
    int main(int argc, char const *argv[])
    {
        execute_from_code("print("hello world!!!")");
        return 0;
    }
    
    /**
     * Execute Lua command directly.
     * @param code  Lua command
     */
    void execute_from_code(char *code)
    {
        lua_State *lua = lua_open();
        luaL_openlibs(lua);
    
        // Load & compile command and execute immediately
        if (luaL_loadbuffer(lua, code, strlen(code), "line") 
                || lua_pcall(lua, 0, 0, 0))
            bail(lua, "Error when executing code", code);
    
        lua_close(lua);
    }

    2.3 执行函数

    在这个例子中,我们执行脚本文件中的函数,而不是直接一段Lua代码。在C代码中调用Lua函数时,如何传入参数值和获取返回值是学习的重点:

    Lua脚本如下:

    function say_hello(name)
        return "Hello, " .. name .. "!"
    end

    C示例代码如下。注意加载并编译函数后,lua_getglobal(lua, funcname)是关键,这一句会在全局中查找函数,并将函数的指针压到栈上。这样后面调用lua_pcall()时才不会报错:

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <lua.h>
    #include <lualib.h>
    #include <lauxlib.h>
    
    void execute_function_from_script(char *filename, char *funcname, char *arg);
    void execute_function_from_code(char *code);
    
    int main(int argc, char const *argv[])
    {
        execute_function_from_script("hellofunc.lua", "say_hello", "cdai008");
        return 0;
    }
    
    /**
     * Execute Lua function from script
     * @param filename  script file name
     * @param funcname  function name
     * @param arg       arguments
     */
    void execute_function_from_script(char *filename, char *funcname, char *arg)
    {
        lua_State *lua = lua_open();
        luaL_openlibs(lua);
    
        /* 1.Load and compile function code */
        if (luaL_loadfile(lua, filename) || lua_pcall(lua, 0, 0, 0))
            bail(lua, "Error when loading/compiling function", filename);
    
        /* 2.Prepare function and arguments */
        lua_getglobal(lua, funcname);
        lua_pushstring(lua, arg);
    
        /* 3.Do the call (1 arg, 1 result) */
        if (lua_pcall(lua, 1, 1, 0) != 0)
            bail(lua, "Error when calling function", funcname);
    
        /* 4.Retrieve result */
        char *ret = lua_tostring(lua, -1);
        printf("Result: %s
    ", ret);
    
        lua_pop(lua, 1);
        lua_close(lua);
    }

    3.深入理解Lua栈

    3.1 关于栈的事实

    首先看几条关于Lua栈的事实:

    • Lua脚本与其他编程语言交换数据的唯一方式
    • lua_state创建后就存在,独立于任何脚本或函数
    • 栈中元素不能修改,只能被替换或移除
    • 栈中元素可以是各种数据类型的

    3.2 “讨厌”的栈顺序

    Lua栈最让人困惑的就是栈操作函数中的下标参数,有的用正数有的用负数。Lua官方文档中解释说:lua_gettop()返回栈中元素个数,也就是栈顶元素的下标。负数下标negative_i = positive_i - (gettop() + 1)这一点与Redis的List数据结构很像,例如当查看List中所有元素时,为了方便我们会用lrange lista 0 -1,而不会将-1写成真的去求一下末尾元素的下标。

    下面看一段示例代码,加深一下理解:

    static void stackDump(lua_State *L)
    {
        int i;
        int top = lua_gettop(L);
        printf("---- Begin Stack %i ----
    ", top);
        for (i = 1; i <= top; i++) {
            int t = lua_type(L, i);
            int ni = i - (top + 1);
            switch (t) {
              case LUA_TSTRING:     /* strings */
                printf("%i -- (%i) ---- '%s'", i, ni, lua_tostring(L, i));
                break;
              case LUA_TBOOLEAN:    /* booleans */
                printf("%i -- (%i) ---- %s", i, ni, lua_toboolean(L, i) ? "true" : "false");
                break;
              case LUA_TNUMBER:     /* numbers */
                printf("%i -- (%i) ---- %g", i, ni, lua_tonumber(L, i));
                break;
              default:              /* other values */
                printf("%i -- (%i) ---- '%s'", i, ni, lua_typename(L, t));
                break;
            }
            printf("
    ");
        }
        printf("---- End Stack ----
    
    ");
    }
    
    void test_lua_stack_order()
    {
        lua_State *L = lua_open();
    
        lua_pushstring(L, "hi there");
        lua_pushnumber(L, 17);
        lua_pushboolean(L, 1);
        lua_pushstring(L, "foobar");
        stackDump(L);
        /*
            ---- Begin Stack 4 ----
            1 -- (-4) ---- 'hi there'
            2 -- (-3) ---- 17
            3 -- (-2) ---- true
            4 -- (-1) ---- 'foobar'
            ---- End Stack ----
        */
    
        lua_pushvalue(L, -4); 
        stackDump(L);
        /*
            ---- Begin Stack 5 ----
            1 -- (-5) ---- 'hi there'
            2 -- (-4) ---- 17
            3 -- (-3) ---- true
            4 -- (-2) ---- 'foobar'
            5 -- (-1) ---- 'hi there'
            ---- End Stack ----
         */
    
        lua_replace(L, 3); 
        stackDump(L);
        /*
            ---- Begin Stack 4 ----
            1 -- (-4) ---- 'hi there'
            2 -- (-3) ---- 17
            3 -- (-2) ---- 'hi there'
            4 -- (-1) ---- 'foobar'
            ---- End Stack ----
         */
    
        lua_settop(L, 6); 
        stackDump(L);
        /*
            ---- Begin Stack 6 ----
            1 -- (-6) ---- 'hi there'
            2 -- (-5) ---- 17
            3 -- (-4) ---- 'hi there'
            4 -- (-3) ---- 'foobar'
            5 -- (-2) ---- 'nil'
            6 -- (-1) ---- 'nil'
            ---- End Stack ----
         */
    
        lua_remove(L, -3); 
        stackDump(L);
        /*
            ---- Begin Stack 5 ----
            1 -- (-5) ---- 'hi there'
            2 -- (-4) ---- 17
            3 -- (-3) ---- 'hi there'
            4 -- (-2) ---- 'nil'
            5 -- (-1) ---- 'nil'
            ---- End Stack ----
         */
    
        lua_settop(L, -5); 
        stackDump(L);
        /*
            ---- Begin Stack 1 ----
            1 -- (-1) ---- 'hi there'
            ---- End Stack ----
         */
    
        lua_close(L);
    }

    注意栈操作函数中参数的意义:

    • lua_pop(L, n):参数指定的是弹出元素个数,想移除指定下标的元素要用lua_remove(L, x)。
    • lua_pushvalue(L, x):将指定下标x的元素拷贝到栈顶,而不是压入一个整数x。
    • lua_replace(L, x):移动栈顶元素到指定下标x。

    3.3 栈与table

    table在栈上的创建方式有些tricky。首先lua_newtable()会压入“table”到栈顶,然后依次压入key-value键值对,然后调用lua_settable()会使键值对被弹出,形成真正的table。此时,栈上又只剩字符串“table”了。数据跑哪里去了?此时要使用lua_next()函数对table进行遍历:

        lua_newtable(L);
        lua_pushnumber(L, 1);
        lua_pushstring(L, "allen");
        stackDump(L);
        lua_settable(L, -3);
        stackDump(L);
        /*
            ---- Begin Stack 4 ----
            1 -- (-4) ---- 'hi there'
            2 -- (-3) ---- 'table'
            3 -- (-2) ---- 1
            4 -- (-1) ---- 'allen'
            ---- End Stack ----
    
            ---- Begin Stack 2 ----
            1 -- (-2) ---- 'hi there'
            2 -- (-1) ---- 'table'
            ---- End Stack ----
         */
        lua_pushstring(L, "hank");
        /* set table at index -2, table["2"]="hank" */
        lua_setfield(L, -2, "2");
        lua_pushstring(L, "carter");
        /* set table at index -2, table[3]="carter" */
        lua_rawseti(L, -2, 3);
    
        /* Push nil as first key */
        lua_pushnil(L);
        /* Pops a key from the stack, and pushes a key–value pair from the table 
            at the given index (the "next" pair after the given key) */
        while(lua_next(L, -2) != 0) {
            /* uses 'key' (at index -2) and 'value' (at index -1) */
            int t = lua_type(L, -2);
            switch (t) {
              case LUA_TSTRING:
                printf("table['%s']='%s'
    ", lua_tostring(L, -2), lua_tostring(L, -1));
                break;
              case LUA_TNUMBER:
                printf("table[%g]='%s'
    ", lua_tonumber(L, -2), lua_tostring(L, -1));
                break;
            }
            /* removes 'value'; keeps 'key' for next iteration */
            lua_pop(L, 1);
        }

    答案就在lua_next()中。我们在栈顶放置了table和一个nil,然后调用lua_next(),并访问key和value后移除栈顶的value而保留key,这样就能依次迭代整个table。注意lua_settable()、lua_setfield()和lua_rawseti()三个函数的用法。

    4.Redis中的Lua

    终于到了本文的重点,模拟一下Redis是如何执行Lua脚本的,分为以下几步:

    • 准备Lua环境:这一步很简单,就是创建Lua解释器和加载类库。
    • 动态创建函数:Redis会将脚本中的代码包装成一个函数,并生成一个函数名。
    • 加载编译函数:这一步与之前完全相同。
    • 准备表对象:创建redis表对象,并将其与函数指针一起压到栈上。
    • 执行函数:这一步与之前完全相同。
    • 清理释放:这一步与之前完全相同。

    核心部分的C示例代码:

    /**
     * Showcase of how to deal with return values
     * @param cmd   Lua command
     */
    void execute_function_from_code(char *cmd)
    {
        // 1.Prepare Lua execution enviornment
        lua_State *lua = lua_open();
        luaL_openlibs(lua);
    
        // 2.Create function dynamically
        char funcdef[100], *funcname = "fun1";
        memset(funcdef, 0, sizeof(funcdef));
        strcat(funcdef, "function ");
        strcat(funcdef, funcname);
        strcat(funcdef, "() ");
        strcat(funcdef, cmd);
        strcat(funcdef, " end");
        printf("Code: %s
    ", funcdef);
    
        // 3.Compile code in buffer and push onto stack
        if(luaL_loadbuffer(lua, funcdef, strlen(funcdef), "@user_script")
                || lua_pcall(lua, 0, 0, 0))
            bail(lua, "Error when loading/compiling function", funcname);
    
        // 4.Prepare function and global table 'redis'
        lua_getglobal(lua, funcname);
        lua_newtable(lua);
        lua_pushstring(lua,"call");
        lua_pushcfunction(lua, luaRedisCallCommand);
        lua_settable(lua, -3);
        lua_setglobal(lua, "redis");
    
        // 5.Execute Lua function
        if (lua_pcall(lua, 0, 0, -2))
            bail(lua, "Error when calling function", funcname);
    
        // 6.Cleanup
        lua_close(lua);
    }

    测试main函数和回调函数。main函数测试一下在Lua代码中执行redis.call(“set”, “foo”, “bar”),而回调函数luaRedisCallCommand()则简单地打印一下入参:

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <lua.h>
    #include <lualib.h>
    #include <lauxlib.h>
    
    void execute_function_from_code(char *code);
    
    int main(int argc, char const *argv[])
    {
        execute_function_from_code("redis.call("set", "foo", "bar")");
        return 0;
    }
    
    int luaRedisCallCommand(lua_State *lua) 
    {
        int i, argc = lua_gettop(lua);
        for (i = 0; i < argc; i++) {
            char *obj_s;
            size_t obj_len;
    
            obj_s = (char *)lua_tolstring(lua, i + 1, &obj_len);
            printf("Argument[%d]=%s
    ", i, obj_s);
        }
        return 1;
    }

    这里只是一个演示的小例子,详细介绍请参考《Redis设计与实现》。但Lua脚本这一章是免费Web版里没有的,得看实体书。真正的Redis代码流程要复杂得多,包括:

    • 执行前:Lua环境里某些东西只初始化一次,准备KEYS和ARGV两个全局变量,设置超时控制hook。
    • 执行后:定时Lua GC回收资源,用字典缓存已经执行过的Lua脚本。
  • 相关阅读:
    WP开发笔记——页面传参
    WP开发笔记——控件倾斜效果
    WP开发笔记——不同Item显示不同ApplicationBar:适用于Pivot与Panorama
    WP开发笔记——WP APP添加页面跳转动画
    WP开发笔记——去除 HTML 标签
    WP开发笔记——字符串 转 MD5 加密
    WP开发笔记——WP7 SDK使用技巧
    sphinx,coreseek安装
    yii2从零开始一,安装
    array_filter函数
  • 原文地址:https://www.cnblogs.com/xiaomaohai/p/6157622.html
Copyright © 2020-2023  润新知