Files
overlord/plugins/sample-rust/native/src/lib.rs
T

277 lines
7.9 KiB
Rust
Raw Normal View History

/*
* 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
}