[Lldb-commits] [lldb] 67f94e5 - [lldb/lua] Supplement Lua bindings for lldb module

Siger Yang via lldb-commits lldb-commits at lists.llvm.org
Tue Oct 12 07:10:42 PDT 2021


Author: Siger Yang
Date: 2021-10-12T22:10:21+08:00
New Revision: 67f94e5a9745665dd62550c75cb16569fbb0059d

URL: https://github.com/llvm/llvm-project/commit/67f94e5a9745665dd62550c75cb16569fbb0059d
DIFF: https://github.com/llvm/llvm-project/commit/67f94e5a9745665dd62550c75cb16569fbb0059d.diff

LOG: [lldb/lua] Supplement Lua bindings for lldb module

Add necessary typemaps for Lua bindings, together with some other files.

Signed-off-by: Siger Yang <sigeryeung at gmail.com>

Reviewed By: tammela

Differential Revision: https://reviews.llvm.org/D108090

Added: 
    lldb/test/API/lua_api/Makefile
    lldb/test/API/lua_api/TestBreakpointAPI.lua
    lldb/test/API/lua_api/TestComprehensive.lua
    lldb/test/API/lua_api/TestFileHandle.lua
    lldb/test/API/lua_api/TestLuaAPI.py
    lldb/test/API/lua_api/TestProcessAPI.lua
    lldb/test/API/lua_api/lua_lldb_test.lua
    lldb/test/API/lua_api/main.c

Modified: 
    lldb/CMakeLists.txt
    lldb/bindings/lua/CMakeLists.txt
    lldb/bindings/lua/lua-typemaps.swig
    lldb/bindings/lua/lua-wrapper.swig
    lldb/bindings/lua/lua.swig
    lldb/source/API/liblldb-private.exports
    lldb/source/API/liblldb.exports
    lldb/test/API/lit.site.cfg.py.in
    lldb/test/API/lldbtest.py

Removed: 
    


################################################################################
diff  --git a/lldb/CMakeLists.txt b/lldb/CMakeLists.txt
index 594c769141b43..028dadbb8c73c 100644
--- a/lldb/CMakeLists.txt
+++ b/lldb/CMakeLists.txt
@@ -51,6 +51,13 @@ if (LLDB_ENABLE_PYTHON)
     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 @@ if (LLDB_ENABLE_PYTHON)
   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
index 7148f13704560..1a739a9805ec8 100644
--- a/lldb/bindings/lua/CMakeLists.txt
+++ b/lldb/bindings/lua/CMakeLists.txt
@@ -17,3 +17,55 @@ add_custom_command(
 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
index d912137a56746..e3b3f5718d15a 100644
--- 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(signed long);
 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 @@ LLDB_NUMBER_TYPEMAP(signed long long);
 %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 @@ LLDB_NUMBER_TYPEMAP(signed long long);
 // 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 @@ LLDB_NUMBER_TYPEMAP(signed long long);
 // 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<lldb_private::NativeFile>(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
index e070bae23683e..c51911bb6bf73 100644
--- a/lldb/bindings/lua/lua-wrapper.swig
+++ b/lldb/bindings/lua/lua-wrapper.swig
@@ -6,6 +6,19 @@ PushSBClass(lua_State* L, T* obj);
 
 %}
 
+%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 @@ LLDBSwigLuaWatchpointCallbackFunction
    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
index c702e4964081f..21fa44c8b4d86 100644
--- 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
index 9b3d86dfc892c..cf1360021c6ce 100644
--- a/lldb/source/API/liblldb-private.exports
+++ b/lldb/source/API/liblldb-private.exports
@@ -4,3 +4,4 @@ _ZN12lldb_private*
 _ZNK12lldb_private*
 init_lld*
 PyInit__lldb*
+luaopen_lldb*

diff  --git a/lldb/source/API/liblldb.exports b/lldb/source/API/liblldb.exports
index 3ceb562c7ed14..5835cf0a02ea1 100644
--- a/lldb/source/API/liblldb.exports
+++ b/lldb/source/API/liblldb.exports
@@ -2,3 +2,4 @@ _ZN4lldb*
 _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
index 82cde5c252f77..987078a53edbb 100644
--- a/lldb/test/API/lit.site.cfg.py.in
+++ b/lldb/test/API/lit.site.cfg.py.in
@@ -19,6 +19,8 @@ config.llvm_use_sanitizer = "@LLVM_USE_SANITIZER@"
 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
index 847f9535d22c5..6337f3a429a84 100644
--- a/lldb/test/API/lldbtest.py
+++ b/lldb/test/API/lldbtest.py
@@ -50,11 +50,17 @@ def execute(self, test, litConfig):
         # 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
index 0000000000000..10495940055b6
--- /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
index 0000000000000..6e38dc3f975c6
--- /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
index 0000000000000..ccea559546958
--- /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
index 0000000000000..be7b570882319
--- /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
index 0000000000000..bf25ded5160f0
--- /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
index 0000000000000..aa788ba741155
--- /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
index 0000000000000..ebf4c7cbae2ac
--- /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
index 0000000000000..1cc39d8495385
--- /dev/null
+++ b/lldb/test/API/lua_api/main.c
@@ -0,0 +1,35 @@
+#include <stdio.h>
+
+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;
+}


        


More information about the lldb-commits mailing list