Using neovim/libuv's AsyncHandle from other threads #586
-
I'm writing a neovim plugin that loads an mlua module. The plugin requires a tokio threadpool, so I need some way to get around the single-threaded nature of lua. My idea was to use neovim's async primitives, specifically a libuv async handle. The handle is a userdata function can be called from any thread to wake up the main neovim thread. My approach looks basically like this: // Create a channel that will accumulate events until they can be consumed by the main thread
let (event_tx, event_rx) = tokio::sync::mpsc::unbounded_channel();
// Get a function that can create a uv_async_t for us
let create_handle = lua
.load(
r#"
function(cb)
return vim.uv.new_async(function()
vim.schedule(cb)
end)
end
"#,
)
.eval::<Function>()?;
let cb = lua.create_function(move |lua, ()| {
// Just print out events for now
let print = lua.globals().get::<Function>("print")?;
// Inside the callback, we'll consume all events
while let Ok(event) = event_rx.try_recv() {
print.call::<()>("received an event!")?;
}
Ok(())
})?;
// Create the handle
let handle = create_handle.call::<AnyUserData>(cb)?;
// Sending events:
event_tx.send(...);
// Notify the async handle
handle.call_method::<()>("send", ())?; This works fine until I call send on the handle from another thread. After looking into the mlua source, I think I've realized the error here: While My question is: Is there any way to call the handle safely from another thread? Could I somehow call it without having to lock the lua runtime? Can I "extract" the |
Beta Was this translation helpful? Give feedback.
Replies: 1 comment
-
Ok, so this is slightly insane, but the following works perfectly: use std::ffi::{CString, c_int, c_void};
use mlua::{AnyUserData, Function, Lua, Result};
use crate::lua_error;
/// A libuv [`uv_async_t`][1] handle that can be used to wake up the main thread and call a function.
/// The passed callback is automatically wrapped in `vim.schedule`.
///
/// [1]: https://docs.libuv.org/en/v1.x/async.html
#[derive(Clone)]
pub struct AsyncHandle(*mut c_void, AnyUserData);
// The `uv_async_t` is safe to be called from any thread.
// However, the AnyUserData reference must be called from the main thread, so all functions that
// act on it should accept a Lua handle and use it to call the function, since those can only be
// accquired on the main thread.
unsafe impl Send for AsyncHandle {}
// We don't need to get any information out of the handle, so we treat it as opaque
unsafe extern "C" {
fn uv_async_send(uv_async_t: *mut c_void) -> c_int;
}
const CREATE_ASYNC_HANDLE: &str = r#"
function(cb)
return vim.uv.new_async(function()
vim.schedule(cb)
end)
end
"#;
const CLOSE_ASYNC_HANDLE: &str = r#"
function(handle)
handle:close()
end
"#;
impl AsyncHandle {
pub fn new<F>(lua: &Lua, cb: F) -> Result<Self>
where
F: Fn(&Lua, ()) -> Result<()> + 'static,
{
let cb = lua.create_function(cb)?;
let create_handle = lua.load(CREATE_ASYNC_HANDLE).eval::<Function>()?;
let handle = create_handle.call::<AnyUserData>(cb)?;
let typename = CString::new("uv_async").expect("create cstring");
let mut uv_async: *mut c_void = std::ptr::null_mut();
unsafe {
lua.exec_raw::<()>(&handle, |lstate| {
// The arg should always be at 1, but let's be safe
let index = mlua::ffi::lua_gettop(lstate);
// Make sure the userdata is actually `uv_async`
let ud = mlua::ffi::luaL_checkudata(lstate, index, typename.as_ptr());
if ud.is_null() {
return;
}
// We get a pointer the userdata, which is in turn a pointer to `uv_async_t`.
uv_async = *(ud as *mut *mut c_void);
})?;
}
if uv_async.is_null() {
lua_error!("failed to get uv_async handle");
}
Ok(AsyncHandle(uv_async, handle))
}
pub fn send(&self) -> Result<()> {
let result = unsafe { uv_async_send(self.0) };
if result < 0 {
lua_error!("uv_async_send failed");
}
Ok(())
}
pub fn close(self, lua: &Lua) -> Result<()> {
let close_handle = lua.load(CLOSE_ASYNC_HANDLE).eval::<Function>()?;
close_handle.call::<()>(self.1)?;
Ok(())
}
} Still interested in any feedback or other solutions! |
Beta Was this translation helpful? Give feedback.
Ok, so this is slightly insane, but the following works perfectly: