到目前为止,我们写的C函数都还是单个的函数,函数之间没有联系,也没有涉及返回C结构体以及指针,但实际的业务场景中肯定不会这么简单,比如我们现在要扩展lua使之能够操作redis,首先至少需要一个连接redis服务器的方法并返回一个连接句柄,然后用这个连接句柄来进行各种查询,最后使用结束还需要关闭这个连接句柄,通常来说,这个连接句柄会是一个结构体,那问题是该怎么返回这个结构体给lua呢。
用C操作Redis大概像这样,这里用redis官方提供的hiredis API进行演示:
#include <stdio.h>
#include <hiredis/hiredis.h>
#include <stdlib.h>
int main()
{
redisContext *conn = redisConnect("127.0.0.1", 6379);
if (conn == NULL)
{
printf("connect fail\n");
exit(EXIT_FAILURE);
}
if (conn->err)
{
printf("connect fail: %s\n", conn->errstr);
exit(EXIT_FAILURE);
}
redisReply *rep = redisCommand(conn, "SET foo bar");
if (rep->type == REDIS_REPLY_ERROR)
{
fprintf(stderr, "redis set error: %s\n", rep->str);
}
freeReplyObject(rep);
rep = redisCommand(conn, "GET foo");
if(rep->type == REDIS_REPLY_STRING)
{
printf("redis get: %s\n", rep->str);
}
freeReplyObject(rep);
redisFree(conn);
return 0;
}
redis get: bar
现在的问题就是该如何返回这个连接句柄的结构体指针,这就要提到lua API 中的userdata数据结构了,userdata分配了一块原始的内存,可以用来存放任何东西,并且,在Lua中userdata 没有任何预定义的操作。
void *lua_newuserdata(luaState *L, size_t size);
上述函数会根据制定的size大小分配一块内存,并将这个userdata压入栈中,然后返回userdata的地址,所以这个函数有点类似于C函数中的malloc,不过他还会将这个userdata压入栈中。
针对示例中的连接句柄,我们可以分配一块redisContext * 结构体指针大小的userdata,该userdata是一个二级指针,利用该二级指针存储连接返回的结构体指针,这个lua连接Redis的函数可以这样写:
#include <lauxlib.h>
#include <hiredis/hiredis.h>
static int connect(lua_State *L)
{
const char *ip = lua_tostring(L, 1);
int port = lua_tointeger(L, 2);
redisContext *conn = redisConnect(ip, port);
if (conn == NULL)
{
lua_pushnil(L);
lua_pushstring(L, "connect server fail");
return 2;
}
if (conn->err)
{
lua_pushnil(L);
lua_pushstring(L, conn->errstr);
return 2;
}
redisContext **rcp = (redisContext **)lua_newuserdata(L, sizeof(redisContext *));
*rcp = conn; //存储连接句柄指针
return 1; //返回这个userdata
}
static const struct luaL_Reg funcs[] = {
{"connect", connect},
{NULL, NULL},
};
int luaopen_redis(lua_State *L)
{
luaL_register(L, "redis", funcs);
return 1;
}
假设现在这个模块编译后的名字叫redis.so, 在Lua中我们就可以这样来调用这个方法
local redis = require "redis"
local conn = redis.connect("127.0.0.1", 6379)
print(type(conn)) --userdata
现在我们在模块中添加执行查询和关闭连接的方法,假设这两个方法分别叫query和close:
#include <lauxlib.h>
#include <hiredis/hiredis.h>
static int connect(lua_State *L)
{
const char *ip = lua_tostring(L, 1);
int port = lua_tointeger(L, 2);
redisContext *conn = redisConnect(ip, port);
if (conn == NULL)
{
lua_pushnil(L);
lua_pushstring(L, "connect server fail");
return 2;
}
if (conn->err)
{
lua_pushnil(L);
lua_pushstring(L, conn->errstr);
return 2;
}
redisContext **rcp = (redisContext **)lua_newuserdata(L, sizeof(redisContext *));
*rcp = conn; //存储连接句柄指针
return 1; //返回这个userdata
}
static int query(lua_State *L)
{
redisContext **conn = (redisContext **)lua_touserdata(L, 1); //lua调用传入的连接句柄
const char *cmd = lua_tostring(L, 2); //查询语句
redisReply *rep = redisCommand(*conn, cmd);
if (rep == NULL)
{
lua_pushnil(L);
lua_pushstring(L, "query fail");
return 2;
}
switch(rep->type)
{
case REDIS_REPLY_ERROR:
lua_pushnil(L);
lua_pushlstring(L, rep->str, rep->len);
break;
case REDIS_REPLY_STATUS:
case REDIS_REPLY_STRING:
lua_pushstring(L, "string");
lua_pushlstring(L, rep->str, rep->len);
break;
case REDIS_REPLY_NIL: //返回空结果,比如GET一个不存在的key
lua_pushstring(L, "nil");
lua_pushnil(L);
break;
case REDIS_REPLY_INTEGER: // 返回数字,比如HSET一个新的field
lua_pushstring(L, "integer");
lua_pushinteger(L, rep->integer);
break;
case REDIS_REPLY_ARRAY:
lua_pushstring(L, "table");
lua_newtable(L);
//todo
break;
}
freeReplyObject(rep);
return 2;
}
static int close(lua_State *L)
{
redisContext **conn = (redisContext **)lua_touserdata(L, 1);
redisFree(*conn); //关闭连接, userdata内存lua会进行管理,不需要我们手动释放
return 0;
}
static const struct luaL_Reg funcs[] = {
{"connect", connect},
{"query", query},
{"close", close},
{NULL, NULL},
};
int luaopen_redis(lua_State *L)
{
luaL_register(L, "redis", funcs);
return 1;
}
这个查询方法返回两个值,第一个返回值表明结果的类型,如果是nil表明查询失败,第二个返回值是结果,对于返回的array类型由于比较麻烦这里暂不做处理,编译成redis.so 编写lua代码再测试下
local redis = require "redis"
local conn = redis.connect("127.0.0.1", 6379)
print(type(conn))
local rtype, res = redis.query(conn, "set name tom")
print(rtype, res)
rtype, res = redis.query(conn, "GET name")
print(rtype, res)
rtype, res = redis.query(conn, "HSET hset1 user Jim")
print(rtype, res)
rtype, res = redis.query(conn, "HGET hset1 user")
print(rtype, res)
rtype, res = redis.query(conn, "HGET hset1 age")
print(rtype, res) -- rtype = "nil" res = nil
rtype, res = redis.query(conn, "HGET name gender")
print(rtype, res) -- rtype = nil 此时res为报错信息
redis.close(conn)
运行结果:
[root@localhost lua]# luajit red.lua
userdata
string OK
string tom
integer 0
string Jim
nil nil
nil ERR Operation against a key holding the wrong kind of value
上述的C模块没有实现对 REDIS_REPLY_ARRAY 类型的返回结果处理,如果有兴趣,大家可以自行实现,或者参考我的Github上对该模块更完整的实现代码, that‘s all ^^ !