-
Notifications
You must be signed in to change notification settings - Fork 52
Description
Hi, I'm working on a plugin for handling async system events in Neovim, see: https://git.sr.ht/~anri/system_events.nvim.
While nvim-oxi
is a really cool project and I could not recommend it more for building these sort of plugins, I find the current API allows a particularly annoying footgun: when using libuv, AsyncHandle
must be created from the main thread, otherwise Neovim panics.
My mind knows about and totally understands this contract, sure, but when refactoring the plugin above sometimes I accidentally do stupid stuff (like creating an handle in another thread) --- with the current API the compiler happily allows it because it doesn't understand the underlying contract, but then Neovim crashes with no chance to recover.
I've resorted to this sort of hack:
static MAIN_THREAD_ID: OnceLock<ThreadId> = OnceLock::new();
struct Uninitialized;
struct Initialized;
struct Usable;
// Empty type which is !Send and !Sync
// The Usable state signals that the code was called from the main thread
struct MainThreadToken<State = Usable> { /* ... */ }
impl MainThreadToken<Uninitialized> {
// *Unsafe* function which inits the static above
// and returns a MainThreadToken<Initialized>
}
impl MainThreadToken<Initialized> {
// Function which checks that
// std::thread::current().id() == MAIN_THREAD_ID
// and, if so, returns Ok(MainThreadToken<Usable>) otherwise Err(...)
}
// In another file that wraps AsyncHandle + UnboundedReceiver
struct NvimChannel<T> { /* ... */ }
impl NvimChannel<T> {
fn new(_token: MainThreadToken<Usable>, f: impl Fn(...) -> ...) -> Self { /* ... */ }
}
This way, it's impossible to instantiate the Handle without initializing it in the main thread. If the conversion between the Initialized -> Usable states fails, then it's at the very least possible to recover for a crash, alert the user and continue working.
While this is pretty much an opinionated setup, a more library-friendly way to do it would be to expose a marker trait that the user can implement, and provide an optional module/feature/crate that does the above for streamlining it. Or just make the function AsyncHandle::new
unsafe.
Obviously, if you don't think there is any need for this idea then you're free to close the issue.