low-luajit 0.3.0

Low level but safe bindings to the LuaJIT C API with no dependencies and no-std support to create Lua modules using Rust.
Documentation
use core::{ffi::{CStr, c_char}, marker::PhantomData, mem::MaybeUninit, ops::AddAssign, pin::Pin, slice};
use crate::{AmountReturned, LuaState, ffi::{self, luaL_Buffer}};

/// Buffer that allows building Lua strings incrementally.
///
/// Construction of the string is done by calling the various `StringBuffer::add_*` methods to append text or data to the buffer.
/// Once complete, the resulting string can be pushed onto the stack by calling [`StringBuffer::push_result`].
///
/// The buffer internally utilizes the stack in an unspecified way until completion,
/// resulting in the [`LuaState`] being mutably borrowed until the `StringBuffer` is dropped
/// *(you can still access the [`LuaState`] using [`StringBuffer::lua`])*.
/// The buffer will however not pop any element that was pushed prior to itself.
///
/// # Creation
///
/// A few steps are necessary to properly create a `StringBuffer`:
/// - It has to be initialized by the C API itself rather than by Rust (see [`LuaState::init_buffer`]).
/// - Before being initialized, it has to be [pinned](https://doc.rust-lang.org/core/pin/index.html).
///   *(The buffer is self-referential, thus moving it would quickly result in undefined behavior.)*
///
/// See below for some examples on how to create a buffer.
///
/// # Examples
///
/// Storing the buffer on the stack using [`pin!`](https://doc.rust-lang.org/core/pin/macro.pin.html) *(recommended)*:
///
/// ```
/// use core::pin::pin;
/// use low_luajit::{AmountReturned, LuaState, StringBuffer};
///
/// extern "C-unwind" fn example(mut lua: LuaState) -> AmountReturned {
///     let buffer = pin!(StringBuffer::uninit());
///     let mut buffer = lua.init_buffer(buffer);
///
///     // (Use the buffer in some way...)
///
///     buffer.push_result();
///     lua.return_any(1)
/// }
/// ```
///
/// Storing the buffer on the heap using a `Box`:
///
/// ```
/// use low_luajit::{AmountReturned, LuaState, StringBuffer};
///
/// extern "C-unwind" fn example_box(mut lua: LuaState) -> AmountReturned {
///     let mut buffer = Box::pin(StringBuffer::uninit());
///     let mut buffer = lua.init_buffer(buffer.as_mut());
///
///     // (Use the buffer in some way...)
///
///     buffer.push_result();
///     lua.return_any(1)
/// }
/// ```
///
/// Unless `Box`'s features are required,
/// [`pin!`](https://doc.rust-lang.org/core/pin/macro.pin.html) should be preferred as it tends to be cheaper to use.
#[repr(transparent)]
#[derive(Debug)]
pub struct StringBuffer<'a>(luaL_Buffer, PhantomData<&'a mut LuaState>);

impl StringBuffer<'_> {
	/// Returns the `StringBuffer` in an unitialized state.
	///
	/// Before it can be utilized, the buffer has to be initialized by [`LuaState::init_buffer`].
	#[inline]
	#[must_use]
	pub const fn uninit() -> MaybeUninit<Self> {
		MaybeUninit::uninit()
	}

	/// Returns a mutable pointer to the wrapped [`luaL_Buffer`].
	/// This can be used to pass this `StringBuffer` to raw [`ffi`] functions.
	///
	/// The caller must ensure that the `StringBuffer` outlives the pointer this function returns, or else it will end up dangling.
	#[inline]
	#[must_use]
	pub fn as_mut_ptr(self: &mut Pin<&mut Self>) -> *mut luaL_Buffer {
		(&raw const self.0).cast_mut()
	}

	/// Appends the given `byte` to the buffer.
	#[inline]
	#[allow(clippy::cast_possible_wrap)]
	pub fn add_byte(self: &mut Pin<&mut Self>, byte: u8) {
		// SAFETY: self.as_mut_ptr() is always valid.
		unsafe { ffi::luaL_addchar(self.as_mut_ptr(), byte as c_char); }
	}

	/// Appends the given character `ch` to the buffer.
	#[inline]
	pub fn add_char(self: &mut Pin<&mut Self>, ch: char) {
		if ch.is_ascii() {
			self.add_byte(ch as u8);
		} else {
			let mut bytes = [0u8; 4];
			self.add_string(ch.encode_utf8(&mut bytes));
		}
	}

	/// Appends a copy of the given `string` to the buffer.
	///
	/// As an alternative syntax, the `+=` operation can be used instead.
	#[inline]
	pub fn add_string(self: &mut Pin<&mut Self>, string: impl AsRef<[u8]>) {
		self.add_byte_string(string.as_ref());
	}

	/// Appends a copy of the given `byte_string` to the buffer.
	#[inline]
	pub fn add_byte_string(self: &mut Pin<&mut Self>, byte_string: &[u8]) {
		// SAFETY: self.as_mut_ptr() is always valid, the pointer remains valid for the duration of this function,
		// and the length is correct as it's returned from the array itself.
		unsafe { ffi::luaL_addlstring(self.as_mut_ptr(), byte_string.as_ptr().cast(), byte_string.len()); }
	}

	/// Appends a copy of the given `cstring` to the buffer.
	#[inline]
	pub fn add_cstring(self: &mut Pin<&mut Self>, cstring: &CStr) {
		// SAFETY: self.as_mut_ptr() is always valid and the pointer either remains valid for the duration of this function.
		unsafe { ffi::luaL_addstring(self.as_mut_ptr(), cstring.as_ptr()); }
	}

	/// Appends the value at the top of the stack to the buffer.
	/// The value should be pushed onto the stack by the given function `f`.
	///
	/// Only strings or numbers are accepted. Anything else will be discarded.
	///
	/// # Panics
	///
	/// Panics if the amount of elements present in the stack after `f` is called does not increase by a single element.
	///
	/// While replacing the elements pushed by the `StringBuffer` itself *is* possible and won't cause a panic,
	/// it's still considered a logic error and it will likely cause a Lua error once [`StringBuffer::push_result`] is called.
	#[inline]
	pub fn add_value(self: &mut Pin<&mut Self>, f: impl FnOnce(&mut LuaState)) {
		// SAFETY: self.0.L is always valid and there should currently not be another accessible LuaState.
		let mut lua = unsafe { LuaState::from_ptr(self.0.L) };
		let top = lua.get_top();
		f(&mut lua);
		assert!(lua.get_top() - top == 1, "tried to append a value to a buffer by pushing {} elements instead of single element", lua.get_top() - top);

		// SAFETY: self.as_mut_ptr() is always valid and the stack should always have a single extra element.
		unsafe { ffi::luaL_addvalue(self.as_mut_ptr()); }
	}

	/// Appends the next `amount` bytes in the buffer area to the buffer.
	///
	/// This is done by initializing the bytes to `fill` and then returning a mutable slice to said bytes, to allow further modifications.
	///
	/// To instead simply mark the next `amount` of bytes as used, without initialization, see [`StringBuffer::add_size_unchecked`].
	///
	/// # Panics
	///
	/// Panics if `amount` is greater than the size of the buffer area (see [`StringBuffer::BUFFER_AREA_SIZE`]).
	pub fn add_size<'a>(self: &'a mut Pin<&mut Self>, amount: usize, fill: u8) -> &'a mut [u8] {
		assert!(
			amount <= StringBuffer::BUFFER_AREA_SIZE,
			"tried to append the next {amount} bytes in the buffer area to a buffer, but the size of the buffer area is only {} bytes",
			StringBuffer::BUFFER_AREA_SIZE
		);

		// SAFETY: self.as_mut_ptr() is always valid, c_char can be casted as u8 (they're both 1 byte),
		// luaL_prepbuffer should always return a non-null pointer valid for amount amount of values,
		// and the buffer area will contain at least amount valid values.
		unsafe {
			let ptr = ffi::luaL_prepbuffer(self.as_mut_ptr()).cast::<u8>();
			ptr.write_bytes(fill, amount);
			ffi::luaL_addsize(self.as_mut_ptr(), amount);
			slice::from_raw_parts_mut(ptr, amount)
		}
	}

	/// Appends the next `amount` bytes in the buffer area to the buffer.
	///
	/// This is done by simply increasing the amount of the area marked as used by `amount`, without doing any checks.
	///
	/// For safer version of this function, see [`StringBuffer::add_size`].
	///
	/// # Safety
	///
	/// The next `amount` bytes in the buffer area must be initialized and not out of bounds.
	///
	/// See [`StringBuffer::flush_buffer`] for details on how to access and initialize the buffer area.
	#[inline]
	pub unsafe fn add_size_unchecked(self: &mut Pin<&mut Self>, amount: usize) {
		// SAFETY: self.as_mut_ptr() is always valid and the caller must make sure to give a valid amount.
		unsafe { ffi::luaL_addsize(self.as_mut_ptr(), amount); }
	}

	/// The size of the "buffer area" used internally by `StringBuffer`.
	/// This size may vary across compilation targets.
	///
	/// To access the buffer area directly, see [`StringBuffer::flush_buffer`].
	pub const BUFFER_AREA_SIZE: usize = ffi::LUAL_BUFFERSIZE;

	/// Flushes the contents of the buffer onto the stack *(in an unspecified way)*,
	/// returning a mutable reference to the array used internally by the buffer (the "buffer area").
	///
	/// It is unspecified whether any bytes in the array are initialized or not, some *may* contain remnants of the flushed data.
	///
	/// Any data written directly to the array will *not* be appended to the buffer unless [`StringBuffer::add_size_unchecked`] is called afterwards.
	/// *(The data will otherwise be overwritten by the other methods or discarded.)*
	///
	/// The size of the array may vary across compilation targets.
	#[inline]
	pub fn flush_buffer<'a>(self: &'a mut Pin<&mut Self>) -> &'a mut [MaybeUninit<u8>; StringBuffer::BUFFER_AREA_SIZE] {
		// SAFETY: self.as_mut_ptr() is always valid, luaL_prepbuffer should always return a non-null pointer for LUAL_BUFFERSIZE amount of values,
		// and c_char can be casted as u8 (they're both 1 byte).
		unsafe { &mut *ffi::luaL_prepbuffer(self.as_mut_ptr()).cast() }
	}

	/// Pushes the entire Lua string onto the stack as a single element, completing it's construction.
	///
	/// Unlike all other `StringBuffer` methods, this function leaves the stack in a well defined state:
	/// only the finalized string remains, alongside any element present before the buffer was created.
	///
	/// After calling this function, the completed string can be returned to the caller by using [`LuaState::return_any`].
	/// Alternatively, it *is* possible to just continue appending more data to the finalized string using the `StringBuffer::add_*` methods
	/// (you will have to call this function again afterwards).
	///
	/// # Lua Errors
	///
	/// If any element that was previously pushed by the buffer (before calling this function) has been manually replaced
	/// (via either [`StringBuffer::add_value`] or [`StringBuffer::lua`]) to an invalid value, this function will most likely error.
	#[inline]
	pub fn push_result(self: &mut Pin<&mut Self>) {
		// SAFETY: self.as_mut_ptr() is always valid.
		unsafe { ffi::luaL_pushresult(self.as_mut_ptr()); }
	}

	/// Finishes the currently called [`CFunction`], returning the completed string to the caller.
	///
	/// This function has the same caveats as [`StringBuffer::push_result`].
	///
	/// # Safety
	///
	/// The returned `AmountReturned` must not be invalidated.
	/// *(There is currently no way to enforce this, hence the function's unsafeness.)*
	///
	/// This function's behavior may be changed in the future once a way to make this function safe is found.
	///
	/// [`CFunction`]: ./type.CFunction.html
	#[inline]
	pub unsafe fn return_result(mut self: Pin<&mut Self>) -> AmountReturned {
		self.push_result();

		// SAFETY: There should always be at least one element in the stack and the caller must not invalidate that.
		unsafe { AmountReturned::unchecked(1) }
	}

	/// Temporarily grants access to the borrowed [`LuaState`] to the given function `f`,
	/// returning whatever `f` returns.
	///
	/// As the stack might have grown in an unspecified way, you should not assume on the size or top elements of the stack.
	///
	/// # Panics
	///
	/// Panics if the amounts of elements present in the stack after `f` is called is not the same as the amount before `f` was called.
	///
	/// While replacing the elements pushed by the `StringBuffer` itself *is* possible and won't cause a panic,
	/// it's still considered a logic error and it will likely cause a Lua error once [`StringBuffer::push_result`] is called.
	#[inline]
	pub fn lua<T>(self: &mut Pin<&mut Self>, f: impl FnOnce(&mut LuaState) -> T) -> T {
		// SAFETY: self.0.L is always valid and there should currently not be another accessible LuaState.
		let mut lua = unsafe { LuaState::from_ptr(self.0.L) };
		let top = lua.get_top();
		let result = f(&mut lua);

		assert!(top == lua.get_top(), "temporarily accessed the Lua state as the stack held {top} elements, but left it with {} elements instead", lua.get_top());
		result
	}
}

impl<T: AsRef<[u8]>> AddAssign<T> for Pin<&mut StringBuffer<'_>> {
	/// Behaves the same as [`StringBuffer::add_string`].
	#[inline]
	fn add_assign(&mut self, rhs: T) {
		self.add_string(rhs);
	}
}

/// [`StringBuffer`] initialization function.
///
/// *[See also the `StringBuffer` struct](struct.StringBuffer.html)*.
impl LuaState {
	#[inline]
	pub fn init_buffer<'a>(&'a mut self, buffer: Pin<&'a mut MaybeUninit<StringBuffer<'a>>>) -> Pin<&'a mut StringBuffer<'a>> {
		unsafe {
			ffi::luaL_buffinit(self.0, buffer.as_ptr().cast_mut().cast());
			buffer.map_unchecked_mut(|buffer| buffer.assume_init_mut())
		}
	}
}