#![doc(html_no_source)]
#![warn(missing_docs)]
use kevy_lua::{Bridge, FlushMode, Reply, ScriptSha1};
use std::cell::Cell;
use std::marker::PhantomData;
#[doc(hidden)]
pub type CurrentTag = usize;
thread_local! {
static CURRENT: Cell<CurrentTag> = const { Cell::new(0) };
}
struct ResetCurrent {
prev: CurrentTag,
}
impl Drop for ResetCurrent {
fn drop(&mut self) {
CURRENT.with(|c| c.set(self.prev));
}
}
fn set_current<T>(ctx: &mut T) -> ResetCurrent {
let new_addr = ctx as *mut T as usize;
let prev = CURRENT.with(|c| {
let p = c.get();
c.set(new_addr);
p
});
ResetCurrent { prev }
}
thread_local! {
static THREAD_HOST: std::cell::RefCell<Option<Box<dyn std::any::Any>>> =
const { std::cell::RefCell::new(None) };
}
pub fn with_thread_host<T: 'static, R>(
build: impl FnOnce() -> LuaHost<T>,
f: impl FnOnce(&mut LuaHost<T>) -> R,
) -> Option<R> {
THREAD_HOST.with(|slot| {
let mut g = slot.try_borrow_mut().ok()?;
if !g.as_ref().is_some_and(|b| b.is::<LuaHost<T>>()) {
*g = Some(Box::new(build()));
}
let host = g
.as_mut()
.expect("slot filled above")
.downcast_mut::<LuaHost<T>>()
.expect("type matched or rebuilt above");
Some(f(host))
})
}
pub fn with_current<T: 'static, R>(f: impl FnOnce(&mut T) -> R) -> Option<R> {
let addr = CURRENT.with(Cell::get);
if addr == 0 {
return None;
}
let r = unsafe { &mut *(addr as *mut T) };
Some(f(r))
}
pub struct LuaHost<T: 'static> {
bridge: Bridge,
_marker: PhantomData<fn() -> T>,
}
impl<T: 'static> LuaHost<T> {
pub fn new<F>(dispatch_fn: F) -> Self
where
F: Fn(&mut T, &[&[u8]], bool) -> Vec<u8> + 'static,
{
let bridge = Bridge::new(move |argv, ro| {
with_current::<T, _>(|t| dispatch_fn(t, argv, ro))
.unwrap_or_else(|| {
b"-ERR kevy-lua-host: dispatch called outside an active eval scope\r\n"
.to_vec()
})
});
LuaHost {
bridge,
_marker: PhantomData,
}
}
pub fn eval(
&mut self,
ctx: &mut T,
script: &[u8],
keys: &[&[u8]],
args: &[&[u8]],
) -> Reply {
let _guard = set_current(ctx);
self.bridge.eval(script, keys, args)
}
pub fn eval_ro(
&mut self,
ctx: &mut T,
script: &[u8],
keys: &[&[u8]],
args: &[&[u8]],
) -> Reply {
let _guard = set_current(ctx);
self.bridge.eval_ro(script, keys, args)
}
pub fn evalsha(
&mut self,
ctx: &mut T,
sha1: ScriptSha1,
keys: &[&[u8]],
args: &[&[u8]],
) -> Reply {
let _guard = set_current(ctx);
self.bridge.evalsha(sha1, keys, args)
}
pub fn evalsha_ro(
&mut self,
ctx: &mut T,
sha1: ScriptSha1,
keys: &[&[u8]],
args: &[&[u8]],
) -> Reply {
let _guard = set_current(ctx);
self.bridge.evalsha_ro(sha1, keys, args)
}
pub fn script_load(&mut self, script: &[u8]) -> ScriptSha1 {
self.bridge.script_load(script)
}
#[must_use]
pub fn script_exists(&self, sha1s: &[ScriptSha1]) -> Vec<bool> {
self.bridge.script_exists(sha1s)
}
pub fn script_flush(&mut self, mode: FlushMode) {
self.bridge.script_flush(mode);
}
pub fn set_instr_budget(&mut self, n: i64) {
self.bridge.set_instr_budget(n);
}
pub fn set_allowed_dialects(&mut self, versions: &[kevy_lua::LuaVersion]) {
self.bridge.set_allowed_dialects(versions);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Default)]
struct ToyStore {
kv: std::collections::HashMap<Vec<u8>, Vec<u8>>,
calls_seen: u32,
}
impl ToyStore {
fn run(&mut self, argv: &[&[u8]], read_only: bool) -> Vec<u8> {
self.calls_seen += 1;
if argv.is_empty() {
return b"-ERR no command\r\n".to_vec();
}
let cmd: Vec<u8> = argv[0].iter().map(|b| b.to_ascii_uppercase()).collect();
let is_write = matches!(cmd.as_slice(), b"SET" | b"DEL");
if read_only && is_write {
return b"-READONLY can't write against a read-only script\r\n".to_vec();
}
match cmd.as_slice() {
b"SET" => {
self.kv.insert(argv[1].to_vec(), argv[2].to_vec());
b"+OK\r\n".to_vec()
}
b"GET" => match self.kv.get(argv[1]) {
Some(v) => {
let mut out = format!("${}\r\n", v.len()).into_bytes();
out.extend_from_slice(v);
out.extend_from_slice(b"\r\n");
out
}
None => b"$-1\r\n".to_vec(),
},
b"DEL" => {
let n = self.kv.remove(argv[1]).is_some() as i64;
format!(":{n}\r\n").into_bytes()
}
_ => b"-ERR unknown\r\n".to_vec(),
}
}
}
fn make_host() -> LuaHost<ToyStore> {
LuaHost::<ToyStore>::new(|store, argv, ro| store.run(argv, ro))
}
#[test]
fn eval_calls_dispatch_with_live_store() {
let mut host = make_host();
let mut store = ToyStore::default();
let reply = host.eval(
&mut store,
b"redis.call('SET', KEYS[1], ARGV[1])\n\
return redis.call('GET', KEYS[1])\n",
&[b"k"],
&[b"hello"],
);
assert_eq!(reply, b"$5\r\nhello\r\n");
assert_eq!(store.kv.get(b"k".as_slice()), Some(&b"hello".to_vec()));
assert_eq!(store.calls_seen, 2);
}
#[test]
fn eval_ro_blocks_writes() {
let mut host = make_host();
let mut store = ToyStore::default();
let reply = host.eval_ro(
&mut store,
b"return redis.call('SET', KEYS[1], 'v')",
&[b"k"],
&[],
);
assert!(reply.starts_with(b"-READONLY "));
assert!(!store.kv.contains_key(b"k".as_slice()));
}
#[test]
fn evalsha_round_trip() {
let mut host = make_host();
let mut store = ToyStore::default();
let sha = host.script_load(b"return redis.call('GET', KEYS[1])");
store.kv.insert(b"x".to_vec(), b"42".to_vec());
let reply = host.evalsha(&mut store, sha, &[b"x"], &[]);
assert_eq!(reply, b"$2\r\n42\r\n");
}
#[test]
fn dispatch_outside_scope_is_a_clear_error() {
let r = with_current::<ToyStore, _>(|_| 1);
assert!(r.is_none());
}
#[test]
fn pointer_is_cleared_after_eval_returns() {
let mut host = make_host();
let mut store = ToyStore::default();
let _ = host.eval(&mut store, b"return 1", &[], &[]);
let r = with_current::<ToyStore, _>(|_| 1);
assert!(r.is_none());
}
#[test]
fn nested_eval_calls_restore_outer_context() {
let sentinel_addr: usize = 0xdead_beef;
CURRENT.with(|c| c.set(sentinel_addr));
let mut host = make_host();
let mut store = ToyStore::default();
let _ = host.eval(&mut store, b"return 1", &[], &[]);
let restored = CURRENT.with(Cell::get);
assert_eq!(restored, sentinel_addr);
CURRENT.with(|c| c.set(0));
}
}
#[cfg(test)]
mod p7e_tests {
use super::*;
#[test]
fn instr_budget_trips_on_long_loop() {
let mut host = LuaHost::<()>::new(|_ctx, _argv, _ro| Vec::new());
host.set_instr_budget(100); let mut nothing = ();
let reply = host.eval(
&mut nothing,
b"local s = 0\nfor i = 1, 10000 do s = s + i end\nreturn s",
&[],
&[],
);
assert!(
reply.starts_with(b"-ERR "),
"expected -ERR budget reply, got: {:?}",
String::from_utf8_lossy(&reply)
);
}
#[test]
fn unlimited_budget_runs_to_completion() {
let mut host = LuaHost::<()>::new(|_ctx, _argv, _ro| Vec::new());
host.set_instr_budget(0); let mut nothing = ();
let reply = host.eval(
&mut nothing,
b"local s = 0\nfor i = 1, 10000 do s = s + i end\nreturn s",
&[],
&[],
);
assert_eq!(reply, b":50005000\r\n");
}
}