mlua icon indicating copy to clipboard operation
mlua copied to clipboard

Implement `Into<&'a [u8]>` for `BorrowedBytes<'a>`

Open sxyazi opened this issue 1 month ago • 2 comments

Currently, to get the underlying &[u8] represented by BorrowedBytes you need to go through Deref, Borrow<[u8]> or AsRef<[u8]>, but they all take a &self rather than self, which causes the returned &[u8] to be tied to a temporary &BorrowedBytes instead of to the lifetime 'a of BorrowedBytes<'a>, which is a reference to &'a mlua::String.

This makes:

fn as_bytes<'a>(s: &'a mlua::String) -> &'a [u8] {
  s.as_bytes().as_ref()
}

fail to compile:

cannot return value referencing temporary value
returns a value referencing data owned by the current function rustc (E0515)

By implementing Into<&'a [u8]>:

impl<'a> From<BorrowedBytes<'a>> for &'a [u8] {
  fn from(value: BorrowedBytes<'a>) -> Self { value.buf }
}

I can convert BorrowedBytes<'a> into &'a [u8]:

fn as_bytes<'a>(s: &'a mlua::String) -> &'a [u8] {
  s.as_bytes().into()
}

sxyazi avatar Nov 11 '25 23:11 sxyazi

Unfortunately it would be unsound. Imagine the situation:

let lua = Lua::new();

let s = lua.create_string("hello, world")?;
let b: &[u8] = s.as_bytes().into();
assert_eq!(b, b"hello, world");

drop(lua);
assert_eq!(b, b"hello, world"); // use after free

khvzak avatar Nov 11 '25 23:11 khvzak

Oh right, I forgot that we can drop the entire Lua instance, and BorrowedBytes needs to keep up to hold a strong reference to Lua, but its lifetime is tied only to the mlua::String

I noticed that when I call s.as_bytes() after drop(lua):

let lua = Lua::new();
let s = lua.create_string("hello, world")?;

drop(lua);
assert_eq!(s.as_bytes(), b"hello, world");

the current behavior is to panic with:

panicked at src/string.rs:119:30:
Lua instance is destroyed

In my scenario the Lua instance is not dropped early, so it can be guaranteed to keep available till the app exits.

Do you think it would make sense to introduce the same behavior (panicking when the Lua instance is dropped) for BorrowedBytes::into() as well? Something like this:

impl<'a> From<BorrowedBytes<'a>> for &'a [u8] {
  fn from(value: BorrowedBytes<'a>) -> Self {
    _ = value._lua.upgrade();  // Ensure the Lua instance is available
    value.buf
  }
}

and turn the existing _lua from a strong Lua to a weak one (Edit: on second thought, this would affect the existing AsRef<[u8]>, maybe it would be more appropriate as a method on mlua::String that returns a &[u8] instead of a BorrowedBytes - is there any reason we need a BorrowedBytes wrapper?):

pub struct BorrowedBytes<'a> {
  // `buf` points to a readonly memory managed by Lua
  pub(crate) buf: &'a [u8],
  pub(crate) borrow: Cow<'a, String>,
-  pub(crate) _lua: Lua,
+  pub(crate) _lua: WeakLua,
}

It might need a better name though since the semantics of into usually don't imply a panic.

sxyazi avatar Nov 12 '25 01:11 sxyazi