use std::future::Future;
use std::pin::Pin;
use std::rc::Rc;
use crate::{
Chunk, Error, Function, FromLuaMulti, IntoLuaMulti, Lua, LuaError, LuaString, Result, Value,
Variadic,
};
pub(crate) type AsyncCallback =
Rc<dyn Fn(Lua, Vec<Value>) -> Pin<Box<dyn Future<Output = Result<Vec<Value>>>>>>;
pub(crate) struct AsyncEntry {
id: Box<u8>,
callback: AsyncCallback,
}
const ASYNC_FACTORY_SRC: &[u8] =
b"local token, yield = ...\nreturn function(...) return yield(token, ...) end\n";
const ASYNC_TRAMPOLINE_SRC: &[u8] = b"local target = ...\nreturn function(...) return target(...) end\n";
fn token_value(entry: &AsyncEntry) -> Value {
Value::LightUserData(token_addr(entry) as *mut core::ffi::c_void)
}
fn token_addr(entry: &AsyncEntry) -> usize {
&*entry.id as *const u8 as usize
}
fn async_primitive_key(name: &str) -> Vec<u8> {
let mut key = b"\0omnilua.async.".to_vec();
key.extend_from_slice(name.as_bytes());
key
}
impl Lua {
pub fn create_async_function<A, R, F, Fut>(&self, f: F) -> Result<Function>
where
A: FromLuaMulti + 'static,
R: IntoLuaMulti + 'static,
F: Fn(Lua, A) -> Fut + 'static,
Fut: Future<Output = Result<R>> + 'static,
{
let callback: AsyncCallback =
Rc::new(move |lua, raw_args| match A::from_lua_multi(raw_args, &lua) {
Err(e) => Box::pin(async move { Err(e) }),
Ok(args) => {
let fut = f(lua.clone(), args);
Box::pin(async move {
let result = fut.await?;
result.into_lua_multi(&lua)
})
}
});
let entry = AsyncEntry { id: Box::new(0u8), callback };
let token = token_value(&entry);
self.inner.async_registry.borrow_mut().push(entry);
let genuine_yield = self.async_coroutine_primitive("yield")?;
let factory = self.load(ASYNC_FACTORY_SRC).into_function()?;
factory.call((token, genuine_yield))
}
pub(crate) fn capture_async_coroutine_primitives(&self) {
for name in ["create", "resume", "status", "yield"] {
if let Ok(func) = self.coroutine_builtin(name) {
let _stored = self.set_named_registry_value(async_primitive_key(name), func);
}
}
}
fn async_coroutine_primitive(&self, name: &str) -> Result<Function> {
self.named_registry_value(async_primitive_key(name))
}
fn async_callback_for(&self, addr: usize) -> Option<AsyncCallback> {
let registry = self.inner.async_registry.borrow();
registry
.iter()
.find(|entry| token_addr(entry) == addr)
.map(|entry| entry.callback.clone())
}
}
impl Function {
pub fn call_async<A, R>(&self, args: A) -> impl Future<Output = Result<R>>
where
A: IntoLuaMulti + 'static,
R: FromLuaMulti + 'static,
{
let func = self.clone();
async move {
let lua = func.root.lua.clone();
drive_async(lua, func, args).await
}
}
}
impl Chunk {
pub fn eval_async<R>(self) -> impl Future<Output = Result<R>>
where
R: FromLuaMulti + 'static,
{
let lua = self.lua.clone();
let compiled = self.into_function();
async move { drive_async(lua, compiled?, ()).await }
}
pub fn exec_async(self) -> impl Future<Output = Result<()>> {
self.eval_async::<()>()
}
}
fn drive_async<A, R>(lua: Lua, func: Function, args: A) -> impl Future<Output = Result<R>>
where
A: IntoLuaMulti + 'static,
R: FromLuaMulti + 'static,
{
async move {
let create = lua.async_coroutine_primitive("create")?;
let resume = lua.async_coroutine_primitive("resume")?;
let status = lua.async_coroutine_primitive("status")?;
let trampoline = lua.load(ASYNC_TRAMPOLINE_SRC).into_function()?;
let wrapped: Function = trampoline.call(Value::Function(func))?;
let co: Value = create.call(Value::Function(wrapped))?;
let mut next: Vec<Value> = args.into_lua_multi(&lua)?;
loop {
let mut call_args = Vec::with_capacity(next.len() + 1);
call_args.push(co.clone());
call_args.append(&mut next);
let mut returns = resume
.call::<_, Variadic<Value>>(Variadic::from(call_args))?
.into_vec();
let ok = match returns.first() {
Some(Value::Boolean(b)) => *b,
_ => {
return Err(Error::from(LuaError::runtime(format_args!(
"coroutine.resume did not return a status boolean"
))))
}
};
returns.remove(0);
if !ok {
let err_val = returns.into_iter().next().unwrap_or(Value::Nil);
let captured: Result<Error> = lua.with_state(|state| {
let raw = err_val.to_raw_for_lua(&lua, state)?;
Ok(lua.capture_error_in_state(state, LuaError::from_value(raw)))
});
return Err(match captured {
Ok(error) => error,
Err(error) => error,
});
}
let state_name: LuaString = status.call(co.clone())?;
match state_name.as_bytes()?.as_slice() {
b"dead" => return R::from_lua_multi(returns, &lua),
b"suspended" => {
let addr = match returns.first() {
Some(Value::LightUserData(p)) => *p as usize,
_ => {
return Err(Error::from(LuaError::runtime(format_args!(
"call_async target performed a plain coroutine.yield; only async \
functions may suspend across an async driver"
))))
}
};
let callback = lua.async_callback_for(addr).ok_or_else(|| {
Error::from(LuaError::runtime(format_args!(
"coroutine yielded a value that is not a known async token"
)))
})?;
returns.remove(0);
next = callback(lua.clone(), returns).await?;
}
other => {
return Err(Error::from(LuaError::runtime(format_args!(
"async coroutine reported an unexpected status: {}",
String::from_utf8_lossy(other)
))))
}
}
}
}
}