organize repo
This commit is contained in:
@@ -0,0 +1,179 @@
|
||||
/*
|
||||
* Sample C++ Plugin — fully unloadable, no runtime baggage.
|
||||
*
|
||||
* Exports the standard Overlord plugin ABI:
|
||||
* PluginOnLoad, PluginOnEvent, PluginOnUnload, PluginSetCallback,
|
||||
* PluginGetRuntime (returns "cpp" — tells the host this DLL can be freed)
|
||||
*
|
||||
* Build (Windows / MSVC):
|
||||
* cl /LD /EHsc /O2 plugin.cpp /Fe:sample-cpp-windows-amd64.dll
|
||||
* — or with MinGW —
|
||||
* x86_64-w64-mingw32-g++ -shared -O2 -o sample-cpp-windows-amd64.dll plugin.cpp
|
||||
*
|
||||
* Build (Linux):
|
||||
* g++ -shared -fPIC -O2 -o sample-cpp-linux-amd64.so plugin.cpp
|
||||
*/
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
#include <mutex>
|
||||
#include <unordered_map>
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Platform helpers */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
#ifdef _WIN32
|
||||
# define EXPORT extern "C" __declspec(dllexport)
|
||||
# define WIN32_LEAN_AND_MEAN
|
||||
# include <windows.h>
|
||||
BOOL WINAPI DllMain(HINSTANCE hDll, DWORD reason, LPVOID reserved) {
|
||||
(void)hDll; (void)reserved;
|
||||
return TRUE;
|
||||
}
|
||||
#else
|
||||
# define EXPORT extern "C" __attribute__((visibility("default")))
|
||||
#endif
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Host callback */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
#ifdef _WIN32
|
||||
typedef void (__stdcall *host_callback_t)(
|
||||
const char *event, uintptr_t eventLen,
|
||||
const char *payload, uintptr_t payloadLen);
|
||||
#else
|
||||
typedef void (*host_callback_t)(
|
||||
uintptr_t ctx,
|
||||
const char *event, int eventLen,
|
||||
const char *payload, int payloadLen);
|
||||
#endif
|
||||
|
||||
static host_callback_t g_callback = nullptr;
|
||||
#ifndef _WIN32
|
||||
static uintptr_t g_callback_ctx = 0;
|
||||
#endif
|
||||
|
||||
static void send_event(const char *event, const char *payload) {
|
||||
if (!g_callback) return;
|
||||
int elen = event ? static_cast<int>(strlen(event)) : 0;
|
||||
int plen = payload ? static_cast<int>(strlen(payload)) : 0;
|
||||
#ifdef _WIN32
|
||||
g_callback(event, static_cast<uintptr_t>(elen),
|
||||
payload, static_cast<uintptr_t>(plen));
|
||||
#else
|
||||
g_callback(g_callback_ctx, event, elen, payload, plen);
|
||||
#endif
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Plugin state — demonstrates C++ features (std::string, maps, mutex) */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
static std::mutex g_mu;
|
||||
static std::string g_client_id;
|
||||
static std::unordered_map<std::string, int> g_event_counts;
|
||||
|
||||
/* Extract a JSON string value by key (simple, no full JSON parser) */
|
||||
static std::string json_extract(const char *json, int len, const char *key) {
|
||||
if (!json || len <= 0) return "";
|
||||
std::string haystack(json, static_cast<size_t>(len));
|
||||
std::string needle = std::string("\"") + key + "\":\"";
|
||||
auto pos = haystack.find(needle);
|
||||
if (pos == std::string::npos) {
|
||||
needle = std::string("\"") + key + "\": \"";
|
||||
pos = haystack.find(needle);
|
||||
}
|
||||
if (pos == std::string::npos) return "";
|
||||
pos += needle.size();
|
||||
auto end = haystack.find('"', pos);
|
||||
if (end == std::string::npos) return "";
|
||||
return haystack.substr(pos, end - pos);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Exported ABI */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
EXPORT const char *PluginGetRuntime() {
|
||||
return "cpp";
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
EXPORT void PluginSetCallback(uint64_t cb) {
|
||||
g_callback = reinterpret_cast<host_callback_t>(static_cast<uintptr_t>(cb));
|
||||
}
|
||||
|
||||
EXPORT int PluginOnLoad(const char *hostInfo, int hostInfoLen, uint64_t cb) {
|
||||
g_callback = reinterpret_cast<host_callback_t>(static_cast<uintptr_t>(cb));
|
||||
#else
|
||||
EXPORT int PluginOnLoad(const char *hostInfo, int hostInfoLen,
|
||||
uintptr_t cb, uintptr_t ctx) {
|
||||
g_callback = reinterpret_cast<host_callback_t>(cb);
|
||||
g_callback_ctx = ctx;
|
||||
#endif
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(g_mu);
|
||||
g_client_id = json_extract(hostInfo, hostInfoLen, "clientId");
|
||||
g_event_counts.clear();
|
||||
}
|
||||
|
||||
fprintf(stderr, "[sample-cpp] loaded, clientId=%s\n", g_client_id.c_str());
|
||||
send_event("ready", "{\"message\":\"sample-cpp plugin ready\"}");
|
||||
return 0;
|
||||
}
|
||||
|
||||
EXPORT int PluginOnEvent(const char *event, int eventLen,
|
||||
const char *payload, int payloadLen) {
|
||||
std::string ev(event, static_cast<size_t>(eventLen));
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(g_mu);
|
||||
g_event_counts[ev]++;
|
||||
}
|
||||
|
||||
if (ev == "ping") {
|
||||
send_event("pong", nullptr);
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (ev == "ui_message") {
|
||||
std::string pl(payload ? payload : "", payload ? static_cast<size_t>(payloadLen) : 0u);
|
||||
fprintf(stderr, "[sample-cpp] got ui_message: %s\n", pl.c_str());
|
||||
std::string resp = "{\"message\":\"echo from C++: " + pl + "\"}";
|
||||
send_event("echo", resp.c_str());
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (ev == "stats") {
|
||||
std::lock_guard<std::mutex> lk(g_mu);
|
||||
std::string json = "{";
|
||||
bool first = true;
|
||||
for (auto &kv : g_event_counts) {
|
||||
if (!first) json += ",";
|
||||
json += "\"" + kv.first + "\":" + std::to_string(kv.second);
|
||||
first = false;
|
||||
}
|
||||
json += "}";
|
||||
send_event("stats_reply", json.c_str());
|
||||
return 0;
|
||||
}
|
||||
|
||||
fprintf(stderr, "[sample-cpp] unhandled event: %s\n", ev.c_str());
|
||||
return 0;
|
||||
}
|
||||
|
||||
EXPORT void PluginOnUnload() {
|
||||
fprintf(stderr, "[sample-cpp] unloading\n");
|
||||
g_callback = nullptr;
|
||||
#ifndef _WIN32
|
||||
g_callback_ctx = 0;
|
||||
#endif
|
||||
std::lock_guard<std::mutex> lk(g_mu);
|
||||
g_client_id.clear();
|
||||
g_event_counts.clear();
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: "Segoe UI", system-ui, sans-serif;
|
||||
background: #0f1115;
|
||||
color: #e8ecf1;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 820px;
|
||||
margin: 40px auto;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
margin: 0 0 4px 0;
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.sub {
|
||||
margin: 0 0 24px 0;
|
||||
color: #9aa4b2;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: #161a22;
|
||||
border: 1px solid #222938;
|
||||
border-radius: 12px;
|
||||
padding: 18px;
|
||||
margin-bottom: 18px;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.row {
|
||||
display: grid;
|
||||
grid-template-columns: 120px 1fr;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.label {
|
||||
color: #9aa4b2;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.input {
|
||||
padding: 10px 12px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #2a3346;
|
||||
background: #0f131b;
|
||||
color: #e8ecf1;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
background: #2563eb;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
background: #1d4ed8;
|
||||
}
|
||||
|
||||
.log {
|
||||
background: #0b0e14;
|
||||
border: 1px solid #1e2533;
|
||||
border-radius: 8px;
|
||||
padding: 14px;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Sample C++ Plugin</title>
|
||||
<link rel="stylesheet" href="/plugins/sample-cpp/assets/sample-cpp.css" />
|
||||
</head>
|
||||
<body>
|
||||
<main class="container">
|
||||
<header class="header">
|
||||
<h1>Sample C++ Plugin</h1>
|
||||
<p class="sub">C++ — fully unloadable, zero runtime leak</p>
|
||||
</header>
|
||||
|
||||
<section class="card">
|
||||
<div class="row">
|
||||
<label class="label">Client ID</label>
|
||||
<input id="client-id" class="input" placeholder="clientId" />
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<label class="label">Message</label>
|
||||
<input id="message" class="input" placeholder="hello from UI" />
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<button id="send-btn" class="btn">Send event to client</button>
|
||||
<button id="ping-btn" class="btn">Ping</button>
|
||||
<button id="stats-btn" class="btn">Get Stats</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2>Log</h2>
|
||||
<pre id="log" class="log"></pre>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<script src="/plugins/sample-cpp/assets/sample-cpp.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,47 @@
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const clientIdInput = document.getElementById("client-id");
|
||||
const messageInput = document.getElementById("message");
|
||||
const logEl = document.getElementById("log");
|
||||
const sendBtn = document.getElementById("send-btn");
|
||||
const pingBtn = document.getElementById("ping-btn");
|
||||
const statsBtn = document.getElementById("stats-btn");
|
||||
|
||||
const pluginId = "sample-cpp";
|
||||
clientIdInput.value = params.get("clientId") || "";
|
||||
messageInput.value = "hello from sample-cpp UI";
|
||||
|
||||
function log(line) {
|
||||
const ts = new Date().toISOString();
|
||||
logEl.textContent = `${ts} ${line}\n` + logEl.textContent;
|
||||
}
|
||||
|
||||
async function sendEvent(event, payload) {
|
||||
const clientId = clientIdInput.value.trim();
|
||||
if (!clientId) { log("Missing clientId"); return; }
|
||||
const res = await fetch(`/api/clients/${encodeURIComponent(clientId)}/plugins/${pluginId}/event`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ event, payload }),
|
||||
});
|
||||
if (!res.ok) { log(`Send failed: ${res.status} ${await res.text()}`); return; }
|
||||
log(`Sent event: ${event}`);
|
||||
}
|
||||
|
||||
sendBtn.addEventListener("click", () => {
|
||||
sendEvent("ui_message", { message: messageInput.value.trim() });
|
||||
});
|
||||
|
||||
pingBtn.addEventListener("click", () => {
|
||||
sendEvent("ping", {});
|
||||
});
|
||||
|
||||
statsBtn.addEventListener("click", () => {
|
||||
sendEvent("stats", {});
|
||||
});
|
||||
|
||||
if (typeof EventSource !== "undefined") {
|
||||
const clientId = params.get("clientId");
|
||||
if (clientId) {
|
||||
log("Ready — C++ plugin (fully unloadable)");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user