From 6df557bbc720ee2a942bf1f17ca7e4a453db9852 Mon Sep 17 00:00:00 2001 From: Konstantin Osipov <kostja.osipov@gmail.com> Date: Fri, 19 Aug 2011 20:19:18 +0400 Subject: [PATCH] Lua: improve the way Lua procedures return results. Instead of using "print" and fiber->iov to return results to the client, change the Lua procedures implementation to simply send to the client all return values of a procedure. A procedure, in turn, can return tuples, integers (and, in future, strings, Lua tables or whatever). This allows nicely to call one procedure from another, and use its results in Lua, and call the very same procedure from the binary protocol, and still be able to process its results on the client. --- core/tarantool_lua.m | 51 ++++++++++- include/tarantool.h | 8 -- mod/box/box.h | 1 + mod/box/box.m | 31 ------- mod/box/box_lua.m | 211 +++++++++++++++++++++++++++++++++++++++++-- mod/box/tuple.m | 2 +- test/box/lua.result | 22 ++--- test/lib/sql_ast.py | 1 + 8 files changed, 262 insertions(+), 65 deletions(-) diff --git a/core/tarantool_lua.m b/core/tarantool_lua.m index d345bc8be9..088839465a 100644 --- a/core/tarantool_lua.m +++ b/core/tarantool_lua.m @@ -142,9 +142,52 @@ tarantool_lua_init() luaL_register(L, "box", boxlib); L = mod_lua_init(L); } + lua_settop(L, 0); /* clear possible left-overs of init */ return L; } +/* + * lua_tostring does no use __tostring metamethod, and it has + * to be called if we want to print Lua userdata correctly. + */ +const char *tarantool_lua_tostring(struct lua_State *L, int index) +{ + lua_pushstring(L, "tostring"); + lua_gettable(L, LUA_GLOBALSINDEX); + lua_pushvalue(L, index); + lua_call(L, 1, 1); /* pops both "tostring" and its argument */ + lua_replace(L, index); + return lua_tostring(L, index); +} + +/* + * Convert Lua stack to YAML and append to + * the given tbuf. + */ +void tarantool_lua_printstack(struct lua_State *L, struct tbuf *out) +{ + for (int i = 1; i <= lua_gettop(L); i++) + tbuf_printf(out, " - %s\r\n", tarantool_lua_tostring(L, i)); +} + +/** + * Attempt to append 'return ' before the chunk: if the chunk is + * an expression, this pushes results of the expression onto the + * stack. If the chunk is a statement, it won't compile. In that + * case try to run the original string. + */ +int tarantool_lua_dostring(struct lua_State *L, const char *str) +{ + struct tbuf *buf = tbuf_alloc(fiber->gc_pool); + tbuf_printf(buf, "%s%s", "return ", str); + int r = luaL_loadstring(L, tbuf_str(buf)); + if (r) { + lua_pop(L, 1); /* pop the error message */ + return luaL_dostring(L, str); + } + return lua_pcall(L, 0, LUA_MULTRET, 0); +} + void tarantool_lua(struct lua_State *L, struct tbuf *out, const char *str) @@ -152,17 +195,15 @@ tarantool_lua(struct lua_State *L, lua_pushstring(L, "out"); lua_pushlightuserdata(L, (void *) out); lua_settable(L, LUA_REGISTRYINDEX); - assert(fiber->iov->len == 0 && fiber->iov_cnt == 0); - int r = luaL_dostring(L, str); + int r = tarantool_lua_dostring(L, str); if (r) { /* Make sure the output is YAMLish */ tbuf_printf(out, "error: '%s'\r\n", luaL_gsub(L, lua_tostring(L, -1), "'", "''")); - lua_pop(L, lua_gettop(L)); } else { - mod_convert_iov_to_yaml(out); + tarantool_lua_printstack(L, out); } - iov_reset(); + lua_settop(L, 0); /* clear the stack from return values. */ } diff --git a/include/tarantool.h b/include/tarantool.h index 20004319b4..f2e484809a 100644 --- a/include/tarantool.h +++ b/include/tarantool.h @@ -49,14 +49,6 @@ void mod_info(struct tbuf *out); * @return L on success, 0 if out of memory */ struct lua_State *mod_lua_init(struct lua_State *L); -/* - * A running stored procedure can attempt to send - * tuples and other fields to the client. - * This is all cached in fiber->iov vector in the format - * of the binary protocol. Convert this output to YAML - * for the administrative console. - */ -void mod_convert_iov_to_yaml(struct tbuf *out); /** * Create an instance of Lua interpreter and load it with diff --git a/mod/box/box.h b/mod/box/box.h index c1392d87c4..8ca28ca920 100644 --- a/mod/box/box.h +++ b/mod/box/box.h @@ -58,6 +58,7 @@ struct box_out { }; extern struct box_out box_out_quiet; +extern struct box_out box_out_iproto; struct box_txn { u16 op; diff --git a/mod/box/box.m b/mod/box/box.m index 1bbc68debf..888a98c463 100644 --- a/mod/box/box.m +++ b/mod/box/box.m @@ -597,7 +597,6 @@ struct box_out box_out_quiet = { struct box_txn * txn_begin() { - assert(in_txn() == NULL); struct box_txn *txn = p0alloc(fiber->gc_pool, sizeof(*txn)); txn->ref_tuples = tbuf_alloc(fiber->gc_pool); assert(fiber->mod_data.txn == NULL); @@ -1458,33 +1457,3 @@ mod_info(struct tbuf *out) recovery_state->recovery_last_update_tstamp); tbuf_printf(out, " status: %s" CRLF, status); } - -/* - * Convert fiber->iov to yaml and append to - * the given tbuf. - */ -void mod_convert_iov_to_yaml(struct tbuf *out) -{ - for (int i = 0; i < fiber->iov_cnt; ++i) { - struct iovec *iov = iovec(fiber->iov)+i; - switch (iov->iov_len) { - case 4: - tbuf_printf(out, " - int: %u\r\n", *(u32*)iov->iov_base); - break; - default: - { - /* - * Sic, we can't access tuple->flags or - * tuple->refs since they may point - * to nowhere, @sa tuple_iov_add(). - */ - struct box_tuple *tuple = iov->iov_base - - offsetof(struct box_tuple, bsize); - tbuf_printf(out, " - "); - tuple_print(out, tuple->cardinality, tuple->data); - break; - } - } - } - -} diff --git a/mod/box/box_lua.m b/mod/box/box_lua.m index 15be32b21c..48fb3a2467 100644 --- a/mod/box/box_lua.m +++ b/mod/box/box_lua.m @@ -37,6 +37,7 @@ #include "third_party/luajit/src/lualib.h" #include "pickle.h" +#include "tuple.h" /** * All box connections share the same Lua state. We use @@ -45,22 +46,195 @@ */ lua_State *root_L; +/* + * Functions, exported in box_lua.h should have prefix + * "box_lua_"; functions, available in Lua "box" + * should start with "lbox_". + */ + +/** {{{ box.tuple Lua library + * + * To avoid extra copying between Lua memory and garbage-collected + * tuple memory, provide a Lua userdata object 'box.tuple'. This + * object refers to a tuple instance in the slab allocator, and + * allows accessing it using Lua primitives (array subscription, + * iteration, etc.). When Lua object is garbage-collected, + * tuple reference counter in the slab allocator is decreased, + * allowing the tuple to be eventually garbage collected in + * the slab allocator. + */ + +static const char *tuplelib_name = "box.tuple"; + +static inline struct box_tuple * +lua_checktuple(struct lua_State *L, int narg) +{ + return *(void **) luaL_checkudata(L, narg, tuplelib_name); +} + +static inline struct box_tuple * +lua_istuple(struct lua_State *L, int narg) +{ + struct box_tuple *tuple = 0; + lua_getmetatable(L, narg); + luaL_getmetatable(L, tuplelib_name); + if (lua_equal(L, -1, -2)) + tuple = * (void **) lua_touserdata(L, narg); + lua_pop(L, 2); + return tuple; +} + +static int +lbox_tuple_gc(struct lua_State *L) +{ + struct box_tuple *tuple = lua_checktuple(L, 1); + tuple_ref(tuple, -1); + return 0; +} + +static int +lbox_tuple_len(struct lua_State *L) +{ + struct box_tuple *tuple = lua_checktuple(L, 1); + lua_pushnumber(L, tuple->cardinality); + return 1; +} + +static int +lbox_tuple_index(struct lua_State *L) +{ + struct box_tuple *tuple = lua_checktuple(L, 1); + int i = luaL_checkint(L, 2); + if (i >= tuple->cardinality) + luaL_error(L, "%s: index %d is out of bounds (0..%d)", + tuplelib_name, i, tuple->cardinality-1); + void *field = tuple_field(tuple, i); + u32 len = load_varint32(&field); + lua_pushlstring(L, field, len); + return 1; +} + +static int +lbox_tuple_tostring(struct lua_State *L) +{ + struct box_tuple *tuple = lua_checktuple(L, 1); + /* @todo: print the tuple */ + struct tbuf *tbuf = tbuf_alloc(fiber->gc_pool); + tuple_print(tbuf, tuple->cardinality, tuple->data); + lua_pushlstring(L, tbuf->data, tbuf->len); + return 1; +} + +static const struct luaL_reg lbox_tuple_meta [] = { + {"__gc", lbox_tuple_gc}, + {"__len", lbox_tuple_len}, + {"__index", lbox_tuple_index}, + {"__tostring", lbox_tuple_tostring}, + {NULL, NULL} +}; + +/* }}} */ + +/** {{{ Lua I/O: facilities to intercept box output + * and push into Lua stack and the opposite: append Lua types + * to fiber IOV. + */ + +void iov_add_ret(struct lua_State *L, int index) +{ + int type = lua_type(L, index); + switch (type) { + case LUA_TNUMBER: + box_out_iproto.dup_u32((u32) lua_tointeger(L, index)); + break; + case LUA_TUSERDATA: + { + struct box_tuple *tuple = lua_istuple(L, index); + if (tuple != NULL) { + box_out_iproto.add_tuple(tuple); + break; + } + } + default: + /* + * LUA_TNONE, LUA_TNIL, LUA_TBOOLEAN, LUA_TSTRING, + * LUA_TTABLE, LUA_THREAD, LUA_TFUNCTION + */ + tnt_raise(ClientError, :ER_PROC_RET, lua_typename(L, type)); + break; + } +} + +/** Add nargs elements on the top of Lua stack to fiber iov. */ + +void iov_add_multret(struct lua_State *L, int nargs) +{ + while (nargs > 0) { + iov_add_ret(L, -nargs); + nargs--; + } +} + +static void +box_lua_dup_u32(u32 u32) +{ + lua_pushinteger(in_txn()->L, u32); +} + +static void +box_lua_add_u32(u32 *p_u32) +{ + box_lua_dup_u32(*p_u32); /* xxx: this can't be done properly in Lua */ +} + +static void +box_lua_add_tuple(struct box_tuple *tuple) +{ + struct lua_State *L = in_txn()->L; + void **ptr = lua_newuserdata(L, sizeof(void *)); + luaL_getmetatable(L, tuplelib_name); + lua_setmetatable(L, -2); + *ptr = tuple; + tuple_ref(tuple, 1); +} + +static struct box_out box_out_lua = { + box_lua_add_u32, + box_lua_dup_u32, + box_lua_add_tuple +}; + +/* }}} */ + +static inline void +txn_enter_lua(lua_State *L) +{ + struct box_txn *txn = in_txn(); + if (txn == NULL) + txn = txn_begin(); + txn->out = &box_out_lua; + txn->L = L; +} + /** - * The main extention provided to Lua by - * Tarantool -- ability to call INSERT/UPDATE/SELECT/DELETE - * from within a Lua procedure. + * The main extension provided to Lua by Tarantool/Box -- + * ability to call INSERT/UPDATE/SELECT/DELETE from within + * a Lua procedure. * * This is a low-level API, and it expects * all arguments to be packed in accordance * with the binary protocol format (iproto * header excluded). + * + * Signature: + * box.process(op_code, request) */ static int lbox_process(lua_State *L) { - u32 op = luaL_checkint(L, 1); + u32 op = lua_tointeger(L, 1); /* Get the first arg. */ struct tbuf req; size_t sz; - req.data = (char *) luaL_checklstring(L, 2, &sz); + req.data = (char *) luaL_checklstring(L, 2, &sz); /* Second arg. */ req.size = req.len = sz; if (op == CALL) { /* @@ -71,12 +245,19 @@ static int lbox_process(lua_State *L) */ return luaL_error(L, "box.process(CALL, ...) is not allowed"); } + int top = lua_gettop(L); /* to know how much is added by rw_callback */ @try { + txn_enter_lua(L); rw_callback(op, &req); + /* + * @todo: when multi-statement transactions are + * implemented, restore the original txn->out here. + */ + assert(in_txn() == NULL); } @catch (ClientError *e) { return luaL_error(L, "%d:%s", e->errcode, e->errmsg); } - return 0; /* nothing is added to the stack */ + return lua_gettop(L) - top; } static const struct luaL_reg boxlib[] = { @@ -84,11 +265,11 @@ static const struct luaL_reg boxlib[] = { {NULL, NULL} }; - /** - * A helper to find a Lua stored procedure by name and put it + * A helper to find a Lua function by name and put it * on top of the stack. */ +static void box_lua_find(lua_State *L, const char *name, const char *name_end) { int index = LUA_GLOBALSINDEX; @@ -106,9 +287,12 @@ void box_lua_find(lua_State *L, const char *name, const char *name_end) } lua_pushlstring(L, start, name_end - start); lua_gettable(L, index); - if (! lua_isfunction(L, -1)) + if (! lua_isfunction(L, -1)) { + /* lua_call or lua_gettable would raise a type error + * for us, but our own message is more verbose. */ tnt_raise(ClientError, :ER_NO_SUCH_PROC, name_end - name, name); + } } @@ -137,7 +321,10 @@ void box_lua_call(struct box_txn *txn __attribute__((unused)), /* Push the rest of args (a tuple) as is. */ lua_pushlstring(L, data->data, data->len); + int top = lua_gettop(L); lua_call(L, 1, LUA_MULTRET); + /* Send results of the called procedure to the client. */ + iov_add_multret(L, lua_gettop(L) - top); } @finally { /* * Allow the used coro to be garbage collected. @@ -150,8 +337,14 @@ void box_lua_call(struct box_txn *txn __attribute__((unused)), struct lua_State * mod_lua_init(struct lua_State *L) { + /* box */ luaL_register(L, "box", boxlib); lua_atpanic(L, box_lua_panic); + /* box.tuple */ + luaL_newmetatable(L, tuplelib_name); + lua_pushstring(L, tuplelib_name); + lua_setfield(L, -2, "__metatable"); + luaL_register(L, NULL, lbox_tuple_meta); return L; } diff --git a/mod/box/tuple.m b/mod/box/tuple.m index 0f29b29aec..76475b1150 100644 --- a/mod/box/tuple.m +++ b/mod/box/tuple.m @@ -147,5 +147,5 @@ tuple_print(struct tbuf *buf, uint8_t cardinality, void *f) if (likely(i + 1 < cardinality)) tbuf_printf(buf, ", "); } - tbuf_printf(buf, "}\r\n"); + tbuf_printf(buf, "}"); } diff --git a/test/box/lua.result b/test/box/lua.result index ba9dcb4d60..d706f2a742 100644 --- a/test/box/lua.result +++ b/test/box/lua.result @@ -4,7 +4,7 @@ unknown command. try typing help. ... lua 1 --- -error: '[string "1"]:1: unexpected symbol near ''1''' + - 1 ... lua print(' lua says: hello') --- @@ -17,15 +17,15 @@ lua for n in pairs(box) do print(' - box.', n) end ... lua box.pack() --- -error: '[string "box.pack()"]:1: bad argument #1 to ''pack'' (string expected, got no value)' +error: 'bad argument #1 to ''?'' (string expected, got no value)' ... lua box.pack(1) --- -error: '[string "box.pack(1)"]:1: box.pack: unsupported pack format specifier ''1''' +error: 'box.pack: unsupported pack format specifier ''1''' ... lua box.pack('abc') --- -error: '[string "box.pack(''abc'')"]:1: bad argument #2 to ''pack'' (string expected, got no value)' +error: 'bad argument #2 to ''?'' (string expected, got no value)' ... lua print(box.pack('a', ' - hello')) --- @@ -45,7 +45,7 @@ lua print(box.pack('www', 0x30, 0x30, 0x30)) ... lua print(box.pack('www', 0x3030, 0x30)) --- -error: '[string "print(box.pack(''www'', 0x3030, 0x30))"]:1: bad argument #4 to ''pack'' (number expected, got no value)' +error: '[string "return print(box.pack(''www'', 0x3030, 0x30))"]:1: bad argument #4 to ''pack'' (number expected, got no value)' ... lua print(string.byte(box.pack('w', 212345), 1, 2)) --- @@ -57,28 +57,28 @@ lua print(box.pack('p', 'this string is 45 characters long 1234567890 ')) ... lua box.process(13, box.pack('iiippp', 0, 1, 3, box.pack('i', 1), 'testing', 'lua rocks')) --- - - int: 1 + - 1 - 1: {'testing', 'lua rocks'} ... lua box.process(17, box.pack('iiiiiip', 0, 0, 0, 2^31, 1, 1, box.pack('i', 1))) --- - - int: 1 + - 0 - 1: {'testing', 'lua rocks'} ... lua box.process(21, box.pack('iiip', 0, 1, 1, box.pack('i', 1))) --- - - int: 1 + - 1 - 1: {'testing', 'lua rocks'} ... lua box.process(17, box.pack('iiiiiip', 0, 0, 0, 2^31, 1, 1, box.pack('i', 1))) --- - - int: 0 + - 0 ... lua box.process(22, box.pack('iii', 0, 0, 0)) --- -error: '[string "box.process(22, box.pack(''iii'', 0, 0, 0))"]:1: box.process(CALL, ...) is not allowed' +error: 'box.process(CALL, ...) is not allowed' ... call box.process('abc', 'def') -An error occurred: ER_PROC_LUA, 'Lua error: bad argument #1 to '?' (number expected, got string)�' +An error occurred: ER_PROC_LUA, 'Lua error: bad argument #2 to '?' (string expected, got no value)�' call box.pack('test') An error occurred: ER_PROC_LUA, 'Lua error: box.pack: unsupported pack format specifier ''�' diff --git a/test/lib/sql_ast.py b/test/lib/sql_ast.py index c636b44e56..4430f5a8e5 100644 --- a/test/lib/sql_ast.py +++ b/test/lib/sql_ast.py @@ -49,6 +49,7 @@ ER = { 37: "ER_UPDATE_ID" , 38: "ER_WRONG_VERSION" , 39: "ER_WAL_IO" , + 48: "ER_PROC_RET" , 49: "ER_TUPLE_NOT_FOUND" , 50: "ER_NO_SUCH_PROC" , 51: "ER_PROC_LUA" , -- GitLab