skynet中upvalue的用法

Jun 25, 2014


今天花了几十分钟看了一下skynet中的lua-clientsocket.c文件,其实在实际项目中,这一部分代码是需要重写的,所以也就大致看了一下流程。仔细看了这个文件中的lreadline,这个函数的实现比较巧妙,主要涉及到lua中的upvalue,之前还没有接触到upvalue,就当学习了一下upvalue的用法。

在客户端,是这么调用:

local socket = require("clientsocket");
socket.readline();

先看下luaopen_clientsocket的源代码:

int luaopen_clientsocket(lua_State *L) 
{
	 luaL_checkversion(L);
	 luaL_Reg l[] = 
	 {
		  { "connect", lconnect },
		  { "recv", lrecv },
		  { "send", lsend },
		  { "close", lclose },
		  { "usleep", lusleep },
		  { NULL, NULL },
	 };
	 luaL_newlib(L, l);

	 struct queue * q = lua_newuserdata(L, sizeof(*q));
	 memset(q, 0, sizeof(*q));
	 lua_pushcclosure(L, lreadline, 1); //说明lreadline函数内部只有一个upvalue值
	 lua_setfield(L, -2, "readline");  // socket.readline = lreadline

	 pthread_t pid ;
	 pthread_create(&pid, NULL, readline_stdin, q);

	return 1;
}

在上面的函数,首先将几个函数加入了lua虚拟机中。注意lreadline并没有添加进去,而是在后面单独做了处理。lua_newuserdata首先申请内存返回一个struct queue指针。然后将一个C函数作为closure压入栈中,接下来:

tb["readline"] = lreadline;

这样使得我们可以在lua中直接使用socket.readline()

在上面的代码中lua_newuserdata会将已经创建的内存块压入堆栈,然后lua_pushcclosure,这里的巧妙就是将创建的*q作为一个upvalue设置到lreadline函数内部,upvalue针对的是值,也就是说lua_pushcclosure直接将内存块当成了lreadlineupvalue,而不是qq只是作为一个指针引用*q存在的。

static int lreadline(lua_State *L) 
{
	 // 找到了在luaopen_clientsocket中push到栈中的struct queue * q
	 struct queue *q = lua_touserdata(L, lua_upvalueindex(1));
	 LOCK(q);
	 if (q->head == q->tail) 
	 {
		  UNLOCK(q);
		  return 0;
	 }
	 char * str = q->queue[q->head];
	 if (++q->head >= QUEUE_SIZE) 
	 {
		  q->head = 0;
	 }
	 UNLOCK(q);
	 lua_pushstring(L, str);
	 free(str);
	 return 1;
}

上面lua_upvalueindex返回了函数环境中index为1的upvalue。然后对返回的upvalue进行操作。

luaopen_clientsocket只是创建了一个没有内容的struct queue,*q中的内容从哪里来呢。在readline_stdin线程中,会去读取console上的内容放在*q中,上面说了upvalue针对的是值而不是“引用”,因为我们在lua中可以通过socket.readline读取到内容。

这里利用到了lua中的upvalue。在lua中,函数有自己的上下文环境,特别是closure,当执行一个closure的时候lua虚拟机会创建一个新的data object,它包含了相应函数原型的引用,环境的引用以及一个由所有upvalue组成的table,这些upvalue只对这个closure可见。

function f1(n)
    local c1 = function ()
        print(n);
    end
    n = n + 10;
    return c1;
end
local c1 = f1(1943);
c1(); -- 1953

在上面的代码中,c1 closure可见n变量,在f1返回时n即将被gc的时候,由于此时c1 closure会引用到n,所以lua会将n作为一个upvalue存在c1的data object中,这也就是c1();时不会报错并且打印出1953.

通过上面就可以知道,upvalue可以实现数据共享的机制。即在多个closure下,在不使用全局变量或者静态变量的情况下实现数据共享。skynet中lreadline的实现就是如此。先将*q作为upvalue pushlreadline函数中,然后在线程中修改*q,当调用lreadline时直接将*q中的内容返回给lua。在没有使用全局变量和静态变量的情况下实现了数据共享,真是巧妙啊,云风果然是厉害。