277 lines
7.9 KiB
Rust
277 lines
7.9 KiB
Rust
/*
|
|
* 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
|
|
}
|