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\n",
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\n", 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\n", 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 ----\n", 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("\n");
}
printf("---- End Stack ----\n\n");
} 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'\n", lua_tostring(L, -2), lua_tostring(L, -1));
break;
case LUA_TNUMBER:
printf("table[%g]='%s'\n", 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\n", 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\n", i, obj_s);
}
return 1;
}

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

  • 执行前:Lua环境里某些东西只初始化一次,准备KEYS和ARGV两个全局变量,设置超时控制hook。
  • 执行后:定时Lua GC回收资源,用字典缓存已经执行过的Lua脚本。

Redis源码学习:Lua脚本的更多相关文章

  1. redis源码学习之slowlog

    目录 背景 环境说明 redis执行命令流程 记录slowlog源码分析 制造一条slowlog slowlog分析 1.slowlog如何开启 2.slowlog数量限制 3.slowlog中的耗时 ...

  2. Redis源码学习:字符串

    Redis源码学习:字符串 1.初识SDS 1.1 SDS定义 Redis定义了一个叫做sdshdr(SDS or simple dynamic string)的数据结构.SDS不仅用于 保存字符串, ...

  3. 柔性数组(Redis源码学习)

    柔性数组(Redis源码学习) 1. 问题背景 在阅读Redis源码中的字符串有如下结构,在sizeof(struct sdshdr)得到结果为8,在后续内存申请和计算中也用到.其实在工作中有遇到过这 ...

  4. __sync_fetch_and_add函数(Redis源码学习)

    __sync_fetch_and_add函数(Redis源码学习) 在学习redis-3.0源码中的sds文件时,看到里面有如下的C代码,之前从未接触过,所以为了全面学习redis源码,追根溯源,学习 ...

  5. redis源码学习之lua执行原理

    聊聊redis执行lua原理 从一次面试场景说起   "看你简历上写的精通redis" "额,还可以啦" "那你说说redis执行lua脚本的原理&q ...

  6. redis源码学习之工作流程初探

    目录 背景 环境准备 下载redis源码 下载Visual Studio Visual Studio打开redis源码 启动过程分析 调用关系图 事件循环分析 工作模型 代码分析 动画演示 网络模块 ...

  7. Redis源码学习-Master&Slave的命令交互

    0. 写在前面 Version Redis2.2.2 Redis中可以支持主从结构,本文主要从master和slave的心跳机制出发(PING),分析redis的命令行交互. 在Redis中,serv ...

  8. redis源码学习-skiplist

    1.初步认识跳跃表 图中所示,跳跃表与普通链表的区别在于,每一个节点可以有多个后置节点,图中是一个4层的跳跃表 第0层: head->3->6->7->9->12-> ...

  9. Redis源码学习1-sds.c

    https://github.com/huangz1990/redis-3.0-annotated/blob/unstable/src/sds.c#L120 /* SDSLib, A C dynami ...

随机推荐

  1. 处理异常、常用类、反射、类加载与垃圾回收、java集合框架

    异常处理概述 检查异常:检查异常通常是用户错误或者不能被程序员所预见的问题.(cheched) 运行时异常:运行时异常是一个程序在运行过程中可能发生的.可以被程序员避免的异常类型.(Unchecked ...

  2. javascript 函数的4种调用方式与 this(上下文)的指向

    前言:这是笔者学习之后自己的理解与整理.如果有错误或者疑问的地方,请大家指正,我会持续更新! javascript中作用域链和this(上下文)的指向是很容易混淆的,简单的说就是: 作用域链取决于函数 ...

  3. vue2.0 带头冲锋(打包时,小心萝卜坑)

    距离上一期,时间间距可能有点长.谁让本人处于兴奋状态,生活已经不能自理. 哈哈哈,嗯,正经一下, 在已有的经验里总结一下那些容易抓狂的坑! 起因:npm run build 打包 本地运行,你以为可以 ...

  4. WPF中自定义GridLengthAnimation

    需求 我们想在编辑一个列表中某一个条目时,将编辑的详情内容也放置当前面,比如右侧. 可以通过将一个Grid,分成两个Cloumn,动态调整两个Cloumn的Width,就可以实现这个需求. 我们知道, ...

  5. Linux OpenGL 实践篇-3 绘制三角形

    本次实践是绘制两个三角形,重点理解顶点数组对象和OpenGL缓存的使用. 顶点数组对象 顶点数组对象负责管理一组顶点属性,顶点属性包括位置.法线.纹理坐标等. OpenGL缓存 OpenGL缓存实质上 ...

  6. MongoDB系列六(聚合).

    一.概念 使用聚合框架可以对集合中的文档进行变换和组合.基本上,可以用多个构件创建一个管道(pipeline),用于对一连串的文档进行处理.这些构件包括筛选(filtering).投射(project ...

  7. Mlecms Getshell

    参考来源:https://bbs.ichunqiu.com/thread-13703-1-1.html 位于:/inc/include/globals.php 第24-28行.有个任意变量覆盖. fo ...

  8. [BZOJ]1027 合金(JSOI2007)

    不知道该如何评价吧,很神的一道题,就算是10年前的题目也不可小觑啊. Description 某公司加工一种由铁.铝.锡组成的合金.他们的工作很简单.首先进口一些铁铝锡合金原材料,不同种类的原材料中铁 ...

  9. zoj2112 树状数组+主席树 区间动第k大

    Dynamic Rankings Time Limit: 10000MS   Memory Limit: 32768KB   64bit IO Format: %lld & %llu Subm ...

  10. Debugging TensorFlow models 调试 TensorFlow 模型

    Debugging TensorFlow models Symbolic nature of TensorFlow makes it relatively more difficult to debu ...