make c, cpp and rust plugins as well as their builders (thank you claude). Also add support for multi lang plugins so that we can actually fucking unload them unlike shitty golang (issue has been opened since 2015 and there is no due date for the issue...)
This commit is contained in:
@@ -0,0 +1,160 @@
|
||||
/*
|
||||
* Sample C Plugin — fully unloadable, no runtime baggage.
|
||||
*
|
||||
* Exports the standard Overlord plugin ABI:
|
||||
* PluginOnLoad, PluginOnEvent, PluginOnUnload, PluginSetCallback,
|
||||
* PluginGetRuntime (returns "c" — tells the host this DLL can be freed)
|
||||
*
|
||||
* Build (Windows):
|
||||
* cl /LD /O2 plugin.c /Fe:sample-c-windows-amd64.dll
|
||||
* — or with MinGW —
|
||||
* x86_64-w64-mingw32-gcc -shared -O2 -o sample-c-windows-amd64.dll plugin.c
|
||||
*
|
||||
* Build (Linux):
|
||||
* gcc -shared -fPIC -O2 -o sample-c-linux-amd64.so plugin.c
|
||||
*/
|
||||
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Platform helpers */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
#ifdef _WIN32
|
||||
# define EXPORT __declspec(dllexport)
|
||||
# define WIN32_LEAN_AND_MEAN
|
||||
# include <windows.h>
|
||||
BOOL WINAPI DllMain(HINSTANCE hDll, DWORD reason, LPVOID reserved) {
|
||||
(void)hDll; (void)reserved;
|
||||
switch (reason) {
|
||||
case DLL_PROCESS_ATTACH: break;
|
||||
case DLL_PROCESS_DETACH: break;
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
#else
|
||||
# define EXPORT __attribute__((visibility("default")))
|
||||
#endif
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Host callback */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
#ifdef _WIN32
|
||||
/* Windows: callback is void(__stdcall*)(ptr,len,ptr,len) */
|
||||
typedef void (__stdcall *host_callback_t)(
|
||||
const char *event, uintptr_t eventLen,
|
||||
const char *payload, uintptr_t payloadLen);
|
||||
#else
|
||||
/* Unix: callback is void(*)(ctx, event, eventLen, payload, payloadLen) */
|
||||
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 = NULL;
|
||||
#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 ? (int)strlen(event) : 0;
|
||||
int plen = payload ? (int)strlen(payload) : 0;
|
||||
#ifdef _WIN32
|
||||
g_callback(event, (uintptr_t)elen, payload, (uintptr_t)plen);
|
||||
#else
|
||||
g_callback(g_callback_ctx, event, elen, payload, plen);
|
||||
#endif
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Plugin state (trivial example) */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
static char g_client_id[256];
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Exported ABI */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
EXPORT const char *PluginGetRuntime(void) {
|
||||
return "c";
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
EXPORT void PluginSetCallback(uint64_t cb) {
|
||||
g_callback = (host_callback_t)(uintptr_t)cb;
|
||||
}
|
||||
|
||||
EXPORT int PluginOnLoad(const char *hostInfo, int hostInfoLen, uint64_t cb) {
|
||||
g_callback = (host_callback_t)(uintptr_t)cb;
|
||||
#else
|
||||
EXPORT int PluginOnLoad(const char *hostInfo, int hostInfoLen,
|
||||
uintptr_t cb, uintptr_t ctx) {
|
||||
g_callback = (host_callback_t)cb;
|
||||
g_callback_ctx = ctx;
|
||||
#endif
|
||||
/* Minimal JSON-ish parse to grab clientId — keeps the example dependency-free */
|
||||
g_client_id[0] = '\0';
|
||||
if (hostInfo && hostInfoLen > 0) {
|
||||
const char *key = "\"clientId\":\"";
|
||||
const char *p = strstr(hostInfo, key); /* safe: hostInfo is nul-terminated by the Go caller copying it */
|
||||
if (!p) {
|
||||
key = "\"clientId\": \"";
|
||||
p = strstr(hostInfo, key);
|
||||
}
|
||||
if (p) {
|
||||
p += strlen(key);
|
||||
const char *end = strchr(p, '"');
|
||||
if (end) {
|
||||
size_t n = (size_t)(end - p);
|
||||
if (n >= sizeof(g_client_id)) n = sizeof(g_client_id) - 1;
|
||||
memcpy(g_client_id, p, n);
|
||||
g_client_id[n] = '\0';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fprintf(stderr, "[sample-c] loaded, clientId=%s\n", g_client_id);
|
||||
send_event("ready", "{\"message\":\"sample-c plugin ready\"}");
|
||||
return 0;
|
||||
}
|
||||
|
||||
EXPORT int PluginOnEvent(const char *event, int eventLen,
|
||||
const char *payload, int payloadLen) {
|
||||
(void)payloadLen;
|
||||
|
||||
/* Simple echo for "ping" */
|
||||
if (eventLen == 4 && memcmp(event, "ping", 4) == 0) {
|
||||
send_event("pong", NULL);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Echo back ui_message */
|
||||
if (eventLen == 10 && memcmp(event, "ui_message", 10) == 0) {
|
||||
fprintf(stderr, "[sample-c] got ui_message: %.*s\n", payloadLen, payload ? payload : "");
|
||||
/* Build a tiny JSON response — payload is already JSON from the host */
|
||||
char buf[512];
|
||||
snprintf(buf, sizeof(buf), "{\"message\":\"echo from C: %.*s\"}",
|
||||
payloadLen < 400 ? payloadLen : 400, payload ? payload : "");
|
||||
send_event("echo", buf);
|
||||
return 0;
|
||||
}
|
||||
|
||||
fprintf(stderr, "[sample-c] unhandled event: %.*s\n", eventLen, event);
|
||||
return 0;
|
||||
}
|
||||
|
||||
EXPORT void PluginOnUnload(void) {
|
||||
fprintf(stderr, "[sample-c] unloading\n");
|
||||
g_callback = NULL;
|
||||
#ifndef _WIN32
|
||||
g_callback_ctx = 0;
|
||||
#endif
|
||||
g_client_id[0] = '\0';
|
||||
}
|
||||
@@ -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,41 @@
|
||||
<!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-c/assets/sample-c.css" />
|
||||
</head>
|
||||
<body>
|
||||
<main class="container">
|
||||
<header class="header">
|
||||
<h1>Sample C Plugin</h1>
|
||||
<p class="sub">Pure 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>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2>Log</h2>
|
||||
<pre id="log" class="log"></pre>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<script src="/plugins/sample-c/assets/sample-c.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,43 @@
|
||||
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 pluginId = "sample-c";
|
||||
clientIdInput.value = params.get("clientId") || "";
|
||||
messageInput.value = "hello from sample-c 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", {});
|
||||
});
|
||||
|
||||
// Listen for plugin events via SSE / polling (same pattern as Go sample)
|
||||
if (typeof EventSource !== "undefined") {
|
||||
const clientId = params.get("clientId");
|
||||
if (clientId) {
|
||||
log("Ready — C plugin (fully unloadable)");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user