mirror of
https://github.com/pound-emu/ballistic.git
synced 2026-10-24 23:45:05 -07:00
647 lines
22 KiB
Lua
647 lines
22 KiB
Lua
local script_path = debug.getinfo(1, "S").source:sub(2)
|
|
local script_dir = script_path:match("(.*[/\\])") or "./"
|
|
package.path = package.path .. ";" .. script_dir .. "/?.lua"
|
|
|
|
local log = require('log')
|
|
local ffi = require('ffi')
|
|
|
|
if not pcall(ffi.typeof, "CXIndex") then
|
|
ffi.cdef[[
|
|
typedef void* CXIndex;
|
|
typedef void* CXTranslationUnit;
|
|
typedef void* CXClientData;
|
|
|
|
typedef struct { const void *ptr_data[2]; unsigned int_data; } CXSourceLocation;
|
|
typedef struct { int kind; int xdata; void *data[3]; } CXCursor;
|
|
typedef struct { int kind; void *data[2]; } CXType;
|
|
typedef struct { const char *data; unsigned private_flags; } CXString;
|
|
|
|
typedef enum { CXChildVisit_Break = 0, CXChildVisit_Continue = 1, CXChildVisit_Recurse = 2 } CXChildVisitResult;
|
|
typedef CXChildVisitResult (*CXCursorVisitor)(CXCursor *cursor, CXCursor *parent, CXClientData client_data);
|
|
|
|
CXIndex clang_createIndex(int excludeDeclarationsFromPCH, int displayDiagnostics);
|
|
void clang_disposeIndex(CXIndex index);
|
|
CXTranslationUnit clang_parseTranslationUnit(CXIndex CIdx, const char *source_filename, const char *const *command_line_args, int num_command_line_args, void *unsaved_files, unsigned num_unsaved_files, unsigned options);
|
|
void clang_disposeTranslationUnit(CXTranslationUnit);
|
|
CXCursor clang_getTranslationUnitCursor(CXTranslationUnit);
|
|
|
|
int clang_getCursorKind(CXCursor);
|
|
CXString clang_getCursorSpelling(CXCursor);
|
|
CXType clang_getCursorType(CXCursor);
|
|
CXType clang_getCursorResultType(CXCursor);
|
|
CXString clang_Cursor_getRawCommentText(CXCursor);
|
|
int clang_isCursorDefinition(CXCursor);
|
|
int clang_Cursor_isAnonymous(CXCursor);
|
|
int clang_Cursor_getNumArguments(CXCursor);
|
|
CXCursor clang_Cursor_getArgument(CXCursor, unsigned i);
|
|
CXSourceLocation clang_getCursorLocation(CXCursor);
|
|
int clang_Location_isFromMainFile(CXSourceLocation);
|
|
long long clang_getEnumConstantDeclValue(CXCursor);
|
|
CXType clang_getTypedefDeclUnderlyingType(CXCursor);
|
|
CXCursor clang_getTypeDeclaration(CXType);
|
|
unsigned clang_visitChildren(CXCursor parent, CXCursorVisitor visitor, CXClientData client_data);
|
|
|
|
CXString clang_getTypeSpelling(CXType);
|
|
CXType clang_getCanonicalType(CXType);
|
|
CXType clang_getPointeeType(CXType);
|
|
CXType clang_getResultType(CXType);
|
|
int clang_getNumArgTypes(CXType);
|
|
|
|
const char *clang_getCString(CXString string);
|
|
void clang_disposeString(CXString string);
|
|
|
|
void clang_getSpellingLocation(CXSourceLocation location, void **file, unsigned *line, unsigned *column, unsigned *offset);
|
|
|
|
typedef struct DIR DIR;
|
|
struct dirent {
|
|
unsigned long d_ino;
|
|
unsigned long d_off;
|
|
unsigned short d_reclen;
|
|
unsigned char d_type;
|
|
char d_name[256];
|
|
};
|
|
DIR *opendir(const char *name);
|
|
struct dirent *readdir(DIR *dirp);
|
|
int closedir(DIR *dirp);
|
|
|
|
void *mmap(void *addr, size_t length, int prot, int flags, int fd, int64_t offset);
|
|
int mprotect(void *addr, size_t len, int prot);
|
|
int munmap(void *addr, size_t len);
|
|
|
|
void* LoadLibraryA(const char*);
|
|
]]
|
|
end
|
|
|
|
local M = {}
|
|
|
|
M.ERROR = {
|
|
SUCCESS = 0,
|
|
INVALID_ARGUMENT = -1,
|
|
STRUCT_CORRUPTED = -2,
|
|
LIBRARY_NOT_FOUND = -50,
|
|
LIBRARY_NOT_LOADED = -52,
|
|
}
|
|
|
|
M.MAGIC_UNINITIALIZED = 0x00000000
|
|
M.MAGIC_ALIVE = 0xC1A2C3A4 -- CLANG
|
|
M.MAGIC_DEAD = 0xDEADC1A2 -- DEADCLANG
|
|
|
|
local function magic_to_string(magic)
|
|
if magic == M.MAGIC_UNINITIALIZED then
|
|
return "CLANG_UNINITIALIZED"
|
|
end
|
|
|
|
if magic == M.MAGIC_ALIVE then
|
|
return "CLANG_ALIVE"
|
|
end
|
|
|
|
if magic == M.MAGIC_DEAD then
|
|
return "CLANG_DEAD"
|
|
end
|
|
|
|
return string.format("Unknown (0x%08X)", magic)
|
|
end
|
|
|
|
local function check_magic(context)
|
|
if context == nil then
|
|
log.error("Aborting function: context is NULL.")
|
|
return false
|
|
end
|
|
|
|
local reason = ""
|
|
|
|
if context.magic == M.MAGIC_ALIVE then
|
|
return true
|
|
elseif context.magic == M.MAGIC_UNINITIALIZED then
|
|
reason = "context was never initialized"
|
|
elseif context.magic == M.MAGIC_DEAD then
|
|
reason = "context was explicitly destroyed"
|
|
else
|
|
reason = "memory corruption or wrong context passes"
|
|
end
|
|
|
|
log.error("Clang context integrity check failed (expected 0x%08X %s, got 0x%08X %s) because %s", M.MAGIC_ALIVE, magic_to_string(context.magic), context.magic, magic_to_string(context.magic), reason)
|
|
context.status = M.ERROR.STRUCT_CORRUPTED
|
|
return false
|
|
end
|
|
|
|
local function scan_directory_for_libraries(directory, prefix, suffix)
|
|
local results = {}
|
|
local d = ffi.C.opendir(directory)
|
|
|
|
if d == nil then
|
|
log.trace("Skipping directory '%s' as it does not exist or is not accessible.", directory)
|
|
return results
|
|
end
|
|
|
|
local scanned = 0
|
|
|
|
while true do
|
|
local ent = ffi.C.readdir(d)
|
|
|
|
if ent == nil then
|
|
break
|
|
end
|
|
|
|
scanned = scanned + 1
|
|
local name = ffi.string(ent.d_name)
|
|
local matches = name:sub(1, #prefix) == prefix
|
|
|
|
if #suffix > 0 then
|
|
matches = matches and name:sub(-#suffix) == suffix
|
|
end
|
|
|
|
if matches then
|
|
local remainder = name:sub(#prefix + 1)
|
|
if #suffix > 0 then
|
|
remainder = remainder:sub(1, -#suffix - 1)
|
|
end
|
|
|
|
local version_string = remainder:match("^%.?([%d%.]+)")
|
|
local version_number = tonumber((version_string or "0"):match("^(%d+)")) or 0
|
|
results[#results + 1] = { path = directory .. "/" .. name, version = version_number }
|
|
end
|
|
end
|
|
|
|
ffi.C.closedir(d)
|
|
log.debug("Scanned %d entries in '%s', found %d matching candidates.", scanned, directory, #results)
|
|
table.sort(results, function(a,b) return a.version > b.version
|
|
end)
|
|
|
|
local paths = {}
|
|
|
|
for _, r in ipairs(results) do
|
|
table.insert(paths, r.path)
|
|
end
|
|
|
|
return paths
|
|
end
|
|
|
|
local function get_common_paths()
|
|
local os_name = jit and jit.os or "Unknown"
|
|
log.info("Detecting libclang paths for %s.", os_name)
|
|
|
|
local paths = {
|
|
"clang", "libclang", "libclang.so", "libclang.dylib", "libclang.dll",
|
|
"libclang-22","libclang-21","libclang-20", "libclang-19", "libclang-18",
|
|
"libclang-17", "libclang-16", "libclang-15", "libclang-14"
|
|
}
|
|
|
|
if os_name == "Linux" then
|
|
local directories = {
|
|
"/usr/lib", "/usr/lib64", "/usr/local/lib",
|
|
"/usr/lib/x86_64-linux-gnu", "/usr/lib/aarch64-linux-gnu",
|
|
}
|
|
|
|
for i = 10, 22 do
|
|
table.insert(directories, string.format("/usr/lib/llvm%d/lib/", i))
|
|
table.insert(directories, string.format("/usr/lib/llvm%d/lib64/", i))
|
|
end
|
|
|
|
log.debug("Searching %d directories.", #directories)
|
|
|
|
for _, directory in ipairs(directories) do
|
|
local found = scan_directory_for_libraries(directory, "libclang.so", "")
|
|
|
|
for _, path in ipairs(found) do
|
|
table.insert(paths, path)
|
|
end
|
|
end
|
|
elseif os_name == "Windows" then
|
|
table.insert(paths, "C:\\Program Files\\LLVM\\bin\\libclang.dll")
|
|
table.insert(paths, "C:\\Program Files (x86)\\LLVM\\bin\\libclang.dll")
|
|
|
|
local visual_studio_years = { "2022", "2019", "2017", "18"}
|
|
local visual_studio_editions = { "Community", "Professional", "Enterprise", "BuildTools" }
|
|
local visual_studio_roots = {
|
|
"C:\\Program Files\\Microsoft Visual Studio",
|
|
"C:\\Program Files (x86)\\Microsoft Visual Studio",
|
|
}
|
|
|
|
for _, root in ipairs(visual_studio_roots) do
|
|
for _, year in ipairs(visual_studio_years) do
|
|
for _, edition in ipairs(visual_studio_editions) do
|
|
local base = root .. "\\" .. year .. "\\" .. edition .. "\\VC\\Tools\\Llvm" .. "\\x64"
|
|
table.insert(paths, base .. "\\bin\\libclang.dll")
|
|
end
|
|
end
|
|
end
|
|
else
|
|
log.warn("Unrecognized OS '%s', using base library names only.", os_name)
|
|
end
|
|
|
|
return paths
|
|
end
|
|
|
|
function M.create_context()
|
|
return {
|
|
library = nil,
|
|
status = M.ERROR.INVALID_ARGUMENT,
|
|
magic = M.MAGIC_UNINITIALIZED,
|
|
}
|
|
end
|
|
|
|
--- To prevent issue with LuaJIT shutdown and unloading libclang in Windows the library must be
|
|
--- loaded with `LoadLibraryA` and then use `ffi.load` to make it available to LuaJIT's global
|
|
--- `ffi.C` namespace. GH Windows Hoster runners will crash some times if this is not do in this way.
|
|
local function load_library(path)
|
|
if jit and jit.os == "Windows" then
|
|
local handle = ffi.C.LoadLibraryA(path)
|
|
|
|
if handle == nil then
|
|
log.error("Failed to load Clang library with LoadLibraryA().")
|
|
return false, nil
|
|
end
|
|
end
|
|
|
|
return pcall(ffi.load, path)
|
|
end
|
|
|
|
|
|
function M.init(context, custom_path)
|
|
if context == nil then
|
|
log.error("Aborting function: context is NULL")
|
|
return false
|
|
end
|
|
|
|
if custom_path and (custom_path == "" or custom_path:find("NOTFOUND")) then
|
|
log.warn("Ignoring invalid custom libclang path: '%s'", custom_path)
|
|
custom_path = nil
|
|
end
|
|
|
|
if custom_path then
|
|
log.info("Initializing libclang with custom path %s.", custom_path)
|
|
local ok, library = load_library(custom_path)
|
|
if ok then
|
|
log.info("Successfully loaded libclang from %s.", custom_path)
|
|
|
|
context.library = library
|
|
context.status = M.ERROR.SUCCESS
|
|
context.magic = M.MAGIC_ALIVE
|
|
return true
|
|
else
|
|
log.warn("Failed to load libclang from custom path '%s': %s", custom_path, tostring(library))
|
|
log.info("Falling back to automatic discovery...")
|
|
end
|
|
else
|
|
log.info("Initializing libclang with automatic discovery.")
|
|
end
|
|
|
|
local paths = get_common_paths()
|
|
local errors = {}
|
|
local attempt = 1
|
|
|
|
for _, path in ipairs(paths) do
|
|
log.trace("Attempt %d/%d: ffi.load('%s')", attempt, #paths, path)
|
|
local ok, library = load_library(path)
|
|
if ok then
|
|
log.info("Successfully loaded libclang from %s (attempt %d/%d).", path, attempt, #paths)
|
|
|
|
context.library = library
|
|
context.status = M.ERROR.SUCCESS
|
|
context.magic = M.MAGIC_ALIVE
|
|
return true
|
|
else
|
|
local error_message = tostring(library)
|
|
table.insert(errors, error_message)
|
|
log.trace("Failed: %s", error_message)
|
|
attempt = attempt + 1
|
|
end
|
|
end
|
|
|
|
log.error("Failed to load libclang after %d attempts.", #paths)
|
|
log.error("Tried paths:")
|
|
|
|
for i, path in ipairs(paths) do
|
|
log.error(" [%d] %s", i, path)
|
|
end
|
|
|
|
log.error("Errors:")
|
|
|
|
for i, error in ipairs(errors) do
|
|
log.error(" [%d] %s", i, error)
|
|
end
|
|
|
|
log.warn("Pass --clang-library <path> to specify your libclang location manually.")
|
|
return false
|
|
end
|
|
|
|
function M.destroy(context)
|
|
if context == nil then
|
|
return
|
|
end
|
|
|
|
if not check_magic(context) then
|
|
log.info("Destroying clang context.")
|
|
context.library = nil
|
|
context.status = M.ERROR.STRUCT_CORRUPTED
|
|
context.magic = M.MAGIC_DEAD
|
|
end
|
|
end
|
|
|
|
function M.resource_directory(context)
|
|
if context == nil then
|
|
log.error("Aborting function: context is NULL.")
|
|
return nil
|
|
end
|
|
|
|
local os_name = jit and jit.os or "Unknown"
|
|
local candidates = {}
|
|
|
|
if os_name == "Linux" then
|
|
for ver = 22, 10, -1 do
|
|
candidates[#candidates + 1] = string.format("/usr/lib/clang/%d/include", ver)
|
|
candidates[#candidates + 1] = string.format("/usr/lib64/clang/%d/include", ver)
|
|
candidates[#candidates + 1] = string.format("/usr/lib/llvm-%d/lib/clang/%d/include", ver, ver)
|
|
candidates[#candidates + 1] = string.format("/usr/local/lib/clang/%d/include", ver)
|
|
end
|
|
elseif os_name == "Windows" then
|
|
local llvm_roots = {
|
|
"C:\\Program Files\\LLVM\\lib\\clang",
|
|
"C:\\Program Files (x86)\\LLVM\\lib\\clang",
|
|
}
|
|
local visual_studio_years = { "2022", "2019", "2017" }
|
|
local visual_studio_editions = { "Community", "Professional", "Enterprise", "BuildTools" }
|
|
local visual_studio_roots = {
|
|
"C:\\Program Files\\Microsoft Visual Studio",
|
|
"C:\\Program Files (x86)\\Microsoft Visual Studio",
|
|
}
|
|
for ver = 22, 10, -1 do
|
|
for _, root in ipairs(llvm_roots) do
|
|
candidates[#candidates + 1] = string.format("%s\\%d\\include",root, ver)
|
|
end
|
|
|
|
for _, root in ipairs(visual_studio_roots) do
|
|
for _, year in ipairs(visual_studio_years) do
|
|
for _, edition in ipairs(visual_studio_editions) do
|
|
local base = root .. "\\" .. year .. "\\" .. edition .. "\\VC\\Tools\\Llvm" .. "\\x86"
|
|
candidates[#candidates + 1] = string.format("%s\\lib\\clang\\%d\\include",base, ver)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
end
|
|
|
|
for _, path in ipairs(candidates) do
|
|
local filepath = path
|
|
if os_name == "Windows" then
|
|
filepath = filepath .. "\\stdarg.h"
|
|
else
|
|
filepath = filepath .. "/stdarg.h"
|
|
end
|
|
|
|
local f = io.open(filepath, "r")
|
|
|
|
if f then
|
|
f:close()
|
|
log.info("Found clang resource dir: %s", path)
|
|
return path
|
|
end
|
|
end
|
|
|
|
log.warn("Could not locate clang resource directory.")
|
|
return nil
|
|
end
|
|
|
|
M.CURSOR_KIND = {
|
|
UNEXPOSED_DECL = 1,
|
|
STRUCT_DECL = 2,
|
|
UNION_DECL = 3,
|
|
CLASS_DECL = 4,
|
|
ENUM_DECL = 5,
|
|
FIELD_DECL = 6,
|
|
ENUM_CONSTANT_DECL = 7,
|
|
FUNCTION_DECL = 8,
|
|
VAR_DECL = 9,
|
|
PARM_DECL = 10,
|
|
TYPEDEF_DECL = 20,
|
|
TYPE_REF = 43,
|
|
}
|
|
|
|
M.TYPE_KIND = {
|
|
INVALID = 0,
|
|
POINTER = 101,
|
|
RECORD = 105,
|
|
ENUM = 106,
|
|
TYPEDEF = 107,
|
|
FUNCTION_NO_PROTO = 110,
|
|
FUNCTION_PROTO = 111,
|
|
}
|
|
|
|
M.CHILD_VISIT = {
|
|
BREAK = 0,
|
|
CONTINUE = 1,
|
|
RECURSE = 2,
|
|
}
|
|
|
|
local function cstring_to_string(context, cx_string)
|
|
if context == nil then
|
|
log.info("Aborting function: context is NULL.")
|
|
return ""
|
|
end
|
|
|
|
if context.library == nil then
|
|
log.error("Aborting function: context.library is NULL.")
|
|
return ""
|
|
end
|
|
|
|
local ok, ptr = pcall(context.library.clang_getCString, cx_string)
|
|
|
|
if not ok then
|
|
log.error("Aborting function: clang_getCString() failed on CXString %s.", cx_string)
|
|
context.library.clang_disposeString(cx_string)
|
|
return ""
|
|
end
|
|
|
|
if ptr == nil then
|
|
log.error("Aborting function: clang_getCString returned a NULL pointer. CXString has no data.")
|
|
context.library.clang_disposeString(cx_string)
|
|
return ""
|
|
end
|
|
|
|
local read_ok, string = pcall(ffi.string, ptr)
|
|
|
|
if not read_ok then
|
|
log.error("Aborting function: failed to read CXString pointer %s.", tostring(string))
|
|
context.library.clang_disposeString(cx_string)
|
|
return ""
|
|
end
|
|
|
|
context.library.clang_disposeString(cx_string)
|
|
return string
|
|
end
|
|
|
|
function M.cursor_spelling(context, cursor)
|
|
return cstring_to_string(context, context.library.clang_getCursorSpelling(cursor))
|
|
end
|
|
|
|
function M.cursor_kind(context, cursor)
|
|
return context.library.clang_getCursorKind(cursor)
|
|
end
|
|
|
|
function M.cursor_type(context, cursor)
|
|
return context.library.clang_getCursorType(cursor)
|
|
end
|
|
|
|
function M.type_spelling(context, cx_type)
|
|
return cstring_to_string(context, context.library.clang_getTypeSpelling(cx_type))
|
|
end
|
|
|
|
function M.raw_comment(context, cursor)
|
|
local cx_str = context.library.clang_Cursor_getRawCommentText(cursor)
|
|
local s = cstring_to_string(context, cx_str)
|
|
if s == "" then
|
|
return nil
|
|
end
|
|
return s
|
|
end
|
|
|
|
function M.is_from_main_file(context, cursor)
|
|
local loc = context.library.clang_getCursorLocation(cursor)
|
|
return context.library.clang_Location_isFromMainFile(loc) ~= 0
|
|
end
|
|
|
|
function M.is_definition(context, cursor)
|
|
return context.library.clang_isCursorDefinition(cursor) ~= 0
|
|
end
|
|
|
|
function M.is_anonymous(context, cursor)
|
|
return context.library.clang_Cursor_isAnonymous(cursor) ~= 0
|
|
end
|
|
|
|
function M.is_skippable(context, cursor)
|
|
if M.is_anonymous(context, cursor) then
|
|
return true
|
|
end
|
|
local name = M.cursor_spelling(context, cursor)
|
|
return not name or #name == 0 or name:find("%(unnamed") ~= nil
|
|
end
|
|
|
|
local cursor_line_number_buffer = ffi.new("unsigned[1]")
|
|
local cursor_column_number_buffer = ffi.new("unsigned[1]")
|
|
|
|
function M.cursor_location(context, cursor, filepath)
|
|
local loc = context.library.clang_getCursorLocation(cursor)
|
|
context.library.clang_getSpellingLocation(loc, nil, cursor_line_number_buffer, cursor_column_number_buffer, nil)
|
|
return {
|
|
filepath = filepath or "",
|
|
line_number = tonumber(cursor_line_number_buffer[0]),
|
|
column_number = tonumber(cursor_column_number_buffer[0]),
|
|
}
|
|
end
|
|
|
|
--------------------------------------------------------------------------------
|
|
-- System V AMD64 ABI Trampoline for `clang_visitChildren`
|
|
--------------------------------------------------------------------------------
|
|
-- WHY THIS IS NEEDED:
|
|
-- 1. Libclang's C API defines CXCursorVisitor as:
|
|
-- enum CXChildVisitResult (*)(CXCursor cursor, CXCursor parent, CXClientData client_data)
|
|
-- where CXCursor is a 32-byte struct passed by value.
|
|
--
|
|
-- 2. LuaJIT FFI callbacks do NOT support passing structs by value. Attempting to
|
|
-- cast a Lua function with struct-by-value arguments throws:
|
|
-- "cannot convert 'function' to 'enum ... (*)()'"
|
|
-- Therefore, athe FFI cdef declares the callback using pointers instead:
|
|
-- typedef enum CXChildVisitResult (*CXCursorVisitor)(CXCursor *cursor, CXCursor *parent, CXClientData client_data);
|
|
--
|
|
-- 3. Calling Convention / ABI Differences:
|
|
-- - Windows x64 (Microsoft x64 ABI): Aggregates larger than 8 bytes are
|
|
-- automatically passed by hidden reference (pointers in RCX and RDX). This
|
|
-- natively matches LuaJIT's expected pointer parameters.
|
|
-- - ARM64 (AAPCS64): Composite types larger than 16 bytes are passed by reference
|
|
-- via hidden pointers in registers (X0 and X1), also matching LuaJIT directly.
|
|
-- - Linux/macOS/BSD x86_64 (System V AMD64 ABI): Structs larger than 16 bytes
|
|
-- are pushed directly onto the stack by value:
|
|
-- [rsp + 8] -> CXCursor cursor (32 bytes)
|
|
-- [rsp + 40] -> CXCursor parent (32 bytes)
|
|
-- Register parameters (RDI, RSI, RDX...) are assigned only to register-
|
|
-- classifiable arguments. Because cursor and parent reside on the stack,
|
|
-- client_data (the 3rd C parameter) is passed in RDI!
|
|
-- If libclang calls the LuaJIT callback directly on System V AMD64, LuaJIT
|
|
-- expects RDI to be cursor* (which holds client_data, often NULL), causing an
|
|
-- immediate SIGSEGV when dereferencing cursor_ptr[0].
|
|
--
|
|
-- 4. The Trampoline Solution:
|
|
-- We allocate a page via mmap and assemble a tiny 17-byte machine code adapter:
|
|
-- - Follows W^X policy: allocated as RW (PROT_READ | PROT_WRITE), populated,
|
|
-- then transitioned to RX (PROT_READ | PROT_EXEC) via mprotect.
|
|
-- - Trampoline execution:
|
|
-- * RDI holds the callback pointer -> saved to RAX.
|
|
-- * RDX is cleared (client_data = NULL for the Lua callback).
|
|
-- * RDI is loaded with the stack address of cursor: lea rdi, [rsp + 8]
|
|
-- * RSI is loaded with the stack address of parent: lea rsi, [rsp + 40]
|
|
-- * Jumps directly to RAX, bridging the ABI gap with zero stack overhead.
|
|
-- - Memory management: an ffi.gc finalizer automatically unmaps the memory
|
|
-- with munmap when the trampoline pointer is collected.
|
|
--------------------------------------------------------------------------------
|
|
local sysv_trampoline = nil
|
|
local function get_sysv_trampoline()
|
|
if sysv_trampoline then
|
|
return sysv_trampoline
|
|
end
|
|
|
|
-- 1. Allocate writable memory page via mmap (PROT_READ | PROT_WRITE = 3)
|
|
-- Linux uses 0x20 for MAP_ANONYMOUS, whereas macOS/BSD uses 0x1000
|
|
local MAP_ANONYMOUS = (jit.os == "OSX" or jit.os == "BSD") and 0x1000 or 0x20
|
|
local MAP_PRIVATE = 0x02
|
|
local mem = ffi.C.mmap(nil, 4096, 3, bit.bor(MAP_PRIVATE, MAP_ANONYMOUS), -1, 0)
|
|
|
|
if mem == ffi.cast("void*", -1) then
|
|
error("Failed to allocate memory for System V ABI trampoline")
|
|
end
|
|
|
|
-- 2. 17-byte machine code trampoline bridging System V AMD64 stack arguments to register pointers:
|
|
-- mov rax, rdi (48 89 f8) : rax = cb (passed via client_data in RDI)
|
|
-- xor edx, edx (31 d2) : rdx = NULL (client_data for Lua callback)
|
|
-- lea rdi, [rsp + 8] (48 8d 7c 24 08) : rdi = &cursor (1st argument)
|
|
-- lea rsi, [rsp + 40] (48 8d 74 24 28) : rsi = &parent (2nd argument)
|
|
-- jmp rax (ff e0) : tail-call LuaJIT callback
|
|
local bytes = ffi.new("uint8_t[17]", {
|
|
0x48, 0x89, 0xf8,
|
|
0x31, 0xd2,
|
|
0x48, 0x8d, 0x7c, 0x24, 0x08,
|
|
0x48, 0x8d, 0x74, 0x24, 0x28,
|
|
0xff, 0xe0
|
|
})
|
|
ffi.copy(mem, bytes, 17)
|
|
|
|
-- 3. Set page to Read + Execute (PROT_READ | PROT_EXEC = 5) enforcing W^X security
|
|
if ffi.C.mprotect(mem, 4096, 5) ~= 0 then
|
|
error("Failed to set executable permissions via mprotect")
|
|
end
|
|
|
|
-- 4. Cast to CXCursorVisitor and register a garbage collection finalizer for munmap
|
|
local func_ptr = ffi.cast("CXCursorVisitor", mem)
|
|
ffi.gc(func_ptr, function(p)
|
|
ffi.C.munmap(ffi.cast("void*", p), 4096)
|
|
end)
|
|
|
|
sysv_trampoline = func_ptr
|
|
return sysv_trampoline
|
|
end
|
|
|
|
|
|
function M.visit_children(context, parent_cursor, visitor_fn)
|
|
local cb = ffi.cast("CXCursorVisitor", function(cursor_ptr, parent_ptr, client_data)
|
|
local res = visitor_fn(cursor_ptr[0], parent_ptr[0])
|
|
return res or M.CHILD_VISIT.CONTINUE
|
|
end)
|
|
|
|
|
|
if (jit.arch == "x64") and (jit.os ~= "Windows") then
|
|
local tramp = get_sysv_trampoline()
|
|
if tramp then
|
|
context.library.clang_visitChildren(parent_cursor, tramp, cb)
|
|
else
|
|
context.library.clang_visitChildren(parent_cursor, cb, nil)
|
|
end
|
|
else
|
|
context.library.clang_visitChildren(parent_cursor, cb, nil)
|
|
end
|
|
|
|
cb:free()
|
|
end
|
|
|
|
return M
|