Files
neovim/src/nvim/generators/gen_api_dispatch.lua
Justin M. Keyes f6e19e7334 fix(logging): skip recursion, fix crash #18764
Problem:
1. The main log routine does not protect itself against recursion.
   log_lock() doesn't guard against recursion, it would deadlock...
2. 22b52dd462 (#11501) regressed 6f27f5ef91 (#10172), because
   set_init_1..process_spawn tries to log (see backtrace below), but the
   mutex isn't initialized yet. Even if the mutex were valid, we don't
   want early logging to fallback to stderr because that can break
   embedders when stdio is used for RPC.

    frame 1: 0x00000001001d54f4 nvim`open_log_file at log.c:205:7
    frame 2: 0x00000001001d5390 nvim`logmsg(log_level=1, context="UI: ", func_name=0x0000000000000000, line_num=-1, eol=true, fmt="win_viewport") at log.c:150:20
    frame : 0x000000010039aea2 nvim`ui_call_win_viewport(grid=2, win=1000, topline=0, botline=1, curline=0, curcol=0, line_count=1) at ui_events_call.generated.h:321:3
    frame 4: 0x00000001003dfefc nvim`ui_ext_win_viewport(wp=0x0000000101816400) at window.c:939:5
    frame 5: 0x00000001003ec5b4 nvim`win_ui_flush at window.c:7303:7
    frame 6: 0x00000001003a04c0 nvim`ui_flush at ui.c:508:3
    frame 7: 0x00000001002966ba nvim`do_os_system(argv=0x0000600000c0c000, input=0x0000000000000000, len=0, output=0x0000000000000000, nread=0x00007ff7bfefe830, silent=false, forward_output=false) at shell.c:894:3
    frame 8: 0x0000000100295f68 nvim`os_call_shell(cmd="unset nonomatch; vimglob() { while [ $# -ge 1 ]; do echo \"$1\"; shift; done }; vimglob >/var/folders/gk/3tttv_md06987tlwpyp62jrw0000gn/T/nvimwwvwfD/0 ~foo", opts=kShellOptExpand | kShellOptSilent | kShellOptHideMess, extra_args=0x0000000000000000) at shell.c:663:18
    frame 9: 0x0000000100295845 nvim`call_shell(cmd="unset nonomatch; vimglob() { while [ $# -ge 1 ]; do echo \"$1\"; shift; done }; vimglob >/var/folders/gk/3tttv_md06987tlwpyp62jrw0000gn/T/nvimwwvwfD/0 ~foo", opts=kShellOptExpand | kShellOptSilent | kShellOptHideMess, extra_shell_arg=0x0000000000000000) at shell.c:712:14
    frame 10: 0x0000000100294c6f nvim`os_expand_wildcards(num_pat=1, pat=0x00007ff7bfefeb20, num_file=0x00007ff7bfefee58, file=0x00007ff7bfefee60, flags=43) at shell.c:328:7
    ...
    frame 23: 0x000000010028ccef nvim`expand_env_esc(srcp=",~foo", dst="~foo", dstlen=4094, esc=false, one=false, prefix=0x0000000000000000) at env.c:673:17
    frame 24: 0x000000010026fdd5 nvim`option_expand(opt_idx=29, val=",~foo") at option.c:1950:3
    frame 25: 0x000000010026f129 nvim`set_init_1(clean_arg=false) at option.c:558:19
    frame 26: 0x00000001001ea25e nvim`early_init(paramp=0x00007ff7bfeff5f0) at main.c:198:3
    frame 27: 0x00000001001ea6bf nvim`main(argc=1, argv=0x00007ff7bfeff848) at main.c:255:3

Solution:
1. Check for recursion, show "internal error" message.
    - FUTURE: when "remote TUI" is merged, can we remove log_lock()?
2. Skip logging if log_init wasn't called yet.
2022-05-30 13:07:33 -07:00

562 lines
18 KiB
Lua

local mpack = require('mpack')
-- we need at least 4 arguments since the last two are output files
if arg[1] == '--help' then
print('Usage: genmsgpack.lua args')
print('Args: 1: source directory')
print(' 2: dispatch output file (dispatch_wrappers.generated.h)')
print(' 3: functions metadata output file (funcs_metadata.generated.h)')
print(' 4: API metadata output file (api_metadata.mpack)')
print(' 5: lua C bindings output file (lua_api_c_bindings.generated.c)')
print(' rest: C files where API functions are defined')
end
assert(#arg >= 4)
local functions = {}
local nvimdir = arg[1]
package.path = nvimdir .. '/?.lua;' .. package.path
_G.vim = loadfile(nvimdir..'/../../runtime/lua/vim/shared.lua')()
local hashy = require'generators.hashy'
-- names of all headers relative to the source root (for inclusion in the
-- generated file)
local headers = {}
-- output h file with generated dispatch functions
local dispatch_outputf = arg[2]
-- output h file with packed metadata
local funcs_metadata_outputf = arg[3]
-- output metadata mpack file, for use by other build scripts
local mpack_outputf = arg[4]
local lua_c_bindings_outputf = arg[5]
-- set of function names, used to detect duplicates
local function_names = {}
local c_grammar = require('generators.c_grammar')
local function startswith(String,Start)
return string.sub(String,1,string.len(Start))==Start
end
-- read each input file, parse and append to the api metadata
for i = 6, #arg do
local full_path = arg[i]
local parts = {}
for part in string.gmatch(full_path, '[^/]+') do
parts[#parts + 1] = part
end
headers[#headers + 1] = parts[#parts - 1]..'/'..parts[#parts]
local input = io.open(full_path, 'rb')
local tmp = c_grammar.grammar:match(input:read('*all'))
for j = 1, #tmp do
local fn = tmp[j]
local public = startswith(fn.name, "nvim_") or fn.deprecated_since
if public and not fn.noexport then
functions[#functions + 1] = tmp[j]
function_names[fn.name] = true
if #fn.parameters ~= 0 and fn.parameters[1][2] == 'channel_id' then
-- this function should receive the channel id
fn.receives_channel_id = true
-- remove the parameter since it won't be passed by the api client
table.remove(fn.parameters, 1)
end
if #fn.parameters ~= 0 and fn.parameters[#fn.parameters][1] == 'error' then
-- function can fail if the last parameter type is 'Error'
fn.can_fail = true
-- remove the error parameter, msgpack has it's own special field
-- for specifying errors
fn.parameters[#fn.parameters] = nil
end
end
end
input:close()
end
local function shallowcopy(orig)
local copy = {}
for orig_key, orig_value in pairs(orig) do
copy[orig_key] = orig_value
end
return copy
end
-- Export functions under older deprecated names.
-- These will be removed eventually.
local deprecated_aliases = require("api.dispatch_deprecated")
for _,f in ipairs(shallowcopy(functions)) do
local ismethod = false
if startswith(f.name, "nvim_") then
if startswith(f.name, "nvim__") then
f.since = -1
elseif f.since == nil then
print("Function "..f.name.." lacks since field.\n")
os.exit(1)
end
f.since = tonumber(f.since)
if f.deprecated_since ~= nil then
f.deprecated_since = tonumber(f.deprecated_since)
end
if startswith(f.name, "nvim_buf_") then
ismethod = true
elseif startswith(f.name, "nvim_win_") then
ismethod = true
elseif startswith(f.name, "nvim_tabpage_") then
ismethod = true
end
f.remote = f.remote_only or not f.lua_only
f.lua = f.lua_only or not f.remote_only
f.eval = (not f.lua_only) and (not f.remote_only)
else
f.deprecated_since = tonumber(f.deprecated_since)
assert(f.deprecated_since == 1)
f.remote = true
f.since = 0
end
f.method = ismethod
local newname = deprecated_aliases[f.name]
if newname ~= nil then
if function_names[newname] then
-- duplicate
print("Function "..f.name.." has deprecated alias\n"
..newname.." which has a separate implementation.\n"..
"Please remove it from src/nvim/api/dispatch_deprecated.lua")
os.exit(1)
end
local newf = shallowcopy(f)
newf.name = newname
if newname == "ui_try_resize" then
-- The return type was incorrectly set to Object in 0.1.5.
-- Keep it that way for clients that rely on this.
newf.return_type = "Object"
end
newf.impl_name = f.name
newf.lua = false
newf.eval = false
newf.since = 0
newf.deprecated_since = 1
functions[#functions+1] = newf
end
end
-- don't expose internal attributes like "impl_name" in public metadata
local exported_attributes = {'name', 'return_type', 'method',
'since', 'deprecated_since'}
local exported_functions = {}
for _,f in ipairs(functions) do
if not startswith(f.name, "nvim__") then
local f_exported = {}
for _,attr in ipairs(exported_attributes) do
f_exported[attr] = f[attr]
end
f_exported.parameters = {}
for i,param in ipairs(f.parameters) do
if param[1] == "DictionaryOf(LuaRef)" then
param = {"Dictionary", param[2]}
elseif startswith(param[1], "Dict(") then
param = {"Dictionary", param[2]}
end
f_exported.parameters[i] = param
end
exported_functions[#exported_functions+1] = f_exported
end
end
-- serialize the API metadata using msgpack and embed into the resulting
-- binary for easy querying by clients
local funcs_metadata_output = io.open(funcs_metadata_outputf, 'wb')
local packed = mpack.pack(exported_functions)
local dump_bin_array = require("generators.dump_bin_array")
dump_bin_array(funcs_metadata_output, 'funcs_metadata', packed)
funcs_metadata_output:close()
-- start building the dispatch wrapper output
local output = io.open(dispatch_outputf, 'wb')
local function real_type(type)
local rv = type
local rmatch = string.match(type, "Dict%(([_%w]+)%)")
if rmatch then
return "KeyDict_"..rmatch
elseif c_grammar.typed_container:match(rv) then
if rv:match('Array') then
rv = 'Array'
else
rv = 'Dictionary'
end
end
return rv
end
local function attr_name(rt)
if rt == 'Float' then
return 'floating'
else
return rt:lower()
end
end
-- start the handler functions. Visit each function metadata to build the
-- handler function with code generated for validating arguments and calling to
-- the real API.
for i = 1, #functions do
local fn = functions[i]
if fn.impl_name == nil and fn.remote then
local args = {}
output:write('Object handle_'..fn.name..'(uint64_t channel_id, Array args, Error *error)')
output:write('\n{')
output:write('\n#if MIN_LOG_LEVEL <= LOGLVL_DBG')
output:write('\n logmsg(LOGLVL_DBG, "RPC: ", NULL, -1, true, "ch %" PRIu64 ": invoke '
..fn.name..'", channel_id);')
output:write('\n#endif')
output:write('\n Object ret = NIL;')
-- Declare/initialize variables that will hold converted arguments
for j = 1, #fn.parameters do
local param = fn.parameters[j]
local rt = real_type(param[1])
local converted = 'arg_'..j
output:write('\n '..rt..' '..converted..';')
end
output:write('\n')
output:write('\n if (args.size != '..#fn.parameters..') {')
output:write('\n api_set_error(error, kErrorTypeException, \
"Wrong number of arguments: expecting '..#fn.parameters..' but got %zu", args.size);')
output:write('\n goto cleanup;')
output:write('\n }\n')
-- Validation/conversion for each argument
for j = 1, #fn.parameters do
local converted, param
param = fn.parameters[j]
converted = 'arg_'..j
local rt = real_type(param[1])
if rt == 'Object' then
output:write('\n '..converted..' = args.items['..(j - 1)..'];\n')
elseif rt:match('^KeyDict_') then
converted = '&' .. converted
output:write('\n if (args.items['..(j - 1)..'].type == kObjectTypeDictionary) {') --luacheck: ignore 631
output:write('\n memset('..converted..', 0, sizeof(*'..converted..'));') -- TODO: neeeee
output:write('\n if (!api_dict_to_keydict('..converted..', '..rt..'_get_field, args.items['..(j - 1)..'].data.dictionary, error)) {')
output:write('\n goto cleanup;')
output:write('\n }')
output:write('\n } else if (args.items['..(j - 1)..'].type == kObjectTypeArray && args.items['..(j - 1)..'].data.array.size == 0) {') --luacheck: ignore 631
output:write('\n memset('..converted..', 0, sizeof(*'..converted..'));')
output:write('\n } else {')
output:write('\n api_set_error(error, kErrorTypeException, \
"Wrong type for argument '..j..' when calling '..fn.name..', expecting '..param[1]..'");')
output:write('\n goto cleanup;')
output:write('\n }\n')
else
if rt:match('^Buffer$') or rt:match('^Window$') or rt:match('^Tabpage$') then
-- Buffer, Window, and Tabpage have a specific type, but are stored in integer
output:write('\n if (args.items['..
(j - 1)..'].type == kObjectType'..rt..' && args.items['..(j - 1)..'].data.integer >= 0) {')
output:write('\n '..converted..' = (handle_T)args.items['..(j - 1)..'].data.integer;')
else
output:write('\n if (args.items['..(j - 1)..'].type == kObjectType'..rt..') {')
output:write('\n '..converted..' = args.items['..(j - 1)..'].data.'..attr_name(rt)..';')
end
if rt:match('^Buffer$') or rt:match('^Window$') or rt:match('^Tabpage$') or rt:match('^Boolean$') then
-- accept nonnegative integers for Booleans, Buffers, Windows and Tabpages
output:write('\n } else if (args.items['..
(j - 1)..'].type == kObjectTypeInteger && args.items['..(j - 1)..'].data.integer >= 0) {')
output:write('\n '..converted..' = (handle_T)args.items['..(j - 1)..'].data.integer;')
end
if rt:match('^Float$') then
-- accept integers for Floats
output:write('\n } else if (args.items['..
(j - 1)..'].type == kObjectTypeInteger) {')
output:write('\n '..converted..' = (Float)args.items['..(j - 1)..'].data.integer;')
end
-- accept empty lua tables as empty dictionaries
if rt:match('^Dictionary') then
output:write('\n } else if (args.items['..(j - 1)..'].type == kObjectTypeArray && args.items['..(j - 1)..'].data.array.size == 0) {') --luacheck: ignore 631
output:write('\n '..converted..' = (Dictionary)ARRAY_DICT_INIT;')
end
output:write('\n } else {')
output:write('\n api_set_error(error, kErrorTypeException, \
"Wrong type for argument '..j..' when calling '..fn.name..', expecting '..param[1]..'");')
output:write('\n goto cleanup;')
output:write('\n }\n')
end
args[#args + 1] = converted
end
if fn.check_textlock then
output:write('\n if (textlock != 0) {')
output:write('\n api_set_error(error, kErrorTypeException, "%s", e_secure);')
output:write('\n goto cleanup;')
output:write('\n }\n')
end
-- function call
local call_args = table.concat(args, ', ')
output:write('\n ')
if fn.return_type ~= 'void' then
-- has a return value, prefix the call with a declaration
output:write(fn.return_type..' rv = ')
end
-- write the function name and the opening parenthesis
output:write(fn.name..'(')
if fn.receives_channel_id then
-- if the function receives the channel id, pass it as first argument
if #args > 0 or fn.can_fail then
output:write('channel_id, '..call_args)
else
output:write('channel_id')
end
else
output:write(call_args)
end
if fn.can_fail then
-- if the function can fail, also pass a pointer to the local error object
if #args > 0 then
output:write(', error);\n')
else
output:write('error);\n')
end
-- and check for the error
output:write('\n if (ERROR_SET(error)) {')
output:write('\n goto cleanup;')
output:write('\n }\n')
else
output:write(');\n')
end
if fn.return_type ~= 'void' then
output:write('\n ret = '..string.upper(real_type(fn.return_type))..'_OBJ(rv);')
end
output:write('\n\ncleanup:');
output:write('\n return ret;\n}\n\n');
end
end
local remote_fns = {}
for _,fn in ipairs(functions) do
if fn.remote then
remote_fns[fn.name] = fn
end
end
remote_fns.redraw = {impl_name="ui_client_redraw", fast=true}
local hashorder, hashfun = hashy.hashy_hash("msgpack_rpc_get_handler_for", vim.tbl_keys(remote_fns), function (idx)
return "method_handlers["..idx.."].name"
end)
output:write("static const MsgpackRpcRequestHandler method_handlers[] = {\n")
for _, name in ipairs(hashorder) do
local fn = remote_fns[name]
output:write(' { .name = "'..name..'", .fn = handle_'.. (fn.impl_name or fn.name)..
', .fast = '..tostring(fn.fast)..'},\n')
end
output:write("};\n\n")
output:write(hashfun)
output:close()
local mpack_output = io.open(mpack_outputf, 'wb')
mpack_output:write(mpack.pack(functions))
mpack_output:close()
local function include_headers(output_handle, headers_to_include)
for i = 1, #headers_to_include do
if headers_to_include[i]:sub(-12) ~= '.generated.h' then
output_handle:write('\n#include "nvim/'..headers_to_include[i]..'"')
end
end
end
local function write_shifted_output(_, str)
str = str:gsub('\n ', '\n')
str = str:gsub('^ ', '')
str = str:gsub(' +$', '')
output:write(str)
end
-- start building lua output
output = io.open(lua_c_bindings_outputf, 'wb')
output:write([[
// This is an open source non-commercial project. Dear PVS-Studio, please check
// it. PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com
#include <lua.h>
#include <lualib.h>
#include <lauxlib.h>
#include "nvim/func_attr.h"
#include "nvim/api/private/defs.h"
#include "nvim/api/private/helpers.h"
#include "nvim/lua/converter.h"
#include "nvim/lua/executor.h"
]])
include_headers(output, headers)
output:write('\n')
local lua_c_functions = {}
local function process_function(fn)
local lua_c_function_name = ('nlua_api_%s'):format(fn.name)
write_shifted_output(output, string.format([[
static int %s(lua_State *lstate)
{
Error err = ERROR_INIT;
if (lua_gettop(lstate) != %i) {
api_set_error(&err, kErrorTypeValidation, "Expected %i argument%s");
goto exit_0;
}
]], lua_c_function_name, #fn.parameters, #fn.parameters,
(#fn.parameters == 1) and '' or 's'))
lua_c_functions[#lua_c_functions + 1] = {
binding=lua_c_function_name,
api=fn.name
}
if not fn.fast then
write_shifted_output(output, string.format([[
if (!nlua_is_deferred_safe()) {
return luaL_error(lstate, e_luv_api_disabled, "%s");
}
]], fn.name))
end
if fn.check_textlock then
write_shifted_output(output, [[
if (textlock != 0) {
api_set_error(&err, kErrorTypeException, "%s", e_secure);
goto exit_0;
}
]])
end
local cparams = ''
local free_code = {}
for j = #fn.parameters,1,-1 do
local param = fn.parameters[j]
local cparam = string.format('arg%u', j)
local param_type = real_type(param[1])
local lc_param_type = real_type(param[1]):lower()
local extra = param_type == "Dictionary" and "false, " or ""
if param[1] == "Object" or param[1] == "DictionaryOf(LuaRef)" then
extra = "true, "
end
local errshift = 0
if string.match(param_type, '^KeyDict_') then
write_shifted_output(output, string.format([[
%s %s = { 0 }; nlua_pop_keydict(lstate, &%s, %s_get_field, %s&err);]], param_type, cparam, cparam, param_type, extra))
cparam = '&'..cparam
errshift = 1 -- free incomplete dict on error
else
write_shifted_output(output, string.format([[
const %s %s = nlua_pop_%s(lstate, %s&err);]], param[1], cparam, param_type, extra))
end
write_shifted_output(output, string.format([[
if (ERROR_SET(&err)) {
goto exit_%u;
}
]], #fn.parameters - j + errshift))
free_code[#free_code + 1] = ('api_free_%s(%s);'):format(
lc_param_type, cparam)
cparams = cparam .. ', ' .. cparams
end
if fn.receives_channel_id then
cparams = 'LUA_INTERNAL_CALL, ' .. cparams
end
if fn.can_fail then
cparams = cparams .. '&err'
else
cparams = cparams:gsub(', $', '')
end
local free_at_exit_code = ''
for i = 1, #free_code do
local rev_i = #free_code - i + 1
local code = free_code[rev_i]
if i == 1 and not string.match(real_type(fn.parameters[1][1]), '^KeyDict_') then
free_at_exit_code = free_at_exit_code .. ('\n %s'):format(code)
else
free_at_exit_code = free_at_exit_code .. ('\n exit_%u:\n %s'):format(
rev_i, code)
end
end
local err_throw_code = [[
exit_0:
if (ERROR_SET(&err)) {
luaL_where(lstate, 1);
lua_pushstring(lstate, err.msg);
api_clear_error(&err);
lua_concat(lstate, 2);
return lua_error(lstate);
}
]]
local return_type
if fn.return_type ~= 'void' then
if fn.return_type:match('^ArrayOf') then
return_type = 'Array'
else
return_type = fn.return_type
end
write_shifted_output(output, string.format([[
const %s ret = %s(%s);
nlua_push_%s(lstate, ret, true);
api_free_%s(ret);
%s
%s
return 1;
]], fn.return_type, fn.name, cparams, return_type, return_type:lower(),
free_at_exit_code, err_throw_code))
else
write_shifted_output(output, string.format([[
%s(%s);
%s
%s
return 0;
]], fn.name, cparams, free_at_exit_code, err_throw_code))
end
write_shifted_output(output, [[
}
]])
end
for _, fn in ipairs(functions) do
if fn.lua or fn.name:sub(1, 4) == '_vim' then
process_function(fn)
end
end
output:write(string.format([[
void nlua_add_api_functions(lua_State *lstate); // silence -Wmissing-prototypes
void nlua_add_api_functions(lua_State *lstate)
FUNC_ATTR_NONNULL_ALL
{
lua_createtable(lstate, 0, %u);
]], #lua_c_functions))
for _, func in ipairs(lua_c_functions) do
output:write(string.format([[
lua_pushcfunction(lstate, &%s);
lua_setfield(lstate, -2, "%s");]], func.binding, func.api))
end
output:write([[
lua_setfield(lstate, -2, "api");
}
]])
output:close()