perf(vim.fn): call Lua-implemented vim.fn.xx() directly #39166

Problem:
- Builtin "Vimscript" functions (f_xx) are mostly implemented in C.
  Partly that's because there is some boilerplate required to call out
  to Lua.
- Calls to `vim.fn.foo()` always marshall over the Lua <=> Vimscript
  ("typval") bridge, even if `fn.foo()` is implemented entirely in Lua:
  ```
  Lua => typval => Object => Lua => Object => typval => Lua.
  ```

Solution:
Functions declared in eval.lua with `func_lua` are implemented in
entirely in Lua (`_core/vimfn.lua`).

- `gen_eval.lua` wires `func_lua` entries to `lua_wrapper`, which handles
  the typval conversion for Vimscript callers (slow path).
- `nlua_call()` detects `func_lua` functions and calls the Lua
  implementation directly. This eliminates all conversion overhead for
  Lua callers (fast path).
- Validate at build-time that `func`, `func_float`, and `func_lua` are
  mutually exclusive.
- Migrate `hostname()` as a toy example, to show the idea.
This commit is contained in:
Justin M. Keyes
2026-04-17 19:10:20 -04:00
committed by GitHub
parent 03193e2963
commit 0d4d285bd2
13 changed files with 162 additions and 71 deletions

View File

@@ -112,6 +112,12 @@ Many of the editor concepts are defined as Lua data files:
- Ex (cmdline) commands: src/nvim/ex_cmds.lua
- Options: src/nvim/options.lua
- Vimscript functions: src/nvim/eval.lua
- Functions can be implemented in C (`f_foo` in eval/funcs.c), or in Lua
(set `func_lua` in eval.lua, implement in _core/vimfn.lua). Lua impl is
preferred, for performance and maintainability.
- VimScript callers go through `lua_wrapper` (typval <=> Object conversion).
- Lua callers (`vim.fn.foo()`) take a fast path in `nlua_call()` that calls
the Lua function directly, avoiding any conversion.
- v: variables: src/nvim/vvars.lua
==============================================================================

View File

@@ -158,7 +158,10 @@ OPTIONS
PERFORMANCE
todo
Nvim architecture allows pure-Lua implementations of some `vim.fn`
functions, which skips the Vimscript <=> Lua "bridge" (no data
conversion/marshalling) entirely, if the `vim.fn` function is called from
Lua.
PLUGINS

View File

@@ -4943,8 +4943,7 @@ hlexists({name}) *hlexists()*
hostname() *hostname()*
Returns the hostname of the machine on which the Nvim server
(not the UI client) is currently running. Names greater than
256 characters long are truncated.
(not the UI client) is currently running.
Return: ~
(`string`)

View File

@@ -0,0 +1,14 @@
-- Lua implementations of "vimfn" builtin functions (via `func_lua`).
--
-- Functions defined here are pure Lua, they don't have any explicit C impl, so they are named with
-- the "f_xx" convention, for discoverability.
local M = {}
--- Returns the hostname of the machine.
--- @return string
function M.f_hostname()
return vim.uv.os_gethostname()
end
return M

View File

@@ -4475,8 +4475,7 @@ function vim.fn.hlID(name) end
function vim.fn.hlexists(name) end
--- Returns the hostname of the machine on which the Nvim server
--- (not the UI client) is currently running. Names greater than
--- 256 characters long are truncated.
--- (not the UI client) is currently running.
---
--- @return string
function vim.fn.hostname() end

View File

@@ -54,10 +54,16 @@ hashpipe:write([[
]])
local funcs = loadfile(eval_file)().funcs
for _, func in pairs(funcs) do
if func.float_func then
for name, func in pairs(funcs) do
local n = (func.func_float and 1 or 0) + (func.func_lua and 1 or 0) + (func.func and 1 or 0)
assert(n <= 1, name .. ': only one of func, func_float, func_lua can be set')
if func.func_float then
func.func = 'float_op_wrapper'
func.data = '{ .float_func = &' .. func.float_func .. ' }'
func.data = '{ .func_float = &' .. func.func_float .. ' }'
elseif func.func_lua then
func.func = 'lua_wrapper'
func.data = '{ .func_lua = "' .. func.func_lua .. '" }'
end
end
@@ -67,7 +73,7 @@ for _, fun in ipairs(metadata) do
funcs[fun.name] = {
args = #fun.parameters,
func = 'api_wrapper',
data = '{ .api_handler = &method_handlers[' .. fun.handler_id .. '] }',
data = '{ .func_api = &method_handlers[' .. fun.handler_id .. '] }',
}
end
end

View File

@@ -1,18 +1,12 @@
-- File containing table with all functions.
--
-- Keys:
-- Defines all "vimfn" (builtin/"eval"/"Vimscript") functions.
--
--- @class vim.EvalFn
--- @field name? string
--- @field args? integer|integer[] Number of arguments, list with maximum and minimum number of arguments
--- or list with a minimum number of arguments only. Defaults to zero
--- arguments.
--- @field base? integer For methods: the argument to use as the base argument (1-indexed):
--- base->method()
--- Defaults to BASE_NONE (function cannot be used as a method).
--- @field func? string Name of the C function which implements the Vimscript function. Defaults to
--- `f_{funcname}`.
--- @field float_func? string
--- @field args? integer|integer[] (default: 0) Number of arguments, list with maximum and minimum number of arguments or list with a minimum number of arguments only.
--- @field base? integer For methods: the argument to use as the base argument (1-indexed): base->method(). Defaults to BASE_NONE (function cannot be used as a method).
--- @field func? string (default: "f_{funcname}") C function which implements the vimfn.
--- @field func_float? string Floating-point C function. Sets func="float_op_wrapper".
--- @field func_lua? string Function name in `vim._core.vimfn.*` (e.g. "f_hostname"). Sets func="lua_wrapper".
--- @field fast? boolean Function can run in |api-fast| events. Defaults to false.
--- @field deprecated? true
--- @field returns? string|false
@@ -76,7 +70,7 @@ M.funcs = {
< 2.094395
]=],
float_func = 'acos',
func_float = 'acos',
name = 'acos',
params = { { 'expr', 'number' } },
returns = 'number',
@@ -276,7 +270,7 @@ M.funcs = {
< -0.523599
]=],
float_func = 'asin',
func_float = 'asin',
name = 'asin',
params = { { 'expr', 'any' } },
returns = 'number',
@@ -567,7 +561,7 @@ M.funcs = {
< -1.326405
]=],
float_func = 'atan',
func_float = 'atan',
name = 'atan',
params = { { 'expr', 'number' } },
returns = 'number',
@@ -1022,7 +1016,7 @@ M.funcs = {
Returns 0.0 if {expr} is not a |Float| or a |Number|.
]=],
float_func = 'ceil',
func_float = 'ceil',
name = 'ceil',
params = { { 'expr', 'number' } },
returns = 'number',
@@ -1597,7 +1591,7 @@ M.funcs = {
< -0.646043
]=],
float_func = 'cos',
func_float = 'cos',
name = 'cos',
params = { { 'expr', 'number' } },
returns = 'number',
@@ -1618,7 +1612,7 @@ M.funcs = {
< -1.127626
]=],
float_func = 'cosh',
func_float = 'cosh',
name = 'cosh',
params = { { 'expr', 'number' } },
returns = 'number',
@@ -2357,7 +2351,7 @@ M.funcs = {
< 0.367879
]=],
float_func = 'exp',
func_float = 'exp',
name = 'exp',
params = { { 'expr', 'number' } },
signature = 'exp({expr})',
@@ -2882,7 +2876,7 @@ M.funcs = {
< 4.0
]=],
float_func = 'floor',
func_float = 'floor',
name = 'floor',
params = { { 'expr', 'number' } },
signature = 'floor({expr})',
@@ -5515,10 +5509,10 @@ M.funcs = {
hostname = {
desc = [=[
Returns the hostname of the machine on which the Nvim server
(not the UI client) is currently running. Names greater than
256 characters long are truncated.
(not the UI client) is currently running.
]=],
fast = true,
func_lua = 'f_hostname',
name = 'hostname',
params = {},
returns = 'string',
@@ -6576,7 +6570,7 @@ M.funcs = {
< 5.0
]=],
float_func = 'log',
func_float = 'log',
name = 'log',
params = { { 'expr', 'number' } },
returns = 'number',
@@ -6596,7 +6590,7 @@ M.funcs = {
< -2.0
]=],
float_func = 'log10',
func_float = 'log10',
name = 'log10',
params = { { 'expr', 'number' } },
returns = 'number',
@@ -9058,7 +9052,7 @@ M.funcs = {
< -5.0
]=],
float_func = 'round',
func_float = 'round',
name = 'round',
params = { { 'expr', 'number' } },
returns = 'number',
@@ -11047,7 +11041,7 @@ M.funcs = {
< 0.763301
]=],
float_func = 'sin',
func_float = 'sin',
name = 'sin',
params = { { 'expr', 'number' } },
returns = 'number',
@@ -11068,7 +11062,7 @@ M.funcs = {
< -1.026517
]=],
float_func = 'sinh',
func_float = 'sinh',
name = 'sinh',
params = { { 'expr', 'number' } },
signature = 'sinh({expr})',
@@ -11335,7 +11329,7 @@ M.funcs = {
NaN may be different, it depends on system libraries.
]=],
float_func = 'sqrt',
func_float = 'sqrt',
name = 'sqrt',
params = { { 'expr', 'number' } },
signature = 'sqrt({expr})',
@@ -12517,7 +12511,7 @@ M.funcs = {
< -1.181502
]=],
float_func = 'tan',
func_float = 'tan',
name = 'tan',
params = { { 'expr', 'number' } },
returns = 'number',
@@ -12538,7 +12532,7 @@ M.funcs = {
< -0.761594
]=],
float_func = 'tanh',
func_float = 'tanh',
name = 'tanh',
params = { { 'expr', 'number' } },
returns = 'number',
@@ -12798,7 +12792,7 @@ M.funcs = {
< 4.0
]=],
float_func = 'trunc',
func_float = 'trunc',
name = 'trunc',
params = { { 'expr', 'number' } },
returns = 'integer',

View File

@@ -227,7 +227,7 @@ char *get_expr_name(expand_T *xp, int idx)
return get_user_var_name(xp, ++intidx);
}
/// Find internal function in hash functions
/// Gets a builtin (aka "vimfn", "eval") function from the generated hash table.
///
/// @param[in] name Name of the function.
///
@@ -240,6 +240,17 @@ const EvalFuncDef *find_internal_func(const char *const name)
return index >= 0 ? &functions[index] : NULL;
}
/// Gets the Lua name of a Lua-implemented "vimfn" function, or NULL if not found.
const char *find_internal_func_lua(const char *const name)
FUNC_ATTR_WARN_UNUSED_RESULT FUNC_ATTR_PURE FUNC_ATTR_NONNULL_ALL
{
const EvalFuncDef *const fdef = find_internal_func(name);
if (fdef && fdef->func == &lua_wrapper) {
return fdef->data.func_lua;
}
return NULL;
}
/// Check the argument count to use for internal function "fdef".
/// @return -1 for failure, 0 if no method base accepted, 1 if method base is
/// first argument, 2 if method base is second argument, etc.
@@ -325,7 +336,7 @@ static bool non_zero_arg(typval_T *argvars)
&& *argvars[0].vval.v_string != NUL));
}
/// Apply a floating point C function on a typval with one float_T.
/// Apply a floating point C function on a typval with one float_T (`func_float` in eval.lua).
///
/// Some versions of glibc on i386 have an optimization that makes it harder to
/// call math functions indirectly from inside an inlined function, causing
@@ -336,19 +347,23 @@ static void float_op_wrapper(typval_T *argvars, typval_T *rettv, EvalFuncData fp
rettv->v_type = VAR_FLOAT;
if (tv_get_float_chk(argvars, &f)) {
rettv->vval.v_float = fptr.float_func(f);
rettv->vval.v_float = fptr.func_float(f);
} else {
rettv->vval.v_float = 0.0;
}
}
/// Invokes an API (nvim_) function from Vimscript.
///
/// Converts `argvars` to API Objects, calls the API handler, and converts the result back.
/// Used by `gen_eval.lua` for `eval=true` API functions.
static void api_wrapper(typval_T *argvars, typval_T *rettv, EvalFuncData fptr)
{
if (check_secure()) {
return;
}
MsgpackRpcRequestHandler handler = *fptr.api_handler;
MsgpackRpcRequestHandler handler = *fptr.func_api;
MAXSIZE_TEMP_ARRAY(args, MAX_FUNC_ARGS);
Arena arena = ARENA_EMPTY;
@@ -375,11 +390,43 @@ end:
api_clear_error(&err);
}
/// Invokes a Lua-implemented vimfn/"f_xx" function from Vimscript (`func_lua` in eval.lua).
///
/// - Converts argvars to API Objects, calls the Lua function, converts the result back.
/// - NOT used when called from Lua; `nlua_call()` calls the function directly.
static void lua_wrapper(typval_T *argvars, typval_T *rettv, EvalFuncData fptr)
{
MAXSIZE_TEMP_ARRAY(args, MAX_FUNC_ARGS);
Arena arena = ARENA_EMPTY;
for (typval_T *tv = argvars; tv->v_type != VAR_UNKNOWN; tv++) {
ADD_C(args, vim_to_object(tv, &arena, false));
}
// `func_lua` is the function name (e.g. "f_hostname") in `_core/vimfn.lua`.
char buf[256];
snprintf(buf, sizeof(buf), "return require('vim._core.vimfn').%s(...)", fptr.func_lua);
Error err = ERROR_INIT;
Object result = nlua_exec(cstr_as_string(buf), NULL, args, kRetObject, &arena, &err);
if (ERROR_SET(&err)) {
semsg_multiline("emsg", e_api_error, err.msg);
goto end;
}
object_to_vim_take_luaref(&result, rettv, true, &err);
end:
arena_mem_free(arena_finish(&arena));
api_clear_error(&err);
}
/// "abs(expr)" function
static void f_abs(typval_T *argvars, typval_T *rettv, EvalFuncData fptr)
{
if (argvars[0].v_type == VAR_FLOAT) {
float_op_wrapper(argvars, rettv, (EvalFuncData){ .float_func = &fabs });
float_op_wrapper(argvars, rettv, (EvalFuncData){ .func_float = &fabs });
} else {
bool error = false;
@@ -2901,16 +2948,6 @@ static void f_hlexists(typval_T *argvars, typval_T *rettv, EvalFuncData fptr)
rettv->vval.v_number = highlight_exists(tv_get_string(&argvars[0]));
}
/// "hostname()" function
static void f_hostname(typval_T *argvars, typval_T *rettv, EvalFuncData fptr)
{
char hostname[256];
os_get_hostname(hostname, 256);
rettv->v_type = VAR_STRING;
rettv->vval.v_string = xstrdup(hostname);
}
/// "index()" function
static void f_index(typval_T *argvars, typval_T *rettv, EvalFuncData fptr)
{

View File

@@ -1207,6 +1207,7 @@ static bool viml_func_is_fast(const char *name)
return false;
}
/// "vim.call()", aka "vim.fn".
int nlua_call(lua_State *lstate)
{
Error err = ERROR_INIT;
@@ -1224,6 +1225,27 @@ int nlua_call(lua_State *lstate)
return luaL_error(lstate, "Function called with too many arguments");
}
// Fast path: if the vimfn is defined with `func_lua`, call the Lua function
// directly without Lua→typval→Object→Lua round-trip.
const char *func_lua = find_internal_func_lua(name);
if (func_lua) {
lua_getglobal(lstate, "require");
lua_pushliteral(lstate, "vim._core.vimfn");
if (lua_pcall(lstate, 1, 1, 0) != 0) {
return lua_error(lstate);
}
lua_getfield(lstate, -1, func_lua);
lua_remove(lstate, -2); // remove module table
// Push args (still on the stack as Lua values).
for (int j = 0; j < nargs; j++) {
lua_pushvalue(lstate, j + 2);
}
if (lua_pcall(lstate, nargs, 1, 0) != 0) {
return lua_error(lstate);
}
return 1;
}
typval_T vim_args[MAX_FUNC_ARGS + 1];
int i = 0; // also used for freeing the variables
for (; i < nargs; i++) {

View File

@@ -32,8 +32,9 @@ typedef double float_T;
typedef struct MsgpackRpcRequestHandler MsgpackRpcRequestHandler;
typedef union {
float_T (*float_func)(float_T);
const MsgpackRpcRequestHandler *api_handler;
float_T (*func_float)(float_T);
const MsgpackRpcRequestHandler *func_api; // Vimscript bridge to API fn.
const char *func_lua; ///< Lua-implemented vimfn.
void *null;
} EvalFuncData;

View File

@@ -231,6 +231,7 @@ describe('vim._core', function()
'vim._core.system',
'vim._core.ui2',
'vim._core.util',
'vim._core.vimfn',
'vim._init_packages',
'vim.filetype',
'vim.fs',

View File

@@ -1391,6 +1391,15 @@ describe('lua stdlib', function()
)
end)
it('vim.fn `func_lua` (fast path for Lua-implemented builtins)', function()
-- hostname() is implemented via func_lua, calling Lua directly when invoked from Lua.
local lua_result = exec_lua([[return vim.fn.hostname()]])
eq(type(lua_result), 'string')
assert(#lua_result > 0, 'hostname() should return a non-empty string')
-- VimScript path (lua_wrapper) should return the same result.
eq(lua_result, eval('hostname()'))
end)
it('vim.call fails in fast context', function()
local screen = Screen.new(120, 10)
exec_lua([[
@@ -2003,14 +2012,14 @@ describe('lua stdlib', function()
local errmsg = api.nvim_get_vvar('errmsg')
matches(
[[
^vim%.on%_key%(%) callbacks:.*
With ns%_id %d+: .*: Dumb Error
stack traceback:
.*: in function 'error'
.*: in function 'ErrF2'
.*: in function 'ErrF1'
.*]],
t.dedent [[
^vim%.on%_key%(%) callbacks:.*
With ns%_id %d+: .*: Dumb Error
stack traceback:
.*: in function 'error'
.*: in function 'ErrF2'
.*: in function 'ErrF1'
.*]],
errmsg
)
end)
@@ -3268,8 +3277,8 @@ describe('vim.keymap', function()
end)
end)
describe('Vimscript function exists()', function()
it('can check a lua function', function()
describe('vim.fn.exists()', function()
it('can check a Lua function', function()
eq(
1,
exec_lua [[

View File

@@ -215,10 +215,10 @@ describe('listing functions using :function', function()
command('let A = {-> 1}')
local num = exec_capture('echo A'):match("function%('<lambda>(%d+)'%)")
eq(
([[
function <lambda>%s(...)
1 return 1
endfunction]]):format(num),
t.dedent([[
function <lambda>%s(...)
1 return 1
endfunction]]):format(num),
exec_capture(('function <lambda>%s'):format(num))
)
end)