Compare commits

..

No commits in common. "8c406a46b36f1a33cc2a5c19c111b887b26027f4" and "27c40c3244d56bf03273eaad74b525bb8291f4d5" have entirely different histories.

14 changed files with 496 additions and 1737 deletions

View File

@ -11,7 +11,7 @@ repository.workspace = true
runtime = ["tokio/rt"] runtime = ["tokio/rt"]
task = ["tokio/rt", "tokio/time"] task = ["tokio/rt", "tokio/time"]
fs = ["tokio/fs", "dep:walkdir", "dep:globset", "dep:tempfile"] fs = ["tokio/fs", "dep:walkdir", "dep:globset", "dep:tempfile"]
net = ["tokio/net", "tokio/io-util"] net = ["tokio/net"]
[dependencies] [dependencies]
derive_more = { version = "2.0.1", features = ["full"] } derive_more = { version = "2.0.1", features = ["full"] }

View File

@ -12,12 +12,9 @@
//! ## Exports //! ## Exports
//! //!
//! See [`lb_fslib`] for items exported by this library. //! See [`lb_fslib`] for items exported by this library.
use derive_more::From;
use luaffi::{cdef, metatype}; use luaffi::{cdef, metatype};
use std::{ use std::{path::PathBuf, time::SystemTime};
cell::{BorrowError, BorrowMutError, RefCell},
path::PathBuf,
time::SystemTime,
};
use thiserror::Error; use thiserror::Error;
/// Errors that can be thrown by this library. /// Errors that can be thrown by this library.
@ -26,12 +23,6 @@ use thiserror::Error;
/// using [`pcall(f, ...)`](https://www.lua.org/manual/5.1/manual.html#pdf-pcall). /// using [`pcall(f, ...)`](https://www.lua.org/manual/5.1/manual.html#pdf-pcall).
#[derive(Debug, Error)] #[derive(Debug, Error)]
pub enum Error { pub enum Error {
/// Attempt to access an object while it is being modified.
#[error("cannot access object while it is being modified")]
Borrow(#[from] BorrowError),
/// Attempt to modify an object while it is in use
#[error("cannot modify object while it is in use")]
BorrowMut(#[from] BorrowMutError),
/// I/O error. /// I/O error.
#[error("{0}")] #[error("{0}")]
Io(#[from] std::io::Error), Io(#[from] std::io::Error),
@ -63,85 +54,38 @@ impl lb_fslib {
Self Self
} }
/// Reads the entire contents of a file.
///
/// # Errors
///
/// This function may throw if the file does not exist or cannot be read.
pub async extern "Lua-C" fn read(path: &str) -> Result<Vec<u8>> { pub async extern "Lua-C" fn read(path: &str) -> Result<Vec<u8>> {
Ok(tokio::fs::read(path).await?) Ok(tokio::fs::read(path).await?)
} }
/// Reads the entire contents of a file synchronously.
///
/// This is a synchronous complement to [`read`](Self::read).
///
/// # Errors
///
/// This function may throw if the file does not exist or cannot be read.
pub extern "Lua-C" fn read_sync(path: &str) -> Result<Vec<u8>> { pub extern "Lua-C" fn read_sync(path: &str) -> Result<Vec<u8>> {
Ok(std::fs::read(path)?) Ok(std::fs::read(path)?)
} }
/// Writes the given contents to a file, replacing its contents if it exists.
///
/// # Errors
///
/// This function may throw if the file cannot be written.
pub async extern "Lua-C" fn write(path: &str, contents: &[u8]) -> Result<()> { pub async extern "Lua-C" fn write(path: &str, contents: &[u8]) -> Result<()> {
Ok(tokio::fs::write(path, contents).await?) Ok(tokio::fs::write(path, contents).await?)
} }
/// Writes the given contents to a file synchronously, replacing its contents if it exists.
///
/// This is a synchronous complement to [`write`](Self::write).
///
/// # Errors
///
/// This function may throw if the file cannot be written.
pub extern "Lua-C" fn write_sync(path: &str, contents: &[u8]) -> Result<()> { pub extern "Lua-C" fn write_sync(path: &str, contents: &[u8]) -> Result<()> {
Ok(std::fs::write(path, contents)?) Ok(std::fs::write(path, contents)?)
} }
/// Reads the entries in a directory.
///
/// # Errors
///
/// This function may throw if the directory cannot be read.
pub async extern "Lua-C" fn read_dir(path: &str) -> Result<lb_read_dir> { pub async extern "Lua-C" fn read_dir(path: &str) -> Result<lb_read_dir> {
Ok(lb_read_dir::new(tokio::fs::read_dir(path).await?)) Ok(tokio::fs::read_dir(path).await?.into())
} }
/// Reads the entries in a directory synchronously.
///
/// This is a synchronous complement to [`read_dir`](Self::read_dir).
///
/// # Errors
///
/// This function may throw if the directory cannot be read.
pub extern "Lua-C" fn read_dir_sync(path: &str) -> Result<lb_read_dir_sync> { pub extern "Lua-C" fn read_dir_sync(path: &str) -> Result<lb_read_dir_sync> {
Ok(lb_read_dir_sync::new(std::fs::read_dir(path)?)) Ok(std::fs::read_dir(path)?.into())
} }
/// Recursively walks a directory, yielding all entries within it.
pub extern "Lua-C" fn walk_dir(path: &str) -> lb_walk_dir { pub extern "Lua-C" fn walk_dir(path: &str) -> lb_walk_dir {
lb_walk_dir::new(walkdir::WalkDir::new(path).into_iter()) walkdir::WalkDir::new(path).into_iter().into()
} }
/// Returns an iterator over all files matching a glob pattern in the current directory. pub extern "Lua-C" fn glob(pattern: &str) -> Result<lb_glob_dir> {
///
/// # Errors
///
/// This function may throw if the pattern is invalid.
pub extern "Lua" fn glob(pattern: &str) -> Result<lb_glob_dir> {
Self::glob_dir(".", pattern) Self::glob_dir(".", pattern)
} }
/// Returns an iterator over all files matching a glob pattern in the given directory.
///
/// # Errors
///
/// This function may throw if the pattern is invalid.
pub extern "Lua-C" fn glob_dir(path: &str, pattern: &str) -> Result<lb_glob_dir> { pub extern "Lua-C" fn glob_dir(path: &str, pattern: &str) -> Result<lb_glob_dir> {
let prefix = PathBuf::from(path); let prefix = PathBuf::from(path);
let iter = walkdir::WalkDir::new(path).min_depth(1).into_iter(); let iter = walkdir::WalkDir::new(path).min_depth(1).into_iter();
@ -149,128 +93,76 @@ impl lb_fslib {
.add(globset::Glob::new(pattern)?) .add(globset::Glob::new(pattern)?)
.build()?; .build()?;
Ok(lb_glob_dir::new(iter, matcher, prefix)) Ok(lb_glob_dir {
iter,
matcher,
prefix,
})
} }
/// Creates a new temporary directory.
///
/// # Errors
///
/// This function may throw if the temporary directory cannot be created.
pub extern "Lua-C" fn temp_dir() -> Result<lb_temp_dir> { pub extern "Lua-C" fn temp_dir() -> Result<lb_temp_dir> {
Ok(lb_temp_dir::new(tempfile::tempdir()?)) Ok(tempfile::tempdir()?.into())
} }
/// Creates a new temporary directory inside the specified path.
///
/// # Errors
///
/// This function may throw if the temporary directory cannot be created.
pub extern "Lua-C" fn temp_dir_in(path: &str) -> Result<lb_temp_dir> { pub extern "Lua-C" fn temp_dir_in(path: &str) -> Result<lb_temp_dir> {
Ok(lb_temp_dir::new(tempfile::tempdir_in(path)?)) Ok(tempfile::tempdir_in(path)?.into())
} }
} }
/// Iterator over the entries in a directory. /// Iterator over the entries in a directory.
#[derive(Debug)] #[derive(Debug, From)]
#[cdef] #[cdef]
pub struct lb_read_dir(#[opaque] RefCell<tokio::fs::ReadDir>); pub struct lb_read_dir(#[opaque] tokio::fs::ReadDir);
#[metatype] #[metatype]
impl lb_read_dir { impl lb_read_dir {
fn new(iter: tokio::fs::ReadDir) -> Self {
Self(RefCell::new(iter))
}
/// Returns the next entry in the directory, or `nil` if there are no more entries.
///
/// # Errors
///
/// This function may throw if the directory cannot be read.
#[call] #[call]
pub async extern "Lua-C" fn next(&self) -> Result<Option<lb_dir_entry>> { pub async extern "Lua-C" fn next(&mut self) -> Result<Option<lb_dir_entry>> {
Ok(self Ok(self.0.next_entry().await?.map(Into::into))
.0
.try_borrow_mut()?
.next_entry()
.await?
.map(lb_dir_entry::new))
} }
} }
/// Synchronous version of [`lb_read_dir`]. /// Synchronous version of [`lb_read_dir`].
#[derive(Debug)] #[derive(Debug, From)]
#[cdef] #[cdef]
pub struct lb_read_dir_sync(#[opaque] RefCell<std::fs::ReadDir>); pub struct lb_read_dir_sync(#[opaque] std::fs::ReadDir);
#[metatype] #[metatype]
impl lb_read_dir_sync { impl lb_read_dir_sync {
fn new(iter: std::fs::ReadDir) -> Self {
Self(RefCell::new(iter))
}
/// Returns the next entry in the directory, or `nil` if there are no more entries.
///
/// # Errors
///
/// This function may throw if the directory cannot be read.
#[call] #[call]
pub extern "Lua-C" fn next(&self) -> Result<Option<lb_dir_entry_sync>> { pub extern "Lua-C" fn next(&mut self) -> Result<Option<lb_dir_entry_sync>> {
Ok(self Ok(self.0.next().transpose()?.map(Into::into))
.0
.try_borrow_mut()?
.next()
.transpose()?
.map(lb_dir_entry_sync::new))
} }
} }
/// Entry inside of a directory on the filesystem. /// Entry inside of a directory on the filesystem.
#[derive(Debug)] #[derive(Debug, From)]
#[cdef] #[cdef]
pub struct lb_dir_entry(#[opaque] tokio::fs::DirEntry); pub struct lb_dir_entry(#[opaque] tokio::fs::DirEntry);
#[metatype] #[metatype]
impl lb_dir_entry { impl lb_dir_entry {
fn new(entry: tokio::fs::DirEntry) -> Self {
Self(entry)
}
/// Returns the full path of this entry.
pub extern "Lua-C" fn path(&self) -> String { pub extern "Lua-C" fn path(&self) -> String {
self.0.path().to_string_lossy().into() self.0.path().to_string_lossy().into()
} }
/// Returns the file name of this entry.
pub extern "Lua-C" fn name(&self) -> String { pub extern "Lua-C" fn name(&self) -> String {
self.0.file_name().to_string_lossy().into() self.0.file_name().to_string_lossy().into()
} }
/// Returns the type of this entry.
///
/// # Errors
///
/// This function may throw if the file type cannot be determined.
pub async extern "Lua-C" fn r#type(&self) -> Result<lb_file_type> { pub async extern "Lua-C" fn r#type(&self) -> Result<lb_file_type> {
Ok(lb_file_type::new(self.0.file_type().await?)) Ok(self.0.file_type().await?.into())
} }
/// Returns the metadata for this entry.
///
/// # Errors
///
/// This function may throw if the metadata cannot be retrieved.
pub async extern "Lua-C" fn metadata(&self) -> Result<lb_file_meta> { pub async extern "Lua-C" fn metadata(&self) -> Result<lb_file_meta> {
Ok(lb_file_meta::new(self.0.metadata().await?)) Ok(self.0.metadata().await?.into())
} }
/// Returns the inode number for this entry.
#[cfg(unix)] #[cfg(unix)]
pub extern "Lua-C" fn ino(&self) -> u64 { pub extern "Lua-C" fn ino(&self) -> u64 {
self.0.ino() self.0.ino()
} }
/// Returns the full path of this entry.
#[tostring] #[tostring]
pub extern "Lua" fn tostring(&self) -> String { pub extern "Lua" fn tostring(&self) -> String {
self.path() self.path()
@ -278,52 +170,34 @@ impl lb_dir_entry {
} }
/// Synchronous version of [`lb_dir_entry`]. /// Synchronous version of [`lb_dir_entry`].
#[derive(Debug)] #[derive(Debug, From)]
#[cdef] #[cdef]
pub struct lb_dir_entry_sync(#[opaque] std::fs::DirEntry); pub struct lb_dir_entry_sync(#[opaque] std::fs::DirEntry);
#[metatype] #[metatype]
impl lb_dir_entry_sync { impl lb_dir_entry_sync {
fn new(entry: std::fs::DirEntry) -> Self {
Self(entry)
}
/// Returns the full path of this entry.
pub extern "Lua-C" fn path(&self) -> String { pub extern "Lua-C" fn path(&self) -> String {
self.0.path().to_string_lossy().into() self.0.path().to_string_lossy().into()
} }
/// Returns the file name of this entry.
pub extern "Lua-C" fn name(&self) -> String { pub extern "Lua-C" fn name(&self) -> String {
self.0.file_name().to_string_lossy().into() self.0.file_name().to_string_lossy().into()
} }
/// Returns the type of this entry.
///
/// # Errors
///
/// This function may throw if the file type cannot be determined.
pub extern "Lua-C" fn r#type(&self) -> Result<lb_file_type> { pub extern "Lua-C" fn r#type(&self) -> Result<lb_file_type> {
Ok(lb_file_type::new(self.0.file_type()?)) Ok(self.0.file_type()?.into())
} }
/// Returns the metadata for this entry.
///
/// # Errors
///
/// This function may throw if the metadata cannot be retrieved.
pub extern "Lua-C" fn metadata(&self) -> Result<lb_file_meta> { pub extern "Lua-C" fn metadata(&self) -> Result<lb_file_meta> {
Ok(lb_file_meta::new(self.0.metadata()?)) Ok(self.0.metadata()?.into())
} }
/// Returns the inode number for this entry.
#[cfg(unix)] #[cfg(unix)]
pub extern "Lua-C" fn ino(&self) -> u64 { pub extern "Lua-C" fn ino(&self) -> u64 {
use std::os::unix::fs::DirEntryExt; use std::os::unix::fs::DirEntryExt;
self.0.ino() self.0.ino()
} }
/// Returns the full path of this entry.
#[tostring] #[tostring]
pub extern "Lua" fn tostring(&self) -> String { pub extern "Lua" fn tostring(&self) -> String {
self.path() self.path()
@ -331,33 +205,24 @@ impl lb_dir_entry_sync {
} }
/// Structure representing the type of a file with accessors for each file type. /// Structure representing the type of a file with accessors for each file type.
#[derive(Debug)] #[derive(Debug, From)]
#[cdef] #[cdef]
pub struct lb_file_type(#[opaque] std::fs::FileType); pub struct lb_file_type(#[opaque] std::fs::FileType);
#[metatype] #[metatype]
impl lb_file_type { impl lb_file_type {
fn new(ty: std::fs::FileType) -> Self {
Self(ty)
}
/// Returns `true` if this file type is a directory.
pub extern "Lua-C" fn is_dir(&self) -> bool { pub extern "Lua-C" fn is_dir(&self) -> bool {
self.0.is_dir() self.0.is_dir()
} }
/// Returns `true` if this file type is a regular file.
pub extern "Lua-C" fn is_file(&self) -> bool { pub extern "Lua-C" fn is_file(&self) -> bool {
self.0.is_file() self.0.is_file()
} }
/// Returns `true` if this file type is a symbolic link.
pub extern "Lua-C" fn is_symlink(&self) -> bool { pub extern "Lua-C" fn is_symlink(&self) -> bool {
self.0.is_file() self.0.is_file()
} }
/// Returns the string `"file"` if this is a regular file, `"dir"` if this is a directory,
/// `"symlink"` if this is a symbolic link, or `"other"` if it is some other type of file.
#[tostring] #[tostring]
pub extern "Lua-C" fn tostring(&self) -> String { pub extern "Lua-C" fn tostring(&self) -> String {
if self.0.is_file() { if self.0.is_file() {
@ -374,51 +239,36 @@ impl lb_file_type {
} }
/// Metadata information about a file. /// Metadata information about a file.
#[derive(Debug)] #[derive(Debug, From)]
#[cdef] #[cdef]
pub struct lb_file_meta(#[opaque] std::fs::Metadata); pub struct lb_file_meta(#[opaque] std::fs::Metadata);
#[metatype] #[metatype]
impl lb_file_meta { impl lb_file_meta {
fn new(meta: std::fs::Metadata) -> Self {
Self(meta)
}
/// Returns `true` if this file is a directory.
pub extern "Lua-C" fn is_dir(&self) -> bool { pub extern "Lua-C" fn is_dir(&self) -> bool {
self.0.is_dir() self.0.is_dir()
} }
/// Returns `true` if this file is a regular file.
pub extern "Lua-C" fn is_file(&self) -> bool { pub extern "Lua-C" fn is_file(&self) -> bool {
self.0.is_file() self.0.is_file()
} }
/// Returns `true` if this file is a symbolic link.
pub extern "Lua-C" fn is_symlink(&self) -> bool { pub extern "Lua-C" fn is_symlink(&self) -> bool {
self.0.is_file() self.0.is_file()
} }
/// Returns the type of this file.
pub extern "Lua-C" fn r#type(&self) -> lb_file_type { pub extern "Lua-C" fn r#type(&self) -> lb_file_type {
lb_file_type::new(self.0.file_type()) self.0.file_type().into()
} }
/// Returns the size of this file in bytes.
pub extern "Lua-C" fn size(&self) -> u64 { pub extern "Lua-C" fn size(&self) -> u64 {
self.0.len() self.0.len()
} }
/// Returns the permissions of this file.
pub extern "Lua-C" fn perms(&self) -> lb_file_perms { pub extern "Lua-C" fn perms(&self) -> lb_file_perms {
lb_file_perms::new(self.0.permissions()) self.0.permissions().into()
} }
/// Returns the creation time of this file as seconds since the Unix epoch.
///
/// # Errors
///
/// This function may throw if the creation time cannot be retrieved.
pub extern "Lua-C" fn created(&self) -> Result<f64> { pub extern "Lua-C" fn created(&self) -> Result<f64> {
Ok(self Ok(self
.0 .0
@ -428,11 +278,6 @@ impl lb_file_meta {
.unwrap_or(0.)) .unwrap_or(0.))
} }
/// Returns the modification time of this file as seconds since the Unix epoch.
///
/// # Errors
///
/// This function may throw if the modification time cannot be retrieved.
pub extern "Lua-C" fn modified(&self) -> Result<f64> { pub extern "Lua-C" fn modified(&self) -> Result<f64> {
Ok(self Ok(self
.0 .0
@ -442,11 +287,6 @@ impl lb_file_meta {
.unwrap_or(0.)) .unwrap_or(0.))
} }
/// Returns the last access time of this file as seconds since the Unix epoch.
///
/// # Errors
///
/// This function may throw if the access time cannot be retrieved.
pub extern "Lua-C" fn accessed(&self) -> Result<f64> { pub extern "Lua-C" fn accessed(&self) -> Result<f64> {
Ok(self Ok(self
.0 .0
@ -456,7 +296,6 @@ impl lb_file_meta {
.unwrap_or(0.)) .unwrap_or(0.))
} }
/// Returns a string representation of this file's metadata.
#[tostring] #[tostring]
pub extern "Lua-C" fn tostring(&self) -> String { pub extern "Lua-C" fn tostring(&self) -> String {
let ty = self.0.file_type(); let ty = self.0.file_type();
@ -473,107 +312,71 @@ impl lb_file_meta {
} }
/// Representation of the various permissions on a file. /// Representation of the various permissions on a file.
#[derive(Debug)] #[derive(Debug, From)]
#[cdef] #[cdef]
pub struct lb_file_perms(#[opaque] RefCell<std::fs::Permissions>); pub struct lb_file_perms(#[opaque] std::fs::Permissions);
#[metatype] #[metatype]
impl lb_file_perms { impl lb_file_perms {
fn new(perms: std::fs::Permissions) -> Self {
Self(RefCell::new(perms))
}
/// Returns `true` if the readonly flag is set.
pub extern "Lua-C" fn readonly(&self) -> bool { pub extern "Lua-C" fn readonly(&self) -> bool {
self.0.borrow().readonly() self.0.readonly()
} }
/// Sets the readonly flag. pub extern "Lua-C" fn set_readonly(&mut self, readonly: bool) {
pub extern "Lua-C" fn set_readonly(&self, readonly: bool) { self.0.set_readonly(readonly);
self.0.borrow_mut().set_readonly(readonly);
} }
} }
/// Iterator for recursively descending into a directory. /// Iterator for recursively descending into a directory.
#[derive(Debug)] #[derive(Debug, From)]
#[cdef] #[cdef]
pub struct lb_walk_dir(#[opaque] RefCell<walkdir::IntoIter>); pub struct lb_walk_dir(#[opaque] walkdir::IntoIter);
#[metatype] #[metatype]
impl lb_walk_dir { impl lb_walk_dir {
fn new(iter: walkdir::IntoIter) -> Self {
Self(RefCell::new(iter))
}
/// Returns the next entry in the walk, or `nil` if there are no more entries.
///
/// # Errors
///
/// This function may throw if the directory cannot be read.
#[call] #[call]
pub extern "Lua-C" fn next(&self) -> Result<Option<lb_walk_dir_entry>> { pub extern "Lua-C" fn next(&mut self) -> Result<Option<lb_walk_dir_entry>> {
Ok(self Ok(self.0.next().transpose()?.map(Into::into))
.0
.try_borrow_mut()?
.next()
.transpose()?
.map(lb_walk_dir_entry::new))
} }
} }
/// Entry inside of a directory on the filesystem obtained from [`lb_walk_dir`]. /// Entry inside of a directory on the filesystem obtained from [`lb_walk_dir`].
#[derive(Debug)] #[derive(Debug, From)]
#[cdef] #[cdef]
pub struct lb_walk_dir_entry(#[opaque] walkdir::DirEntry); pub struct lb_walk_dir_entry(#[opaque] walkdir::DirEntry);
#[metatype] #[metatype]
impl lb_walk_dir_entry { impl lb_walk_dir_entry {
fn new(entry: walkdir::DirEntry) -> Self {
Self(entry)
}
/// Returns the full path of this entry.
pub extern "Lua-C" fn path(&self) -> String { pub extern "Lua-C" fn path(&self) -> String {
self.0.path().to_string_lossy().into() self.0.path().to_string_lossy().into()
} }
/// Returns the file name of this entry.
pub extern "Lua-C" fn name(&self) -> String { pub extern "Lua-C" fn name(&self) -> String {
self.0.file_name().to_string_lossy().into() self.0.file_name().to_string_lossy().into()
} }
/// Returns the type of this entry.
pub extern "Lua-C" fn r#type(&self) -> lb_file_type { pub extern "Lua-C" fn r#type(&self) -> lb_file_type {
lb_file_type::new(self.0.file_type()) self.0.file_type().into()
} }
/// Returns the metadata for this entry.
///
/// # Errors
///
/// This function may throw if the metadata cannot be retrieved.
pub extern "Lua-C" fn metadata(&self) -> Result<lb_file_meta> { pub extern "Lua-C" fn metadata(&self) -> Result<lb_file_meta> {
Ok(lb_file_meta::new(self.0.metadata()?)) Ok(self.0.metadata()?.into())
} }
/// Returns `true` if this entry was created from a symbolic link.
pub extern "Lua-C" fn is_symlink(&self) -> bool { pub extern "Lua-C" fn is_symlink(&self) -> bool {
self.0.path_is_symlink() self.0.path_is_symlink()
} }
/// Returns the depth of this entry in the walk.
pub extern "Lua-C" fn depth(&self) -> u32 { pub extern "Lua-C" fn depth(&self) -> u32 {
self.0.depth() as u32 self.0.depth() as u32
} }
/// Returns the inode number for this entry.
#[cfg(unix)] #[cfg(unix)]
pub extern "Lua-C" fn ino(&self) -> u64 { pub extern "Lua-C" fn ino(&self) -> u64 {
use walkdir::DirEntryExt; use walkdir::DirEntryExt;
self.0.ino() self.0.ino()
} }
/// Returns the full path of this entry.
#[tostring] #[tostring]
pub extern "Lua" fn tostring(&self) -> String { pub extern "Lua" fn tostring(&self) -> String {
self.path() self.path()
@ -585,7 +388,7 @@ impl lb_walk_dir_entry {
#[cdef] #[cdef]
pub struct lb_glob_dir { pub struct lb_glob_dir {
#[opaque] #[opaque]
iter: RefCell<walkdir::IntoIter>, iter: walkdir::IntoIter,
#[opaque] #[opaque]
matcher: globset::GlobSet, matcher: globset::GlobSet,
#[opaque] #[opaque]
@ -594,26 +397,13 @@ pub struct lb_glob_dir {
#[metatype] #[metatype]
impl lb_glob_dir { impl lb_glob_dir {
fn new(iter: walkdir::IntoIter, matcher: globset::GlobSet, prefix: PathBuf) -> Self {
Self {
iter: RefCell::new(iter),
matcher,
prefix,
}
}
/// Returns the next entry matching the glob pattern, or `nil` if there are no more entries.
///
/// # Errors
///
/// This function may throw if the directory cannot be read.
#[call] #[call]
pub extern "Lua-C" fn next(&self) -> Result<Option<lb_walk_dir_entry>> { pub extern "Lua-C" fn next(&mut self) -> Result<Option<lb_walk_dir_entry>> {
while let Some(res) = self.iter.try_borrow_mut()?.next() { while let Some(res) = self.iter.next() {
let entry = res?; let entry = res?;
let path = entry.path().strip_prefix(&self.prefix).unwrap(); let path = entry.path().strip_prefix(&self.prefix).unwrap();
if self.matcher.is_match(path) { if self.matcher.is_match(path) {
return Ok(Some(lb_walk_dir_entry::new(entry))); return Ok(Some(entry.into()));
} }
} }
@ -622,22 +412,16 @@ impl lb_glob_dir {
} }
/// Directory in the filesystem that is automatically deleted when it is garbage-collected. /// Directory in the filesystem that is automatically deleted when it is garbage-collected.
#[derive(Debug)] #[derive(Debug, From)]
#[cdef] #[cdef]
pub struct lb_temp_dir(#[opaque] tempfile::TempDir); pub struct lb_temp_dir(#[opaque] tempfile::TempDir);
#[metatype] #[metatype]
impl lb_temp_dir { impl lb_temp_dir {
fn new(dir: tempfile::TempDir) -> Self {
Self(dir)
}
/// Returns the full path of this temporary directory.
pub extern "Lua-C" fn path(&self) -> String { pub extern "Lua-C" fn path(&self) -> String {
self.0.path().to_string_lossy().into() self.0.path().to_string_lossy().into()
} }
/// Returns the full path of this temporary directory.
#[tostring] #[tostring]
pub extern "Lua" fn tostring(&self) -> String { pub extern "Lua" fn tostring(&self) -> String {
self.path() self.path()

File diff suppressed because it is too large Load Diff

View File

@ -12,8 +12,8 @@ use luaffi::{
marker::{function, many}, marker::{function, many},
metatype, metatype,
}; };
use luajit::LUA_MULTRET; use luajit::{LUA_MULTRET, Type};
use std::{cell::RefCell, ffi::c_int, time::Duration}; use std::{ffi::c_int, time::Duration};
use tokio::{task::JoinHandle, time::sleep}; use tokio::{task::JoinHandle, time::sleep};
/// Items exported by the `lb:task` library. /// Items exported by the `lb:task` library.
@ -36,47 +36,43 @@ impl lb_tasklib {
} }
pub async extern "Lua-C" fn sleep(ms: f64) { pub async extern "Lua-C" fn sleep(ms: f64) {
sleep(Duration::from_secs_f64(ms.max(0.) / 1000.)).await; sleep(Duration::from_secs_f64(ms / 1000.)).await;
} }
pub extern "Lua" fn spawn(f: function, ...) -> lb_task { pub extern "Lua" fn spawn(f: function, ...) -> lb_task {
// pack the function and its arguments into a table and pass its ref to rust. // pack the function and its arguments into a table and pass its ref to rust.
// //
// this "state" table is used from rust-side to call the function with its args, and it's // this table is used from rust-side to call the function with its args, and it's also
// also reused to store its return values that the task handle can return when awaited. the // reused to store its return values that the task handle can return when awaited. the ref
// ref is owned by the task handle and unref'ed when it's gc'ed. // is owned by the task handle and unref'ed when it's gc'ed.
assert( Self::__spawn(__ref(__tpack(f, variadic!())))
r#type(f) == "function",
concat!("function expected in argument 'f', got ", r#type(f)),
);
// we need two refs: one for the spawn call, and the other for the task handle. this is to
// ensure the task handle isn't gc'ed and the state table unref'ed before the spawn callback
// runs and puts the state table on the stack.
let state = __tpack(f, variadic!());
Self::__spawn(__ref(state), __ref(state))
} }
extern "Lua-C" fn __spawn(spawn_ref: c_int, handle_ref: c_int) -> lb_task { extern "Lua-C" fn __spawn(key: c_int) -> lb_task {
let handle = spawn(async move |cx| { let handle = spawn(async move |cx| {
// SAFETY: handle_ref is always unique, created in Self::spawn above. // SAFETY: key is always unique, created by __ref above.
let state = unsafe { cx.new_ref_unchecked(spawn_ref) }; let arg = unsafe { cx.new_ref_unchecked(key) };
let mut s = cx.guard(); let mut s = cx.guard();
s.resize(0); s.resize(0);
s.push(state); // this drops the state table ref, but the table is still on the stack s.push(&arg);
let narg = s.unpack(1, 1, None) - 1; // unpack the function and its args from the state table let narg = s.unpack(1, 1, None) - 1; // unpack the function and its args from the table
debug_assert!(s.slot(2).type_of() == Type::Function);
match s.call_async(narg, LUA_MULTRET).await { match s.call_async(narg, LUA_MULTRET).await {
Ok(nret) => { Ok(nret) => {
s.pack(1, nret); // pack the return values back into the state table s.pack(1, nret); // pack the return values back into the table
} }
Err(err) => { Err(err) => {
drop(s); drop(s);
cx.report_error(&err); cx.report_error(&err);
} }
} }
let _ = arg.into_raw(); // the original ref is owned by the task handle and unref'ed there
}); });
// spawn_ref is owned by the task handle and unref'ed there when the handle gets gc'ed lb_task {
lb_task::new(handle, handle_ref) handle: Some(handle),
__ref: key,
}
} }
} }
@ -84,30 +80,23 @@ impl lb_tasklib {
#[cdef] #[cdef]
pub struct lb_task { pub struct lb_task {
#[opaque] #[opaque]
handle: RefCell<Option<JoinHandle<()>>>, handle: Option<JoinHandle<()>>,
__ref: c_int, __ref: c_int,
} }
#[metatype] #[metatype]
impl lb_task { impl lb_task {
fn new(handle: JoinHandle<()>, ref_key: c_int) -> Self {
lb_task {
handle: RefCell::new(Some(handle)),
__ref: ref_key,
}
}
pub async extern "Lua" fn r#await(&self) -> many { pub async extern "Lua" fn r#await(&self) -> many {
self.__await(); self.__await();
let ret = __registry[self.__ref]; let ret = __registry[self.__ref];
__tunpack(ret, 1, ret.n) __tunpack(ret, 1, ret.n)
} }
async extern "Lua-C" fn __await(&self) { async extern "Lua-C" fn __await(&mut self) {
if let Some(handle) = self.handle.borrow_mut().take() { if let Some(handle) = self.handle.take() {
handle handle
.await // task handler should never panic .await
.unwrap_or_else(|err| std::panic::resume_unwind(err.into_panic())); .unwrap_or_else(|err| panic!("task handler panicked: {err}"));
} }
} }

View File

@ -1,188 +1,17 @@
local ok, net = pcall(require, "lb:net") local ok, net = pcall(require, "lb:net")
if not ok then return end if not ok then return end
describe("ipaddr", function()
test("invalid ipaddr throws", function()
assert(not pcall(net.ipaddr, "invalid ip"))
end)
test("comparison", function()
local a = net.ipaddr("10.0.0.1")
local b = net.ipaddr("10.0.0.1")
local c = net.ipaddr("10.0.0.2")
assert(a ~= nil and a ~= {} and a ~= "10.0.0.1" and a ~= 167772161)
assert(a == a and a == b and a ~= c and b ~= c and c == c and c ~= a)
assert(a <= b and a < c and a <= c and b < c and b <= c and a <= a and c <= c)
assert(not (a < b or a > b or a > c or b > c or a >= c or b >= c))
end)
test("tostring", function()
local ip = net.ipaddr("10.0.0.1")
assert(tostring(ip) == "10.0.0.1")
end)
end)
describe("tcp", function() describe("tcp", function()
describe("socket", function() describe("socket", function()
test("bind", function() test("bind", function()
local socket = net.bind_tcp("127.0.0.1") local socket = net.bind_tcp("127.0.0.1")
-- binds to the correct port -- binds to the correct port
assert(tostring(socket:local_addr():ip()) == "127.0.0.1") assert(tostring(socket:local_addr():ip()) == "127.0.0.1")
assert(socket:local_addr():port() ~= 0) assert(socket:local_addr():port() ~= 0)
-- should not be able to rebind socket -- should not be able to rebind socket
assert(not pcall(socket.bind, socket, net.socketaddr("127.0.0.1"))) assert(not pcall(socket.bind, socket, net.socketaddr("127.0.0.1")))
end) end)
test("options", function()
local socket = net.tcp()
-- keepalive
socket:set_keepalive(true)
assert(socket:keepalive() == true)
socket:set_keepalive(false)
assert(socket:keepalive() == false)
-- reuseaddr
socket:set_reuseaddr(true)
assert(socket:reuseaddr() == true)
socket:set_reuseaddr(false)
assert(socket:reuseaddr() == false)
-- reuseport not always supported on all platforms
-- sendbuf
socket:set_sendbuf(4096)
assert(socket:sendbuf() >= 4096)
assert(not pcall(socket.set_sendbuf, socket, 0))
assert(not pcall(socket.set_sendbuf, socket, -1))
-- recvbuf
socket:set_recvbuf(4096)
assert(socket:recvbuf() >= 4096)
assert(not pcall(socket.set_recvbuf, socket, 0))
assert(not pcall(socket.set_recvbuf, socket, -1))
-- linger
socket:set_linger(0)
assert(socket:linger() == 0)
socket:set_linger(2)
assert(math.abs(socket:linger() - 2) < 0.1)
socket:set_linger(-1)
assert(socket:linger() == 0)
-- nodelay
socket:set_nodelay(true)
assert(socket:nodelay() == true)
socket:set_nodelay(false)
assert(socket:nodelay() == false)
end)
test("can't use socket after conversion", function()
local socket = net.tcp()
socket:bind(net.socketaddr("127.0.0.1"))
socket:listen(10) -- convert to listener
assert(not pcall(socket.listen, socket, 10)) -- socket consumed
assert(not pcall(socket.local_addr, socket))
end)
end)
describe("stream", function()
test("no concurrent two reads/writes", function()
local listener = net.listen_tcp(net.localhost())
local client = net.connect_tcp(listener:local_addr())
local server = listener()
local reader = spawn(function()
assert(client:read(1) == nil) -- this should block first, then return nil from disconnection
end)
spawn(function()
assert(not pcall(client.read, client, 1)) -- this should fail, since the first task is still reading
end):await()
server:shutdown()
reader:await()
end)
test("allow concurrent read/write", function()
local listener = net.listen_tcp(net.localhost())
local client = net.connect_tcp(listener:local_addr())
local server = listener()
local reader = spawn(function()
assert(client:read(1) == nil) -- this should block first, then return nil from disconnection
end)
spawn(function()
client:write("hello") -- should be able to write while the first task is reading
end):await()
server:shutdown()
reader:await()
end)
test("stop reading from disconnected stream", function()
local listener = net.listen_tcp(net.localhost())
local client = net.connect_tcp(listener:local_addr())
local server = listener()
local reader = spawn(function()
while client:read(4) ~= nil do
end
assert(client:try_read(4) == nil)
assert(client:read_partial(4) == nil)
assert(client:read(4) == nil)
end)
for _ = 1, 10 do
assert(server:write("ping") == true)
end
sleep(100)
server:shutdown()
server = nil
collectgarbage()
reader:await()
end)
test("stop writing to disconnected stream", function()
local listener = net.listen_tcp(net.localhost())
local client = net.connect_tcp(listener:local_addr())
local server = listener()
local writer = spawn(function()
while client:write("pong") do
end
assert(client:try_write("pong") == nil)
assert(client:write_partial("pong") == nil)
assert(client:write("pong") == false)
end)
for _ = 1, 10 do
assert(server:read(4) == "pong")
end
sleep(100)
server:shutdown()
server = nil
collectgarbage()
writer:await()
end)
end)
describe("listener", function()
test("accept", function()
local listener = net.listen_tcp(net.localhost())
local addr = listener:local_addr()
local accepted = false
local client = net.tcp()
local accepted_stream
listener:on_accept(function(stream)
accepted = true
accepted_stream = stream
-- configure stream
stream:set_nodelay(true)
assert(stream:nodelay() == true)
end)
-- connect client
local client_stream = client:connect(addr)
local server_stream = listener()
assert(accepted)
assert(accepted_stream ~= nil)
-- check addresses
assert(server_stream:local_addr() ~= nil)
assert(server_stream:peer_addr() ~= nil)
assert(client_stream:local_addr() ~= nil)
assert(client_stream:peer_addr() ~= nil)
-- test data transfer
server_stream:write("hello")
local buf = client_stream:read(5)
assert(buf ~= nil and #buf == 5)
assert(buf == "hello")
-- shutdown
server_stream:shutdown()
client_stream:shutdown()
end)
end) end)
end) end)

View File

@ -36,13 +36,6 @@ describe("spawn", function()
assert(res.n == 4 and res[1] == 1 and res[2] == 2 and res[3] == nil and res[4] == 3) assert(res.n == 4 and res[1] == 1 and res[2] == 2 and res[3] == nil and res[4] == 3)
end) end)
test("handles invalid args", function()
assert(not pcall(spawn))
assert(not pcall(spawn, 123))
assert(not pcall(spawn, 1, 2, 3))
assert(not pcall(spawn, {}, 2, 3))
end)
test("callback args and results", function() test("callback args and results", function()
local res = table.pack(spawn(function(...) local res = table.pack(spawn(function(...)
assert(select("#", ...) == 5) assert(select("#", ...) == 5)
@ -53,25 +46,6 @@ describe("spawn", function()
assert(res.n == 3 and res[1] == 1 and res[2] == 3 and res[3] == nil) assert(res.n == 3 and res[1] == 1 and res[2] == 3 and res[3] == nil)
end) end)
test("large number of args", function()
local args = {}
for i = 1, 1000 do
args[i] = i
end
local res = table.pack(spawn(function(...)
return ...
end, table.unpack(args)):await())
assert(res.n == 1000 and res[1] == 1 and res[1000] == 1000)
end)
test("callback closes over upvalues", function()
local x = 42
local function f()
return x
end
assert(spawn(f):await() == 42)
end)
test("order is consistent", function() test("order is consistent", function()
-- all tasks spawned in one batch should be resumed in the spawn order -- all tasks spawned in one batch should be resumed in the spawn order
local tasks, nums = {}, {} local tasks, nums = {}, {}
@ -91,31 +65,9 @@ describe("spawn", function()
assert(nums[i] == i) assert(nums[i] == i)
end end
end) end)
test("nested spawns", function()
local result = {}
local function inner()
table.insert(result, "inner")
return "done"
end
local function outer()
table.insert(result, "outer")
local v = spawn(inner):await()
table.insert(result, v)
return v
end
local v = spawn(outer):await()
assert(v == "done")
assert(result[1] == "outer" and result[2] == "inner" and result[3] == "done")
end)
end) end)
describe("sleep", function() describe("sleep", function()
test("invalid arg", function()
assert(not pcall(task.sleep, "invalid"))
task.sleep(-1) -- negative sleep should just become 0ms
end)
test("sleep is asynchronous", function() test("sleep is asynchronous", function()
local value local value
spawn(function() spawn(function()
@ -125,44 +77,6 @@ describe("sleep", function()
task.sleep(100) -- implicit await: if it's synchronous, value wouldn't change task.sleep(100) -- implicit await: if it's synchronous, value wouldn't change
assert(value == "value") assert(value == "value")
end) end)
test("sleep in nested spawns", function()
local value1, value2, value3 = nil, nil, nil
local results = {}
local function inner()
task.sleep(30)
value1 = "set by inner"
table.insert(results, "inner")
return value1
end
local function middle()
task.sleep(20)
value2 = "set by middle"
local v = spawn(inner):await()
table.insert(results, v)
table.insert(results, "middle")
return v, value2
end
local function outer()
task.sleep(10)
value3 = "set by outer"
local v1, v2 = spawn(middle):await()
table.insert(results, v1)
table.insert(results, v2)
table.insert(results, "outer")
return v1, v2, value3
end
assert(value1 == nil and value2 == nil and value3 == nil)
local r1, r2, r3 = spawn(outer):await()
assert(r1 == "set by inner" and r2 == "set by middle" and r3 == "set by outer")
assert(value1 == "set by inner" and value2 == "set by middle" and value3 == "set by outer")
assert(results[1] == "inner")
assert(results[2] == "set by inner")
assert(results[3] == "middle")
assert(results[4] == "set by inner")
assert(results[5] == "set by middle")
assert(results[6] == "outer")
end)
end) end)
describe("task", function() describe("task", function()

View File

@ -156,12 +156,7 @@ impl lua_pollable {
pub fn is_valid(&self) -> bool { pub fn is_valid(&self) -> bool {
// TODO: signature check can currently read out-of-bounds if lua code for some reason yields // TODO: signature check can currently read out-of-bounds if lua code for some reason yields
// a cdata of size less than 8 bytes that is not a lua_future. there is no easy way to fix // a cdata of size less than 8 bytes that is not a lua_future. there is no easy way to fix
// AFAIK this because there is no way to find the size of a cdata payload using the C API. // afaik this because there is no way to find the size of a cdata payload using the C API.
// unfortunately we have to trust that the user won't do that right now.
//
// the only "saving grace" is that the user should never be running untrusted code anyway,
// because this whole project is based on the ffi library which should never be exposed to
// untrusted code in the first place.
self.sig == SIGNATURE self.sig == SIGNATURE
} }
} }

View File

@ -2,21 +2,24 @@
local LUA_REFNIL = -1 -- lib_aux.c local LUA_REFNIL = -1 -- lib_aux.c
local FREELIST_REF = 0 local FREELIST_REF = 0
local function __ref(value) local function __ref(value, t)
if rawequal(value, nil) then return LUA_REFNIL end if value == nil then return LUA_REFNIL end
local ref = __registry[FREELIST_REF] if t == nil then t = __registry end
local ref = t[FREELIST_REF]
if ref ~= nil and ref ~= 0 then if ref ~= nil and ref ~= 0 then
__registry[FREELIST_REF] = __registry[ref] t[FREELIST_REF] = t[ref]
else else
ref = rawlen(__registry) + 1 ref = #t + 1
end end
__registry[ref] = value t[ref] = value
return ref return ref
end end
local function __unref(ref) local function __unref(ref, t)
if ref > 0 then if ref < 0 then return nil end
__registry[ref] = __registry[FREELIST_REF] if t == nil then t = __registry end
__registry[FREELIST_REF] = ref local value = t[ref]
end t[ref] = t[FREELIST_REF]
t[FREELIST_REF] = ref
return value
end end

View File

@ -3,8 +3,8 @@ use crate::{
string::{DROP_BUFFER_FN, IS_UTF8_FN, lua_buffer}, string::{DROP_BUFFER_FN, IS_UTF8_FN, lua_buffer},
}; };
pub use luaffi_impl::*; pub use luaffi_impl::*;
use rustc_hash::FxHashSet;
use std::{ use std::{
collections::HashSet,
ffi::{c_double, c_float, c_void}, ffi::{c_double, c_float, c_void},
fmt::{self, Display, Formatter, Write}, fmt::{self, Display, Formatter, Write},
marker::PhantomData, marker::PhantomData,
@ -26,8 +26,8 @@ pub mod string;
// `ffi.keep(obj)`. // `ffi.keep(obj)`.
// //
// https://github.com/LuaJIT/LuaJIT/issues/1167 // https://github.com/LuaJIT/LuaJIT/issues/1167
pub(crate) const KEEP_FN: &str = "__lf_keep"; pub(crate) const KEEP_FN: &str = "luaffi_keep";
#[unsafe(export_name = "__lf_keep")] #[unsafe(export_name = "luaffi_keep")]
extern "C" fn __keep(_ptr: *const c_void) {} extern "C" fn __keep(_ptr: *const c_void) {}
export![__keep]; export![__keep];
@ -128,8 +128,8 @@ fn cache_local(f: &mut Formatter, list: &[(&str, &str)]) -> fmt::Result {
#[derive(Debug, Clone, Default)] #[derive(Debug, Clone, Default)]
pub struct Registry { pub struct Registry {
types: FxHashSet<String>, types: HashSet<String>,
decls: FxHashSet<String>, decls: HashSet<String>,
cdef: String, cdef: String,
lua: String, lua: String,
} }
@ -458,7 +458,6 @@ pub struct MetatypeFunctionBuilder<'r, 'm> {
cargs: String, // C call arguments cargs: String, // C call arguments
prelude: String, // lua function body prelude prelude: String, // lua function body prelude
postlude: String, // lua function body postlude postlude: String, // lua function body postlude
is_eqmm: bool,
} }
impl<'r, 'm> MetatypeFunctionBuilder<'r, 'm> { impl<'r, 'm> MetatypeFunctionBuilder<'r, 'm> {
@ -470,16 +469,15 @@ impl<'r, 'm> MetatypeFunctionBuilder<'r, 'm> {
cargs: String::new(), cargs: String::new(),
prelude: String::new(), prelude: String::new(),
postlude: String::new(), postlude: String::new(),
is_eqmm: false,
} }
} }
pub fn set_eqmm(&mut self, eq: bool) -> &mut Self {
self.is_eqmm = eq; // are we building an __eq metamethod?
self
}
pub fn param<T: FromFfi>(&mut self, name: impl Display) -> &mut Self { pub fn param<T: FromFfi>(&mut self, name: impl Display) -> &mut Self {
assert!(
T::From::ty() != TypeType::Void,
"cannot declare void parameter"
);
let Self { let Self {
metatype: MetatypeBuilder { reg, .. }, metatype: MetatypeBuilder { reg, .. },
lparams, lparams,
@ -490,14 +488,6 @@ impl<'r, 'm> MetatypeFunctionBuilder<'r, 'm> {
.. ..
} = self; } = self;
assert!(
T::From::ty() != TypeType::Void,
"cannot declare void parameter"
);
// should already be prevented by #[metatype] macro
debug_assert!(!name.to_string().starts_with("__"));
reg.include::<T::From>(); reg.include::<T::From>();
(!lparams.is_empty()).then(|| lparams.push_str(", ")); (!lparams.is_empty()).then(|| lparams.push_str(", "));
@ -517,26 +507,6 @@ impl<'r, 'm> MetatypeFunctionBuilder<'r, 'm> {
self self
} }
pub fn param_ctchecked<T: FromFfi, U: Type + Metatype<Target = U>>(
&mut self,
name: impl Display,
) -> &mut Self {
let ct = U::name();
if self.is_eqmm {
// special case for building __eq metamethods which are expected to return false if the
// arguments aren't of the expected type, because otherwise the `==` and `~=` operators
// could throw instead of returning false.
write!(
self.prelude,
r#"if not __istype(__ct.{ct}, {name}) then return false; end; "#
)
} else {
write!(self.prelude, r#"assert(__istype(__ct.{ct}, {name})); "#)
}
.unwrap();
self.param::<T>(name)
}
pub fn param_str( pub fn param_str(
&mut self, &mut self,
name: impl Display, name: impl Display,
@ -547,9 +517,7 @@ impl<'r, 'm> MetatypeFunctionBuilder<'r, 'm> {
// //
// this passes one lua `string` argument as two C `const uint8_t *ptr` and `uintptr_t len` // this passes one lua `string` argument as two C `const uint8_t *ptr` and `uintptr_t len`
// arguments, bypassing the slower generic `&[u8]: FromFfi` path which constructs a // arguments, bypassing the slower generic `&[u8]: FromFfi` path which constructs a
// temporary cdata to pass the string and its length in one argument. this is used when the // temporary cdata to pass the string and its length in one argument.
// #[metatype] macro detects a string-like parameter, currently defined to be &[u8], &str,
// &BStr (from the bstr crate), or those types wrapped in Option<T>.
let Self { let Self {
lparams, lparams,
cparams, cparams,
@ -558,9 +526,6 @@ impl<'r, 'm> MetatypeFunctionBuilder<'r, 'm> {
.. ..
} = self; } = self;
// should already be prevented by #[metatype] macro
debug_assert!(!name.to_string().starts_with("__"));
let param_ptr = <*const u8>::cdecl(&name); let param_ptr = <*const u8>::cdecl(&name);
let param_len = usize::cdecl(format!("{name}_len")); let param_len = usize::cdecl(format!("{name}_len"));
@ -571,11 +536,7 @@ impl<'r, 'm> MetatypeFunctionBuilder<'r, 'm> {
write!(lparams, "{name}").unwrap(); write!(lparams, "{name}").unwrap();
write!(cparams, "{param_ptr}, {param_len}").unwrap(); write!(cparams, "{param_ptr}, {param_len}").unwrap();
write!(cargs, "{name}, __{name}_len").unwrap(); write!(cargs, "{name}, __{name}_len").unwrap();
write!( write!(prelude, "local __{name}_len = 0; if {name} ~= nil then ").unwrap();
prelude,
"local __{name}_len = 0; if not rawequal({name}, nil) then "
)
.unwrap();
write!(prelude, r#"assert(type({name}) == "string", "string expected in argument '{name}', got " .. type({name})); "#).unwrap(); write!(prelude, r#"assert(type({name}) == "string", "string expected in argument '{name}', got " .. type({name})); "#).unwrap();
write!(prelude, r#"__{name}_len = #{name}; "#).unwrap(); write!(prelude, r#"__{name}_len = #{name}; "#).unwrap();
@ -616,22 +577,22 @@ impl<'r, 'm> MetatypeFunctionBuilder<'r, 'm> {
if T::Into::ty() == TypeType::Void { if T::Into::ty() == TypeType::Void {
write!(lua, "__C.{func}({cargs}); {postlude}end").unwrap(); write!(lua, "__C.{func}({cargs}); {postlude}end").unwrap();
} else { } else {
let check = T::postlude("ret"); let check = T::postlude("__ret");
write!(lua, "local ret = __C.{func}({cargs}); ").unwrap(); write!(lua, "local __ret = __C.{func}({cargs}); ").unwrap();
write!(lua, "{check}{postlude}return ret; end").unwrap(); write!(lua, "{check}{postlude}return __ret; end").unwrap();
} }
writeln!(cdef, "{};", T::Into::cdecl(display!("{func}({cparams})"))).unwrap(); writeln!(cdef, "{};", T::Into::cdecl(display!("{func}({cparams})"))).unwrap();
} }
FfiReturnConvention::ByOutParam => { FfiReturnConvention::ByOutParam => {
let ct = T::Into::name(); let ct = T::Into::name();
let check = T::postlude("__ret"); let check = T::postlude("__out");
write!(lua, "local __ret = __new(__ct.{ct}); __C.{func}(__ret").unwrap(); write!(lua, "local __out = __new(__ct.{ct}); __C.{func}(__out").unwrap();
if !cargs.is_empty() { if !cargs.is_empty() {
write!(lua, ", {cargs}").unwrap(); write!(lua, ", {cargs}").unwrap();
} }
write!(lua, "); {check}{postlude}return __ret; end").unwrap(); write!(lua, "); {check}{postlude}return __out; end").unwrap();
write!(cdef, "void {func}({}", <*mut T::Into>::cdecl("__out")).unwrap(); write!(cdef, "void {func}({}", <*mut T::Into>::cdecl("out")).unwrap();
if !cparams.is_empty() { if !cparams.is_empty() {
write!(cdef, ", {cparams}").unwrap(); write!(cdef, ", {cparams}").unwrap();
} }
@ -809,7 +770,8 @@ macro_rules! impl_bigint_intoabi {
fn postlude(ret: &str) -> impl Display { fn postlude(ret: &str) -> impl Display {
// this isn't "correct" per se, but it's much more ergonomic to work with numbers in // this isn't "correct" per se, but it's much more ergonomic to work with numbers in
// lua than with long longs wrapped in cdata. we gracefully accept the loss of // lua than with long longs wrapped in cdata. we gracefully accept the loss of
// precision here and that 53 bits of precision for big integers are enough. // precision here and that 53 bits of precision for big integers are enough. (the
// vain of Lua 5.3 integer subtype ;D )
display!("{ret} = tonumber({ret}); ") display!("{ret} = tonumber({ret}); ")
} }
} }
@ -865,6 +827,22 @@ impl_ptr!(*mut T, true);
impl_ptr!(Option<&T>, false); impl_ptr!(Option<&T>, false);
impl_ptr!(Option<&mut T>, true); impl_ptr!(Option<&mut T>, true);
macro_rules! impl_ptr_annotation {
($ty:ty) => {
impl<T> Annotation for $ty
where
T: Annotation,
{
fn annotation() -> impl Display {
"lightuserdata"
}
}
};
}
impl_ptr_annotation!(*const T);
impl_ptr_annotation!(*mut T);
macro_rules! impl_ref_annotation { macro_rules! impl_ref_annotation {
($ty:ty) => { ($ty:ty) => {
impl<T> Annotation for $ty impl<T> Annotation for $ty
@ -941,14 +919,34 @@ impl_ptr_intoabi!(Option<&'static T>);
#[cfg(feature = "option_ref_abi")] #[cfg(feature = "option_ref_abi")]
impl_ptr_intoabi!(Option<&'static mut T>); impl_ptr_intoabi!(Option<&'static mut T>);
unsafe impl<'s, T> FromFfi for &'s T //
where // SAFETY: `FromFfi` for *mutable* references is safe because it is guaranteed that no two Rust code
// called via FFI can be running at the same time on the same OS thread (no Lua reentrancy).
//
// i.e. The call stack will always look something like this:
//
// * Runtime (LuaJIT/Rust) -> Lua (via C) -> Rust (via FFI): This is SAFE and the only use case we
// support. All references (mutable or not) to Rust user objects will be dropped before
// returning to Lua.
//
// * Runtime (LuaJIT/Rust) -> Lua (via C) -> Rust (via FFI) -> Lua (via callback): This is UNSAFE
// because we cannot prevent the Lua callback from calling back into Rust code via FFI which
// could violate exclusive borrow semantics. This is prevented by not implementing `FromFfi` for
// function pointers (see below).
//
// The runtime does not keep any references to Rust user objects boxed in cdata (futures are the
// only exception; their ownership is transferred to the runtime via yield).
//
macro_rules! impl_ref_fromabi {
($ty:ty) => {
unsafe impl<'s, T> FromFfi for $ty
where
T: Type, T: Type,
{ {
type From = Option<&'s T>; type From = Option<$ty>;
fn prelude(arg: &str) -> impl Display { fn prelude(arg: &str) -> impl Display {
display!(r#"assert(not rawequal({arg}, nil), "argument '{arg}' cannot be nil"); "#) display!(r#"assert({arg} ~= nil, "argument '{arg}' cannot be nil"); "#)
} }
fn convert(from: Self::From) -> Self { fn convert(from: Self::From) -> Self {
@ -961,23 +959,35 @@ where
unsafe { from.unwrap_unchecked() } unsafe { from.unwrap_unchecked() }
} }
}
};
} }
impl_ref_fromabi!(&'s T);
impl_ref_fromabi!(&'s mut T);
// //
// SAFETY: `IntoFfi` only for 'static references because we cannot guarantee that the pointer will // SAFETY: `IntoFfi` only for 'static references because we cannot guarantee that the pointer will
// not outlive the pointee otherwise. // not outlive the pointee otherwise.
// //
unsafe impl<T> IntoFfi for &'static T macro_rules! impl_ref_intoabi {
where ($ty:ty) => {
unsafe impl<T> IntoFfi for $ty
where
T: Type, T: Type,
{ {
type Into = Self; type Into = Self;
fn convert(self) -> Self::Into { fn convert(self) -> Self::Into {
self self
} }
}
};
} }
impl_ref_intoabi!(&'static T);
impl_ref_intoabi!(&'static mut T);
// //
// SAFETY: No `FromFfi` and `IntoFfi` for arrays because passing or returning them by value is not a // SAFETY: No `FromFfi` and `IntoFfi` for arrays because passing or returning them by value is not a
// thing in C (they are just pointers). // thing in C (they are just pointers).
@ -1052,6 +1062,13 @@ macro_rules! impl_externcfn {
}; };
($ty:ident, fn($($arg:ident),*) -> $ret:ident) => { ($ty:ident, fn($($arg:ident),*) -> $ret:ident) => {
//
// SAFETY: No `FromFfi` for function pointers because of borrow safety invariants (see above
// in `&mut T`).
//
// We also can't implement `IntoFfi` because we can't call `FromFfi` and `IntoFfi` for the
// function's respective argument and return values.
//
unsafe impl<$($arg,)* $ret> Type for $ty<($($arg,)*), $ret> unsafe impl<$($arg,)* $ret> Type for $ty<($($arg,)*), $ret>
where where
$($arg: Type,)* $($arg: Type,)*
@ -1106,24 +1123,12 @@ impl_externcfn!(fn(A, B, C, D, E, F, G) -> H);
impl_externcfn!(fn(A, B, C, D, E, F, G, H) -> I); impl_externcfn!(fn(A, B, C, D, E, F, G, H) -> I);
impl_externcfn!(fn(A, B, C, D, E, F, G, H, I) -> J); impl_externcfn!(fn(A, B, C, D, E, F, G, H, I) -> J);
impl<'s> Annotation for &'s [u8] {
fn annotation() -> impl Display {
"string"
}
}
impl<'s> Annotation for &'s str { impl<'s> Annotation for &'s str {
fn annotation() -> impl Display { fn annotation() -> impl Display {
"string" "string"
} }
} }
impl Annotation for Vec<u8> {
fn annotation() -> impl Display {
"string"
}
}
impl Annotation for String { impl Annotation for String {
fn annotation() -> impl Display { fn annotation() -> impl Display {
"string" "string"

View File

@ -6,16 +6,16 @@ use bstr::{BStr, BString};
use luaffi_impl::{cdef, metatype}; use luaffi_impl::{cdef, metatype};
use std::{fmt::Display, mem::ManuallyDrop, slice}; use std::{fmt::Display, mem::ManuallyDrop, slice};
pub(crate) const IS_UTF8_FN: &str = "__lf_is_utf8"; pub(crate) const IS_UTF8_FN: &str = "luaffi_is_utf8";
pub(crate) const DROP_BUFFER_FN: &str = "__lf_drop_buffer"; pub(crate) const DROP_BUFFER_FN: &str = "luaffi_drop_buffer";
#[unsafe(export_name = "__lf_is_utf8")] #[unsafe(export_name = "luaffi_is_utf8")]
unsafe extern "C" fn __is_utf8(ptr: *const u8, len: usize) -> bool { unsafe extern "C" fn __is_utf8(ptr: *const u8, len: usize) -> bool {
debug_assert!(!ptr.is_null()); debug_assert!(!ptr.is_null());
simdutf8::basic::from_utf8(unsafe { slice::from_raw_parts(ptr, len) }).is_ok() simdutf8::basic::from_utf8(unsafe { slice::from_raw_parts(ptr, len) }).is_ok()
} }
#[unsafe(export_name = "__lf_drop_buffer")] #[unsafe(export_name = "luaffi_drop_buffer")]
unsafe extern "C" fn __drop_buffer(buf: *mut lua_buffer) { unsafe extern "C" fn __drop_buffer(buf: *mut lua_buffer) {
debug_assert!(!buf.is_null()); debug_assert!(!buf.is_null());
debug_assert!(!unsafe { (*buf).__ptr.is_null() }); debug_assert!(!unsafe { (*buf).__ptr.is_null() });

View File

@ -101,7 +101,7 @@ fn generate_cdef_structure(str: &mut ItemStruct) -> Result<TokenStream> {
let ffi = ffi_crate(); let ffi = ffi_crate();
let ty = &str.ident; let ty = &str.ident;
let build = generate_cdef_build(&parse_cfields(&mut str.fields)?)?; let build = generate_cdef_build(&get_cfields(&mut str.fields)?)?;
Ok(quote!( Ok(quote!(
unsafe impl #ffi::Cdef for #ty { unsafe impl #ffi::Cdef for #ty {
@ -123,7 +123,7 @@ fn generate_cdef_enum(enu: &mut ItemEnum) -> Result<TokenStream> {
.variants .variants
.iter_mut() .iter_mut()
.map(|variant| { .map(|variant| {
let build = generate_cdef_build(&parse_cfields(&mut variant.fields)?)?; let build = generate_cdef_build(&get_cfields(&mut variant.fields)?)?;
Ok(quote!(b.inner_struct(|b| { #build }))) Ok(quote!(b.inner_struct(|b| { #build })))
}) })
.collect::<Result<Vec<_>>>()?; .collect::<Result<Vec<_>>>()?;
@ -148,7 +148,7 @@ struct CFieldAttrs {
opaque: bool, opaque: bool,
} }
fn parse_cfields(fields: &mut Fields) -> Result<Vec<CField>> { fn get_cfields(fields: &mut Fields) -> Result<Vec<CField>> {
match fields { match fields {
Fields::Named(fields) => fields.named.iter_mut(), Fields::Named(fields) => fields.named.iter_mut(),
Fields::Unnamed(fields) => fields.unnamed.iter_mut(), Fields::Unnamed(fields) => fields.unnamed.iter_mut(),

View File

@ -1,6 +1,6 @@
use crate::utils::{ use crate::utils::{
StringLike, ffi_crate, is_optionlike, is_primitivelike, is_resultlike, is_stringlike, is_unit, StringLike, ffi_crate, is_optionlike, is_primitivelike, is_stringlike, is_unit, pat_ident,
pat_ident, syn_assert, syn_error, ty_name, syn_assert, syn_error, ty_name,
}; };
use darling::FromMeta; use darling::FromMeta;
use proc_macro2::TokenStream; use proc_macro2::TokenStream;
@ -78,7 +78,7 @@ fn generate_impls(_args: &Args, imp: &mut ItemImpl) -> Result<TokenStream> {
}); });
// process extern "Lua-C" ffi functions // process extern "Lua-C" ffi functions
for func in parse_ffi_functions(imp)? { for func in get_ffi_functions(imp)? {
if let Some(mm) = func.attrs.metamethod { if let Some(mm) = func.attrs.metamethod {
syn_assert!( syn_assert!(
mms.insert(mm), mms.insert(mm),
@ -87,11 +87,11 @@ fn generate_impls(_args: &Args, imp: &mut ItemImpl) -> Result<TokenStream> {
); );
} }
generate_ffi_function(&mut registry, &func)?; add_ffi_function(&mut registry, &func)?;
} }
// process extern "Lua" lua functions // process extern "Lua" lua functions
for func in parse_lua_functions(imp)? { for func in get_lua_functions(imp)? {
if let Some(mm) = func.attrs.metamethod { if let Some(mm) = func.attrs.metamethod {
syn_assert!( syn_assert!(
mms.insert(mm), mms.insert(mm),
@ -100,10 +100,10 @@ fn generate_impls(_args: &Args, imp: &mut ItemImpl) -> Result<TokenStream> {
); );
} }
if func.attrs.metamethod == Some(Metamethod::Gc) { if let Some(Metamethod::Gc) = func.attrs.metamethod {
lua_drop = Some(func); lua_drop = Some(func);
} else { } else {
generate_lua_function(&mut registry, &func)?; add_lua_function(&mut registry, &func)?;
} }
} }
@ -155,25 +155,6 @@ enum Metamethod {
Ipairs, Ipairs,
} }
impl Metamethod {
fn narg(&self) -> Option<usize> {
Some(match *self {
Self::Eq
| Self::Lt
| Self::Le
| Self::Concat
| Self::Add
| Self::Sub
| Self::Mul
| Self::Div
| Self::Mod
| Self::Pow => 2,
Self::Gc | Self::Len | Self::Unm | Self::ToString | Self::Pairs | Self::Ipairs => 1,
Self::Call | Self::New => return None,
})
}
}
impl TryFrom<&Ident> for Metamethod { impl TryFrom<&Ident> for Metamethod {
type Error = Error; type Error = Error;
@ -249,8 +230,8 @@ struct FfiFunctionAttrs {
metamethod: Option<Metamethod>, metamethod: Option<Metamethod>,
} }
fn parse_ffi_functions(imp: &mut ItemImpl) -> Result<Vec<FfiFunction>> { fn get_ffi_functions(imp: &mut ItemImpl) -> Result<Vec<FfiFunction>> {
let mut parsed = vec![]; let mut funcs = vec![];
for item in imp.items.iter_mut() { for item in imp.items.iter_mut() {
if let ImplItem::Fn(func) = item if let ImplItem::Fn(func) = item
@ -304,30 +285,21 @@ fn parse_ffi_functions(imp: &mut ItemImpl) -> Result<Vec<FfiFunction>> {
} }
let attrs = parse_ffi_function_attrs(&mut func.attrs)?; let attrs = parse_ffi_function_attrs(&mut func.attrs)?;
attrs.metamethod.map(|mm| document_metamethod(func, mm));
func.sig.asyncness.is_some().then(|| document_async(func));
document_ffi_function(func);
if let Some(mm) = attrs.metamethod funcs.push(FfiFunction {
&& let Some(narg) = mm.narg()
{
syn_assert!(
params.len() == narg,
func.sig.inputs,
"expected {narg} parameters for `{mm}` metamethod"
);
}
parsed.push(FfiFunction {
name: func.sig.ident.clone(), name: func.sig.ident.clone(),
params, params,
ret, ret,
attrs, attrs,
is_async: func.sig.asyncness.is_some(), is_async: func.sig.asyncness.is_some(),
}); });
document_ffi_function(func, parsed.last().unwrap());
} }
} }
Ok(parsed) Ok(funcs)
} }
fn parse_ffi_function_attrs(attrs: &mut Vec<Attribute>) -> Result<FfiFunctionAttrs> { fn parse_ffi_function_attrs(attrs: &mut Vec<Attribute>) -> Result<FfiFunctionAttrs> {
@ -335,10 +307,14 @@ fn parse_ffi_function_attrs(attrs: &mut Vec<Attribute>) -> Result<FfiFunctionAtt
let mut i = 0; let mut i = 0;
while let Some(attr) = attrs.get(i) { while let Some(attr) = attrs.get(i) {
if let Some(name) = attr.path().get_ident() if let Some(name) = attr.path().get_ident()
&& let Ok(mm) = Metamethod::try_from(&name.unraw()) && let Ok(method) = Metamethod::try_from(&name.unraw())
{ {
syn_assert!(mm != Metamethod::Gc, attr, "implement `Drop` instead"); match method {
parsed.metamethod = Some(mm); Metamethod::Gc => syn_error!(attr, "implement `Drop` instead"),
_ => {}
}
parsed.metamethod = Some(method);
attrs.remove(i); attrs.remove(i);
} else { } else {
i += 1; i += 1;
@ -397,7 +373,7 @@ fn get_ffi_ret_type(ty: &Type) -> FfiReturnType {
} }
} }
fn generate_ffi_function(registry: &mut Registry, func: &FfiFunction) -> Result<()> { fn add_ffi_function(registry: &mut Registry, func: &FfiFunction) -> Result<()> {
let ffi = ffi_crate(); let ffi = ffi_crate();
let ty = &registry.ty; let ty = &registry.ty;
let func_name = &func.name; let func_name = &func.name;
@ -416,23 +392,13 @@ fn generate_ffi_function(registry: &mut Registry, func: &FfiFunction) -> Result<
let mut asserts = vec![]; // compile-time asserts let mut asserts = vec![]; // compile-time asserts
let mut build = vec![]; // ffi builder body let mut build = vec![]; // ffi builder body
match func.attrs.metamethod { // for __new metamethods, ignore the first argument (ctype of self, for which there is no
Some(Metamethod::New) => { // equivalent in C)
// for __new metamethods, ignore the first argument (ctype of self, for which there is if func.attrs.metamethod == Some(Metamethod::New) {
// no equivalent in C)
build.push(quote!( build.push(quote!(
b.param_ignored(); b.param_ignored();
)); ));
} }
Some(Metamethod::Eq) => {
// for __eq metamethods, set special eq mode to return false on argument type mismatch
// instead of throwing
build.push(quote!(
b.set_eqmm(true);
));
}
_ => {}
}
for (i, (name, func_param)) in func_params.iter().enumerate() { for (i, (name, func_param)) in func_params.iter().enumerate() {
let span = func_param.span(); let span = func_param.span();
@ -447,18 +413,9 @@ fn generate_ffi_function(registry: &mut Registry, func: &FfiFunction) -> Result<
func_args.push(quote_spanned!(span => func_args.push(quote_spanned!(span =>
<#func_param as #ffi::FromFfi>::convert(#shim_param) <#func_param as #ffi::FromFfi>::convert(#shim_param)
)); ));
build.push(if func.attrs.metamethod == Some(Metamethod::Eq) { build.push(quote_spanned!(span =>
quote_spanned!(span =>
// preliminary `return false` for type mismatch in __eq mode. we just check
// against the ctype of Self because that's what __eq should be comparing
// anyway.
b.param_ctchecked::<#func_param, Self>(#name);
)
} else {
quote_spanned!(span =>
b.param::<#func_param>(#name); b.param::<#func_param>(#name);
) ));
});
} }
ty @ (FfiParameterType::StringLike(str) | FfiParameterType::OptionStringLike(str)) => { ty @ (FfiParameterType::StringLike(str) | FfiParameterType::OptionStringLike(str)) => {
let shim_param_len = format_ident!("arg{i}_len"); let shim_param_len = format_ident!("arg{i}_len");
@ -540,21 +497,19 @@ fn generate_ffi_function(registry: &mut Registry, func: &FfiFunction) -> Result<
} }
}; };
build.push(if func.is_async { build.push({
// we can't name the future from an async function, so instead we provide a closure with a
// "dummy call" that never gets called so that the builder can infer the return type from
// the closure
let infer_args = iter::repeat_n(quote!(::std::unreachable!()), func_params.len()); let infer_args = iter::repeat_n(quote!(::std::unreachable!()), func_params.len());
quote!(b.call_inferred(#c_name, || { let infer = if func.is_async {
#ffi::future::lua_future::new(Self::#func_name(#(#infer_args),*)) quote!(|| #ffi::future::lua_future::new(Self::#func_name(#(#infer_args),*)))
});)
} else { } else {
quote!(b.call::<#func_ret>(#c_name);) quote!(|| Self::#func_name(#(#infer_args),*))
};
quote!(b.call_inferred(#c_name, #infer);)
}); });
registry.build.push(quote!(#(::std::assert!(#asserts);)*)); registry.build.push(quote!(#(::std::assert!(#asserts);)*));
registry.build.push(match func.attrs.metamethod { registry.build.push(match func.attrs.metamethod {
Some(mm) => quote!(b.metatable(#mm, |b| { #(#build)* });), Some(ref mm) => quote!(b.metatable(#mm, |b| { #(#build)* });),
None => quote!(b.index(#lua_name, |b| { #(#build)* });), None => quote!(b.index(#lua_name, |b| { #(#build)* });),
}); });
@ -592,8 +547,8 @@ struct LuaFunctionAttrs {
metamethod: Option<Metamethod>, metamethod: Option<Metamethod>,
} }
fn parse_lua_functions(imp: &mut ItemImpl) -> Result<Vec<LuaFunction>> { fn get_lua_functions(imp: &mut ItemImpl) -> Result<Vec<LuaFunction>> {
let mut parsed = vec![]; let mut funcs = vec![];
for item in imp.items.iter_mut() { for item in imp.items.iter_mut() {
if let ImplItem::Fn(func) = item if let ImplItem::Fn(func) = item
@ -624,53 +579,26 @@ fn parse_lua_functions(imp: &mut ItemImpl) -> Result<Vec<LuaFunction>> {
.collect(); .collect();
if let Some(ref variadic) = func.sig.variadic { if let Some(ref variadic) = func.sig.variadic {
// need to transform variadic to the `varidic!()` luaify builtin macro because we params.push(parse_quote_spanned!(variadic.span() => variadic!())); // luaify builtin macro
// luaify a closure below, and closures don't have variadics.
params.push(parse_quote_spanned!(variadic.span() => variadic!()));
} }
let attrs = parse_lua_function_attrs(&mut func.attrs)?; let attrs = parse_lua_function_attrs(&mut func.attrs)?;
attrs.metamethod.map(|mm| document_metamethod(func, mm));
func.sig.asyncness.is_some().then(|| document_async(func));
document_lua_function(func);
if let Some(mm) = attrs.metamethod funcs.push(LuaFunction {
&& let Some(narg) = mm.narg()
{
syn_assert!(
params.len() == narg,
func.sig.inputs,
"expected {narg} parameters for `{mm}` metamethod"
);
}
parsed.push(LuaFunction {
name: func.sig.ident.clone(), name: func.sig.ident.clone(),
params, params,
body: func.block.clone(), body: func.block.clone(),
attrs, attrs,
}); });
document_lua_function(func, parsed.last().unwrap());
stub_lua_function(func)?; stub_lua_function(func)?;
} }
} }
Ok(parsed) Ok(funcs)
}
fn parse_lua_function_attrs(attrs: &mut Vec<Attribute>) -> Result<LuaFunctionAttrs> {
let mut parsed = LuaFunctionAttrs::default();
let mut i = 0;
while let Some(attr) = attrs.get(i) {
if let Some(name) = attr.path().get_ident()
&& let Ok(mm) = Metamethod::try_from(&name.unraw())
{
parsed.metamethod = Some(mm);
attrs.remove(i);
} else {
i += 1;
}
}
Ok(parsed)
} }
fn stub_lua_function(func: &mut ImplItemFn) -> Result<()> { fn stub_lua_function(func: &mut ImplItemFn) -> Result<()> {
@ -721,7 +649,24 @@ fn stub_lua_function(func: &mut ImplItemFn) -> Result<()> {
Ok(()) Ok(())
} }
fn generate_lua_function(registry: &mut Registry, func: &LuaFunction) -> Result<()> { fn parse_lua_function_attrs(attrs: &mut Vec<Attribute>) -> Result<LuaFunctionAttrs> {
let mut parsed = LuaFunctionAttrs::default();
let mut i = 0;
while let Some(attr) = attrs.get(i) {
if let Some(name) = attr.path().get_ident()
&& let Ok(method) = Metamethod::try_from(&name.unraw())
{
parsed.metamethod = Some(method);
attrs.remove(i);
} else {
i += 1;
}
}
Ok(parsed)
}
fn add_lua_function(registry: &mut Registry, func: &LuaFunction) -> Result<()> {
let ffi = ffi_crate(); let ffi = ffi_crate();
let luaify = quote!(#ffi::__internal::luaify!); let luaify = quote!(#ffi::__internal::luaify!);
let func_name = &func.name; let func_name = &func.name;
@ -730,7 +675,7 @@ fn generate_lua_function(registry: &mut Registry, func: &LuaFunction) -> Result<
let name = func_name.unraw().to_string(); let name = func_name.unraw().to_string();
registry.build.push(match func.attrs.metamethod { registry.build.push(match func.attrs.metamethod {
Some(mm) => quote!(b.metatable_raw(#mm, #luaify(|#(#params),*| #body));), Some(ref mm) => quote!(b.metatable_raw(#mm, #luaify(|#(#params),*| #body));),
None => quote!(b.index_raw(#name, #luaify(|#(#params),*| #body));), None => quote!(b.index_raw(#name, #luaify(|#(#params),*| #body));),
}); });
@ -756,10 +701,16 @@ fn inject_merged_drop(registry: &mut Registry, lua: Option<&LuaFunction>) -> Res
let luaify = quote!(#ffi::__internal::luaify!); let luaify = quote!(#ffi::__internal::luaify!);
let ty = &registry.ty; let ty = &registry.ty;
let shim_name = format_ident!("__ffi_drop"); let shim_name = format_ident!("__ffi_drop");
let c_name = format_ident!("__{}_drop", ty.unraw()); let c_name = format_ident!("{}_drop", ty.unraw());
let c_name_str = c_name.to_string(); let c_name_str = c_name.to_string();
if let Some(lua) = lua { if let Some(lua) = lua {
syn_assert!(
lua.params.len() == 1,
lua.name,
"finaliser must take exactly one parameter"
);
let param = pat_ident(&lua.params[0])?; let param = pat_ident(&lua.params[0])?;
let body = &lua.body; let body = &lua.body;
@ -799,149 +750,53 @@ fn inject_merged_drop(registry: &mut Registry, lua: Option<&LuaFunction>) -> Res
Ok(()) Ok(())
} }
// the transparency makes it work in dark mode too fn document_ffi_function(func: &mut ImplItemFn) {
// const FFI_COLOR: &str = "rgba(255, 222, 118, 0.3)"; // yellow func.attrs.insert(0, parse_quote!(#[doc =
// const LUA_COLOR: &str = "rgba(188, 222, 255, 0.3)"; // blue r#"<span
const FALLIBLE_COLOR: &str = "rgba(255, 168, 168, 0.3)"; // red class="stab"
const ASYNC_COLOR: &str = "rgba(188, 222, 255, 0.3)"; // blue title="This function is implemented in Rust and called via FFI."
const METAMETHOD_COLOR: &str = "rgba(255, 222, 118, 0.3)"; // yellow style="float: right; background: #fff5d6; font-weight: 500; margin-left: 3px; padding-left: 5px; padding-right: 5px;"
>FFI</span>"#
struct StabConfig { ]));
text: &'static str,
desc: &'static str,
color: &'static str,
strong: bool,
} }
fn generate_stab( fn document_lua_function(func: &mut ImplItemFn) {
StabConfig { func.attrs.insert(0, parse_quote!(#[doc =
text, r#"<span
desc, class="stab"
color, title="This function is implemented in Lua."
strong, style="float: right; background: #ebf5ff; font-weight: 500; margin-left: 3px; padding-left: 5px; padding-right: 5px;"
}: StabConfig, >Lua</span>"#
) -> Attribute { ]));
let attrs = vec![
("class", "stab".into()),
("title", desc.into()),
("aria-label", desc.into()),
("style", {
let mut style = vec![
("float", "right"),
("margin", "1px"),
("margin-left", "4px"),
("padding-left", "5px"),
("padding-right", "5px"),
("background", color),
];
if strong {
style.push(("font-weight", "600"));
}
style
.into_iter()
.map(|(k, v)| format!("{k}: {v}"))
.collect::<Vec<_>>()
.join("; ")
}),
];
let attrs = attrs
.into_iter()
.map(|(k, v)| format!(r#"{k}="{v}""#))
.collect::<Vec<_>>()
.join(" ");
let tag = format!(r#"<span {attrs}>{text}</span>"#);
parse_quote!(#[doc = #tag])
} }
fn document_ffi_function(item: &mut ImplItemFn, func: &FfiFunction) { fn document_async(func: &mut ImplItemFn) {
let mut attrs = vec![]; func.attrs.insert(0, parse_quote!(#[doc =
r#"<span
// attrs.push(generate_stab(StabConfig { class="stab"
// text: "FFI", title="This function is asynchronous and will yield the calling thread."
// desc: "This function is implemented in Rust and called via FFI.", style="float: right; background: #ebf5ff; margin-left: 3px; padding-left: 5px; padding-right: 5px;"
// color: FFI_COLOR, >Async</span>"#
// strong: true, ]));
// }));
generate_stab_fallible(item).map(|stab| attrs.push(stab));
generate_stab_async(item).map(|stab| attrs.push(stab));
generate_stab_metamethod(func.attrs.metamethod).map(|(stab, help)| {
attrs.push(stab);
item.attrs.push(help);
});
item.attrs.splice(0..0, attrs);
} }
fn document_lua_function(item: &mut ImplItemFn, func: &LuaFunction) { fn document_metamethod(func: &mut ImplItemFn, method: Metamethod) {
let mut attrs = vec![]; func.attrs.insert(0, parse_quote!(#[doc =
r#"<span
class="stab"
title="This function is a metamethod."
style="float: right; background: #ebf5ff; margin-left: 3px; padding-left: 5px; padding-right: 5px;"
>Metamethod</span>"#
]));
// attrs.push(generate_stab(StabConfig { let doc = match method {
// text: "Lua",
// desc: "This function is implemented in Lua.",
// color: LUA_COLOR,
// strong: true,
// }));
generate_stab_fallible(item).map(|stab| attrs.push(stab));
generate_stab_async(item).map(|stab| attrs.push(stab));
generate_stab_metamethod(func.attrs.metamethod).map(|(stab, help)| {
attrs.push(stab);
item.attrs.push(help);
});
item.attrs.splice(0..0, attrs);
}
fn generate_stab_async(item: &ImplItemFn) -> Option<Attribute> {
item.sig.asyncness.as_ref().map(|_| {
generate_stab(StabConfig {
text: "Async",
desc: "This function is asynchronous and will yield the calling thread.",
color: ASYNC_COLOR,
strong: false,
})
})
}
fn generate_stab_fallible(item: &ImplItemFn) -> Option<Attribute> {
match item.sig.output {
ReturnType::Default => None,
ReturnType::Type(_, ref ty) => is_resultlike(ty).map(|_| {
generate_stab(StabConfig {
text: "Fallible",
desc: "This function is fallible and may throw an error on failure.",
color: FALLIBLE_COLOR,
strong: false,
})
}),
}
}
fn generate_stab_metamethod(mm: Option<Metamethod>) -> Option<(Attribute, Attribute)> {
mm.map(|mm| {
let stab = generate_stab(StabConfig {
text: "Metamethod",
desc: "This function is a metamethod.",
color: METAMETHOD_COLOR,
strong: false,
});
let help = match mm {
Metamethod::Eq => "This function is a metamethod which is called by the `==` operator.", Metamethod::Eq => "This function is a metamethod which is called by the `==` operator.",
Metamethod::Len => "This function is a metamethod which is called by the `#` operator.", Metamethod::Len => "This function is a metamethod which is called by the `#` operator.",
Metamethod::Lt => "This function is a metamethod which is called by the `<` operator.", Metamethod::Lt => "This function is a metamethod which is called by the `<` operator.",
Metamethod::Le => "This function is a metamethod which is called by the `<=` operator.", Metamethod::Le => "This function is a metamethod which is called by the `<=` operator.",
Metamethod::Concat => { Metamethod::Concat => "This function is a metamethod which is called by the `..` operator.",
"This function is a metamethod which is called by the `..` operator."
}
Metamethod::Call => { Metamethod::Call => {
"This function is a metamethod which can be called by calling \ "This function is a metamethod which can be called by calling `(...)` on the value directly."
`(...)` on the value directly."
} }
Metamethod::Add => "This function is a metamethod which is called by the `+` operator.", Metamethod::Add => "This function is a metamethod which is called by the `+` operator.",
Metamethod::Sub => "This function is a metamethod which is called by the `-` operator.", Metamethod::Sub => "This function is a metamethod which is called by the `-` operator.",
@ -951,24 +806,19 @@ fn generate_stab_metamethod(mm: Option<Metamethod>) -> Option<(Attribute, Attrib
Metamethod::Pow => "This function is a metamethod which is called by the `^` operator.", Metamethod::Pow => "This function is a metamethod which is called by the `^` operator.",
Metamethod::Unm => "This function is a metamethod which is called by the `-` operator.", Metamethod::Unm => "This function is a metamethod which is called by the `-` operator.",
Metamethod::ToString => { Metamethod::ToString => {
"This function is a metamethod which is called by the \ "This function is a metamethod which is called by the [`tostring(...)`](https://www.lua.org/manual/5.1/manual.html#pdf-tostring) built-in function."
[`tostring(...)`](https://www.lua.org/manual/5.1/manual.html#pdf-tostring) \
built-in function."
} }
Metamethod::Pairs => { Metamethod::Pairs => {
"This function is a metamethod which is called by the \ "This function is a metamethod which is called by the [`pairs(...)`](https://www.lua.org/manual/5.1/manual.html#pdf-pairs) built-in function."
[`pairs(...)`](https://www.lua.org/manual/5.1/manual.html#pdf-pairs) \
built-in function."
} }
Metamethod::Ipairs => { Metamethod::Ipairs => {
"This function is a metamethod which is called by the \ "This function is a metamethod which is called by the [`ipairs(...)`](https://www.lua.org/manual/5.1/manual.html#pdf-ipairs) built-in function."
[`ipairs(...)`](https://www.lua.org/manual/5.1/manual.html#pdf-ipairs) \
built-in function."
} }
_ => "This function is a metamethod and cannot be called directly.", _ => "This function is a metamethod and cannot be called directly.",
}; };
let help = format!("\n# Metamethod\n\n{help}"); func.attrs.push(parse_quote!(#[doc = ""]));
(stab, parse_quote!(#[doc = #help])) func.attrs.push(parse_quote!(#[doc = "# Metamethod"]));
}) func.attrs.push(parse_quote!(#[doc = ""]));
func.attrs.push(parse_quote!(#[doc = #doc]));
} }

View File

@ -163,19 +163,3 @@ pub fn is_optionlike(ty: &Type) -> Option<&Type> {
None None
} }
} }
pub fn is_resultlike(ty: &Type) -> Option<&Type> {
if let Type::Path(path) = ty
&& path.path.leading_colon.is_none()
&& path.path.segments.len() == 1
&& let Some(segment) = path.path.segments.get(0)
&& segment.ident == "Result"
&& let PathArguments::AngleBracketed(ref angle) = segment.arguments
&& angle.args.len() <= 2 // allow Result<T, E> or Result<T>
&& let Some(GenericArgument::Type(ty)) = angle.args.get(0)
{
Some(ty)
} else {
None
}
}

View File

@ -19,7 +19,7 @@ local icons = {
} }
local function color(name, s) local function color(name, s)
return ("%s %s %s"):format(colors[name], s, colors.reset) return colors[name] .. s .. colors.reset
end end
local function create_test(name, f, group) local function create_test(name, f, group)
@ -54,7 +54,7 @@ local function name_test(test)
local name = test.name local name = test.name
local group = test.group local group = test.group
while group ~= nil do while group ~= nil do
if group.name ~= "" then name = ("%s %s %s"):format(group.name, icons.chevron, name) end if group.name ~= "" then name = string.format("%s %s %s", group.name, icons.chevron, name) end
group = group.parent group = group.parent
end end
return name return name
@ -68,12 +68,11 @@ local function run_test(test)
local ok, res = xpcall(test.f, trace, test) local ok, res = xpcall(test.f, trace, test)
if ok then if ok then
test.state = "pass" test.state = "pass"
print("", ("%s %s"):format(color("pass", "PASS"), name_test(test))) print("", string.format("%s %s", color("pass", "PASS"), name_test(test)))
else else
test.state = "fail" test.state = "fail"
print("", ("%s %s\n\n%s\n"):format(color("fail", "FAIL"), name_test(test), res)) print("", string.format("%s %s\n\n%s\n", color("fail", "FAIL"), name_test(test), res))
end end
collectgarbage() -- gc after each test to test destructors
return test return test
end end
@ -87,23 +86,6 @@ local function start(cx, item)
end end
end end
local function check_unrefs()
-- ensure all refs were properly unref'ed
local registry = debug.getregistry()
local count = #registry
local ref = 0 -- FREELIST_REF
while type(registry[ref]) == "number" do
local next = registry[ref]
registry[ref], ref = nil, next
end
for i = 1, count do
local value = registry[i]
if type(value) ~= "thread" then -- ignore threads pinned by the runtime
assert(rawequal(registry[i], nil), ("ref %d not unref'ed: %s"):format(i, registry[i]))
end
end
end
local function main(item) local function main(item)
local cx = { tasks = {} } local cx = { tasks = {} }
local pass, fail = 0, 0 local pass, fail = 0, 0
@ -115,23 +97,17 @@ local function main(item)
fail = fail + 1 fail = fail + 1
end end
end end
local code = 1
if fail == 0 then if fail == 0 then
print("", color("pass", ("%s %d tests passed"):format(icons.check, pass))) print("", color("pass", string.format("%s %d tests passed", icons.check, pass)))
code = 0 return 0
else end
print( print(
"", "",
("%s, %s"):format( color("pass", string.format("%s %d tests passed", icons.check, pass))
color("pass", ("%s %d tests passed"):format(icons.check, pass)), .. ", "
color("fail", ("%s %d tests failed"):format(icons.cross, fail)) .. color("fail", string.format("%s %d tests failed", icons.cross, fail))
) )
) return 1 -- report error to cargo
end
cx = nil
collectgarbage()
check_unrefs()
return code -- report error to cargo
end end
return main(create_group("", function() return main(create_group("", function()