mlua icon indicating copy to clipboard operation
mlua copied to clipboard

Wrong garbage collection local variable

Open rise0chen opened this issue 1 year ago • 6 comments

use core::time::Duration;
use futures_util::future::select_all;
use mlua::prelude::*;

async fn sleep(_lua: Lua, ms: u64) -> LuaResult<()> {
    tokio::time::sleep(Duration::from_millis(ms)).await;
    Ok(())
}
async fn select(_lua: Lua, threads: LuaMultiValue) -> LuaResult<(LuaValue, i64)> {
    let futs: Vec<LuaAsyncThread<_, LuaValue>> = threads
        .into_iter()
        .filter_map(|thread| {
            if let LuaValue::Thread(thread) = thread {
                Some(thread.into_async(()))
            } else {
                None
            }
        })
        .collect();
    if futs.is_empty() {
        return Ok((LuaValue::Nil, -1));
    }
    let (val, index, _threads) = select_all(futs).await;
    Ok((val?, index as i64 + 1))
}

#[tokio::main(flavor = "current_thread")]
async fn main() -> LuaResult<()> {
    let lua = Lua::new();
    lua.globals().set("sleep", lua.create_async_function(sleep)?)?;
    lua.globals()
        .set("select_all", lua.create_async_function(select)?)?;
    lua.set_hook(LuaHookTriggers::EVERY_LINE, move |_lua, debug| {
        let source = debug.source().source.unwrap_or(std::borrow::Cow::Borrowed("~"));
        let is_rust = source.len() < 2;
        // println!("lua hook({}): {}", is_rust, source);
        if is_rust {
            Ok(LuaVmState::Continue)
        } else {
            Ok(LuaVmState::Yield)
        }
    });

    let f = lua
        .load(
            r#"
            function f_test()
                while true
                do
                    local d = {0,0,0,0,0,0}
                    d[1] = d[1] + 1
                    d[2] = d[2] + 2
                    d[3] = d[3] + 3
                    d[4] = d[4] + 4
                    d[5] = d[5] + 5
                    d[6] = d[6] + 6
            
                    sleep(5)
                end
            end

            function f_gc()
                while true
                do
                    sleep(50)
                    collectgarbage("collect")
                end
            end

            ret, index = select_all(coroutine.create(f_test), coroutine.create(f_gc))
            print(ret, index)
        "#,
        )
        .into_function()?;
    let thread = lua.create_thread(f)?;
    thread.into_async(()).await?;

    Ok(())
}
cargo run --release --features=lua54,vendored,async --example b 
   Compiling mlua v0.10.2
    Finished `release` profile [optimized] target(s) in 8.11s
     Running `target/release/examples/b`
Error: CallbackError { traceback: "stack traceback:\n\t[C]: in local 'poll'\n\t[string \"?\"]:4: in function 'select_all'\n\t[string \"examples/b.rs:45:10\"]:25: in main chunk", cause: RuntimeError("[string \"examples/b.rs:45:10\"]:6: attempt to index a nil value (local 'd')\nstack traceback:\n\t[string \"examples/b.rs:45:10\"]:6: in function 'f_test'") }

It throw an error: attempt to index a nil value (local 'd')

I think this issue is related to set_hook.

rise0chen avatar Jan 17 '25 03:01 rise0chen

I'm not getting any error, the program runs without printing anything

khvzak avatar Jan 17 '25 11:01 khvzak

Because of I use this version https://github.com/mlua-rs/mlua/pull/498. It set a global hook.

I modify code that set hook for every thread manually:

use core::time::Duration;
use futures_util::future::select_all;
use mlua::prelude::*;

fn global_hook(_lua: &Lua, debug: mlua::Debug) -> LuaResult<LuaVmState> {
    let source = debug
        .source()
        .source
        .unwrap_or(std::borrow::Cow::Borrowed("~"));
    let is_rust = source.len() < 2;
    println!("lua hook({}): {}", is_rust, source);
    if is_rust {
        Ok(LuaVmState::Continue)
    } else {
        Ok(LuaVmState::Yield)
    }
}

async fn sleep(_lua: Lua, ms: u64) -> LuaResult<()> {
    tokio::time::sleep(Duration::from_millis(ms)).await;
    Ok(())
}
async fn select(_lua: Lua, threads: LuaMultiValue) -> LuaResult<(LuaValue, i64)> {
    let futs: Vec<LuaAsyncThread<_, LuaValue>> = threads
        .into_iter()
        .filter_map(|thread| {
            if let LuaValue::Thread(thread) = thread {
                thread.set_hook(LuaHookTriggers::EVERY_LINE, global_hook);
                Some(thread.into_async(()))
            } else {
                None
            }
        })
        .collect();
    if futs.is_empty() {
        return Ok((LuaValue::Nil, -1));
    }
    let (val, index, _threads) = select_all(futs).await;
    Ok((val?, index as i64 + 1))
}

#[tokio::main(flavor = "current_thread")]
async fn main() -> LuaResult<()> {
    let lua = Lua::new();
    lua.globals()
        .set("sleep", lua.create_async_function(sleep)?)?;
    lua.globals()
        .set("select_all", lua.create_async_function(select)?)?;

    let f = lua
        .load(
            r#"
            function f_test()
                while true
                do
                    local d = {0,0,0,0,0,0}
                    d[1] = d[1] + 1
                    d[2] = d[2] + 2
                    d[3] = d[3] + 3
                    d[4] = d[4] + 4
                    d[5] = d[5] + 5
                    d[6] = d[6] + 6
            
                    sleep(5)
                end
            end

            function f_gc()
                while true
                do
                    sleep(50)
                    collectgarbage("collect")
                end
            end

            local co_test = coroutine.create(f_test)
            debug.sethook(co_test, global_hook, "l", 1)
            local co_gc = coroutine.create(f_gc)
            debug.sethook(co_gc, global_hook, "l", 1)
            ret, index = select_all(co_test, co_gc)
            print(ret, index)
        "#,
        )
        .into_function()?;
    let thread = lua.create_thread(f)?;
    thread.set_hook(LuaHookTriggers::EVERY_LINE, global_hook);
    let _: () = thread.into_async(()).await?;

    Ok(())
}

rise0chen avatar Jan 18 '25 03:01 rise0chen

let lua = Lua::new();

must be let lua = unsafe { Lua::unsafe_new_with(LuaStdLib::ALL, LuaOptions::default()) }; since debug module is not enabled by default.

thread.set_hook(LuaHookTriggers::EVERY_LINE, global_hook);

You cannot have more than one hook overall, attaching hook to a new thread will disable hook on previous thread. So this code makes little sense.

Apart from that the program does not crash.

khvzak avatar Jan 19 '25 23:01 khvzak

let lua = Lua::new();

must be let lua = unsafe { Lua::unsafe_new_with(LuaStdLib::ALL, LuaOptions::default()) }; since debug module is not enabled by default.

thread.set_hook(LuaHookTriggers::EVERY_LINE, global_hook);

You cannot have more than one hook overall, attaching hook to a new thread will disable hook on previous thread. So this code makes little sense.

Apart from that the program does not crash.

Yes, you are right. But, I want to set a global hook for every thread. debug.sethook support more than one hook, but it can yield the current thread.

I close this issue, beacuse this bug is caused by multi hook https://github.com/mlua-rs/mlua/issues/489

rise0chen avatar Jan 20 '25 01:01 rise0chen

use core::time::Duration;
use futures_util::future::select_all;
use mlua::prelude::*;

fn global_hook_ok(_lua: &Lua, debug: mlua::Debug) -> LuaResult<LuaVmState> {
    let source = debug
        .source()
        .source
        .unwrap_or(std::borrow::Cow::Borrowed("~"));
    let is_rust = source.len() < 2;
    println!("lua hook({}): {}", is_rust, source);
    Ok(LuaVmState::Continue)
}
fn global_hook_not_resume(_lua: &Lua, debug: mlua::Debug) -> LuaResult<LuaVmState> {
    let source = debug
        .source()
        .source
        .unwrap_or(std::borrow::Cow::Borrowed("~"));
    let is_rust = source.len() < 2;
    println!("lua hook({}): {}", is_rust, source);
    Ok(LuaVmState::Yield)
}
fn global_hook_error_gc(_lua: &Lua, debug: mlua::Debug) -> LuaResult<LuaVmState> {
    let source = debug
        .source()
        .source
        .unwrap_or(std::borrow::Cow::Borrowed("~"));
    let is_rust = source.len() < 2;
    println!("lua hook({}): {}", is_rust, source);
    if is_rust {
        Ok(LuaVmState::Continue)
    } else {
        Ok(LuaVmState::Yield)
    }
}

async fn sleep(_lua: Lua, ms: u64) -> LuaResult<()> {
    tokio::time::sleep(Duration::from_millis(ms)).await;
    Ok(())
}
async fn select(_lua: Lua, threads: LuaMultiValue) -> LuaResult<(LuaValue, i64)> {
    let futs: Vec<LuaAsyncThread<LuaValue>> = threads
        .into_iter()
        .filter_map(|thread| {
            if let LuaValue::Thread(thread) = thread {
                thread.into_async(()).ok()
            } else {
                None
            }
        })
        .collect();
    if futs.is_empty() {
        return Ok((LuaValue::Nil, -1));
    }
    let (val, index, _threads) = select_all(futs).await;
    Ok((val?, index as i64 + 1))
}

#[tokio::main(flavor = "current_thread")]
async fn main() -> LuaResult<()> {
    let lua = Lua::new();
    lua.globals()
        .set("sleep", lua.create_async_function(sleep)?)?;
    lua.globals()
        .set("select_all", lua.create_async_function(select)?)?;
    //lua.set_global_hook(LuaHookTriggers::EVERY_LINE, global_hook_ok)?;
    //lua.set_global_hook(LuaHookTriggers::EVERY_LINE, global_hook_not_resume)?;
    lua.set_global_hook(LuaHookTriggers::EVERY_LINE, global_hook_error_gc)?;

    let f = lua
        .load(
            r#"
            function f_test()
                while true
                do
                    local d = {0,0,0,0,0,0}
                    d[1] = d[1] + 1
                    d[2] = d[2] + 2
                    d[3] = d[3] + 3
                    d[4] = d[4] + 4
                    d[5] = d[5] + 5
                    d[6] = d[6] + 6
            
                    sleep(5)
                end
            end

            function f_gc()
                while true
                do
                    sleep(50)
                    collectgarbage("collect")
                end
            end

            local co_test = coroutine.create(f_test)
            local co_gc = coroutine.create(f_gc)
            ret, index = select_all(co_test, co_gc)
            print(ret, index)
        "#,
        )
        .into_function()?;
    let thread = lua.create_thread(f)?;
    let _: () = thread.into_async(())?.await?;

    Ok(())
}

Reopen this issue. It is cased by global_hook and LuaVmState::Yield. Maybe we should not use LuaVmState::Yield in global_hook.

rise0chen avatar Feb 10 '25 12:02 rise0chen

On first sight, it looks like a bug in Lua itself. It should never collect locals. I need deep investigation.

khvzak avatar Feb 10 '25 18:02 khvzak