diff options
author | TJ DeVries <devries.timothyj@gmail.com> | 2020-06-30 02:05:06 -0400 |
---|---|---|
committer | TJ DeVries <devries.timothyj@gmail.com> | 2020-07-10 20:23:12 -0400 |
commit | 6360cf7ce87407bd8a519b9a17f45b2148291904 (patch) | |
tree | 6f770f564307cb8a31f6ead427114267fcba3953 | |
parent | 971a191c4d772493535d55524b994fe385fae546 (diff) | |
download | rneovim-6360cf7ce87407bd8a519b9a17f45b2148291904.tar.gz rneovim-6360cf7ce87407bd8a519b9a17f45b2148291904.tar.bz2 rneovim-6360cf7ce87407bd8a519b9a17f45b2148291904.zip |
lua: Add ability to pass tables with __call
vim-patch:8.2.1054: not so easy to pass a lua function to Vim
vim-patch:8.2.1084: Lua: registering function has useless code
I think I have also opened up the possibility for people to use these
callbacks elsewhere, since I've added a new struct that we should be
able to use.
Also, this should allow us to determine what the state of a list is in
Lua or a dictionary in Lua, since we now can track the luaref as we go.
-rw-r--r-- | src/nvim/eval.c | 10 | ||||
-rw-r--r-- | src/nvim/eval/funcs.c | 3 | ||||
-rw-r--r-- | src/nvim/eval/typval.c | 3 | ||||
-rw-r--r-- | src/nvim/eval/typval.h | 9 | ||||
-rw-r--r-- | src/nvim/eval/userfunc.c | 5 | ||||
-rw-r--r-- | src/nvim/lua/converter.c | 13 | ||||
-rw-r--r-- | src/nvim/lua/converter.h | 5 | ||||
-rw-r--r-- | src/nvim/lua/executor.c | 192 | ||||
-rw-r--r-- | test/functional/lua/luaeval_spec.lua | 78 |
9 files changed, 268 insertions, 50 deletions
diff --git a/src/nvim/eval.c b/src/nvim/eval.c index 27c2ed1ea5..e1f9fe0253 100644 --- a/src/nvim/eval.c +++ b/src/nvim/eval.c @@ -7143,6 +7143,16 @@ bool callback_from_typval(Callback *const callback, typval_T *const arg) func_ref(name); callback->data.funcref = vim_strsave(name); callback->type = kCallbackFuncref; + } else if (nlua_is_table_from_lua(arg)) { + char_u *name = nlua_register_table_as_callable(arg); + + if (name != NULL) { + func_ref(name); + callback->data.funcref = vim_strsave(name); + callback->type = kCallbackFuncref; + } else { + r = FAIL; + } } else if (arg->v_type == VAR_NUMBER && arg->vval.v_number == 0) { callback->type = kCallbackNone; } else { diff --git a/src/nvim/eval/funcs.c b/src/nvim/eval/funcs.c index 1071e75c06..99014d1a09 100644 --- a/src/nvim/eval/funcs.c +++ b/src/nvim/eval/funcs.c @@ -824,9 +824,12 @@ static void f_call(typval_T *argvars, typval_T *rettv, FunPtr fptr) } else if (argvars[0].v_type == VAR_PARTIAL) { partial = argvars[0].vval.v_partial; func = partial_name(partial); + } else if (nlua_is_table_from_lua(&argvars[0])) { + func = nlua_register_table_as_callable(&argvars[0]); } else { func = (char_u *)tv_get_string(&argvars[0]); } + if (*func == NUL) { return; // type error or empty name } diff --git a/src/nvim/eval/typval.c b/src/nvim/eval/typval.c index 0daaf6c878..89ca2db59b 100644 --- a/src/nvim/eval/typval.c +++ b/src/nvim/eval/typval.c @@ -16,6 +16,7 @@ #include "nvim/eval/typval_encode.h" #include "nvim/eval.h" #include "nvim/eval/userfunc.h" +#include "nvim/lua/executor.h" #include "nvim/types.h" #include "nvim/assert.h" #include "nvim/memory.h" @@ -301,6 +302,7 @@ void tv_list_free_list(list_T *const l) } list_log(l, NULL, NULL, "freelist"); + nlua_free_typval_list(l); xfree(l); } @@ -1374,6 +1376,7 @@ void tv_dict_free_dict(dict_T *const d) d->dv_used_next->dv_used_prev = d->dv_used_prev; } + nlua_free_typval_dict(d); xfree(d); } diff --git a/src/nvim/eval/typval.h b/src/nvim/eval/typval.h index cb13bec50d..503a32a81e 100644 --- a/src/nvim/eval/typval.h +++ b/src/nvim/eval/typval.h @@ -180,6 +180,8 @@ struct listvar_S { int lv_idx; ///< Index of a cached item, used for optimising repeated l[idx]. int lv_copyID; ///< ID used by deepcopy(). VarLockStatus lv_lock; ///< Zero, VAR_LOCKED, VAR_FIXED. + + LuaRef lua_table_ref; }; // Static list with 10 items. Use tv_list_init_static10() to initialize. @@ -245,6 +247,8 @@ struct dictvar_S { dict_T *dv_used_next; ///< Next dictionary in used dictionaries list. dict_T *dv_used_prev; ///< Previous dictionary in used dictionaries list. QUEUE watchers; ///< Dictionary key watchers set by user code. + + LuaRef lua_table_ref; }; /// Type used for script ID @@ -271,9 +275,10 @@ typedef struct { /// Number of fixed variables used for arguments #define FIXVAR_CNT 12 -/// Callback interface for C function reference +/// Callback interface for C function reference> +/// Used for managing functions that were registered with |register_cfunc| typedef int (*cfunc_T)(int argcount, typval_T *argvars, typval_T *rettv, void *state); // NOLINT -/// Callback to clear cfunc_T +/// Callback to clear cfunc_T and any associated state. typedef void (*cfunc_free_T)(void *state); // Structure to hold info for a function that is currently being executed. diff --git a/src/nvim/eval/userfunc.c b/src/nvim/eval/userfunc.c index ca2f4a32ca..229f0e8dde 100644 --- a/src/nvim/eval/userfunc.c +++ b/src/nvim/eval/userfunc.c @@ -3458,7 +3458,6 @@ char_u *register_cfunc(cfunc_T cb, cfunc_free_T cb_free, void *state) { char_u *name = get_lambda_name(); ufunc_T *fp = NULL; - int flags = FC_CFUNC; fp = xcalloc(1, offsetof(ufunc_T, uf_name) + STRLEN(name) + 1); if (fp == NULL) { @@ -3467,7 +3466,7 @@ char_u *register_cfunc(cfunc_T cb, cfunc_free_T cb_free, void *state) fp->uf_refcount = 1; fp->uf_varargs = true; - fp->uf_flags = flags; + fp->uf_flags = FC_CFUNC; fp->uf_calls = 0; fp->uf_script_ctx = current_sctx; fp->uf_cb = cb; @@ -3477,5 +3476,5 @@ char_u *register_cfunc(cfunc_T cb, cfunc_free_T cb_free, void *state) STRCPY(fp->uf_name, name); hash_add(&func_hashtab, UF2HIKEY(fp)); - return name; + return fp->uf_name; } diff --git a/src/nvim/lua/converter.c b/src/nvim/lua/converter.c index 9fc4ca7e7e..32e804d213 100644 --- a/src/nvim/lua/converter.c +++ b/src/nvim/lua/converter.c @@ -316,6 +316,13 @@ bool nlua_pop_typval(lua_State *lstate, typval_T *ret_tv) break; } case LUA_TTABLE: { + // Only need to track table refs if we have a metatable associated. + LuaRef table_ref = LUA_NOREF; + if (lua_getmetatable(lstate, -1)) { + lua_pop(lstate, 1); + table_ref = nlua_ref(lstate, -1); + } + const LuaTableProps table_props = nlua_traverse_table(lstate); for (size_t i = 0; i < kv_size(stack); i++) { @@ -331,6 +338,7 @@ bool nlua_pop_typval(lua_State *lstate, typval_T *ret_tv) case kObjectTypeArray: { cur.tv->v_type = VAR_LIST; cur.tv->vval.v_list = tv_list_alloc((ptrdiff_t)table_props.maxidx); + cur.tv->vval.v_list->lua_table_ref = table_ref; tv_list_ref(cur.tv->vval.v_list); if (table_props.maxidx != 0) { cur.container = true; @@ -344,6 +352,7 @@ bool nlua_pop_typval(lua_State *lstate, typval_T *ret_tv) cur.tv->v_type = VAR_DICT; cur.tv->vval.v_dict = tv_dict_alloc(); cur.tv->vval.v_dict->dv_refcount++; + cur.tv->vval.v_dict->lua_table_ref = table_ref; } else { cur.special = table_props.has_string_with_nul; if (table_props.has_string_with_nul) { @@ -354,11 +363,13 @@ bool nlua_pop_typval(lua_State *lstate, typval_T *ret_tv) S_LEN("_VAL")); assert(val_di != NULL); cur.tv = &val_di->di_tv; + cur.tv->vval.v_list->lua_table_ref = table_ref; assert(cur.tv->v_type == VAR_LIST); } else { cur.tv->v_type = VAR_DICT; cur.tv->vval.v_dict = tv_dict_alloc(); cur.tv->vval.v_dict->dv_refcount++; + cur.tv->vval.v_dict->lua_table_ref = table_ref; } cur.container = true; cur.idx = lua_gettop(lstate); @@ -388,8 +399,8 @@ nlua_pop_typval_table_processing_end: } case LUA_TFUNCTION: { LuaCFunctionState *state = xmalloc(sizeof(LuaCFunctionState)); - state->lua_callable.func_ref = nlua_ref(lstate, -1); + state->lua_callable.table_ref = LUA_NOREF; char_u *name = register_cfunc( &nlua_CFunction_func_call, diff --git a/src/nvim/lua/converter.h b/src/nvim/lua/converter.h index e74e3fb084..8601a32418 100644 --- a/src/nvim/lua/converter.h +++ b/src/nvim/lua/converter.h @@ -10,11 +10,12 @@ #include "nvim/eval.h" typedef struct { - LuaRef func_ref; + LuaRef func_ref; + LuaRef table_ref; } LuaCallable; typedef struct { - LuaCallable lua_callable; + LuaCallable lua_callable; } LuaCFunctionState; #ifdef INCLUDE_GENERATED_DECLARATIONS diff --git a/src/nvim/lua/executor.c b/src/nvim/lua/executor.c index 5e924a9f90..9f30609d66 100644 --- a/src/nvim/lua/executor.c +++ b/src/nvim/lua/executor.c @@ -53,6 +53,15 @@ typedef struct { # include "lua/executor.c.generated.h" #endif +#define PUSH_ALL_TYPVALS(lstate, args, argcount, special) \ + for (int i = 0; i < argcount; i++) { \ + if (args[i].v_type == VAR_UNKNOWN) { \ + lua_pushnil(lstate); \ + } else { \ + nlua_push_typval(lstate, &args[i], special); \ + } \ + } + /// Convert lua error into a Vim error message /// /// @param lstate Lua interpreter state. @@ -700,24 +709,25 @@ int nlua_call(lua_State *lstate) } TRY_WRAP({ - // TODO(bfredl): this should be simplified in error handling refactor - force_abort = false; - suppress_errthrow = false; - current_exception = NULL; - did_emsg = false; - - try_start(); - typval_T rettv; - int dummy; - // call_func() retval is deceptive, ignore it. Instead we set `msg_list` - // (TRY_WRAP) to capture abort-causing non-exception errors. - (void)call_func(name, (int)name_len, &rettv, nargs, - vim_args, NULL, curwin->w_cursor.lnum, curwin->w_cursor.lnum, - &dummy, true, NULL, NULL); - if (!try_end(&err)) { - nlua_push_typval(lstate, &rettv, false); - } - tv_clear(&rettv); + // TODO(bfredl): this should be simplified in error handling refactor + force_abort = false; + suppress_errthrow = false; + current_exception = NULL; + did_emsg = false; + + try_start(); + typval_T rettv; + int dummy; + // call_func() retval is deceptive, ignore it. Instead we set `msg_list` + // (TRY_WRAP) to capture abort-causing non-exception errors. + (void)call_func(name, (int)name_len, &rettv, nargs, + vim_args, NULL, + curwin->w_cursor.lnum, curwin->w_cursor.lnum, + &dummy, true, NULL, NULL); + if (!try_end(&err)) { + nlua_push_typval(lstate, &rettv, false); + } + tv_clear(&rettv); }); free_vim_args: @@ -839,6 +849,19 @@ void nlua_pushref(lua_State *lstate, LuaRef ref) lua_rawgeti(lstate, LUA_REGISTRYINDEX, ref); } +/// Gets a new reference to an object stored at original_ref +/// +/// NOTE: It does not copy the value, it creates a new ref to the lua object. +/// Leaves the stack unchanged. +LuaRef nlua_newref(lua_State *lstate, LuaRef original_ref) +{ + nlua_pushref(lstate, original_ref); + LuaRef new_ref = nlua_ref(lstate, -1); + lua_pop(lstate, 1); + + return new_ref; +} + /// Evaluate lua string /// /// Used for luaeval(). @@ -916,13 +939,8 @@ static void typval_exec_lua(const char *lcmd, size_t lcmd_len, const char *name, return; } - for (int i = 0; i < argcount; i++) { - if (args[i].v_type == VAR_UNKNOWN) { - lua_pushnil(lstate); - } else { - nlua_push_typval(lstate, &args[i], special); - } - } + PUSH_ALL_TYPVALS(lstate, args, argcount, special); + if (lua_pcall(lstate, argcount, ret_tv ? 1 : 0, 0)) { nlua_error(lstate, _("E5108: Error executing lua %.*s")); return; @@ -934,6 +952,14 @@ static void typval_exec_lua(const char *lcmd, size_t lcmd_len, const char *name, } /// Call a LuaCallable given some typvals +/// +/// Used to call any lua callable passed from Lua into VimL +/// +/// @param[in] lstate Lua State +/// @param[in] lua_cb Lua Callable +/// @param[in] argcount Count of typval arguments +/// @param[in] argvars Typval Arguments +/// @param[out] rettv The return value from the called function. int typval_exec_lua_callable( lua_State *lstate, LuaCallable lua_cb, @@ -942,22 +968,32 @@ int typval_exec_lua_callable( typval_T *rettv ) { - LuaRef cb = lua_cb.func_ref; + int offset = 0; + LuaRef cb = lua_cb.func_ref; - nlua_pushref(lstate, cb); + if (cb == LUA_NOREF) { + // This shouldn't happen. + luaL_error(lstate, "Invalid function passed to VimL"); + return ERROR_OTHER; + } - for (int i = 0; i < argcount; i++) { - nlua_push_typval(lstate, &argvars[i], false); - } + nlua_pushref(lstate, cb); - if (lua_pcall(lstate, argcount, 1, 0)) { - luaL_error(lstate, "nlua_CFunction_func_call failed."); - return ERROR_OTHER; - } + if (lua_cb.table_ref != LUA_NOREF) { + offset += 1; + nlua_pushref(lstate, lua_cb.table_ref); + } + + PUSH_ALL_TYPVALS(lstate, argvars, argcount, false); + + if (lua_pcall(lstate, argcount + offset, 1, 0)) { + luaL_error(lstate, "nlua_CFunction_func_call failed."); + return ERROR_OTHER; + } - nlua_pop_typval(lstate, rettv); + nlua_pop_typval(lstate, rettv); - return ERROR_NONE; + return ERROR_NONE; } /// Execute Lua string @@ -1331,7 +1367,91 @@ void nlua_CFunction_func_free(void *state) LuaCFunctionState *funcstate = (LuaCFunctionState *)state; nlua_unref(lstate, funcstate->lua_callable.func_ref); + nlua_unref(lstate, funcstate->lua_callable.table_ref); xfree(funcstate); } +bool nlua_is_table_from_lua(typval_T *const arg) +{ + if (arg->v_type != VAR_DICT && arg->v_type != VAR_LIST) { + return false; + } + + if (arg->v_type == VAR_DICT) { + return arg->vval.v_dict->lua_table_ref > 0 + && arg->vval.v_dict->lua_table_ref != LUA_NOREF; + } else if (arg->v_type == VAR_LIST) { + return arg->vval.v_list->lua_table_ref > 0 + && arg->vval.v_list->lua_table_ref != LUA_NOREF; + } + + return false; +} +char_u *nlua_register_table_as_callable(typval_T *const arg) +{ + if (!nlua_is_table_from_lua(arg)) { + return NULL; + } + + LuaRef table_ref; + if (arg->v_type == VAR_DICT) { + table_ref = arg->vval.v_dict->lua_table_ref; + } else if (arg->v_type == VAR_LIST) { + table_ref = arg->vval.v_list->lua_table_ref; + } else { + return NULL; + } + + lua_State *const lstate = nlua_enter(); + + int top = lua_gettop(lstate); + + nlua_pushref(lstate, table_ref); + if (!lua_getmetatable(lstate, -1)) { + return NULL; + } + + lua_getfield(lstate, -1, "__call"); + if (!lua_isfunction(lstate, -1)) { + return NULL; + } + + LuaRef new_table_ref = nlua_newref(lstate, table_ref); + + LuaCFunctionState *state = xmalloc(sizeof(LuaCFunctionState)); + state->lua_callable.func_ref = nlua_ref(lstate, -1); + state->lua_callable.table_ref = new_table_ref; + + char_u *name = register_cfunc( + &nlua_CFunction_func_call, + &nlua_CFunction_func_free, + state); + + + lua_pop(lstate, 3); + assert(top == lua_gettop(lstate)); + + return name; +} + +/// Helper function to free a list_T +void nlua_free_typval_list(list_T *const l) +{ + if (l->lua_table_ref != LUA_NOREF && l->lua_table_ref > 0) { + lua_State *const lstate = nlua_enter(); + nlua_unref(lstate, l->lua_table_ref); + l->lua_table_ref = LUA_NOREF; + } +} + + +/// Helper function to free a dict_T +void nlua_free_typval_dict(dict_T *const d) +{ + if (d->lua_table_ref != LUA_NOREF && d->lua_table_ref > 0) { + lua_State *const lstate = nlua_enter(); + nlua_unref(lstate, d->lua_table_ref); + d->lua_table_ref = LUA_NOREF; + } +} diff --git a/test/functional/lua/luaeval_spec.lua b/test/functional/lua/luaeval_spec.lua index 4faacecd86..964ea4561e 100644 --- a/test/functional/lua/luaeval_spec.lua +++ b/test/functional/lua/luaeval_spec.lua @@ -295,13 +295,12 @@ describe('luaeval()', function() ]]) end) - -- TODO(tjdevries): Need to figure - pending('should work with metatables using __call', function() - eq(true, exec_lua [[ + it('should work with metatables using __call', function() + eq(1, exec_lua [[ local this_is_local_variable = false - local callable_table = setmetatable({}, { - __call = function(...) - this_is_local_variable = true + local callable_table = setmetatable({x = 1}, { + __call = function(t, ...) + this_is_local_variable = t.x end }) @@ -315,6 +314,73 @@ describe('luaeval()', function() ]]) end) + it('should handle being called from a timer once.', function() + eq(3, exec_lua [[ + local this_is_local_variable = false + local callable_table = setmetatable({5, 4, 3, 2, 1}, { + __call = function(t, ...) this_is_local_variable = t[3] end + }) + + vim.fn.timer_start(5, callable_table) + vim.wait(1000, function() + return this_is_local_variable + end) + + return this_is_local_variable + ]]) + end) + + it('should call functions once with __call metamethod', function() + eq(true, exec_lua [[ + local this_is_local_variable = false + local callable_table = setmetatable({a = true, b = false}, { + __call = function(t, ...) this_is_local_variable = t.a end + }) + + assert(getmetatable(callable_table).__call) + vim.fn.call(callable_table, {}) + + return this_is_local_variable + ]]) + end) + + it('should work with lists using __call', function() + eq(3, exec_lua [[ + local this_is_local_variable = false + local mt = { + __call = function(t, ...) + this_is_local_variable = t[3] + end + } + local callable_table = setmetatable({5, 4, 3, 2, 1}, mt) + + -- Call it once... + vim.fn.timer_start(5, callable_table) + vim.wait(1000, function() + return this_is_local_variable + end) + + assert(this_is_local_variable) + this_is_local_variable = false + + vim.fn.timer_start(5, callable_table) + vim.wait(1000, function() + return this_is_local_variable + end) + + return this_is_local_variable + ]]) + end) + + it('should not work with tables not using __call', function() + eq({false, 'Vim:E921: Invalid callback argument'}, exec_lua [[ + local this_is_local_variable = false + local callable_table = setmetatable({x = 1}, {}) + + return {pcall(function() vim.fn.timer_start(5, callable_table) end)} + ]]) + end) + it('correctly converts containers with type_idx', function() eq(5, eval('type(luaeval("{[vim.type_idx]=vim.types.float, [vim.val_idx]=0}"))')) eq(4, eval([[type(luaeval('{[vim.type_idx]=vim.types.dictionary}'))]])) |