Step 161: add plugin API and loader

This commit is contained in:
Bill
2026-02-09 19:51:42 -07:00
parent 300cf070b2
commit 898f68543b
6 changed files with 279 additions and 1 deletions

View File

@@ -508,3 +508,4 @@ Sprint 5 in progress. Step 141 (Emacs daemon with user config) done. Next: Step
| 2026-02-10 | Codex | Step 158: Agent workflow recorder with JSON export, replay support, and RPC wiring. 5/5 tests pass. |
| 2026-02-10 | Codex | Step 159: Agent marketplace registry with UI panel, install toggles, and registry serialization. 6/6 tests pass. |
| 2026-02-10 | Codex | Step 160: Theme engine with JSON themes, ImGui styling, and syntax/editor color mapping. 4/4 tests pass. |
| 2026-02-10 | Codex | Step 161: Plugin API + loader with registry hooks for concepts/generators/grammars/annotations/panels/commands. 10/10 tests pass. |

View File

@@ -898,6 +898,9 @@ add_executable(step160_test tests/step160_test.cpp)
target_include_directories(step160_test PRIVATE src)
target_link_libraries(step160_test PRIVATE nlohmann_json::nlohmann_json imgui::imgui)
add_executable(step161_test tests/step161_test.cpp)
target_include_directories(step161_test PRIVATE src)
find_package(SDL2 CONFIG REQUIRED)
find_package(OpenGL REQUIRED)
find_package(glad CONFIG REQUIRED)

28
editor/src/PluginAPI.h Normal file
View File

@@ -0,0 +1,28 @@
#pragma once
// Step 161: Plugin API definition
#include <string>
struct PluginDescriptor {
std::string name;
std::string version;
std::string apiVersion;
std::string author;
std::string entrySymbol;
};
struct PluginHost {
void* ctx = nullptr;
void (*registerConcept)(void* ctx, const char* name) = nullptr;
void (*registerGenerator)(void* ctx, const char* language) = nullptr;
void (*registerGrammar)(void* ctx, const char* language) = nullptr;
void (*registerAnnotation)(void* ctx, const char* name) = nullptr;
void (*registerPanel)(void* ctx, const char* name) = nullptr;
void (*registerCommand)(void* ctx, const char* commandId) = nullptr;
void (*registerKeybinding)(void* ctx, const char* commandId,
const char* keybinding) = nullptr;
};
using PluginInitFn = bool (*)(const PluginHost* host,
PluginDescriptor* outDescriptor);
using PluginShutdownFn = void (*)();

189
editor/src/PluginLoader.h Normal file
View File

@@ -0,0 +1,189 @@
#pragma once
// Step 161: Plugin loader
#include <string>
#include <vector>
#include <unordered_map>
#include <filesystem>
#include <algorithm>
#include "PluginAPI.h"
#ifdef _WIN32
#include <windows.h>
#else
#include <dlfcn.h>
#endif
struct PluginRegistry {
std::vector<std::string> concepts;
std::vector<std::string> generators;
std::vector<std::string> grammars;
std::vector<std::string> annotations;
std::vector<std::string> panels;
std::vector<std::string> commands;
std::vector<std::pair<std::string, std::string>> keybindings;
void clear() {
concepts.clear();
generators.clear();
grammars.clear();
annotations.clear();
panels.clear();
commands.clear();
keybindings.clear();
}
};
struct PluginHandle {
std::string path;
PluginDescriptor descriptor;
void* handle = nullptr;
PluginShutdownFn shutdown = nullptr;
};
class PluginLoader {
public:
bool loadFromDirectory(const std::string& dir, std::string& error) {
namespace fs = std::filesystem;
std::error_code ec;
if (!fs::exists(dir, ec)) {
error = "Directory not found: " + dir;
return false;
}
bool any = false;
for (const auto& entry : fs::directory_iterator(dir)) {
if (!entry.is_regular_file()) continue;
auto path = entry.path();
#ifdef _WIN32
if (path.extension() != ".dll") continue;
#elif __APPLE__
if (path.extension() != ".dylib") continue;
#else
if (path.extension() != ".so") continue;
#endif
std::string err;
if (!loadPlugin(path.string(), err)) {
error = err;
return false;
}
any = true;
}
if (!any) {
error = "No plugin libraries found in: " + dir;
}
return any;
}
bool loadPlugin(const std::string& path, std::string& error) {
#ifdef _WIN32
HMODULE lib = LoadLibraryA(path.c_str());
if (!lib) {
error = "LoadLibrary failed: " + path;
return false;
}
auto init = reinterpret_cast<PluginInitFn>(
GetProcAddress(lib, "WhetstonePluginInit"));
auto shutdown = reinterpret_cast<PluginShutdownFn>(
GetProcAddress(lib, "WhetstonePluginShutdown"));
#else
void* lib = dlopen(path.c_str(), RTLD_LAZY);
if (!lib) {
error = std::string("dlopen failed: ") + dlerror();
return false;
}
auto init = reinterpret_cast<PluginInitFn>(
dlsym(lib, "WhetstonePluginInit"));
auto shutdown = reinterpret_cast<PluginShutdownFn>(
dlsym(lib, "WhetstonePluginShutdown"));
#endif
if (!init) {
error = "Missing WhetstonePluginInit in: " + path;
unloadLibrary(lib);
return false;
}
PluginDescriptor desc;
if (!invokeInit(init, desc)) {
error = "Plugin init failed: " + path;
unloadLibrary(lib);
return false;
}
PluginHandle handle;
handle.path = path;
handle.descriptor = desc;
handle.handle = lib;
handle.shutdown = shutdown;
plugins_.push_back(std::move(handle));
return true;
}
bool loadFromEntryPoint(PluginInitFn init,
const std::string& pathLabel,
std::string& error) {
PluginDescriptor desc;
if (!invokeInit(init, desc)) {
error = "Plugin init failed: " + pathLabel;
return false;
}
PluginHandle handle;
handle.path = pathLabel;
handle.descriptor = desc;
plugins_.push_back(std::move(handle));
return true;
}
void unloadAll() {
for (auto& plugin : plugins_) {
if (plugin.shutdown) plugin.shutdown();
if (plugin.handle) {
unloadLibrary(plugin.handle);
}
}
plugins_.clear();
registry_.clear();
}
const std::vector<PluginHandle>& plugins() const { return plugins_; }
const PluginRegistry& registry() const { return registry_; }
private:
bool invokeInit(PluginInitFn init, PluginDescriptor& desc) {
PluginHost host;
host.ctx = this;
host.registerConcept = [](void* ctx, const char* name) {
static_cast<PluginLoader*>(ctx)->registry_.concepts.emplace_back(name ? name : "");
};
host.registerGenerator = [](void* ctx, const char* language) {
static_cast<PluginLoader*>(ctx)->registry_.generators.emplace_back(language ? language : "");
};
host.registerGrammar = [](void* ctx, const char* language) {
static_cast<PluginLoader*>(ctx)->registry_.grammars.emplace_back(language ? language : "");
};
host.registerAnnotation = [](void* ctx, const char* name) {
static_cast<PluginLoader*>(ctx)->registry_.annotations.emplace_back(name ? name : "");
};
host.registerPanel = [](void* ctx, const char* name) {
static_cast<PluginLoader*>(ctx)->registry_.panels.emplace_back(name ? name : "");
};
host.registerCommand = [](void* ctx, const char* commandId) {
static_cast<PluginLoader*>(ctx)->registry_.commands.emplace_back(commandId ? commandId : "");
};
host.registerKeybinding = [](void* ctx, const char* commandId, const char* keybinding) {
static_cast<PluginLoader*>(ctx)->registry_.keybindings.emplace_back(
commandId ? commandId : "", keybinding ? keybinding : "");
};
return init(&host, &desc);
}
static void unloadLibrary(void* handle) {
#ifdef _WIN32
FreeLibrary(reinterpret_cast<HMODULE>(handle));
#else
dlclose(handle);
#endif
}
PluginRegistry registry_;
std::vector<PluginHandle> plugins_;
};

View File

@@ -0,0 +1,57 @@
// Step 161 TDD Test: Plugin API + loader
#include "PluginLoader.h"
#include <iostream>
static void expect(bool cond, const std::string& name, int& passed, int& failed) {
if (cond) {
std::cout << "Test " << (passed + failed + 1) << " PASS: " << name << "\n";
++passed;
} else {
std::cout << "Test " << (passed + failed + 1) << " FAIL: " << name << "\n";
++failed;
}
}
static bool FakeInit(const PluginHost* host, PluginDescriptor* outDesc) {
outDesc->name = "FakePlugin";
outDesc->version = "0.1";
outDesc->apiVersion = "1";
outDesc->author = "Test";
outDesc->entrySymbol = "WhetstonePluginInit";
host->registerConcept(host->ctx, "ExternalThing");
host->registerGenerator(host->ctx, "zig");
host->registerGrammar(host->ctx, "zig");
host->registerAnnotation(host->ctx, "TraceSafe");
host->registerPanel(host->ctx, "CustomPanel");
host->registerCommand(host->ctx, "plugin.command");
host->registerKeybinding(host->ctx, "plugin.command", "Ctrl+Alt+P");
return true;
}
int main() {
int passed = 0;
int failed = 0;
PluginLoader loader;
std::string error;
expect(loader.loadFromEntryPoint(&FakeInit, "fake", error),
"load from entry point", passed, failed);
const auto& registry = loader.registry();
expect(registry.concepts.size() == 1, "concept registered", passed, failed);
expect(registry.generators.size() == 1, "generator registered", passed, failed);
expect(registry.grammars.size() == 1, "grammar registered", passed, failed);
expect(registry.annotations.size() == 1, "annotation registered", passed, failed);
expect(registry.panels.size() == 1, "panel registered", passed, failed);
expect(registry.commands.size() == 1, "command registered", passed, failed);
expect(registry.keybindings.size() == 1, "keybinding registered", passed, failed);
expect(loader.plugins().size() == 1, "plugin tracked", passed, failed);
expect(loader.plugins()[0].descriptor.name == "FakePlugin",
"descriptor stored", passed, failed);
std::cout << "\n=== Step 161 Results: " << passed << " passed, "
<< failed << " failed ===\n";
return failed == 0 ? 0 : 1;
}

View File

@@ -349,7 +349,7 @@ Final polish, documentation, and ecosystem features.
Theme gallery in Settings panel.
*New:* `ThemeEngine.h`
- [ ] **Step 161: Extension/plugin API**
- [x] **Step 161: Extension/plugin API**
Define a stable plugin interface. Plugins can:
- Register new AST concept types
- Add generators for new languages