diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index a10327c23d82b07d22de7b9f501594f0d5738be3..3f04eebbccac119cc00d817882d75ee085df367b 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -104,6 +104,7 @@ set (server_sources backtrace.cc proc_title.c coeio_file.c + lua/console.c lua/digest.c lua/init.c lua/fiber.c diff --git a/src/lua/console.c b/src/lua/console.c new file mode 100644 index 0000000000000000000000000000000000000000..560394404d898b097e7d14c144873b6328a2a148 --- /dev/null +++ b/src/lua/console.c @@ -0,0 +1,580 @@ +/* + * Copyright 2010-2016, Tarantool AUTHORS, please see AUTHORS file. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * 1. Redistributions of source code must retain the above + * copyright notice, this list of conditions and the + * following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY <COPYRIGHT HOLDER> ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * <COPYRIGHT HOLDER> OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR + * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF + * THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +#include "lua/console.h" +#include "lua/utils.h" +#include "fiber.h" +#include "coio.h" +#include <lua.h> +#include <lauxlib.h> +#include <lualib.h> +#include <readline/readline.h> +#include <readline/history.h> +#include <stdlib.h> +#include <ctype.h> + +/* + * Completion engine (Mike Paul's). + * Used internally when collecting completions locally. Also a Lua + * wrapper is provided enabling a remote server to compute completions + * for a client. + */ +static char ** +lua_rl_complete(lua_State *L, const char *text, int start, int end); + +/* + * Lua state that made the pending readline call. + * This Lua state is accessed in readline callbacks. Unfortunately + * readline library doesn't allow to pass it as a function argument. + * Two concurrent readline() calls never happen. + */ +static struct lua_State *readline_L; + +/* + * console_completion_handler() + * Called by readline to collect plausible completions; + * The call stack is as follows: + * + * - lbox_console_readline + * - (loop) rl_callback_read_char + * - console_completion_handler + * + * Delegates to the func selected when the call to lbox_console_readline + * was made, e.g. readline({ completion = ... }). + */ +static char ** +console_completion_handler(const char *text, int start, int end) +{ + size_t n, i; + char **res; + + /* + * The lbox_console_readline() frame is still on the top of Lua + * stack. We can reach the function arguments. Assuming arg#1 is + * the options table. + */ + lua_getfield(readline_L, 1, "completion"); + if (lua_isnil(readline_L, -1)) { + lua_pop(readline_L, 1); + return NULL; + } + + /* + * If the completion func is lbox_console_completion_handler() + * /we have it in upvalue #1/ which is a wrapper on top of + * lua_rl_complete, call lua_rl_complete func directly. + */ + if (lua_equal(readline_L, -1, lua_upvalueindex(1))) { + lua_pop(readline_L, 1); + return lua_rl_complete(readline_L, text, start, end); + } + + /* Slow path - arbitrary completion handler. */ + lua_pushstring(readline_L, text); + lua_pushinteger(readline_L, start); + lua_pushinteger(readline_L, end); + if (lua_pcall(readline_L, 3, 1, 0) != 0 || + !lua_istable(readline_L, -1)) { + + lua_pop(readline_L, 1); + return NULL; + } + n = lua_objlen(readline_L, -1); + res = malloc(sizeof(res[0]) * (n + 1)); + if (res == NULL) { + lua_pop(readline_L, 1); + return NULL; + } + res[n] = NULL; + for (i = 0; i < n; i++) { + lua_pushinteger(readline_L, i + 1); + lua_gettable(readline_L, -2); + res[i] = strdup(lua_tostring(readline_L, -1)); + lua_pop(readline_L, 1); + } + lua_pop(readline_L, 1); + return res; +} + +/* + * console_push_line() + * Readline invokes this callback once the whole line is ready. + * The call stack is as follows: + * + * - lbox_console_readline + * - (loop) rl_callback_read_char + * - console_push_line + * + * The callback creates a copy of the line on the Lua stack; this copy + * becomes the lbox_console_readline()'s ultimate result. + */ +static void +console_push_line(char *line) +{ + /* XXX pushnil/pushstring may err */ + if (line == NULL) + lua_pushnil(readline_L); + else + lua_pushstring(readline_L, line); + +#ifdef HAVE_GNU_READLINE + /* + * This is to avoid a stray prompt on the next line with GNU + * readline. Interestingly, it botches the terminal when + * attempted with libeditline. + */ + rl_callback_handler_install(NULL, NULL); +#endif +} + +/* implements readline() Lua API */ +static int +lbox_console_readline(struct lua_State *L) +{ + const char *prompt = NULL; + int top; + + rl_attempted_completion_function = NULL; + + if (lua_gettop(L) > 0) { + switch (lua_type(L, 1)) { + case LUA_TSTRING: + prompt = lua_tostring(L, 1); + break; + case LUA_TTABLE: + lua_getfield(L, 1, "prompt"); + prompt = lua_tostring(L, -1); + lua_pop(L, 1); + /* the handler assumes arg #1 is a table */ + rl_attempted_completion_function = + console_completion_handler; + break; + default: + luaL_error(L, "readline([prompt])"); + } + } + + if (prompt == NULL) + prompt = "> "; + + if (readline_L != NULL) + luaL_error(L, "readline(): earlier call didn't complete yet"); + + readline_L = L; + rl_completer_word_break_characters = + "\t\r\n !\"#$%&'()*+,-/;<=>?@[\\]^`{|}~"; + rl_completer_quote_characters = "\"'"; +#if RL_READLINE_VERSION < 0x0600 + rl_completion_append_character = '\0'; +#endif + /* + * Readline library provides eventloop-friendly API; repeat + * until console_push_line() manages to capture the result. + */ + rl_callback_handler_install(prompt, console_push_line); + top = lua_gettop(L); + while (top == lua_gettop(L) && + coio_wait(STDIN_FILENO, COIO_READ, TIMEOUT_INFINITY)) { + + rl_callback_read_char(); + } + + readline_L = NULL; + /* Incidents happen. */ +#pragma GCC poison readline_L + rl_attempted_completion_function = NULL; + luaL_testcancel(L); + return 1; +} + +/* C string array to lua table converter */ +static int +console_completion_helper(struct lua_State *L) +{ + size_t i; + char **res = *(char ***)lua_topointer(L, -1); + assert(lua_islightuserdata(L, -1)); + assert(L != NULL); + lua_createtable(L, 0, 0); + for (i = 0; res[i]; i++) { + lua_pushstring(L, res[i]); + lua_rawseti(L, -2, i + i); + } + return 1; +} + +/* + * completion_handler() Lua API + * Exposing completion engine to Lua. + */ +static int +lbox_console_completion_handler(struct lua_State *L) +{ + size_t i; + char **res; + int st; + + /* + * Prepare for the future pcall; + * this may err, hence do it before res is created + */ + lua_pushcfunction(L, console_completion_helper); + lua_pushlightuserdata(L, &res); + + res = lua_rl_complete(L, lua_tostring(L, 1), + lua_tointeger(L, 2), lua_tointeger(L, 3)); + + if (res == NULL) { + return 0; + } + + st = lua_pcall(L, 1, 1, 0); + + /* free res */ + for (i = 0; res[i]; i++) { + free(res[i]); + } + free(res); + res = NULL; + + if (st != 0) { + lua_error(L); + } + + return 1; +} + +static int +lbox_console_add_history(struct lua_State *L) +{ + if (lua_gettop(L) < 1 || !lua_isstring(L, 1)) + luaL_error(L, "add_history(string)"); + + add_history(lua_tostring(L, 1)); + return 0; +} + +void +tarantool_lua_console_init(struct lua_State *L) +{ + static const struct luaL_reg consolelib[] = { + {"add_history", lbox_console_add_history}, + {"completion_handler", lbox_console_completion_handler}, + {NULL, NULL} + }; + luaL_register_module(L, "console", consolelib); + + /* readline() func neads a ref to completion_handler (in upvalue) */ + lua_getfield(L, -1, "completion_handler"); + lua_pushcclosure(L, lbox_console_readline, 1); + lua_setfield(L, -2, "readline"); +} + +/* + * Completion engine from "Mike Paul's advanced readline patch". + * With minor fixes and code style tweaks. + */ +#define lua_pushglobaltable(L) lua_pushvalue(L, LUA_GLOBALSINDEX) + +enum { + /* + * Suggest a keyword if a prefix of KEYWORD_MATCH_MIN + * characters or more was entered. + */ + KEYWORD_MATCH_MIN = 1, + /* + * Metatables are consulted recursively when learning items; + * avoid infinite metatable loops. + */ + METATABLE_RECURSION_MAX = 20, + /* + * Extracting all items matching a given prefix is O(n); + * stop once that many items were considered. + */ + ITEMS_CHECKED_MAX = 500 +}; + +/* goto intentionally omited */ +static const char * +const lua_rl_keywords[] = { + "and", "break", "do", "else", "elseif", "end", "false", + "for", "function", "if", "in", "local", "nil", "not", "or", + "repeat", "return", "then", "true", "until", "while", NULL +}; + +static int +valid_identifier(const char *s) +{ + if (!(isalpha(*s) || *s == '_')) return 0; + for (s++; *s; s++) + if (!(isalpha(*s) || isdigit(*s) || *s == '_')) return 0; + return 1; +} + +/* + * Dynamically resizable match list. + * Readline consumes argv-style string list; both the list itself and + * individual strings should be malloc-ed; readline is responsible for + * releasing them once done. Item #0 is the longest common prefix + * (inited last). Idx is the last index assigned (i.e. len - 1.) + */ +typedef struct { + char **list; + size_t idx, allocated, matchlen; +} dmlist; + +static void +lua_rl_dmfree(dmlist *ml) +{ + size_t i; + /* + * Note: item #0 isn't initialized until the very end of + * lua_rl_complete, the only function calling dmfree(). + */ + for (i = 1; i <= ml->idx; i++) { + free(ml->list[i]); + } + free(ml->list); + ml->list = NULL; +} + +/* Add prefix + string + suffix to list and compute common prefix. */ +static int +lua_rl_dmadd(dmlist *ml, const char *p, size_t pn, const char *s, int suf) +{ + char *t = NULL; + + if (ml->idx+1 >= ml->allocated) { + char **new_list; + new_list = realloc( + ml->list, sizeof(char *)*(ml->allocated += 32)); + if (!new_list) + return -1; + ml->list = new_list; + } + + if (s) { + size_t n = strlen(s); + if (!(t = (char *)malloc(sizeof(char)*(pn + n + 2)))) + return 1; + memcpy(t, p, pn); + memcpy(t + pn, s, n); + n += pn; + t[n] = suf; + if (suf) t[++n] = '\0'; + + if (ml->idx == 0) { + ml->matchlen = n; + } else { + size_t i; + for (i = 0; i < ml->matchlen && i < n && + ml->list[1][i] == t[i]; i++) ; + /* Set matchlen to common prefix. */ + ml->matchlen = i; + } + } + + ml->list[++ml->idx] = t; + return 0; +} + +/* Get __index field of metatable of object on top of stack. */ +static int +lua_rl_getmetaindex(lua_State *L) +{ + if (!lua_getmetatable(L, -1)) { + lua_pop(L, 1); + return 0; + } + lua_pushstring(L, "__index"); + lua_rawget(L, -2); + lua_replace(L, -2); + if (lua_isnil(L, -1) || lua_rawequal(L, -1, -2)) { + lua_pop(L, 2); + return 0; + } + lua_replace(L, -2); + return 1; +} /* 1: obj -- val, 0: obj -- */ + +/* Get field from object on top of stack. Avoid calling metamethods. */ +static int +lua_rl_getfield(lua_State *L, const char *s, size_t n) +{ + int loop = METATABLE_RECURSION_MAX; + do { + if (lua_istable(L, -1)) { + lua_pushlstring(L, s, n); + lua_rawget(L, -2); + if (!lua_isnil(L, -1)) { + lua_replace(L, -2); + return 1; + } + lua_pop(L, 1); + } + if (--loop == 0) { + lua_pop(L, 1); + return 0; + } + } while (lua_rl_getmetaindex(L)); + return 0; +} /* 1: obj -- val, 0: obj -- */ + +static char ** +lua_rl_complete(lua_State *L, const char *text, int start, int end) +{ + dmlist ml; + const char *s; + size_t i, n, dot, items_checked; + int loop, savetop, is_method_ref = 0; + + if (!(text[0] == '\0' || isalpha(text[0]) || text[0] == '_')) + return NULL; + + ml.list = NULL; + ml.idx = ml.allocated = ml.matchlen = 0; + + savetop = lua_gettop(L); + lua_pushglobaltable(L); + for (n = (size_t)(end-start), i = dot = 0; i < n; i++) { + if (text[i] == '.' || text[i] == ':') { + is_method_ref = (text[i] == ':'); + if (!lua_rl_getfield(L, text+dot, i-dot)) + goto error; /* Invalid prefix. */ + dot = i+1; + /* Points to first char after dot/colon. */ + } + } + + /* Add all matches against keywords if there is no dot/colon. */ + if (dot == 0) { + for (i = 0; (s = lua_rl_keywords[i]) != NULL; i++) { + if (n >= KEYWORD_MATCH_MIN && + !strncmp(s, text, n) && + lua_rl_dmadd(&ml, NULL, 0, s, ' ')) { + + goto error; + } + } + } + + /* Add all valid matches from all tables/metatables. */ + loop = 0; + items_checked = 0; + lua_pushglobaltable(L); + lua_insert(L, -2); + do { + if (!lua_istable(L, -1) || + (loop != 0 && lua_rawequal(L, -1, -2))) + continue; + + for (lua_pushnil(L); lua_next(L, -2); lua_pop(L, 1)) { + + /* Beware huge tables */ + if (++items_checked > ITEMS_CHECKED_MAX) + break; + + if (lua_type(L, -2) != LUA_TSTRING) + continue; + + s = lua_tostring(L, -2); + /* + * Only match names starting with '_' + * if explicitly requested. + */ + if (strncmp(s, text+dot, n-dot) || + !valid_identifier(s) || + (*s == '_' && text[dot] != '_')) continue; + + int suf = 0; /* Omit suffix by default. */ + int type = lua_type(L, -1); + switch (type) { + case LUA_TTABLE: + case LUA_TUSERDATA: + /* + * For tables and userdata omit a + * suffix, since all variants, i.e. + * T, T.field, T:method and T() + * are likely valid. + */ + break; + case LUA_TFUNCTION: + /* + * Prepend '(' for a function. This + * helps to differentiate functions + * visually in completion lists. It is + * believed that in interactive console + * functions are most often called + * rather then assigned to a variable or + * passed as a parameter, hence + * an ocasional need to delete an + * unwanted '(' shouldn't be a burden. + */ + suf = '('; + break; + } + /* + * If completing a method ref, i.e + * foo:meth<TAB>, show functions only. + */ + if (!is_method_ref || type == LUA_TFUNCTION) { + if (lua_rl_dmadd(&ml, text, dot, s, suf)) + goto error; + } + } + } while (++loop < METATABLE_RECURSION_MAX && lua_rl_getmetaindex(L)); + + lua_pop(L, 1); + + if (ml.idx == 0) { +error: + lua_rl_dmfree(&ml); + lua_settop(L, savetop); + return NULL; + } else { + /* list[0] holds the common prefix of all matches (may + * be ""). If there is only one match, list[0] and + * list[1] will be the same. */ + ml.list[0] = malloc(sizeof(char)*(ml.matchlen+1)); + if (!ml.list[0]) + goto error; + memcpy(ml.list[0], ml.list[1], ml.matchlen); + ml.list[0][ml.matchlen] = '\0'; + /* Add the NULL list terminator. */ + if (lua_rl_dmadd(&ml, NULL, 0, NULL, 0)) goto error; + } + + lua_settop(L, savetop); +#if RL_READLINE_VERSION >= 0x0600 + rl_completion_suppress_append = 1; +#endif + return ml.list; +} diff --git a/src/lua/console.h b/src/lua/console.h new file mode 100644 index 0000000000000000000000000000000000000000..208b314909eadf7ba682e97f26a1f48b0dda5e8a --- /dev/null +++ b/src/lua/console.h @@ -0,0 +1,46 @@ +#ifndef TARANTOOL_LUA_CONSOLE_H_INCLUDED +#define TARANTOOL_LUA_CONSOLE_H_INCLUDED +/* + * Copyright 2010-2016, Tarantool AUTHORS, please see AUTHORS file. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * 1. Redistributions of source code must retain the above + * copyright notice, this list of conditions and the + * following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY <COPYRIGHT HOLDER> ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * <COPYRIGHT HOLDER> OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR + * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF + * THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ +#if defined(__cplusplus) +extern "C" { +#endif /* defined(__cplusplus) */ + +struct lua_State; + +void +tarantool_lua_console_init(struct lua_State *L); + +#if defined(__cplusplus) +} /* extern "C" */ +#endif /* defined(__cplusplus) */ + +#endif /* TARANTOOL_LUA_CONSOLE_H_INCLUDED */ diff --git a/src/lua/console.lua b/src/lua/console.lua index 1a2fb26dfeea8a0804ff5c1269c59478d28694e8..f4be9170a9b8ce9705267c29b1a8941288c11733 100644 --- a/src/lua/console.lua +++ b/src/lua/console.lua @@ -94,6 +94,7 @@ local function remote_eval(self, line) self.remote = nil self.eval = nil self.prompt = nil + self.completion = nil return "" end -- @@ -123,7 +124,10 @@ local function local_read(self) local prompt = self.prompt while true do local delim = self.delimiter - local line = internal.readline(prompt.. "> ") + local line = internal.readline({ + prompt = prompt.. "> ", + completion = self.completion + }) if not line then return nil end @@ -207,6 +211,7 @@ local repl_mt = { read = local_read; eval = local_eval; print = local_print; + completion = internal.completion_handler; }; } @@ -319,6 +324,7 @@ local function connect(uri) self.remote = remote self.eval = remote_eval self.prompt = string.format("%s:%s", self.remote.host, self.remote.port) + self.completion = function () end -- no completion for remote console log.info("connected to %s:%s", self.remote.host, self.remote.port) return true end diff --git a/src/lua/init.c b/src/lua/init.c index 92c75c45d6d729ac673d97a9ba143d09fbf6bf21..3f7b9c6a7b238ca76ab7f5999960ad62ad33d8c4 100644 --- a/src/lua/init.c +++ b/src/lua/init.c @@ -42,7 +42,8 @@ #include <luajit.h> #include <fiber.h> -#include "coeio.h" +#include "coio.h" +#include "lua/console.h" #include "lua/fiber.h" #include "lua/ipc.h" #include "lua/errno.h" @@ -193,74 +194,6 @@ lbox_coredump(struct lua_State *L __attribute__((unused))) /* }}} */ -/* - * {{{ console library - */ - -static ssize_t -readline_cb(va_list ap) -{ - const char **line = va_arg(ap, const char **); - const char *prompt = va_arg(ap, const char *); - /* - * libeio threads blocks all signals by default. Therefore, nobody - * can interrupt read(2) syscall inside readline() to correctly - * cleanup resources and restore terminal state. In case of signal - * a signal_cb(), a ev watcher in tarantool.cc will stop event - * loop and and stop entire process by exiting the main thread. - * rl_cleanup_after_signal() is called from tarantool_lua_free() - * in order to restore terminal state. - */ - *line = readline(prompt); - return 0; -} - -static int -tarantool_console_readline(struct lua_State *L) -{ - const char *prompt = ">"; - if (lua_gettop(L) > 0) { - if (!lua_isstring(L, 1)) - luaL_error(L, "console.readline([prompt])"); - prompt = lua_tostring(L, 1); - } - - char *line; - if (coio_call(readline_cb, &line, prompt) != 0) { - lua_pushnil(L); - return 1; - } - - if (!line) { - lua_pushnil(L); - } else { - /* Make sure the line doesn't leak. */ - static __thread char *line_buf = NULL; - if (line_buf) - free(line_buf); - - line_buf = line; - lua_pushstring(L, line); - - free(line_buf); - line_buf = NULL; - } - - return 1; -} - -static int -tarantool_console_add_history(struct lua_State *L) -{ - if (lua_gettop(L) < 1 || !lua_isstring(L, 1)) - luaL_error(L, "console.add_history(string)"); - - add_history(lua_tostring(L, 1)); - return 0; -} - -/* }}} */ - /** * Prepend the variable list of arguments to the Lua * package search path @@ -395,12 +328,7 @@ tarantool_lua_init(const char *tarantool_bin, int argc, char **argv) rl_catch_signals = 0; rl_catch_sigwinch = 0; #endif - static const struct luaL_reg consolelib[] = { - {"readline", tarantool_console_readline}, - {"add_history", tarantool_console_add_history}, - {NULL, NULL} - }; - luaL_register_module(L, "console", consolelib); + tarantool_lua_console_init(L); lua_pop(L, 1); lua_getfield(L, LUA_REGISTRYINDEX, "_LOADED"); @@ -557,15 +485,4 @@ tarantool_lua_free() lua_close(tarantool_L); } tarantool_L = NULL; - -#if 0 - /* Temporarily moved to tarantool_free(), tarantool_lua_free() not - * being called due to cleanup order issues - */ - if (isatty(STDIN_FILENO)) { - /* See comments in readline_cb() */ - rl_cleanup_after_signal(); - } -#endif } - diff --git a/src/main.cc b/src/main.cc index d12d36f874cefec5058eef986199f210caecf852..edb82198825aedc34e8734622b1e1e1cf0bea9a7 100644 --- a/src/main.cc +++ b/src/main.cc @@ -495,13 +495,6 @@ tarantool_free(void) } if (script) free(script); - /* tarantool_lua_free() was formerly reponsible for terminal reset, - * but it is no longer called - */ - if (isatty(STDIN_FILENO)) { - /* See comments in readline_cb() */ - rl_cleanup_after_signal(); - } #ifdef HAVE_BFD symbols_free(); #endif