Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

(0.10-beta) send+module: invoking lua functions from another thread #443

Open
alemidev opened this issue Aug 28, 2024 · 2 comments
Open

Comments

@alemidev
Copy link

i'm developing a nvim plugin which is mostly a thin wrapper around a native library. such library works in background and wants to invoke callbacks to do arbitrary work on-demand for new events.

a minimal example of my use case:

# Cargo.toml
[package]
name = "example-mlua"
version = "0.1.0"
edition = "2021"

[lib]
name = "mluaexample"
crate-type = ["cdylib"]

[dependencies]
mlua = { version = "0.10.0-beta.1", features = ["luajit", "send", "module"] }
// lib.rs
use mlua::prelude::*;

#[mlua::lua_module]
fn libmluaexample(lua: &Lua) -> LuaResult<LuaTable> {
  let exports = lua.create_table()?;

  exports.set("callback", lua.create_function(|_, (cb,):(LuaFunction,)| {
    std::thread::spawn(move || {
      let mut count = 0;
      loop {
        std::thread::sleep(std::time::Duration::from_millis(200));
        if cb.call::<(String,),()>((format!("calling callback: {count} errors"),)).is_err() {
          count += 1;
        }
      }
    });
    Ok(())
  })?)?;

  Ok(exports)
}

in addition to this, nvim exposes a lua interface to libuv's async handle, which is safe to invoke across threads and will wake the main loop thread to run the wrapped function

-- your neovim init.lua

local native = require('libmluaexample')
local async = vim.loop.new_async(function() print("something") end)
native.callback(function() async:send() end)

with this setup the callback given to the foreign thread should only interact with the async handle, which should be threadsafe. however after requiring the native module neovim will consistently crash after not many invocations.


since this is the new beta built this may be a temporary or already known bug, so feel free to close.

also i'm not sure this is supported use at all, i see with the send feature enabled lua values hold a mutexed weak ref, but it's not clear to me how this mutex is handled when mlua is a module

if my usage is unsupported, is it possible to clear the callback environment or run it in another context to call it safely? is it possible to lock the lua state and allow callbacks to execute safely from other threads?

anyhow, thanks for the wonderful library and your time, looking forward to mlua 0.10!

@khvzak
Copy link
Member

khvzak commented Aug 30, 2024

Unfortunately send feature flag is not compatible with module mode.
In module mode mlua does not control access to the Lua VM and cannot guarantee that lock is always acquired before accessing lua api.

One of possible solution can be using channels for communicating between threads and invoking callback when a value is present in the channel (checking periodically).

@alemidev
Copy link
Author

alemidev commented Sep 10, 2024

Thanks for your reply, sorry for taking a while to get back at you!

I think it could be possible to handle the state mutex "the other way around": acquiring it when leaving module code and releasing it as soon as we enter module code (basically as first step of every mlua function). This would still mean that the main Lua thread needs to yield from time to time but it can either be done manually or periodically with a set_hook() invocation. I may try to do this sooner or later!


In the meantime I'm doing as you suggest: keeping a global static channel where i send my LuaFunctions and exposing a poll_callback() function to Lua, which returns an Option<LuaFunction> to invoke. I'm struggling to pass function arguments, as IntoLuaMulti is Sized and can't be dynamically dispatched, but I think a solution is possible.

However, since beta.2 released, this workaround doesn't compile anymore: LuaFunctions need to be Send+Sync to be passed on the channel, but send and module are incompatible. I think my use case is safe: the callbacks are only executed by Lua main thread, and so will never run concurrently or after Lua ended, but beta.2 won't allow (due to Cargo resolution order beta.2 is picked over beta.1, also breaking projects relying on beta.1 and send+module).

You can check our project for context: build with --features=lua54, relevant mlua file is here

Would you be open to either drop this restriction or allow something like unsafe-send feature which is compatible with module?

@alemidev alemidev changed the title (0.10-beta+send) segfault calling LuaFunction from another thread (0.10-beta) send+module: invoking lua functions from another thread Sep 27, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants