diff --git a/lldb/CMakeLists.txt b/lldb/CMakeLists.txt --- a/lldb/CMakeLists.txt +++ b/lldb/CMakeLists.txt @@ -51,6 +51,13 @@ CACHE STRING "Path where Python modules are installed, relative to install prefix") endif () +if (LLDB_ENABLE_LUA) + find_program(Lua_EXECUTABLE lua5.3) + set(LLDB_LUA_DEFAULT_RELATIVE_PATH "lib/lua/5.3") + set(LLDB_LUA_RELATIVE_PATH ${LLDB_LUA_DEFAULT_RELATIVE_PATH} + CACHE STRING "Path where Lua modules are installed, relative to install prefix") +endif () + if (LLDB_ENABLE_PYTHON OR LLDB_ENABLE_LUA) add_subdirectory(bindings) endif () @@ -94,6 +101,16 @@ finish_swig_python("lldb-python" "${lldb_python_bindings_dir}" "${lldb_python_target_dir}") endif() +if (LLDB_ENABLE_LUA) + if(LLDB_BUILD_FRAMEWORK) + set(lldb_lua_target_dir "${LLDB_FRAMEWORK_ABSOLUTE_BUILD_DIR}/LLDB.framework/Resources/Lua") + else() + set(lldb_lua_target_dir "${CMAKE_BINARY_DIR}/${CMAKE_CFG_INTDIR}/${LLDB_LUA_RELATIVE_PATH}") + endif() + get_target_property(lldb_lua_bindings_dir swig_wrapper_lua BINARY_DIR) + finish_swig_lua("lldb-lua" "${lldb_lua_bindings_dir}" "${lldb_lua_target_dir}") +endif() + option(LLDB_INCLUDE_TESTS "Generate build targets for the LLDB unit tests." ${LLVM_INCLUDE_TESTS}) if(LLDB_INCLUDE_TESTS) add_subdirectory(test) diff --git a/lldb/bindings/lua/CMakeLists.txt b/lldb/bindings/lua/CMakeLists.txt --- a/lldb/bindings/lua/CMakeLists.txt +++ b/lldb/bindings/lua/CMakeLists.txt @@ -17,3 +17,55 @@ add_custom_target(swig_wrapper_lua ALL DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/LLDBWrapLua.cpp ) + +function(create_lua_package swig_target working_dir pkg_dir) + cmake_parse_arguments(ARG "NOINIT" "" "FILES" ${ARGN}) + add_custom_command(TARGET ${swig_target} POST_BUILD VERBATIM + COMMAND ${CMAKE_COMMAND} -E make_directory ${pkg_dir} + WORKING_DIRECTORY ${working_dir}) +endfunction() + +function(finish_swig_lua swig_target lldb_lua_bindings_dir lldb_lua_target_dir) + add_custom_target(${swig_target} ALL VERBATIM + COMMAND ${CMAKE_COMMAND} -E make_directory ${lldb_lua_target_dir} + DEPENDS swig_wrapper_lua + COMMENT "LLDB Lua API") + if(LLDB_BUILD_FRAMEWORK) + set(LIBLLDB_SYMLINK_DEST "${LLDB_FRAMEWORK_ABSOLUTE_BUILD_DIR}/LLDB.framework/LLDB") + else() + set(LIBLLDB_SYMLINK_DEST "${LLVM_SHLIB_OUTPUT_INTDIR}/liblldb${CMAKE_SHARED_LIBRARY_SUFFIX}") + endif() + if(WIN32) + if(CMAKE_BUILD_TYPE STREQUAL Debug) + set(LIBLLDB_SYMLINK_OUTPUT_FILE "_lldb_d.pyd") + else() + set(LIBLLDB_SYMLINK_OUTPUT_FILE "_lldb.pyd") + endif() + else() + set(LIBLLDB_SYMLINK_OUTPUT_FILE "lldb.so") + endif() + create_relative_symlink(${swig_target} ${LIBLLDB_SYMLINK_DEST} + ${lldb_lua_target_dir} ${LIBLLDB_SYMLINK_OUTPUT_FILE}) + set(lldb_lua_library_target "${swig_target}-library") + add_custom_target(${lldb_lua_library_target}) + add_dependencies(${lldb_lua_library_target} ${swig_target}) + + # Ensure we do the Lua post-build step when building lldb. + add_dependencies(lldb ${swig_target}) + + if(LLDB_BUILD_FRAMEWORK) + set(LLDB_LUA_INSTALL_PATH ${LLDB_FRAMEWORK_INSTALL_DIR}/LLDB.framework/Resources/Python) + else() + set(LLDB_LUA_INSTALL_PATH ${LLDB_LUA_RELATIVE_PATH}) + endif() + install(DIRECTORY ${lldb_lua_target_dir}/ + DESTINATION ${LLDB_LUA_INSTALL_PATH} + COMPONENT ${lldb_lua_library_target}) + + set(lldb_lua_library_install_target "install-${lldb_lua_library_target}") + if (NOT LLVM_ENABLE_IDE) + add_llvm_install_targets(${lldb_lua_library_install_target} + COMPONENT ${lldb_lua_library_target} + DEPENDS ${lldb_lua_library_target}) + endif() +endfunction() diff --git a/lldb/bindings/lua/lua-typemaps.swig b/lldb/bindings/lua/lua-typemaps.swig --- a/lldb/bindings/lua/lua-typemaps.swig +++ b/lldb/bindings/lua/lua-typemaps.swig @@ -12,7 +12,7 @@ // Primitive integer mapping %typemap(in,checkfn="lua_isinteger") TYPE -%{ $1 = (TYPE)lua_tointeger(L, $input); %} +%{ $1 = ($type)lua_tointeger(L, $input); %} %typemap(in,checkfn="lua_isinteger") const TYPE&($basetype temp) %{ temp=($basetype)lua_tointeger(L,$input); $1=&temp;%} %typemap(out) TYPE @@ -54,6 +54,7 @@ LLDB_NUMBER_TYPEMAP(long long); LLDB_NUMBER_TYPEMAP(unsigned long long); LLDB_NUMBER_TYPEMAP(signed long long); +LLDB_NUMBER_TYPEMAP(enum SWIGTYPE); %apply unsigned long { size_t }; %apply const unsigned long & { const size_t & }; @@ -77,7 +78,7 @@ %typemap(in) (char *dst, size_t dst_len) { $2 = luaL_checkinteger(L, $input); if ($2 <= 0) { - return luaL_error(L, "Positive integer expected"); + return luaL_error(L, "Positive integer expected"); } $1 = (char *) malloc($2); } @@ -86,6 +87,9 @@ // as char data instead of byte data. %typemap(in) (void *char_buf, size_t size) = (char *dst, size_t dst_len); +// Also SBProcess::ReadMemory. +%typemap(in) (void *buf, size_t size) = (char *dst, size_t dst_len); + // Return the char buffer. Discarding any previous return result %typemap(argout) (char *dst, size_t dst_len) { lua_pop(L, 1); // Blow away the previous result @@ -102,4 +106,211 @@ // as char data instead of byte data. %typemap(argout) (void *char_buf, size_t size) = (char *dst, size_t dst_len); +// Also SBProcess::ReadMemory. +%typemap(argout) (void *buf, size_t size) = (char *dst, size_t dst_len); + +//===----------------------------------------------------------------------===// + +// Typemap for handling a snprintf-like API like SBThread::GetStopDescription. + +%typemap(in) (char *dst_or_null, size_t dst_len) { + $2 = luaL_checkinteger(L, $input); + if ($2 <= 0) { + return luaL_error(L, "Positive integer expected"); + } + $1 = (char *)malloc($2); +} + +%typemap(argout) (char *dst_or_null, size_t dst_len) { + lua_pop(L, 1); // Blow away the previous result + lua_pushlstring(L, (const char *)$1, $result); + free($1); + // SWIG_arg was already incremented +} + +//===----------------------------------------------------------------------===// + +// Typemap for handling SBModule::GetVersion + +%typemap(in) (uint32_t *versions, uint32_t num_versions) { + $2 = 99; + $1 = (uint32_t *)malloc(sizeof(uint32_t) * $2); +} + +%typemap(argout) (uint32_t *versions, uint32_t num_versions) { + uint32_t count = result; + if (count >= $2) + count = $2; + lua_newtable(L); + int i = 0; + while (i++ < count) { + lua_pushinteger(L, $1[i - 1]); + lua_seti(L, -2, i); + } + SWIG_arg++; + free($1); +} + +//===----------------------------------------------------------------------===// + +// Typemap for handling SBDebugger::SetLoggingCallback + +%typemap(in) (lldb::LogOutputCallback log_callback, void *baton) { + $1 = LLDBSwigLuaCallLuaLogOutputCallback; + $2 = (void *)L; + + luaL_checktype(L, 2, LUA_TFUNCTION); + lua_settop(L, 2); + + lua_pushlightuserdata(L, (void *)&LLDBSwigLuaCallLuaLogOutputCallback); + lua_insert(L, 2); + lua_settable(L, LUA_REGISTRYINDEX); +} + +//===----------------------------------------------------------------------===// + +// Typemap for handling SBEvent::SBEvent(uint32_t event, const char *cstr, uint32_t cstr_len) + +%typemap(in) (const char *cstr, uint32_t cstr_len) { + $1 = (char *)luaL_checklstring(L, $input, (size_t *)&$2); +} + +// Typemap for handling SBProcess::PutSTDIN + +%typemap(in) (const char *src, size_t src_len) { + $1 = (char *)luaL_checklstring(L, $input, &$2); +} + +// Typemap for handling SBProcess::WriteMemory, SBTarget::GetInstructions... + +%typemap(in) (const void *buf, size_t size), + (const void *data, size_t data_len) { + $1 = (void *)luaL_checklstring(L, $input, &$2); +} + +//===----------------------------------------------------------------------===// + +// Typemap for handling char ** in SBTarget::LaunchSimple, SBTarget::Launch... + +// It should accept a Lua table of strings, for stuff like "argv" and "envp". + +%typemap(in) char ** { + if (lua_istable(L, $input)) { + size_t size = lua_rawlen(L, $input); + $1 = (char **)malloc((size + 1) * sizeof(char *)); + int i = 0, j = 0; + while (i++ < size) { + lua_rawgeti(L, $input, i); + if (!lua_isstring(L, -1)) { + // if current element cannot be converted to string, raise an error + lua_pop(L, 1); + return luaL_error(L, "List should only contain strings"); + } + $1[j++] = (char *)lua_tostring(L, -1); + lua_pop(L, 1); + } + $1[j] = 0; + } else if (lua_isnil(L, $input)) { + // "nil" is also acceptable, equivalent as an empty table + $1 = NULL; + } else { + return luaL_error(L, "A list of strings expected"); + } +} + +%typemap(freearg) char ** { + free((char *) $1); +} + +%typecheck(SWIG_TYPECHECK_STRING_ARRAY) char ** { + $1 = (lua_istable(L, $input) || lua_isnil(L, $input)); +} + +//===----------------------------------------------------------------------===// + +// Typemap for file handles (e.g. used in SBDebugger::SetOutputFile) + +%typemap(in) lldb::FileSP { + luaL_Stream *p = (luaL_Stream *)luaL_checkudata(L, $input, LUA_FILEHANDLE); + lldb::FileSP file_sp; + file_sp = std::make_shared(p->f, false); + if (!file_sp->IsValid()) + return luaL_error(L, "Invalid file"); + $1 = file_sp; +} + +%typecheck(SWIG_TYPECHECK_POINTER) lldb::FileSP { + $1 = (lua_isuserdata(L, $input)) && + (luaL_testudata(L, $input, LUA_FILEHANDLE) != nullptr); +} + +// Typemap for file handles (e.g. used in SBDebugger::GetOutputFileHandle) + +%typemap(out) lldb::FileSP { + lldb::FileSP &sp = $1; + if (sp && sp->IsValid()) { + luaL_Stream *p = (luaL_Stream *)lua_newuserdata(L, sizeof(luaL_Stream)); + p->closef = &LLDBSwigLuaCloseFileHandle; + p->f = sp->GetStream(); + luaL_setmetatable(L, LUA_FILEHANDLE); + SWIG_arg++; + } +} + +//===----------------------------------------------------------------------===// + +// Typemap for SBData::CreateDataFromUInt64Array, SBData::SetDataFromUInt64Array ... + +%typemap(in) (uint64_t* array, size_t array_len), + (uint32_t* array, size_t array_len), + (int64_t* array, size_t array_len), + (int32_t* array, size_t array_len), + (double* array, size_t array_len) { + if (lua_istable(L, $input)) { + // It should accept a table of numbers. + $2 = lua_rawlen(L, $input); + $1 = ($1_ltype)malloc(($2) * sizeof($*1_type)); + int i = 0, j = 0; + while (i++ < $2) { + lua_rawgeti(L, $input, i); + if (!lua_isnumber(L, -1)) { + // if current element cannot be converted to number, raise an error + lua_pop(L, 1); + return luaL_error(L, "List should only contain numbers"); + } + $1[j++] = ($*1_ltype)lua_tonumber(L, -1); + lua_pop(L, 1); + } + } else if (lua_isnil(L, $input)) { + // "nil" is also acceptable, equivalent as an empty table + $1 = NULL; + $2 = 0; + } else { + // else raise an error + return luaL_error(L, "A list of numbers expected."); + } +} + +%typemap(freearg) (uint64_t* array, size_t array_len), + (uint32_t* array, size_t array_len), + (int64_t* array, size_t array_len), + (int32_t* array, size_t array_len), + (double* array, size_t array_len) { + free($1); +} + +//===----------------------------------------------------------------------===// + +// Typemap for SBCommandReturnObject::PutCString + +%typemap(in) (const char *string, int len) { + if (lua_isnil(L, $input)) { + $1 = NULL; + $2 = 0; + } + else { + $1 = (char *)luaL_checklstring(L, $input, (size_t *)&$2); + } +} + //===----------------------------------------------------------------------===// diff --git a/lldb/bindings/lua/lua-wrapper.swig b/lldb/bindings/lua/lua-wrapper.swig --- a/lldb/bindings/lua/lua-wrapper.swig +++ b/lldb/bindings/lua/lua-wrapper.swig @@ -6,6 +6,19 @@ %} +%runtime %{ +#ifdef __cplusplus +extern "C" { +#endif + +void LLDBSwigLuaCallLuaLogOutputCallback(const char *str, void *baton); +int LLDBSwigLuaCloseFileHandle(lua_State *L); + +#ifdef __cplusplus +} +#endif +%} + %wrapper %{ // This function is called from Lua::CallBreakpointCallback @@ -88,5 +101,20 @@ return stop; } +SWIGEXPORT void +LLDBSwigLuaCallLuaLogOutputCallback(const char *str, void *baton) { + lua_State *L = (lua_State *)baton; + + lua_pushlightuserdata(L, (void *)&LLDBSwigLuaCallLuaLogOutputCallback); + lua_gettable(L, LUA_REGISTRYINDEX); + + // FIXME: There's no way to report errors back to the user + lua_pushstring(L, str); + lua_pcall(L, 1, 0, 0); +} + +int LLDBSwigLuaCloseFileHandle(lua_State *L) { + return luaL_error(L, "You cannot close a file handle used by lldb."); +} %} diff --git a/lldb/bindings/lua/lua.swig b/lldb/bindings/lua/lua.swig --- a/lldb/bindings/lua/lua.swig +++ b/lldb/bindings/lua/lua.swig @@ -17,6 +17,10 @@ #include "llvm/Support/Error.h" #include "llvm/Support/FormatVariadic.h" #include "../bindings/lua/lua-swigsafecast.swig" + +// required headers for typemaps +#include "lldb/Host/File.h" + using namespace lldb_private; using namespace lldb; %} diff --git a/lldb/source/API/liblldb-private.exports b/lldb/source/API/liblldb-private.exports --- a/lldb/source/API/liblldb-private.exports +++ b/lldb/source/API/liblldb-private.exports @@ -4,3 +4,4 @@ _ZNK12lldb_private* init_lld* PyInit__lldb* +luaopen_lldb* diff --git a/lldb/source/API/liblldb.exports b/lldb/source/API/liblldb.exports --- a/lldb/source/API/liblldb.exports +++ b/lldb/source/API/liblldb.exports @@ -2,3 +2,4 @@ _ZNK4lldb* init_lld* PyInit__lldb* +luaopen_lldb* diff --git a/lldb/test/API/lit.site.cfg.py.in b/lldb/test/API/lit.site.cfg.py.in --- a/lldb/test/API/lit.site.cfg.py.in +++ b/lldb/test/API/lit.site.cfg.py.in @@ -19,6 +19,8 @@ config.target_triple = "@TARGET_TRIPLE@" config.lldb_build_directory = "@LLDB_TEST_BUILD_DIRECTORY@" config.python_executable = "@Python3_EXECUTABLE@" +config.lua_executable = "@Lua_EXECUTABLE@" +config.lua_test_entry = "TestLuaAPI.py" config.dotest_args_str = "@LLDB_DOTEST_ARGS@" config.lldb_enable_python = @LLDB_ENABLE_PYTHON@ config.dotest_lit_args_str = None diff --git a/lldb/test/API/lldbtest.py b/lldb/test/API/lldbtest.py --- a/lldb/test/API/lldbtest.py +++ b/lldb/test/API/lldbtest.py @@ -50,11 +50,17 @@ # build with. executable = test.config.python_executable + isLuaTest = testFile == test.config.lua_test_entry + # On Windows, the system does not always correctly interpret # shebang lines. To make sure we can execute the tests, add # python exe as the first parameter of the command. cmd = [executable] + self.dotest_cmd + [testPath, '-p', testFile] + if isLuaTest: + luaExecutable = test.config.lua_executable + cmd.extend(['--env', 'LUA_EXECUTABLE=%s' % luaExecutable]) + timeoutInfo = None try: out, err, exitCode = lit.util.executeCommand( diff --git a/lldb/test/API/lua_api/Makefile b/lldb/test/API/lua_api/Makefile new file mode 100644 --- /dev/null +++ b/lldb/test/API/lua_api/Makefile @@ -0,0 +1,3 @@ +C_SOURCES := main.c + +include Makefile.rules diff --git a/lldb/test/API/lua_api/TestBreakpointAPI.lua b/lldb/test/API/lua_api/TestBreakpointAPI.lua new file mode 100644 --- /dev/null +++ b/lldb/test/API/lua_api/TestBreakpointAPI.lua @@ -0,0 +1,52 @@ +_T = require('lua_lldb_test').create_test('TestBreakpointAPI') + +function _T:TestBreakpointIsValid() + local target = self:create_target() + local breakpoint = target:BreakpointCreateByName('AFunction', 'a.out') + assertTrue(breakpoint:IsValid() and breakpoint:GetNumLocations() == 1) + local did_delete = target:BreakpointDelete(breakpoint:GetID()) + assertTrue(did_delete) + local del_bkpt = target:FindBreakpointByID(breakpoint:GetID()) + assertFalse(del_bkpt:IsValid()) + assertFalse(breakpoint:IsValid()) +end + +function _T:TestTargetDelete() + local target = self:create_target() + local breakpoint = target:BreakpointCreateByName('AFunction', 'a.out') + assertTrue(breakpoint:IsValid() and breakpoint:GetNumLocations() == 1) + local location = breakpoint:GetLocationAtIndex(0) + assertTrue(location:IsValid()) + assertEquals(target, breakpoint:GetTarget()) + assertTrue(self.debugger:DeleteTarget(target)) + assertFalse(breakpoint:IsValid()) + assertFalse(location:IsValid()) +end + +function _T:TestBreakpointHitCount() + local target = self:create_target() + local breakpoint = target:BreakpointCreateByName('BFunction', 'a.out') + assertTrue(breakpoint:IsValid() and breakpoint:GetNumLocations() == 1) + breakpoint:SetAutoContinue(true) + target:LaunchSimple(nil, nil, nil) + assertEquals(breakpoint:GetHitCount(), 100) +end + +function _T:TestBreakpointFrame() + local target = self:create_target() + local breakpoint = target:BreakpointCreateByName('main', 'a.out') + assertTrue(breakpoint:IsValid() and breakpoint:GetNumLocations() == 1) + local process = target:LaunchSimple({ 'arg1', 'arg2' }, nil, nil) + local thread = get_stopped_thread(process, lldb.eStopReasonBreakpoint) + assertNotNil(thread) + assertTrue(thread:IsValid()) + local frame = thread:GetFrameAtIndex(0) + assertTrue(frame:IsValid()) + local error = lldb.SBError() + local var_argc = frame:FindVariable('argc') + local var_argc_value = var_argc:GetValueAsSigned(error, 0) + assertTrue(error:Success()) + assertEquals(var_argc_value, 3) +end + +os.exit(_T:run()) diff --git a/lldb/test/API/lua_api/TestComprehensive.lua b/lldb/test/API/lua_api/TestComprehensive.lua new file mode 100644 --- /dev/null +++ b/lldb/test/API/lua_api/TestComprehensive.lua @@ -0,0 +1,99 @@ +_T = require('lua_lldb_test').create_test('TestComprehensive') + +function _T:Test0_CreateTarget() + self.target = self:create_target() + assertTrue(self.target:IsValid()) +end + +function _T:Test1_Breakpoint() + self.main_bp = self.target:BreakpointCreateByName('main', 'a.out') + self.loop_bp = self.target:BreakpointCreateByLocation('main.c', 28) + assertTrue(self.main_bp:IsValid() and self.main_bp:GetNumLocations() == 1) + assertTrue(self.loop_bp:IsValid() and self.loop_bp:GetNumLocations() == 1) +end + +function _T:Test2_Launch() + local error = lldb.SBError() + self.args = { 'arg' } + self.process = self.target:Launch( + self.debugger:GetListener(), + self.args, + nil, + nil, + self.output, + nil, + nil, + 0, + false, + error + ) + assertTrue(error:Success()) + assertTrue(self.process:IsValid()) +end + +function _T:Test3_BreakpointFindVariables() + -- checking "argc" value + local thread = get_stopped_thread(self.process, lldb.eStopReasonBreakpoint) + assertNotNil(thread) + assertTrue(thread:IsValid()) + local frame = thread:GetFrameAtIndex(0) + assertTrue(frame:IsValid()) + local error = lldb.SBError() + local var_argc = frame:FindVariable('argc') + assertTrue(var_argc:IsValid()) + local var_argc_value = var_argc:GetValueAsSigned(error, 0) + assertTrue(error:Success()) + assertEquals(var_argc_value, 2) + + -- checking "inited" value + local continue = self.process:Continue() + assertTrue(continue:Success()) + thread = get_stopped_thread(self.process, lldb.eStopReasonBreakpoint) + assertNotNil(thread) + assertTrue(thread:IsValid()) + frame = thread:GetFrameAtIndex(0) + assertTrue(frame:IsValid()) + error = lldb.SBError() + local var_inited = frame:FindVariable('inited') + assertTrue(var_inited:IsValid()) + self.var_inited = var_inited + local var_inited_value = var_inited:GetValueAsUnsigned(error, 0) + assertTrue(error:Success()) + assertEquals(var_inited_value, 0xDEADBEEF) +end + +function _T:Test3_RawData() + local error = lldb.SBError() + local address = self.var_inited:GetAddress() + assertTrue(address:IsValid()) + local size = self.var_inited:GetByteSize() + local raw_data = self.process:ReadMemory(address:GetOffset(), size, error) + assertTrue(error:Success()) + local data_le = lldb.SBData.CreateDataFromUInt32Array(lldb.eByteOrderLittle, 1, {0xDEADBEEF}) + local data_be = lldb.SBData.CreateDataFromUInt32Array(lldb.eByteOrderBig, 1, {0xDEADBEEF}) + assertTrue(data_le:GetUnsignedInt32(error, 0) == 0xDEADBEEF or data_be:GetUnsignedInt32(error, 0) == 0xDEADBEEF) + assertTrue(raw_data == "\xEF\xBE\xAD\xDE" or raw_data == "\xDE\xAD\xBE\xEF") +end + +function _T:Test4_ProcessExit() + self.loop_bp:SetAutoContinue(true) + local continue = self.process:Continue() + assertTrue(continue:Success()) + assertTrue(self.process:GetExitStatus() == 0) +end + +function _T:Test5_FileOutput() + local f = io.open(self.output, 'r') + assertEquals( + read_file_non_empty_lines(f), + { + self.exe, + table.unpack(self.args), + 'I am a function.', + 'sum = 5050' + } + ) + f:close() +end + +os.exit(_T:run()) diff --git a/lldb/test/API/lua_api/TestFileHandle.lua b/lldb/test/API/lua_api/TestFileHandle.lua new file mode 100644 --- /dev/null +++ b/lldb/test/API/lua_api/TestFileHandle.lua @@ -0,0 +1,37 @@ +_T = require('lua_lldb_test').create_test('TestFileHandle') + +function _T:TestLegacyFileOutScript() + local f = io.open(self.output, 'w') + self.debugger:SetOutputFile(f) + self:handle_command('script print(1+1)') + self.debugger:GetOutputFileHandle():write('FOO\n') + self.debugger:GetOutputFileHandle():flush() + f:close() + + f = io.open(self.output, 'r') + assertEquals(read_file_non_empty_lines(f), {'2', 'FOO'}) + f:close() +end + +function _T:TestLegacyFileOut() + local f = io.open(self.output, 'w') + self.debugger:SetOutputFile(f) + self:handle_command('p/x 3735928559', false) + f:close() + + f = io.open(self.output, 'r') + assertStrContains(f:read('*l'), 'deadbeef') + f:close() +end + +function _T:TestLegacyFileErr() + local f = io.open(self.output, 'w') + self.debugger:SetErrorFile(f) + self:handle_command('lol', false) + + f = io.open(self.output, 'r') + assertStrContains(f:read('*l'), 'is not a valid command') + f:close() +end + +os.exit(_T:run()) diff --git a/lldb/test/API/lua_api/TestLuaAPI.py b/lldb/test/API/lua_api/TestLuaAPI.py new file mode 100644 --- /dev/null +++ b/lldb/test/API/lua_api/TestLuaAPI.py @@ -0,0 +1,186 @@ +""" +Test Lua API wrapper +""" + +from lldbsuite.test.decorators import * +from lldbsuite.test.lldbtest import * +from lldbsuite.test import lldbutil +import subprocess + +def to_string(b): + """Return the parameter as type 'str', possibly encoding it. + + In Python2, the 'str' type is the same as 'bytes'. In Python3, the + 'str' type is (essentially) Python2's 'unicode' type, and 'bytes' is + distinct. + + """ + if isinstance(b, str): + # In Python2, this branch is taken for types 'str' and 'bytes'. + # In Python3, this branch is taken only for 'str'. + return b + if isinstance(b, bytes): + # In Python2, this branch is never taken ('bytes' is handled as 'str'). + # In Python3, this is true only for 'bytes'. + try: + return b.decode('utf-8') + except UnicodeDecodeError: + # If the value is not valid Unicode, return the default + # repr-line encoding. + return str(b) + + # By this point, here's what we *don't* have: + # + # - In Python2: + # - 'str' or 'bytes' (1st branch above) + # - In Python3: + # - 'str' (1st branch above) + # - 'bytes' (2nd branch above) + # + # The last type we might expect is the Python2 'unicode' type. There is no + # 'unicode' type in Python3 (all the Python3 cases were already handled). In + # order to get a 'str' object, we need to encode the 'unicode' object. + try: + return b.encode('utf-8') + except AttributeError: + raise TypeError('not sure how to convert %s to %s' % (type(b), str)) + +class ExecuteCommandTimeoutException(Exception): + def __init__(self, msg, out, err, exitCode): + assert isinstance(msg, str) + assert isinstance(out, str) + assert isinstance(err, str) + assert isinstance(exitCode, int) + self.msg = msg + self.out = out + self.err = err + self.exitCode = exitCode + + +# Close extra file handles on UNIX (on Windows this cannot be done while +# also redirecting input). +kUseCloseFDs = not (platform.system() == 'Windows') + + +def executeCommand(command, cwd=None, env=None, input=None, timeout=0): + """Execute command ``command`` (list of arguments or string) with. + + * working directory ``cwd`` (str), use None to use the current + working directory + * environment ``env`` (dict), use None for none + * Input to the command ``input`` (str), use string to pass + no input. + * Max execution time ``timeout`` (int) seconds. Use 0 for no timeout. + + Returns a tuple (out, err, exitCode) where + * ``out`` (str) is the standard output of running the command + * ``err`` (str) is the standard error of running the command + * ``exitCode`` (int) is the exitCode of running the command + + If the timeout is hit an ``ExecuteCommandTimeoutException`` + is raised. + + """ + if input is not None: + input = to_bytes(input) + p = subprocess.Popen(command, cwd=cwd, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + env=env, close_fds=kUseCloseFDs) + timerObject = None + # FIXME: Because of the way nested function scopes work in Python 2.x we + # need to use a reference to a mutable object rather than a plain + # bool. In Python 3 we could use the "nonlocal" keyword but we need + # to support Python 2 as well. + hitTimeOut = [False] + try: + if timeout > 0: + def killProcess(): + # We may be invoking a shell so we need to kill the + # process and all its children. + hitTimeOut[0] = True + killProcessAndChildren(p.pid) + + timerObject = threading.Timer(timeout, killProcess) + timerObject.start() + + out, err = p.communicate(input=input) + exitCode = p.wait() + finally: + if timerObject != None: + timerObject.cancel() + + # Ensure the resulting output is always of string type. + out = to_string(out) + err = to_string(err) + + if hitTimeOut[0]: + raise ExecuteCommandTimeoutException( + msg='Reached timeout of {} seconds'.format(timeout), + out=out, + err=err, + exitCode=exitCode + ) + + # Detect Ctrl-C in subprocess. + if exitCode == -signal.SIGINT: + raise KeyboardInterrupt + + return out, err, exitCode + +class TestLuaAPI(TestBase): + + mydir = TestBase.compute_mydir(__file__) + NO_DEBUG_INFO_TESTCASE = True + + def get_tests(self): + tests = [] + for filename in os.listdir(): + # Ignore dot files and excluded tests. + if filename.startswith('.'): + continue + + # Ignore files that don't start with 'Test'. + if not filename.startswith('Test'): + continue + + if not os.path.isdir(filename): + base, ext = os.path.splitext(filename) + if ext == '.lua': + tests.append(filename) + return tests + + def test_lua_api(self): + if "LUA_EXECUTABLE" not in os.environ or len(os.environ["LUA_EXECUTABLE"]) == 0: + self.skipTest("Lua API tests could not find Lua executable.") + return + lua_executable = os.environ["LUA_EXECUTABLE"] + + self.build() + test_exe = self.getBuildArtifact("a.out") + test_output = self.getBuildArtifact("output") + test_input = self.getBuildArtifact("input") + + lua_lldb_cpath = "%s/lua/5.3/?.so" % configuration.lldb_libs_dir + + lua_prelude = "package.cpath = '%s;' .. package.cpath" % lua_lldb_cpath + + lua_env = { + "TEST_EXE": os.path.join(self.getBuildDir(), test_exe), + "TEST_OUTPUT": os.path.join(self.getBuildDir(), test_output), + "TEST_INPUT": os.path.join(self.getBuildDir(), test_input) + } + + for lua_test in self.get_tests(): + cmd = [lua_executable] + ["-e", lua_prelude] + [lua_test] + out, err, exitCode = executeCommand(cmd, env=lua_env) + + # Redirect Lua output + print(out) + print(err, file=sys.stderr) + + self.assertTrue( + exitCode == 0, + "Lua test '%s' failure." % lua_test + ) diff --git a/lldb/test/API/lua_api/TestProcessAPI.lua b/lldb/test/API/lua_api/TestProcessAPI.lua new file mode 100644 --- /dev/null +++ b/lldb/test/API/lua_api/TestProcessAPI.lua @@ -0,0 +1,59 @@ +_T = require('lua_lldb_test').create_test('TestProcessAPI') + +function _T:TestProcessLaunchSimple() + local target = self:create_target() + local args = { 'arg1', 'arg2', 'arg3' } + local process = target:LaunchSimple( + -- argv + args, + -- envp + nil, + -- working directory + nil + ) + assertTrue(process:IsValid()) + local stdout = process:GetSTDOUT(1000) + assertEquals(split_lines(stdout), {self.exe, table.unpack(args)}) +end + +function _T:TestProcessLaunch() + local target = self:create_target() + local args = { 'arg1', 'arg2', 'arg3' } + local error = lldb.SBError() + local f = io.open(self.output, 'w') + f:write() + f:close() + local process = target:Launch( + -- listener + self.debugger:GetListener(), + -- argv + args, + -- envp + nil, + -- stdin + nil, + -- stdout + self.output, + -- stderr + nil, + -- working directory + nil, + -- launch flags + 0, + -- stop at entry + true, + -- error + error + ) + assertTrue(error:Success()) + assertTrue(process:IsValid()) + local threads = get_stopped_threads(process, lldb.eStopReasonSignal) + assertTrue(#threads ~= 0) + local continue = process:Continue() + assertTrue(continue:Success()) + local f = io.open(self.output, 'r') + assertEquals(read_file_non_empty_lines(f), {self.exe, table.unpack(args)}) + f:close() +end + +os.exit(_T:run()) diff --git a/lldb/test/API/lua_api/lua_lldb_test.lua b/lldb/test/API/lua_api/lua_lldb_test.lua new file mode 100644 --- /dev/null +++ b/lldb/test/API/lua_api/lua_lldb_test.lua @@ -0,0 +1,155 @@ +-- Make lldb available in global +lldb = require('lldb') + +-- Global assertion functions +function assertTrue(x) + if not x then error('assertTrue failure') end +end + +function assertFalse(x) + if x then error('assertNotNil failure') end +end + +function assertNotNil(x) + if x == nil then error('assertNotNil failure') end +end + +function assertEquals(x, y) + if type(x) == 'table' and type(y) == 'table' then + for k, _ in pairs(x) do + assertEquals(x[k], y[k]) + end + elseif type(x) ~= type(y) then + error('assertEquals failure') + elseif x ~= y then + error('assertEquals failure') + end +end + +function assertStrContains(x, y) + if not string.find(x, y, 1, true) then + error('assertStrContains failure') + end +end + +-- Global helper functions +function read_file_non_empty_lines(f) + local lines = {} + while true do + local line = f:read('*l') + if not line then break end + if line ~= '\n' then table.insert(lines, line) end + end + return lines +end + +function split_lines(str) + local lines = {} + for line in str:gmatch("[^\r\n]+") do + table.insert(lines, line) + end + return lines +end + +function get_stopped_threads(process, reason) + local threads = {} + for i = 0, process:GetNumThreads() - 1 do + local t = process:GetThreadAtIndex(i) + if t:IsValid() and t:GetStopReason() == reason then + table.insert(threads, t) + end + end + return threads +end + +function get_stopped_thread(process, reason) + local threads = get_stopped_threads(process, reason) + if #threads ~= 0 then return threads[1] + else return nil end +end + +-- Test helper + +local _M = {} +local _m = {} + +local _mt = { __index = _m } + +function _M.create_test(name, exe, output, input) + print('[lldb/lua] Create test ' .. name) + exe = exe or os.getenv('TEST_EXE') + output = output or os.getenv('TEST_OUTPUT') + input = input or os.getenv('TEST_INPUT') + lldb.SBDebugger.Initialize() + local debugger = lldb.SBDebugger.Create() + -- Ensure that debugger is created + assertNotNil(debugger) + assertTrue(debugger:IsValid()) + + debugger:SetAsync(false) + + local lua_language = debugger:GetScriptingLanguage('lua') + assertNotNil(lua_language) + debugger:SetScriptLanguage(lua_language) + + local test = setmetatable({ + output = output, + input = input, + name = name, + exe = exe, + debugger = debugger + }, _mt) + _G[name] = test + return test +end + +function _m:create_target(exe) + local target + if not exe then exe = self.exe end + target = self.debugger:CreateTarget(exe) + -- Ensure that target is created + assertNotNil(target) + assertTrue(target:IsValid()) + return target +end + +function _m:handle_command(command, collect) + if collect == nil then collect = true end + if collect then + local ret = lldb.SBCommandReturnObject() + local interpreter = self.debugger:GetCommandInterpreter() + assertTrue(interpreter:IsValid()) + interpreter:HandleCommand(command, ret) + self.debugger:GetOutputFile():Flush() + self.debugger:GetErrorFile():Flush() + assertTrue(ret:Succeeded()) + return ret:GetOutput() + else + self.debugger:HandleCommand(command) + self.debugger:GetOutputFile():Flush() + self.debugger:GetErrorFile():Flush() + end +end + +function _m:run() + local tests = {} + for k, v in pairs(self) do + if string.sub(k, 1, 4) == 'Test' then + table.insert(tests, k) + end + end + table.sort(tests) + for _, t in ipairs(tests) do + print('[lldb/lua] Doing test ' .. self.name .. ' - ' .. t) + local success = xpcall(self[t], function(e) + print(debug.traceback()) + end, self) + if not success then + print('[lldb/lua] Failure in test ' .. self.name .. ' - ' .. t) + return 1 + end + end + return 0 +end + +return _M diff --git a/lldb/test/API/lua_api/main.c b/lldb/test/API/lua_api/main.c new file mode 100644 --- /dev/null +++ b/lldb/test/API/lua_api/main.c @@ -0,0 +1,35 @@ +#include + +void BFunction() +{ +} + +void AFunction() +{ + printf("I am a function.\n"); +} + +int main(int argc, const char *argv[]) +{ + int inited = 0xDEADBEEF; + int sum = 0; + if(argc > 1) + { + for(int i = 0; i < argc; i++) + { + puts(argv[i]); + } + if(argc > 2) + { + return argc; + } + } + AFunction(); + for(int i = 1; i <= 100; i++) + { + BFunction(); + sum += i; + } + printf("sum = %d\n", sum); + return 0; +}