Skip to content

Commit

Permalink
patch 8.2.1054: not so easy to pass a lua function to Vim
Browse files Browse the repository at this point in the history
Problem:    Not so easy to pass a lua function to Vim.
Solution:   Convert a Lua function and closure to a Vim funcref. (Prabir
            Shrestha, closes #6246)
  • Loading branch information
brammool committed Jun 25, 2020
1 parent 832adf9 commit 801ab06
Show file tree
Hide file tree
Showing 7 changed files with 213 additions and 1 deletion.
8 changes: 8 additions & 0 deletions runtime/doc/if_lua.txt
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,14 @@ Examples:
:lua l = d.len -- assign d as 'self'
:lua print(l())
<
Lua functions and closures are automatically converted to a Vim |Funcref| and
can be accessed in Vim scripts. Example:
>
lua <<EOF
vim.fn.timer_start(1000, function(timer)
print('timer callback')
end)
EOF
==============================================================================
7. Buffer userdata *lua-buffer*
Expand Down
101 changes: 100 additions & 1 deletion src/if_lua.c
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,13 @@ typedef struct {
} luaV_Funcref;
typedef void (*msgfunc_T)(char_u *);

typedef struct {
int lua_funcref; // ref to a lua func
int lua_tableref; // ref to a lua table if metatable else LUA_NOREF. used
// for __call
lua_State *L;
} luaV_CFuncState;

static const char LUAVIM_DICT[] = "dict";
static const char LUAVIM_LIST[] = "list";
static const char LUAVIM_BLOB[] = "blob";
Expand All @@ -45,6 +52,8 @@ static const char LUAVIM_FREE[] = "luaV_free";
static const char LUAVIM_LUAEVAL[] = "luaV_luaeval";
static const char LUAVIM_SETREF[] = "luaV_setref";

static const char LUA___CALL[] = "__call";

// most functions are closures with a cache table as first upvalue;
// get/setudata manage references to vim userdata in cache table through
// object pointers (light userdata)
Expand All @@ -64,14 +73,16 @@ static const char LUAVIM_SETREF[] = "luaV_setref";
#define luaV_emsg(L) luaV_msgfunc((L), (msgfunc_T) emsg)
#define luaV_checktypval(L, a, v, msg) \
do { \
if (luaV_totypval(L, a, v) == FAIL) \
if (luaV_totypval(L, a, v) == FAIL) \
luaL_error(L, msg ": cannot convert value"); \
} while (0)

static luaV_List *luaV_pushlist(lua_State *L, list_T *lis);
static luaV_Dict *luaV_pushdict(lua_State *L, dict_T *dic);
static luaV_Blob *luaV_pushblob(lua_State *L, blob_T *blo);
static luaV_Funcref *luaV_pushfuncref(lua_State *L, char_u *name);
static int luaV_call_lua_func(int argcount, typval_T *argvars, typval_T *rettv, void *state);
static void luaV_call_lua_func_free(void *state);

#if LUA_VERSION_NUM <= 501
#define luaV_openlib(L, l, n) luaL_openlib(L, NULL, l, n)
Expand Down Expand Up @@ -591,6 +602,45 @@ luaV_totypval(lua_State *L, int pos, typval_T *tv)
tv->vval.v_number = (varnumber_T) lua_tointeger(L, pos);
#endif
break;
case LUA_TFUNCTION:
{
char_u *name;
lua_pushvalue(L, pos);
luaV_CFuncState *state = ALLOC_CLEAR_ONE(luaV_CFuncState);
state->lua_funcref = luaL_ref(L, LUA_REGISTRYINDEX);
state->L = L;
state->lua_tableref = LUA_NOREF;
name = register_cfunc(&luaV_call_lua_func,
&luaV_call_lua_func_free, state);
tv->v_type = VAR_FUNC;
tv->vval.v_string = vim_strsave(name);
break;
}
case LUA_TTABLE:
{
lua_pushvalue(L, pos);
int lua_tableref = luaL_ref(L, LUA_REGISTRYINDEX);
if (lua_getmetatable(L, pos)) {
lua_getfield(L, -1, LUA___CALL);
if (lua_isfunction(L, -1)) {
char_u *name;
int lua_funcref = luaL_ref(L, LUA_REGISTRYINDEX);
luaV_CFuncState *state = ALLOC_CLEAR_ONE(luaV_CFuncState);
state->lua_funcref = lua_funcref;
state->L = L;
state->lua_tableref = lua_tableref;
name = register_cfunc(&luaV_call_lua_func,
&luaV_call_lua_func_free, state);
tv->v_type = VAR_FUNC;
tv->vval.v_string = vim_strsave(name);
break;
}
}
tv->v_type = VAR_NUMBER;
tv->vval.v_number = 0;
status = FAIL;
break;
}
case LUA_TUSERDATA:
{
void *p = lua_touserdata(L, pos);
Expand Down Expand Up @@ -2415,4 +2465,53 @@ update_package_paths_in_lua()
}
}

/*
* Native C function callback
*/
static int
luaV_call_lua_func(
int argcount,
typval_T *argvars,
typval_T *rettv,
void *state)
{
int i;
int luaargcount = argcount;
luaV_CFuncState *funcstate = (luaV_CFuncState*)state;
lua_rawgeti(funcstate->L, LUA_REGISTRYINDEX, funcstate->lua_funcref);

if (funcstate->lua_tableref != LUA_NOREF)
{
// First arg for metatable __call method is a table
luaargcount += 1;
lua_rawgeti(funcstate->L, LUA_REGISTRYINDEX, funcstate->lua_tableref);
}

for (i = 0; i < argcount; ++i)
luaV_pushtypval(funcstate->L, &argvars[i]);

if (lua_pcall(funcstate->L, luaargcount, 1, 0))
{
luaV_emsg(funcstate->L);
return FCERR_OTHER;
}

luaV_checktypval(funcstate->L, -1, rettv, "get return value");
return FCERR_NONE;
}

/*
* Free up any lua references held by the func state.
*/
static void
luaV_call_lua_func_free(void *state)
{
luaV_CFuncState *funcstate = (luaV_CFuncState*)state;
luaL_unref(L, LUA_REGISTRYINDEX, funcstate->lua_funcref);
funcstate->L = NULL;
if (funcstate->lua_tableref != LUA_NOREF)
luaL_unref(L, LUA_REGISTRYINDEX, funcstate->lua_tableref);
VIM_CLEAR(funcstate);
}

#endif
1 change: 1 addition & 0 deletions src/proto/userfunc.pro
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ hashtab_T *func_tbl_get(void);
int get_function_args(char_u **argp, char_u endchar, garray_T *newargs, garray_T *argtypes, int *varargs, garray_T *default_args, int skip, exarg_T *eap, char_u **line_to_free);
char_u *get_lambda_name(void);
int get_lambda_tv(char_u **arg, typval_T *rettv, int evaluate);
char_u *register_cfunc(cfunc_T cb, cfunc_free_T free_cb, void *state);
char_u *deref_func_name(char_u *name, int *lenp, partial_T **partialp, int no_autoload);
void emsg_funcname(char *ermsg, char_u *name);
int get_func_tv(char_u *name, int len, typval_T *rettv, char_u **arg, funcexe_T *funcexe);
Expand Down
9 changes: 9 additions & 0 deletions src/structs.h
Original file line number Diff line number Diff line change
Expand Up @@ -1529,6 +1529,9 @@ struct blobvar_S
char bv_lock; // zero, VAR_LOCKED, VAR_FIXED
};

typedef int (*cfunc_T)(int argcount, typval_T *argvars, typval_T *rettv, void *state);
typedef void (*cfunc_free_T)(void *state);

#if defined(FEAT_EVAL) || defined(PROTO)
typedef struct funccall_S funccall_T;

Expand Down Expand Up @@ -1562,6 +1565,11 @@ typedef struct
char_u *uf_va_name; // name from "...name" or NULL
type_T *uf_va_type; // type from "...name: type" or NULL
type_T *uf_func_type; // type of the function, &t_func_any if unknown
# if defined(FEAT_LUA)
cfunc_T uf_cb; // callback function for cfunc
cfunc_free_T uf_cb_free; // callback function to free cfunc
void *uf_cb_state; // state of uf_cb
# endif

garray_T uf_lines; // function lines
# ifdef FEAT_PROFILE
Expand Down Expand Up @@ -1607,6 +1615,7 @@ typedef struct
#define FC_EXPORT 0x100 // "export def Func()"
#define FC_NOARGS 0x200 // no a: variables in lambda
#define FC_VIM9 0x400 // defined in vim9 script file
#define FC_CFUNC 0x800 // defined as Lua C func

#define MAX_FUNC_ARGS 20 // maximum number of function arguments
#define VAR_SHORT_LEN 20 // short variable name length
Expand Down
29 changes: 29 additions & 0 deletions src/testdir/test_lua.vim
Original file line number Diff line number Diff line change
Expand Up @@ -541,6 +541,35 @@ func Test_update_package_paths()
call assert_equal("hello from lua", luaeval("require('testluaplugin').hello()"))
endfunc

func Vim_func_call_lua_callback(Concat, Cb)
let l:message = a:Concat("hello", "vim")
call a:Cb(l:message)
endfunc

func Test_pass_lua_callback_to_vim_from_lua()
lua pass_lua_callback_to_vim_from_lua_result = ""
call assert_equal("", luaeval("pass_lua_callback_to_vim_from_lua_result"))
lua <<EOF
vim.funcref('Vim_func_call_lua_callback')(
function(greeting, message)
return greeting .. " " .. message
end,
function(message)
pass_lua_callback_to_vim_from_lua_result = message
end)
EOF
call assert_equal("hello vim", luaeval("pass_lua_callback_to_vim_from_lua_result"))
endfunc

func Vim_func_call_metatable_lua_callback(Greet)
return a:Greet("world")
endfunc

func Test_pass_lua_metatable_callback_to_vim_from_lua()
let result = luaeval("vim.funcref('Vim_func_call_metatable_lua_callback')(setmetatable({ space = ' '}, { __call = function(tbl, msg) return 'hello' .. tbl.space .. msg end }) )")
call assert_equal("hello world", result)
endfunc

" Test vim.line()
func Test_lua_line()
new
Expand Down
64 changes: 64 additions & 0 deletions src/userfunc.c
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,51 @@ get_lambda_name(void)
return name;
}

#if defined(FEAT_LUA) || defined(PROTO)
/*
* Registers a native C callback which can be called from Vim script.
* Returns the name of the Vim script function.
*/
char_u *
register_cfunc(cfunc_T cb, cfunc_free_T cb_free, void *state)
{
char_u *name = get_lambda_name();
ufunc_T *fp = NULL;
garray_T newargs;
garray_T newlines;

ga_init(&newargs);
ga_init(&newlines);

fp = alloc_clear(offsetof(ufunc_T, uf_name) + STRLEN(name) + 1);
if (fp == NULL)
goto errret;

fp->uf_dfunc_idx = UF_NOT_COMPILED;
fp->uf_refcount = 1;
fp->uf_varargs = TRUE;
fp->uf_flags = FC_CFUNC;
fp->uf_calls = 0;
fp->uf_script_ctx = current_sctx;
fp->uf_lines = newlines;
fp->uf_args = newargs;
fp->uf_cb = cb;
fp->uf_cb_free = cb_free;
fp->uf_cb_state = state;

set_ufunc_name(fp, name);
hash_add(&func_hashtab, UF2HIKEY(fp));

return name;

errret:
ga_clear_strings(&newargs);
ga_clear_strings(&newlines);
vim_free(fp);
return NULL;
}
#endif

/*
* Parse a lambda expression and get a Funcref from "*arg".
* Return OK or FAIL. Returns NOTDONE for dict or {expr}.
Expand Down Expand Up @@ -1027,6 +1072,17 @@ func_clear_items(ufunc_T *fp)
vim_free(((type_T **)fp->uf_type_list.ga_data)
[--fp->uf_type_list.ga_len]);
ga_clear(&fp->uf_type_list);

#ifdef FEAT_LUA
if (fp->uf_cb_free != NULL)
{
fp->uf_cb_free(fp->uf_cb_state);
fp->uf_cb_free = NULL;
}

fp->uf_cb_state = NULL;
fp->uf_cb = NULL;
#endif
#ifdef FEAT_PROFILE
VIM_CLEAR(fp->uf_tml_count);
VIM_CLEAR(fp->uf_tml_total);
Expand Down Expand Up @@ -1973,6 +2029,14 @@ call_func(

if (fp != NULL && (fp->uf_flags & FC_DELETED))
error = FCERR_DELETED;
#ifdef FEAT_LUA
else if (fp != NULL && (fp->uf_flags & FC_CFUNC))
{
cfunc_T cb = fp->uf_cb;

error = (*cb)(argcount, argvars, rettv, fp->uf_cb_state);
}
#endif
else if (fp != NULL)
{
if (funcexe->argv_func != NULL)
Expand Down
2 changes: 2 additions & 0 deletions src/version.c
Original file line number Diff line number Diff line change
Expand Up @@ -754,6 +754,8 @@ static char *(features[]) =

static int included_patches[] =
{ /* Add new patch number below this line */
/**/
1054,
/**/
1053,
/**/
Expand Down

1 comment on commit 801ab06

@lacygoill
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This commit breaks Vim's compilation when enabling the lua interface with --enable-luainterp=dynamic. The compiler complains about an undefined reference to luaL_ref:

/home/user/Vcs/vim/src/if_lua.c:622: undefined reference to `luaL_ref'
/home/user/Vcs/vim/src/if_lua.c:627: undefined reference to `luaL_ref'
/home/user/Vcs/vim/src/if_lua.c:610: undefined reference to `luaL_ref'
objects/if_lua.o: In function `luaV_call_lua_func_free':
/home/user/Vcs/vim/src/if_lua.c:2510: undefined reference to `luaL_unref'
/home/user/Vcs/vim/src/if_lua.c:2513: undefined reference to `luaL_unref'
collect2: error: ld returned 1 exit status
link.sh: Linking failed
Makefile:2132: recipe for target 'vim' failed
make[1]: *** [vim] Error 1
make[1]: Leaving directory '/home/user/Vcs/vim/src'
Makefile:26: recipe for target 'first' failed
make: *** [first] Error 2

Please sign in to comment.