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,12 @@
|
||||
[package]
|
||||
name = "sample-rust"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[profile.release]
|
||||
opt-level = "z"
|
||||
lto = true
|
||||
strip = true
|
||||
@@ -0,0 +1,276 @@
|
||||
/*
|
||||
* Sample Rust Plugin — fully unloadable, no GC runtime.
|
||||
*
|
||||
* Exports the standard Overlord plugin ABI:
|
||||
* PluginOnLoad, PluginOnEvent, PluginOnUnload, PluginSetCallback,
|
||||
* PluginGetRuntime (returns "rust" — tells the host this DLL can be freed)
|
||||
*
|
||||
* Build:
|
||||
* cargo build --release
|
||||
* Output: target/release/sample_rust.dll (Windows) or libsample_rust.so (Linux)
|
||||
*
|
||||
* Note: Uses C-style globals instead of std::sync::Mutex because the DLL is
|
||||
* loaded via an in-memory PE loader, and Rust's std Mutex (SRWLOCK) may not
|
||||
* initialize correctly without OS-level LoadLibrary. The Go host already
|
||||
* serializes all calls through its own mutex, so this is safe.
|
||||
*/
|
||||
|
||||
use std::os::raw::{c_char, c_int};
|
||||
use std::slice;
|
||||
use std::ptr;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Host callback types — platform-specific calling conventions
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[cfg(windows)]
|
||||
type HostCallback = unsafe extern "stdcall" fn(
|
||||
event: *const u8,
|
||||
event_len: usize,
|
||||
payload: *const u8,
|
||||
payload_len: usize,
|
||||
);
|
||||
|
||||
#[cfg(not(windows))]
|
||||
type HostCallback = unsafe extern "C" fn(
|
||||
ctx: usize,
|
||||
event: *const u8,
|
||||
event_len: i32,
|
||||
payload: *const u8,
|
||||
payload_len: i32,
|
||||
);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Global state — C-style, no Mutex.
|
||||
// The Go host serializes all calls via dllPlugin.mu / soPlugin.mu.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static mut G_CALLBACK: Option<HostCallback> = None;
|
||||
#[cfg(not(windows))]
|
||||
static mut G_CALLBACK_CTX: usize = 0;
|
||||
static mut G_CLIENT_ID: [u8; 256] = [0u8; 256];
|
||||
static mut G_CLIENT_ID_LEN: usize = 0;
|
||||
static mut G_EVENT_COUNT: u64 = 0;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Internal helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
unsafe fn send_event(event: &[u8], payload: &[u8]) {
|
||||
if let Some(cb) = G_CALLBACK {
|
||||
#[cfg(windows)]
|
||||
cb(
|
||||
event.as_ptr(),
|
||||
event.len(),
|
||||
payload.as_ptr(),
|
||||
payload.len(),
|
||||
);
|
||||
#[cfg(not(windows))]
|
||||
cb(
|
||||
G_CALLBACK_CTX,
|
||||
event.as_ptr(),
|
||||
event.len() as i32,
|
||||
payload.as_ptr(),
|
||||
payload.len() as i32,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Minimal JSON string extraction by key — zero allocations.
|
||||
unsafe fn json_extract_into(json: &[u8], key: &[u8], out: &mut [u8]) -> usize {
|
||||
// Search for "key":" or "key": "
|
||||
let klen = key.len();
|
||||
if json.len() < klen + 4 {
|
||||
return 0;
|
||||
}
|
||||
let mut i = 0;
|
||||
while i + klen + 3 < json.len() {
|
||||
if json[i] == b'"'
|
||||
&& json[i + 1..i + 1 + klen] == *key
|
||||
&& json[i + 1 + klen] == b'"'
|
||||
{
|
||||
// Skip past "key"
|
||||
let mut j = i + 1 + klen + 1; // past closing quote
|
||||
// Skip : and optional space
|
||||
while j < json.len() && (json[j] == b':' || json[j] == b' ') {
|
||||
j += 1;
|
||||
}
|
||||
// Expect opening quote
|
||||
if j < json.len() && json[j] == b'"' {
|
||||
j += 1;
|
||||
let start = j;
|
||||
while j < json.len() && json[j] != b'"' {
|
||||
j += 1;
|
||||
}
|
||||
let vlen = j - start;
|
||||
let copy_len = if vlen < out.len() { vlen } else { out.len() - 1 };
|
||||
ptr::copy_nonoverlapping(json[start..].as_ptr(), out.as_mut_ptr(), copy_len);
|
||||
return copy_len;
|
||||
}
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
0
|
||||
}
|
||||
|
||||
unsafe fn bytes_to_slice<'a>(p: *const c_char, len: c_int) -> &'a [u8] {
|
||||
if p.is_null() || len <= 0 {
|
||||
return &[];
|
||||
}
|
||||
slice::from_raw_parts(p as *const u8, len as usize)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Exported ABI
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn PluginGetRuntime() -> *const c_char {
|
||||
b"rust\0".as_ptr() as *const c_char
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn PluginSetCallback(cb: u64) {
|
||||
G_CALLBACK = Some(std::mem::transmute(cb as usize));
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn PluginOnLoad(
|
||||
host_info: *const c_char,
|
||||
host_info_len: c_int,
|
||||
cb: u64,
|
||||
) -> c_int {
|
||||
G_CALLBACK = Some(std::mem::transmute(cb as usize));
|
||||
G_EVENT_COUNT = 0;
|
||||
|
||||
let info = bytes_to_slice(host_info, host_info_len);
|
||||
G_CLIENT_ID_LEN = json_extract_into(info, b"clientId", &mut G_CLIENT_ID);
|
||||
|
||||
// Build a small on-stack response and send
|
||||
send_event(
|
||||
b"ready",
|
||||
br#"{"message":"sample-rust plugin ready"}"#,
|
||||
);
|
||||
0
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn PluginOnLoad(
|
||||
host_info: *const c_char,
|
||||
host_info_len: c_int,
|
||||
cb: usize,
|
||||
ctx: usize,
|
||||
) -> c_int {
|
||||
G_CALLBACK = Some(std::mem::transmute(cb));
|
||||
G_CALLBACK_CTX = ctx;
|
||||
G_EVENT_COUNT = 0;
|
||||
|
||||
let info = bytes_to_slice(host_info, host_info_len);
|
||||
G_CLIENT_ID_LEN = json_extract_into(info, b"clientId", &mut G_CLIENT_ID);
|
||||
|
||||
send_event(
|
||||
b"ready",
|
||||
br#"{"message":"sample-rust plugin ready"}"#,
|
||||
);
|
||||
0
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn PluginOnEvent(
|
||||
event: *const c_char,
|
||||
event_len: c_int,
|
||||
payload: *const c_char,
|
||||
payload_len: c_int,
|
||||
) -> c_int {
|
||||
let ev = bytes_to_slice(event, event_len);
|
||||
let pl = bytes_to_slice(payload, payload_len);
|
||||
|
||||
G_EVENT_COUNT += 1;
|
||||
|
||||
if ev == b"ping" {
|
||||
send_event(b"pong", &[]);
|
||||
return 0;
|
||||
}
|
||||
|
||||
if ev == b"ui_message" {
|
||||
// Build echo response on the stack — limited to 512 bytes
|
||||
let prefix = br#"{"message":"echo from Rust: "#;
|
||||
let suffix = br#""}"#;
|
||||
let max_pl = 450;
|
||||
let pl_copy = if pl.len() < max_pl { pl.len() } else { max_pl };
|
||||
let total = prefix.len() + pl_copy + suffix.len();
|
||||
let mut buf = [0u8; 512];
|
||||
if total <= buf.len() {
|
||||
ptr::copy_nonoverlapping(prefix.as_ptr(), buf.as_mut_ptr(), prefix.len());
|
||||
ptr::copy_nonoverlapping(
|
||||
pl.as_ptr(),
|
||||
buf.as_mut_ptr().add(prefix.len()),
|
||||
pl_copy,
|
||||
);
|
||||
ptr::copy_nonoverlapping(
|
||||
suffix.as_ptr(),
|
||||
buf.as_mut_ptr().add(prefix.len() + pl_copy),
|
||||
suffix.len(),
|
||||
);
|
||||
send_event(b"echo", &buf[..total]);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
if ev == b"stats" {
|
||||
// Format event count into a stack buffer
|
||||
let mut buf = [0u8; 128];
|
||||
let prefix = br#"{"event_count":"#;
|
||||
let suffix = b"}";
|
||||
ptr::copy_nonoverlapping(prefix.as_ptr(), buf.as_mut_ptr(), prefix.len());
|
||||
let mut num_buf = [0u8; 20];
|
||||
let num_len = fmt_u64(G_EVENT_COUNT, &mut num_buf);
|
||||
ptr::copy_nonoverlapping(
|
||||
num_buf[20 - num_len..].as_ptr(),
|
||||
buf.as_mut_ptr().add(prefix.len()),
|
||||
num_len,
|
||||
);
|
||||
ptr::copy_nonoverlapping(
|
||||
suffix.as_ptr(),
|
||||
buf.as_mut_ptr().add(prefix.len() + num_len),
|
||||
suffix.len(),
|
||||
);
|
||||
let total = prefix.len() + num_len + suffix.len();
|
||||
send_event(b"stats_reply", &buf[..total]);
|
||||
return 0;
|
||||
}
|
||||
|
||||
0
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn PluginOnUnload() {
|
||||
G_CALLBACK = None;
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
G_CALLBACK_CTX = 0;
|
||||
}
|
||||
G_CLIENT_ID_LEN = 0;
|
||||
G_EVENT_COUNT = 0;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tiny u64-to-decimal without std::fmt (avoids allocator)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn fmt_u64(mut n: u64, buf: &mut [u8; 20]) -> usize {
|
||||
if n == 0 {
|
||||
buf[19] = b'0';
|
||||
return 1;
|
||||
}
|
||||
let mut i = 20;
|
||||
while n > 0 {
|
||||
i -= 1;
|
||||
buf[i] = b'0' + (n % 10) as u8;
|
||||
n /= 10;
|
||||
}
|
||||
20 - i
|
||||
}
|
||||
Reference in New Issue
Block a user