Skip to content

Commit e069eec

Browse files
committed
v0.1.6: implement IDA-style plugins menu and lua API backend
This commit implements the plugin architecture described in docs/plugins.md, allowing users to extend Hyperion with Lua scripts loaded at startup. Changes include: - `LuaEngine` Backend: Added internal structures (`PluginEntry`, `PluginMenuItem`) and implemented `load_plugins(dir)` to automatically scan and parse `.lua` scripts from the `plugins/` directory. - Lua C API Registration: Implemented and registered four new Lua API functions (`register_plugin`, `register_menu_item`, `register_hotkey`, `register_on_analysis_complete`) that interface directly with the plugin registry. - UI Integration (`App`): Added a "Plugins" menu dynamically populated with flat items (for single-action plugins) and submenus (for multi-action plugins). - Plugin Manager: Added a new `Plugin Manager...` dialog displaying loaded plugins, descriptions, action counts, and error statuses. - Execution & Callbacks: Wired `lua_.check_hotkeys()` into the frame loop and `run_analysis_complete_callbacks()` into the analysis handoff to support custom keyboard shortcuts and post-analysis automation safely on the main thread. - Included `example_plugin.lua` built into the output directory as a working reference.
1 parent f0858e4 commit e069eec

5 files changed

Lines changed: 472 additions & 16 deletions

File tree

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,5 @@ Release/
2525
*.dll
2626
hyperion_layout.ini
2727
imgui.ini
28+
CONTRIBUTING.md
29+
SKILL.md

src/scripting/lua_engine.cpp

Lines changed: 304 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
#include "lua_engine.h"
22
#include <fmt/format.h>
33
#include <spdlog/spdlog.h>
4+
#include <imgui.h>
45
#include <cstring>
6+
#include <algorithm>
57

68
extern "C" {
79
#include <lua.h>
@@ -13,6 +15,10 @@ namespace hype {
1315

1416
namespace {
1517

18+
// -----------------------------------------------------------------------
19+
// Registry helpers
20+
// -----------------------------------------------------------------------
21+
1622
LuaEngine* get_engine(lua_State* L) {
1723
lua_getfield(L, LUA_REGISTRYINDEX, "__hype_engine");
1824
auto* eng = static_cast<LuaEngine*>(lua_touserdata(L, -1));
@@ -34,6 +40,76 @@ PEImage* get_img(lua_State* L) {
3440
return img;
3541
}
3642

43+
// -----------------------------------------------------------------------
44+
// Hotkey parsing helpers
45+
// -----------------------------------------------------------------------
46+
47+
struct HotkeySpec {
48+
ImGuiKey key = ImGuiKey_None;
49+
bool ctrl = false;
50+
bool shift = false;
51+
bool alt = false;
52+
};
53+
54+
// Parse strings like "Ctrl+Shift+R", "Alt+F7", "F5", "Ctrl+G"
55+
HotkeySpec parse_hotkey(const std::string& s) {
56+
HotkeySpec spec;
57+
std::string token;
58+
std::string src = s;
59+
// Split on '+'
60+
std::vector<std::string> parts;
61+
size_t pos = 0;
62+
while ((pos = src.find('+')) != std::string::npos) {
63+
parts.push_back(src.substr(0, pos));
64+
src.erase(0, pos + 1);
65+
}
66+
parts.push_back(src); // last part
67+
68+
for (auto& p : parts) {
69+
// to lower for comparison
70+
std::string lp = p;
71+
for (auto& c : lp) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
72+
if (lp == "ctrl") { spec.ctrl = true; continue; }
73+
if (lp == "shift") { spec.shift = true; continue; }
74+
if (lp == "alt") { spec.alt = true; continue; }
75+
76+
// Match ImGuiKey
77+
if (p.size() == 1) {
78+
char c = static_cast<char>(std::toupper(static_cast<unsigned char>(p[0])));
79+
if (c >= 'A' && c <= 'Z')
80+
spec.key = static_cast<ImGuiKey>(ImGuiKey_A + (c - 'A'));
81+
else if (c >= '0' && c <= '9')
82+
spec.key = static_cast<ImGuiKey>(ImGuiKey_0 + (c - '0'));
83+
} else if (lp[0] == 'f' && lp.size() <= 3) {
84+
// F1–F12
85+
int n = 0;
86+
try { n = std::stoi(lp.substr(1)); } catch (...) {}
87+
if (n >= 1 && n <= 12)
88+
spec.key = static_cast<ImGuiKey>(ImGuiKey_F1 + (n - 1));
89+
} else if (lp == "space") { spec.key = ImGuiKey_Space; }
90+
else if (lp == "tab") { spec.key = ImGuiKey_Tab; }
91+
else if (lp == "enter") { spec.key = ImGuiKey_Enter; }
92+
else if (lp == "escape") { spec.key = ImGuiKey_Escape; }
93+
else if (lp == "delete") { spec.key = ImGuiKey_Delete; }
94+
else if (lp == "home") { spec.key = ImGuiKey_Home; }
95+
else if (lp == "end") { spec.key = ImGuiKey_End; }
96+
}
97+
return spec;
98+
}
99+
100+
bool check_hotkey(const HotkeySpec& spec) {
101+
if (spec.key == ImGuiKey_None) return false;
102+
auto& io = ImGui::GetIO();
103+
if (spec.ctrl != io.KeyCtrl) return false;
104+
if (spec.shift != io.KeyShift) return false;
105+
if (spec.alt != io.KeyAlt) return false;
106+
return ImGui::IsKeyPressed(spec.key);
107+
}
108+
109+
// -----------------------------------------------------------------------
110+
// Existing Lua API
111+
// -----------------------------------------------------------------------
112+
37113
int l_get_name(lua_State* L) {
38114
auto* db = get_db(L);
39115
if (!db) { lua_pushnil(L); return 1; }
@@ -102,9 +178,9 @@ int l_get_bytes(lua_State* L) {
102178

103179
for (auto& seg : img->segments) {
104180
if (!seg.contains(addr)) continue;
105-
size_t off = static_cast<size_t>(addr - seg.va);
181+
size_t off = static_cast<size_t>(addr - seg.va);
106182
size_t avail = seg.data.size() - off;
107-
size_t n = static_cast<size_t>(len) < avail ? static_cast<size_t>(len) : avail;
183+
size_t n = static_cast<size_t>(len) < avail ? static_cast<size_t>(len) : avail;
108184
lua_pushlstring(L, reinterpret_cast<const char*>(seg.data.data() + off), n);
109185
return 1;
110186
}
@@ -178,19 +254,104 @@ int l_goto(lua_State* L) {
178254
return 0;
179255
}
180256

181-
} // anon
257+
// -----------------------------------------------------------------------
258+
// Plugin registration Lua API
259+
// -----------------------------------------------------------------------
260+
261+
int l_register_plugin(lua_State* L) {
262+
auto* eng = get_engine(L);
263+
if (!eng) return 0;
264+
const char* name = luaL_optstring(L, 1, "Unnamed Plugin");
265+
const char* desc = luaL_optstring(L, 2, "");
266+
267+
// Find the entry that was pre-created for the current file, or create one
268+
auto& plugins = const_cast<std::vector<PluginEntry>&>(eng->plugins());
269+
for (auto& p : plugins) {
270+
if (p.path == eng->current_plugin_name()) {
271+
p.name = name;
272+
p.desc = desc;
273+
return 0;
274+
}
275+
}
276+
// Fallback: shouldn't normally reach here
277+
PluginEntry e;
278+
e.name = name;
279+
e.desc = desc;
280+
e.path = eng->current_plugin_name();
281+
plugins.push_back(std::move(e));
282+
return 0;
283+
}
284+
285+
int l_register_menu_item(lua_State* L) {
286+
auto* eng = get_engine(L);
287+
if (!eng) return 0;
288+
289+
const char* label = luaL_checkstring(L, 1);
290+
luaL_checktype(L, 2, LUA_TFUNCTION);
291+
lua_pushvalue(L, 2);
292+
int ref = luaL_ref(L, LUA_REGISTRYINDEX);
293+
294+
auto& plugins = const_cast<std::vector<PluginEntry>&>(eng->plugins());
295+
// Add to the most recently active plugin
296+
for (auto it = plugins.rbegin(); it != plugins.rend(); ++it) {
297+
if (it->path == eng->current_plugin_name()) {
298+
it->items.push_back({label, ref});
299+
return 0;
300+
}
301+
}
302+
// If no plugin registered yet, attach to a default entry
303+
if (!plugins.empty()) {
304+
plugins.back().items.push_back({label, ref});
305+
}
306+
return 0;
307+
}
308+
309+
int l_register_hotkey(lua_State* L) {
310+
auto* eng = get_engine(L);
311+
if (!eng) return 0;
312+
const char* key_str = luaL_checkstring(L, 1);
313+
luaL_checktype(L, 2, LUA_TFUNCTION);
314+
lua_pushvalue(L, 2);
315+
int ref = luaL_ref(L, LUA_REGISTRYINDEX);
316+
eng->add_hotkey(key_str, ref);
317+
return 0;
318+
}
319+
320+
int l_register_on_analysis_complete(lua_State* L) {
321+
auto* eng = get_engine(L);
322+
if (!eng) return 0;
323+
luaL_checktype(L, 1, LUA_TFUNCTION);
324+
lua_pushvalue(L, 1);
325+
int ref = luaL_ref(L, LUA_REGISTRYINDEX);
326+
eng->add_analysis_cb(ref);
327+
return 0;
328+
}
329+
330+
} // anon namespace
331+
332+
// -----------------------------------------------------------------------
333+
// LuaEngine implementation
334+
// -----------------------------------------------------------------------
182335

183336
LuaEngine::LuaEngine() {
184337
L_ = luaL_newstate();
185338
luaL_openlibs(L_);
186339
}
187340

188341
LuaEngine::~LuaEngine() {
342+
// Release all Lua registry refs before closing
343+
for (auto& p : plugins_)
344+
for (auto& item : p.items)
345+
if (item.cb_ref != LUA_NOREF) luaL_unref(L_, LUA_REGISTRYINDEX, item.cb_ref);
346+
for (auto& [_, ref] : hotkeys_)
347+
if (ref != LUA_NOREF) luaL_unref(L_, LUA_REGISTRYINDEX, ref);
348+
for (auto ref : on_analysis_cbs_)
349+
if (ref != LUA_NOREF) luaL_unref(L_, LUA_REGISTRYINDEX, ref);
189350
if (L_) lua_close(L_);
190351
}
191352

192353
void LuaEngine::init(AnalysisDB* db, PEImage* img) {
193-
db_ = db;
354+
db_ = db;
194355
img_ = img;
195356
register_api();
196357
}
@@ -207,16 +368,147 @@ void LuaEngine::register_api() {
207368
lua_pushlightuserdata(L_, &nav_cb_);
208369
lua_setfield(L_, LUA_REGISTRYINDEX, "__hype_nav");
209370

210-
lua_register(L_, "get_name", l_get_name);
211-
lua_register(L_, "set_name", l_set_name);
212-
lua_register(L_, "get_func", l_get_func);
213-
lua_register(L_, "get_insn", l_get_insn);
214-
lua_register(L_, "get_bytes", l_get_bytes);
371+
// Core API
372+
lua_register(L_, "get_name", l_get_name);
373+
lua_register(L_, "set_name", l_set_name);
374+
lua_register(L_, "get_func", l_get_func);
375+
lua_register(L_, "get_insn", l_get_insn);
376+
lua_register(L_, "get_bytes", l_get_bytes);
215377
lua_register(L_, "set_comment", l_set_comment);
216378
lua_register(L_, "get_xrefs_to", l_get_xrefs_to);
217379
lua_register(L_, "get_functions", l_get_functions);
218-
lua_register(L_, "print", l_print);
219-
lua_register(L_, "goto_addr", l_goto);
380+
lua_register(L_, "print", l_print);
381+
lua_register(L_, "goto_addr", l_goto);
382+
383+
// Plugin registration API
384+
lua_register(L_, "register_plugin", l_register_plugin);
385+
lua_register(L_, "register_menu_item", l_register_menu_item);
386+
lua_register(L_, "register_hotkey", l_register_hotkey);
387+
lua_register(L_, "register_on_analysis_complete", l_register_on_analysis_complete);
388+
}
389+
390+
// Register engine pointer and api before analysis (plugins load pre-loop).
391+
// We call register_api early so plugins can call register_* at load time
392+
// even though db_ and img_ are still null — API functions guard for null.
393+
void LuaEngine::load_plugins(const std::filesystem::path& dir) {
394+
// Register the engine pointer early so register_* functions work
395+
lua_pushlightuserdata(L_, this);
396+
lua_setfield(L_, LUA_REGISTRYINDEX, "__hype_engine");
397+
lua_pushlightuserdata(L_, &output_);
398+
lua_setfield(L_, LUA_REGISTRYINDEX, "__hype_output");
399+
400+
// Register plugin API functions (safe even with null db_/img_)
401+
lua_register(L_, "get_name", l_get_name);
402+
lua_register(L_, "set_name", l_set_name);
403+
lua_register(L_, "get_func", l_get_func);
404+
lua_register(L_, "get_insn", l_get_insn);
405+
lua_register(L_, "get_bytes", l_get_bytes);
406+
lua_register(L_, "set_comment", l_set_comment);
407+
lua_register(L_, "get_xrefs_to", l_get_xrefs_to);
408+
lua_register(L_, "get_functions", l_get_functions);
409+
lua_register(L_, "print", l_print);
410+
lua_register(L_, "goto_addr", l_goto);
411+
lua_register(L_, "register_plugin", l_register_plugin);
412+
lua_register(L_, "register_menu_item", l_register_menu_item);
413+
lua_register(L_, "register_hotkey", l_register_hotkey);
414+
lua_register(L_, "register_on_analysis_complete", l_register_on_analysis_complete);
415+
416+
if (!std::filesystem::exists(dir) || !std::filesystem::is_directory(dir)) {
417+
spdlog::info("lua_engine: plugins dir not found at {}", dir.string());
418+
return;
419+
}
420+
421+
// Collect and sort .lua files alphabetically
422+
std::vector<std::filesystem::path> files;
423+
for (auto& entry : std::filesystem::directory_iterator(dir)) {
424+
if (entry.path().extension() == ".lua")
425+
files.push_back(entry.path());
426+
}
427+
std::sort(files.begin(), files.end());
428+
429+
for (auto& fp : files) {
430+
PluginEntry pe;
431+
pe.path = fp.string();
432+
pe.name = fp.stem().string(); // default name = filename stem
433+
pe.desc = "";
434+
435+
current_plugin_ = pe.path;
436+
plugins_.push_back(std::move(pe));
437+
438+
output_.clear();
439+
int err = luaL_dofile(L_, fp.string().c_str());
440+
if (err) {
441+
const char* msg = lua_tostring(L_, -1);
442+
plugins_.back().error = true;
443+
plugins_.back().error_msg = msg ? msg : "unknown error";
444+
lua_pop(L_, 1);
445+
spdlog::warn("lua_engine: plugin error [{}]: {}", fp.filename().string(), plugins_.back().error_msg);
446+
} else {
447+
spdlog::info("lua_engine: loaded plugin [{}] -> \"{}\"",
448+
fp.filename().string(), plugins_.back().name);
449+
}
450+
}
451+
452+
spdlog::info("lua_engine: {} plugin(s) loaded from {}", plugins_.size(), dir.string());
453+
}
454+
455+
void LuaEngine::invoke_menu_item(int plugin_idx, int item_idx) {
456+
if (plugin_idx < 0 || plugin_idx >= static_cast<int>(plugins_.size())) return;
457+
auto& plugin = plugins_[static_cast<size_t>(plugin_idx)];
458+
if (item_idx < 0 || item_idx >= static_cast<int>(plugin.items.size())) return;
459+
int ref = plugin.items[static_cast<size_t>(item_idx)].cb_ref;
460+
if (ref == LUA_NOREF) return;
461+
462+
output_.clear();
463+
lua_rawgeti(L_, LUA_REGISTRYINDEX, ref);
464+
int err = lua_pcall(L_, 0, 0, 0);
465+
if (err) {
466+
const char* msg = lua_tostring(L_, -1);
467+
output_ += std::string("[plugin error] ") + (msg ? msg : "unknown") + "\n";
468+
lua_pop(L_, 1);
469+
}
470+
// output_ is forwarded to the Script Console by scriptc_ which reads it via execute()
471+
// For plugin callbacks we flush directly via the output panel pointer stored in registry
472+
lua_getfield(L_, LUA_REGISTRYINDEX, "__hype_output");
473+
auto* out_ptr = static_cast<std::string*>(lua_touserdata(L_, -1));
474+
lua_pop(L_, 1);
475+
if (out_ptr && out_ptr != &output_) {
476+
*out_ptr += output_;
477+
}
478+
}
479+
480+
void LuaEngine::run_analysis_complete_callbacks() {
481+
for (int ref : on_analysis_cbs_) {
482+
if (ref == LUA_NOREF) continue;
483+
output_.clear();
484+
lua_rawgeti(L_, LUA_REGISTRYINDEX, ref);
485+
int err = lua_pcall(L_, 0, 0, 0);
486+
if (err) {
487+
const char* msg = lua_tostring(L_, -1);
488+
spdlog::warn("lua_engine: on_analysis_complete callback error: {}",
489+
msg ? msg : "unknown");
490+
lua_pop(L_, 1);
491+
}
492+
}
493+
}
494+
495+
void LuaEngine::check_hotkeys() {
496+
auto& io = ImGui::GetIO();
497+
if (io.WantTextInput) return;
498+
for (auto& [key_str, ref] : hotkeys_) {
499+
if (ref == LUA_NOREF) continue;
500+
auto spec = parse_hotkey(key_str);
501+
if (!check_hotkey(spec)) continue;
502+
503+
output_.clear();
504+
lua_rawgeti(L_, LUA_REGISTRYINDEX, ref);
505+
int err = lua_pcall(L_, 0, 0, 0);
506+
if (err) {
507+
const char* msg = lua_tostring(L_, -1);
508+
spdlog::warn("lua_engine: hotkey callback error: {}", msg ? msg : "unknown");
509+
lua_pop(L_, 1);
510+
}
511+
}
220512
}
221513

222514
std::string LuaEngine::execute(const std::string& code) {
@@ -231,4 +523,4 @@ std::string LuaEngine::execute(const std::string& code) {
231523
return output_;
232524
}
233525

234-
}
526+
} // namespace hype

0 commit comments

Comments
 (0)