Compare commits

...

18 Commits

Author SHA1 Message Date
40829cdfc6
Implement basic lua test harness 2025-06-27 03:11:49 +10:00
db4faea821
Implement walk_dir and more robust globbing 2025-06-27 01:36:08 +10:00
2964ebbe1b
Add basic glob function 2025-06-26 23:56:09 +10:00
b1572cc9f1
Rename lsdir to read_dir and dirent to dir_entry 2025-06-26 23:24:48 +10:00
862fcfe891
Add links to lua pdf in metamethod doc 2025-06-26 23:08:28 +10:00
6a194e98e8
Document structs in fslib 2025-06-26 23:08:13 +10:00
d2e06c9a70
Add stab tag for async functions 2025-06-26 22:58:30 +10:00
679ffed807
Implement lsdir and file info functions 2025-06-26 22:46:27 +10:00
549f96d4dc
Warn missing documentation 2025-06-26 21:32:38 +10:00
09d7e51345
Add tokio/rt feature only if task feature is enabled 2025-06-26 21:10:07 +10:00
8a74ade6a6
Implement task handle awaiting 2025-06-26 20:18:16 +10:00
9338be7eb0
Add many lua annotation 2025-06-26 19:25:03 +10:00
240e0111bf
Fix lua finaliser parameter check 2025-06-26 19:24:19 +10:00
01f459ffaf
Implement task sleep function 2025-06-26 18:47:43 +10:00
9b7dbcc141
Implement proper error handlign in spawned tasks 2025-06-26 18:47:31 +10:00
24c5e9edc2
Include lb_tasklib::spawn in globals 2025-06-26 17:52:31 +10:00
0cafac0948
Let modules decide their own name via #[cdef] macro 2025-06-26 17:47:21 +10:00
1c1753234d
Rename MetatypeMethodBuilder to MetatypeFunctionBuilder 2025-06-26 17:12:00 +10:00
22 changed files with 1004 additions and 205 deletions

View File

@ -1,5 +1,9 @@
{
"$schema": "https://raw.githubusercontent.com/LuaLS/vscode-lua/master/setting/schema.json",
"runtime.version": "LuaJIT",
"diagnostics.disable": ["redefined-local", "lowercase-global"]
"diagnostics.disable": [
"undefined-global",
"redefined-local",
"lowercase-global"
]
}

43
Cargo.lock generated
View File

@ -691,6 +691,19 @@ version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2"
[[package]]
name = "globset"
version = "0.4.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "54a1028dfc5f5df5da8a56a73e6c153c9a9708ec57232470703592a3f18e49f5"
dependencies = [
"aho-corasick",
"bstr",
"log",
"regex-automata 0.4.9",
"regex-syntax 0.8.5",
]
[[package]]
name = "h2"
version = "0.4.10"
@ -1032,11 +1045,13 @@ name = "lb"
version = "0.0.1"
dependencies = [
"derive_more",
"globset",
"luaffi",
"luajit",
"sysexits",
"thiserror",
"tokio",
"walkdir",
]
[[package]]
@ -1578,6 +1593,15 @@ version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
[[package]]
name = "same-file"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
dependencies = [
"winapi-util",
]
[[package]]
name = "semver"
version = "1.0.26"
@ -2069,6 +2093,16 @@ dependencies = [
"rustversion",
]
[[package]]
name = "walkdir"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
dependencies = [
"same-file",
"winapi-util",
]
[[package]]
name = "want"
version = "0.3.1"
@ -2120,6 +2154,15 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-util"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"

View File

@ -30,6 +30,10 @@ repository.workspace = true
dev.panic = "abort"
release.panic = "abort"
[[test]]
name = "main"
harness = false
[features]
default = ["task", "fs", "net"]
task = ["lb/task"]

View File

@ -8,14 +8,16 @@ homepage.workspace = true
repository.workspace = true
[features]
task = []
fs = ["tokio/fs"]
task = ["tokio/rt", "tokio/time"]
fs = ["tokio/fs", "dep:walkdir", "dep:globset"]
net = ["tokio/net"]
[dependencies]
derive_more = { version = "2.0.1", features = ["full"] }
globset = { version = "0.4.16", optional = true }
luaffi = { path = "../luaffi" }
luajit = { path = "../luajit" }
sysexits = "0.9.0"
thiserror = "2.0.12"
tokio = { version = "1.45.1", features = ["rt"] }
tokio = { version = "1.45.1" }
walkdir = { version = "2.5.0", optional = true }

View File

@ -1,7 +1,22 @@
// use flume::{Receiver, Sender};
//! Channel library
//!
//! The `lb:chan` library provides primitives for asynchronous communication between tasks via
//! message passing channels.
//!
//! ## Exports
//!
//! See [`lb_chanlib`] for items exported by this library.
use luaffi::{cdef, metatype};
#[cdef]
/// Items exported by the `lb:chan` library.
///
/// This library can be acquired by calling
/// [`require("lb:chan")`](https://www.lua.org/manual/5.1/manual.html#pdf-require) in Lua.
///
/// ```lua
/// local chan = require("lb:chan");
/// ```
#[cdef(module = "lb:chan")]
pub struct lb_chanlib;
#[metatype]

View File

@ -1,21 +1,49 @@
//! The `lb:fs` library provides synchronous and asynchronous utilities for interacting with the
//! file system.
//! # Filesystem library
//!
//! # Exports
//! The `lb:fs` library provides synchronous and asynchronous utilities for interacting with the
//! filesystem.
//!
//! ## Asynchronous by default
//!
//! Filesystem operations are blocking by nature; to provide asynchronicity, luby performs blocking
//! operations in a background thread pool by default. Synchronous complements to all asynchronous
//! functions are always provided.
//!
//! ## Exports
//!
//! See [`lb_fslib`] for items exported by this library.
use derive_more::From;
use luaffi::{cdef, metatype};
use std::io;
use tokio::fs;
use std::{path::PathBuf, time::SystemTime};
use thiserror::Error;
/// Errors that can be thrown by this library.
///
/// Functions which return this error will **throw** in Lua. The error message can be caught by
/// using [`pcall(f, ...)`](https://www.lua.org/manual/5.1/manual.html#pdf-pcall).
#[derive(Debug, Error)]
pub enum Error {
/// I/O error.
#[error("{0}")]
Io(#[from] std::io::Error),
/// Walk directory error.
#[error("{0}")]
Walk(#[from] walkdir::Error),
/// Glob pattern error.
#[error("{0}")]
Glob(#[from] globset::Error),
}
type Result<T> = std::result::Result<T, Error>;
/// Items exported by the `lb:fs` library.
///
/// This library can be obtained by calling `require` in Lua.
/// This library can be acquired by calling `require` in Lua.
///
/// ```lua
/// local fs = require("lb:fs");
/// ```
#[cdef]
#[cdef(module = "lb:fs")]
pub struct lb_fslib;
#[metatype]
@ -25,19 +53,351 @@ impl lb_fslib {
Self
}
pub async extern "Lua-C" fn read(&self, path: &str) -> io::Result<Vec<u8>> {
fs::read(path).await
pub async extern "Lua-C" fn read(&self, path: &str) -> Result<Vec<u8>> {
Ok(tokio::fs::read(path).await?)
}
pub extern "Lua-C" fn read_sync(&self, path: &str) -> io::Result<Vec<u8>> {
std::fs::read(path)
pub extern "Lua-C" fn read_sync(&self, path: &str) -> Result<Vec<u8>> {
Ok(std::fs::read(path)?)
}
pub async extern "Lua-C" fn write(&self, path: &str, contents: &[u8]) -> io::Result<()> {
fs::write(path, contents).await
pub async extern "Lua-C" fn write(&self, path: &str, contents: &[u8]) -> Result<()> {
Ok(tokio::fs::write(path, contents).await?)
}
pub extern "Lua-C" fn write_sync(&self, path: &str, contents: &[u8]) -> io::Result<()> {
std::fs::write(path, contents)
pub extern "Lua-C" fn write_sync(&self, path: &str, contents: &[u8]) -> Result<()> {
Ok(std::fs::write(path, contents)?)
}
pub async extern "Lua-C" fn read_dir(&self, path: &str) -> Result<lb_read_dir> {
Ok(tokio::fs::read_dir(path).await?.into())
}
pub extern "Lua-C" fn read_dir_sync(&self, path: &str) -> Result<lb_read_dir_sync> {
Ok(std::fs::read_dir(path)?.into())
}
pub extern "Lua-C" fn walk_dir(&self, path: &str) -> lb_walk_dir {
walkdir::WalkDir::new(path).into_iter().into()
}
pub extern "Lua-C" fn glob(&self, pattern: &str) -> Result<lb_glob_dir> {
self.glob_dir(".", pattern)
}
pub extern "Lua-C" fn glob_dir(&self, path: &str, pattern: &str) -> Result<lb_glob_dir> {
let prefix = PathBuf::from(path);
let iter = walkdir::WalkDir::new(path).min_depth(1).into_iter();
let matcher = globset::GlobSet::builder()
.add(globset::Glob::new(pattern)?)
.build()?;
Ok(lb_glob_dir {
iter,
matcher,
prefix,
})
}
}
/// Iterator over the entries in a directory.
#[derive(Debug, From)]
#[cdef]
pub struct lb_read_dir(#[opaque] tokio::fs::ReadDir);
#[metatype]
impl lb_read_dir {
#[call]
pub async extern "Lua-C" fn next(&mut self) -> Result<Option<lb_dir_entry>> {
Ok(self.0.next_entry().await?.map(Into::into))
}
}
/// Synchronous version of [`lb_read_dir`].
#[derive(Debug, From)]
#[cdef]
pub struct lb_read_dir_sync(#[opaque] std::fs::ReadDir);
#[metatype]
impl lb_read_dir_sync {
#[call]
pub extern "Lua-C" fn next(&mut self) -> Result<Option<lb_dir_entry_sync>> {
Ok(self.0.next().transpose()?.map(Into::into))
}
}
/// Entry inside of a directory on the filesystem.
#[derive(Debug, From)]
#[cdef]
pub struct lb_dir_entry(#[opaque] tokio::fs::DirEntry);
#[metatype]
impl lb_dir_entry {
pub extern "Lua-C" fn path(&self) -> String {
self.0.path().to_string_lossy().into()
}
pub extern "Lua-C" fn name(&self) -> String {
self.0.file_name().to_string_lossy().into()
}
pub async extern "Lua-C" fn r#type(&self) -> Result<lb_file_type> {
Ok(self.0.file_type().await?.into())
}
pub async extern "Lua-C" fn metadata(&self) -> Result<lb_file_meta> {
Ok(self.0.metadata().await?.into())
}
#[cfg(unix)]
pub extern "Lua-C" fn ino(&self) -> u64 {
self.0.ino()
}
#[tostring]
pub extern "Lua" fn tostring(&self) -> String {
self.path()
}
}
/// Synchronous version of [`lb_dir_entry`].
#[derive(Debug, From)]
#[cdef]
pub struct lb_dir_entry_sync(#[opaque] std::fs::DirEntry);
#[metatype]
impl lb_dir_entry_sync {
pub extern "Lua-C" fn path(&self) -> String {
self.0.path().to_string_lossy().into()
}
pub extern "Lua-C" fn name(&self) -> String {
self.0.file_name().to_string_lossy().into()
}
pub extern "Lua-C" fn r#type(&self) -> Result<lb_file_type> {
Ok(self.0.file_type()?.into())
}
pub extern "Lua-C" fn metadata(&self) -> Result<lb_file_meta> {
Ok(self.0.metadata()?.into())
}
#[cfg(unix)]
pub extern "Lua-C" fn ino(&self) -> u64 {
use std::os::unix::fs::DirEntryExt;
self.0.ino()
}
#[tostring]
pub extern "Lua" fn tostring(&self) -> String {
self.path()
}
}
/// Structure representing the type of a file with accessors for each file type.
#[derive(Debug, From)]
#[cdef]
pub struct lb_file_type(#[opaque] std::fs::FileType);
#[metatype]
impl lb_file_type {
pub extern "Lua-C" fn is_dir(&self) -> bool {
self.0.is_dir()
}
pub extern "Lua-C" fn is_file(&self) -> bool {
self.0.is_file()
}
pub extern "Lua-C" fn is_symlink(&self) -> bool {
self.0.is_file()
}
#[tostring]
pub extern "Lua-C" fn tostring(&self) -> String {
if self.0.is_file() {
"file"
} else if self.0.is_dir() {
"dir"
} else if self.0.is_symlink() {
"symlink"
} else {
"other"
}
.into()
}
}
/// Metadata information about a file.
#[derive(Debug, From)]
#[cdef]
pub struct lb_file_meta(#[opaque] std::fs::Metadata);
#[metatype]
impl lb_file_meta {
pub extern "Lua-C" fn is_dir(&self) -> bool {
self.0.is_dir()
}
pub extern "Lua-C" fn is_file(&self) -> bool {
self.0.is_file()
}
pub extern "Lua-C" fn is_symlink(&self) -> bool {
self.0.is_file()
}
pub extern "Lua-C" fn r#type(&self) -> lb_file_type {
self.0.file_type().into()
}
pub extern "Lua-C" fn size(&self) -> u64 {
self.0.len()
}
pub extern "Lua-C" fn perms(&self) -> lb_file_perms {
self.0.permissions().into()
}
pub extern "Lua-C" fn created(&self) -> Result<f64> {
Ok(self
.0
.created()?
.duration_since(SystemTime::UNIX_EPOCH)
.map(|dur| dur.as_secs_f64())
.unwrap_or(0.))
}
pub extern "Lua-C" fn modified(&self) -> Result<f64> {
Ok(self
.0
.modified()?
.duration_since(SystemTime::UNIX_EPOCH)
.map(|dur| dur.as_secs_f64())
.unwrap_or(0.))
}
pub extern "Lua-C" fn accessed(&self) -> Result<f64> {
Ok(self
.0
.accessed()?
.duration_since(SystemTime::UNIX_EPOCH)
.map(|dur| dur.as_secs_f64())
.unwrap_or(0.))
}
#[tostring]
pub extern "Lua-C" fn tostring(&self) -> String {
let ty = self.0.file_type();
if ty.is_file() {
format!("file {}", self.0.len())
} else if ty.is_dir() {
"dir".into()
} else if ty.is_symlink() {
"symlink".into()
} else {
"other".into()
}
}
}
/// Representation of the various permissions on a file.
#[derive(Debug, From)]
#[cdef]
pub struct lb_file_perms(#[opaque] std::fs::Permissions);
#[metatype]
impl lb_file_perms {
pub extern "Lua-C" fn readonly(&self) -> bool {
self.0.readonly()
}
pub extern "Lua-C" fn set_readonly(&mut self, readonly: bool) {
self.0.set_readonly(readonly);
}
}
/// Iterator for recursively descending into a directory.
#[derive(Debug, From)]
#[cdef]
pub struct lb_walk_dir(#[opaque] walkdir::IntoIter);
#[metatype]
impl lb_walk_dir {
#[call]
pub extern "Lua-C" fn next(&mut self) -> Result<Option<lb_walk_dir_entry>> {
Ok(self.0.next().transpose()?.map(Into::into))
}
}
/// Entry inside of a directory on the filesystem obtained from [`lb_walk_dir`].
#[derive(Debug, From)]
#[cdef]
pub struct lb_walk_dir_entry(#[opaque] walkdir::DirEntry);
#[metatype]
impl lb_walk_dir_entry {
pub extern "Lua-C" fn path(&self) -> String {
self.0.path().to_string_lossy().into()
}
pub extern "Lua-C" fn name(&self) -> String {
self.0.file_name().to_string_lossy().into()
}
pub extern "Lua-C" fn r#type(&self) -> lb_file_type {
self.0.file_type().into()
}
pub extern "Lua-C" fn metadata(&self) -> Result<lb_file_meta> {
Ok(self.0.metadata()?.into())
}
pub extern "Lua-C" fn is_symlink(&self) -> bool {
self.0.path_is_symlink()
}
pub extern "Lua-C" fn depth(&self) -> u32 {
self.0.depth() as u32
}
#[cfg(unix)]
pub extern "Lua-C" fn ino(&self) -> u64 {
use walkdir::DirEntryExt;
self.0.ino()
}
#[tostring]
pub extern "Lua" fn tostring(&self) -> String {
self.path()
}
}
/// Iterator that yields paths from the filesystem that match a particular pattern.
#[derive(Debug)]
#[cdef]
pub struct lb_glob_dir {
#[opaque]
iter: walkdir::IntoIter,
#[opaque]
matcher: globset::GlobSet,
#[opaque]
prefix: PathBuf,
}
#[metatype]
impl lb_glob_dir {
#[call]
pub extern "Lua-C" fn next(&mut self) -> Result<Option<lb_walk_dir_entry>> {
while let Some(res) = self.iter.next() {
let entry = res?;
let path = entry.path().strip_prefix(&self.prefix).unwrap();
if self.matcher.is_match(path) {
return Ok(Some(entry.into()));
}
}
Ok(None)
}
}

View File

@ -1,3 +1,5 @@
//! luby standard library
#![warn(missing_docs)]
pub mod runtime;
#[cfg(feature = "task")]

View File

@ -1,7 +1,9 @@
//! Networking library
//!
//! The `lb:net` library provides an asynchronous network API for creating TCP or UDP servers and
//! clients.
//!
//! # Exports
//! ## Exports
//!
//! See [`lb_netlib`] for items exported by this library.
use derive_more::{From, FromStr};
@ -19,10 +21,13 @@ use tokio::net::{TcpListener, TcpSocket, TcpStream};
/// using [`pcall(f, ...)`](https://www.lua.org/manual/5.1/manual.html#pdf-pcall).
#[derive(Debug, Error)]
pub enum Error {
/// I/O error.
#[error("{0}")]
Io(#[from] std::io::Error),
/// IP or socket address syntax error.
#[error("{0}")]
InvalidAddr(#[from] AddrParseError),
/// Socket was already converted and cannot be used anymore.
#[error("socket was already converted")]
SocketConsumed,
}
@ -31,13 +36,13 @@ type Result<T> = std::result::Result<T, Error>;
/// Items exported by the `lb:net` library.
///
/// This library can be obtained by calling
/// This library can be acquired by calling
/// [`require("lb:net")`](https://www.lua.org/manual/5.1/manual.html#pdf-require) in Lua.
///
/// ```lua
/// local net = require("lb:net");
/// ```
#[cdef]
#[cdef(module = "lb:net")]
pub struct lb_netlib;
#[metatype]
@ -136,7 +141,7 @@ impl lb_netlib {
Ok(Some(TcpSocket::new_v6()?).into())
}
pub extern "Lua" fn bind_tcp(&self, addr: any, port: any) -> Result<lb_tcpsocket> {
pub async extern "Lua" fn bind_tcp(&self, addr: any, port: any) -> Result<lb_tcpsocket> {
let addr = self.socketaddr(addr, port);
let socket;
if addr.ip().is_v6() {
@ -148,7 +153,7 @@ impl lb_netlib {
socket
}
pub extern "Lua" fn connect_tcp(&self, addr: any, port: any) -> Result<lb_tcpstream> {
pub async extern "Lua" fn connect_tcp(&self, addr: any, port: any) -> Result<lb_tcpstream> {
let addr = self.socketaddr(addr, port);
let socket;
if addr.ip().is_v6() {
@ -159,12 +164,12 @@ impl lb_netlib {
socket.connect(addr)
}
pub extern "Lua" fn listen_tcp(&self, addr: any, port: any) -> Result<lb_tcplistener> {
pub async extern "Lua" fn listen_tcp(&self, addr: any, port: any) -> Result<lb_tcplistener> {
self.bind_tcp(addr, port).listen(1024)
}
}
/// An IP address, either IPv4 or IPv6.
/// IP address, either IPv4 or IPv6.
///
/// # Example
///
@ -289,7 +294,7 @@ impl lb_ipaddr {
}
}
/// A socket address, which is an IP address with a port number.
/// Socket address, which is an IP address with a port number.
#[derive(Debug, Clone, Copy, PartialEq, Eq, From, FromStr)]
#[cdef]
pub struct lb_socketaddr(#[opaque] SocketAddr);
@ -345,7 +350,7 @@ impl lb_socketaddr {
}
}
/// A TCP socket which has not yet been converted to an [`lb_tcpstream`] or [`lb_tcplistener`].
/// TCP socket which has not yet been converted to an [`lb_tcpstream`] or [`lb_tcplistener`].
#[derive(Debug, From)]
#[cdef]
pub struct lb_tcpsocket(#[opaque] Option<TcpSocket>);
@ -465,6 +470,7 @@ impl lb_tcpsocket {
}
}
/// TCP connection between a local and a remote socket.
#[derive(Debug, From)]
#[cdef]
pub struct lb_tcpstream(#[opaque] TcpStream);
@ -472,6 +478,7 @@ pub struct lb_tcpstream(#[opaque] TcpStream);
#[metatype]
impl lb_tcpstream {}
/// TCP socket server, listening for connections.
#[derive(Debug, From)]
#[cdef]
pub struct lb_tcplistener(#[opaque] TcpListener);

View File

@ -1,21 +1,28 @@
#![doc(hidden)]
use derive_more::{Deref, DerefMut};
use luaffi::{Registry, Type};
use luaffi::{Module, Registry};
use luajit::{Chunk, State};
use std::fmt::Display;
use std::rc::Rc;
use tokio::{
task::{JoinHandle, LocalSet, futures::TaskLocalFuture, spawn_local},
task_local,
};
#[derive(Debug, Default)]
pub type ErrorFn = dyn Fn(&luajit::Error);
pub struct Builder {
registry: Registry,
report_err: Rc<ErrorFn>,
}
impl Builder {
pub fn new() -> Self {
Self {
registry: Registry::new(),
report_err: Rc::new(|err| match err.trace() {
Some(trace) => eprintln!("unhandled lua error: {err}\n{trace}"),
None => eprintln!("unhandled lua error: {err}"),
}),
}
}
@ -23,57 +30,86 @@ impl Builder {
&self.registry
}
pub fn module<T: Type>(&mut self, name: impl Display) -> &mut Self {
self.registry.preload::<T>(name);
pub fn unhandled_error(&mut self, handler: impl Fn(&luajit::Error) + 'static) -> &mut Self {
self.report_err = Rc::new(handler);
self
}
pub fn module<T: Module>(&mut self) -> &mut Self {
self.registry.preload::<T>();
self
}
pub fn build(&self) -> luajit::Result<Runtime> {
Ok(Runtime {
cx: Context {
state: {
let mut s = State::new()?;
s.eval(Chunk::new(self.registry.build()).path("[luby]"), 0, 0)?;
s
},
report_err: self.report_err.clone(),
},
tasks: LocalSet::new(),
})
}
}
#[derive(Debug, Deref, DerefMut)]
#[derive(Deref, DerefMut)]
pub struct Runtime {
#[deref]
#[deref_mut]
state: State,
cx: Context,
tasks: LocalSet,
}
task_local! {
static STATE: State;
}
impl Runtime {
pub fn spawn<T: 'static>(
&self,
f: impl AsyncFnOnce(&mut State) -> T + 'static,
f: impl AsyncFnOnce(&mut Context) -> T + 'static,
) -> JoinHandle<T> {
self.tasks
.spawn_local(async move { f(&mut STATE.with(|s| s.new_thread())).await })
.spawn_local(async move { f(&mut CURRENT.with(|s| s.new_thread())).await })
}
}
pub fn spawn<T: 'static>(f: impl AsyncFnOnce(&mut State) -> T + 'static) -> JoinHandle<T> {
// SAFETY: `new_thread` must be called inside `spawn_local` because this free-standing spawn
// function may be called via ffi from lua, and it is not safe to access the lua state within
// ffi calls.
spawn_local(async move { f(&mut STATE.with(|s| s.new_thread())).await })
}
impl IntoFuture for Runtime {
type Output = ();
type IntoFuture = TaskLocalFuture<State, LocalSet>;
type IntoFuture = TaskLocalFuture<Context, LocalSet>;
fn into_future(self) -> Self::IntoFuture {
STATE.scope(self.state, self.tasks)
CURRENT.scope(self.cx, self.tasks)
}
}
task_local! {
static CURRENT: Context;
}
#[derive(Deref, DerefMut)]
pub struct Context {
#[deref]
#[deref_mut]
state: State,
report_err: Rc<ErrorFn>,
}
impl Context {
pub fn new_thread(&self) -> Self {
Self {
state: self.state.new_thread(),
report_err: self.report_err.clone(),
}
}
pub fn report_error(&self, err: &luajit::Error) {
(self.report_err)(&err);
}
}
pub fn spawn<T: 'static>(f: impl AsyncFnOnce(&mut Context) -> T + 'static) -> JoinHandle<T> {
// SAFETY: `new_thread` must be called inside `spawn_local` because this free-standing spawn
// function may be called via ffi from lua, and it is not safe to access the lua state within
// ffi calls.
spawn_local(async move { f(&mut CURRENT.with(|s| s.new_thread())).await })
}

10
crates/lb/src/task.lua Normal file
View File

@ -0,0 +1,10 @@
-- include task functions in the global scope
local task = require("lb:task")
function sleep(ms)
return task:sleep(ms)
end
function spawn(f, ...)
return task:spawn(f, ...)
end

View File

@ -1,46 +1,103 @@
//! Task library
//!
//! The `lb:task` library primitives for asynchronous communication between tasks via message
//! passing channels.
//!
//! ## Exports
//!
//! See [`lb_tasklib`] for items exported by this library.
use crate::runtime::spawn;
use luaffi::{cdef, metatype};
use std::{ffi::c_int, process};
use tokio::task::JoinHandle;
use luajit::{LUA_MULTRET, Type};
use std::{ffi::c_int, time::Duration};
use tokio::{task::JoinHandle, time::sleep};
#[cdef]
/// Items exported by the `lb:task` library.
///
/// This library can be acquired by calling
/// [`require("lb:task")`](https://www.lua.org/manual/5.1/manual.html#pdf-require) in Lua.
///
/// ```lua
/// local task = require("lb:task");
/// ```
#[cdef(module = "lb:task")]
pub struct lb_tasklib;
#[metatype]
#[include("task.lua")]
impl lb_tasklib {
#[new]
extern "Lua-C" fn new() -> Self {
Self
}
pub extern "Lua" fn spawn(self, f: function, ...) {
// pack the function and its arguments into a table and pass its ref to rust
pub async extern "Lua-C" fn sleep(&self, ms: f64) {
sleep(Duration::from_secs_f64(ms / 1000.)).await;
}
pub extern "Lua" fn spawn(&self, f: function, ...) -> lb_task {
// pack the function and its arguments into a table and pass its ref to rust.
//
// this table is used from rust-side to call the function with its args, and it's also
// reused to store its return values that the task handle can return when awaited. the ref
// is owned by the task handle and unref'ed when it's gc'ed.
self.__spawn(__ref(__tpack(f, variadic!())))
}
extern "Lua-C" fn __spawn(&self, key: c_int) -> lb_task {
let handle = spawn(async move |s| {
// SAFETY: key is always unique, created by __ref above
let arg = unsafe { s.new_ref_unchecked(key) };
let handle = spawn(async move |cx| {
// SAFETY: key is always unique, created by __ref above.
let arg = unsafe { cx.new_ref_unchecked(key) };
let mut s = cx.guard();
s.resize(0);
s.push(arg);
let narg = s.unpack(1, 1, None) - 1;
println!("{s:?}");
if let Err(_err) = s.call_async(narg, 0).await {
process::exit(1)
s.push(&arg);
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 {
Ok(nret) => {
s.pack(1, nret); // pack the return values back into the table
}
println!("{s:?}");
Err(err) => {
drop(s);
cx.report_error(&err);
}
}
let _ = arg.into_raw(); // the original ref is owned by the task handle and unref'ed there
});
lb_task { handle }
lb_task {
handle: Some(handle),
__ref: key,
}
}
}
/// Handle for an asynchronous task created by [`spawn`](lb_tasklib::spawn).
#[cdef]
pub struct lb_task {
#[opaque]
handle: JoinHandle<()>,
handle: Option<JoinHandle<()>>,
__ref: c_int,
}
#[metatype]
impl lb_task {}
impl lb_task {
pub async extern "Lua" fn r#await(&self) -> many {
self.__await();
let ret = __registry[self.__ref];
__tunpack(ret, 1, ret.n)
}
async extern "Lua-C" fn __await(&mut self) {
if let Some(handle) = self.handle.take() {
handle
.await
.unwrap_or_else(|err| panic!("task handler panicked: {err}"));
}
}
#[gc]
extern "Lua" fn gc(&self) {
__unref(self.__ref);
}
}

View File

@ -9,6 +9,7 @@ use std::{
#[allow(non_camel_case_types)]
pub mod stub_types {
pub struct any;
pub struct many;
pub struct nil;
pub type boolean = bool;
pub struct lightuserdata;

View File

@ -38,7 +38,7 @@ const CACHE_LIBS: &[(&str, &str)] = &[
("package", "package"),
("debug", "debug"),
("jit", "jit"),
// require
// requires
("bit", r#"require("bit")"#),
("ffi", r#"require("ffi")"#),
("__tnew", r#"require("table.new")"#),
@ -47,7 +47,7 @@ const CACHE_LIBS: &[(&str, &str)] = &[
// https://www.lua.org/manual/5.1/manual.html#5.1
const CACHE_LOCALS: &[(&str, &str)] = &[
// base
// baselib
("assert", "assert"),
("error", "error"),
("type", "type"),
@ -68,7 +68,8 @@ const CACHE_LOCALS: &[(&str, &str)] = &[
("tonumber", "tonumber"),
("tostring", "tostring"),
("require", "require"),
// table
("__yield", "coroutine.yield"), // (used in future.rs)
// tablib
("__tconcat", "table.concat"),
("__tinsert", "table.insert"),
("__tmaxn", "table.maxn"),
@ -76,23 +77,21 @@ const CACHE_LOCALS: &[(&str, &str)] = &[
("__tsort", "table.sort"),
("__tpack", "table.pack"),
("__tunpack", "table.unpack"),
// string
// strlib
("__slen", "string.len"),
("__sprintf", "string.format"),
("__ssub", "string.sub"),
("__sgsub", "string.gsub"),
("__sgmatch", "string.gmatch"),
("__sdump", "string.dump"),
// math (used in luaify! macro)
// mathlib (used in luaify! macro)
("__fmod", "math.fmod"),
// coroutine (used in future.rs)
("__yield", "coroutine.yield"),
// package
// loadlib
("__preload", "package.preload"),
// debug
// dblib
("__traceback", "debug.traceback"),
("__registry", "debug.getregistry()"), // (used in lib.lua)
// ffi
// ffilib
("__C", "ffi.C"),
("__ct", "{}"),
("__cdef", "ffi.cdef"),
@ -105,7 +104,7 @@ const CACHE_LOCALS: &[(&str, &str)] = &[
("__sizeof", "ffi.sizeof"),
("__alignof", "ffi.alignof"),
("__intern", "ffi.string"), // (used in string.rs)
// bit (used in luaify! macro)
// bitlib (used in luaify! macro)
("__bnot", "bit.bnot"),
("__band", "bit.band"),
("__bor", "bit.bor"),
@ -159,16 +158,16 @@ impl Registry {
self
}
pub fn preload<T: Type>(&mut self, name: impl Display) -> &mut Self {
pub fn preload<T: Module>(&mut self) -> &mut Self {
assert!(T::ty() != TypeType::Void, "cannot declare void type");
self.include::<T>();
let ct = T::name();
let name = <T as Module>::name();
let ct = <T as Type>::name();
writeln!(
self.lua,
r#"__preload["{name}"] = function(...) return __ct.{ct}(...); end;"#,
)
.unwrap();
self
self.include::<T>()
}
pub fn build(&self) -> String {
@ -241,6 +240,10 @@ impl<'r> TypeBuilder<'r> {
}
}
pub trait Module: Type {
fn name() -> impl Display;
}
pub unsafe trait Cdef: Type {
fn build(b: &mut CdefBuilder);
}
@ -321,6 +324,7 @@ pub struct MetatypeBuilder<'r> {
ct: String,
cdef: String,
lua: String,
lua_includes: Vec<&'static str>,
}
impl<'r> MetatypeBuilder<'r> {
@ -330,6 +334,7 @@ impl<'r> MetatypeBuilder<'r> {
ct: T::Target::name().to_string(),
cdef: String::new(),
lua: r#"do local __mt, __idx = {}, {}; __mt.__index = __idx; "#.into(),
lua_includes: vec![],
}
}
@ -338,13 +343,18 @@ impl<'r> MetatypeBuilder<'r> {
self
}
pub fn include_lua(&mut self, lua: &'static str) -> &mut Self {
self.lua_includes.push(lua);
self
}
pub fn index(
&mut self,
name: impl Display,
f: impl FnOnce(&mut MetatypeMethodBuilder),
f: impl FnOnce(&mut MetatypeFunctionBuilder),
) -> &mut Self {
write!(self.lua, "__idx.{name} = ").unwrap();
f(&mut MetatypeMethodBuilder::new(self));
f(&mut MetatypeFunctionBuilder::new(self));
writeln!(self.lua, ";").unwrap();
self
}
@ -357,10 +367,10 @@ impl<'r> MetatypeBuilder<'r> {
pub fn metatable(
&mut self,
name: impl Display,
f: impl FnOnce(&mut MetatypeMethodBuilder),
f: impl FnOnce(&mut MetatypeFunctionBuilder),
) -> &mut Self {
write!(self.lua, "__mt.__{name} = ").unwrap();
f(&mut MetatypeMethodBuilder::new(self));
f(&mut MetatypeFunctionBuilder::new(self));
writeln!(self.lua, ";").unwrap();
self
}
@ -378,12 +388,15 @@ impl<'r> Drop for MetatypeBuilder<'r> {
ct,
cdef,
lua,
..
lua_includes: lua_postlude,
} = self;
registry.cdef.push_str(cdef);
registry.lua.push_str(lua);
writeln!(registry.lua, r#"__metatype(__ct.{ct}, __mt); end;"#).unwrap();
for lua in lua_postlude {
writeln!(registry.lua, r#"do {lua} end;"#).unwrap();
}
}
}
@ -429,16 +442,16 @@ pub unsafe trait IntoFfi: Sized {
}
#[derive(Debug)]
pub struct MetatypeMethodBuilder<'r, 'm> {
pub struct MetatypeFunctionBuilder<'r, 'm> {
metatype: &'m mut MetatypeBuilder<'r>,
lparams: String, // parameters to the lua function
cparams: String, // parameters to the lua function
cargs: String, // arguments to the C call
prelude: String, // function body prelude
postlude: String, // function body postlude
lparams: String, // lua function parameters
cparams: String, // C function parameters
cargs: String, // C call arguments
prelude: String, // lua function body prelude
postlude: String, // lua function body postlude
}
impl<'r, 'm> MetatypeMethodBuilder<'r, 'm> {
impl<'r, 'm> MetatypeFunctionBuilder<'r, 'm> {
pub fn new(metatype: &'m mut MetatypeBuilder<'r>) -> Self {
Self {
metatype,
@ -517,12 +530,15 @@ impl<'r, 'm> MetatypeMethodBuilder<'r, 'm> {
write!(prelude, "local __{name}_len = 0; if {name} ~= nil then ").unwrap();
write!(prelude, r#"assert(type({name}) == "string", "string expected in argument '{name}', got " .. type({name})); "#).unwrap();
write!(prelude, r#"__{name}_len = #{name}; "#).unwrap();
if check_utf8 {
write!(prelude, r#"assert(__C.{IS_UTF8_FN}({name}, __{name}_len), "argument '{name}' must be a valid utf-8 string"); "#).unwrap();
}
if !allow_nil {
write!(prelude, r#"else return error("string expected in argument '{name}', got " .. type({name})); "#).unwrap();
}
write!(prelude, r#"end; "#).unwrap();
self
}

View File

@ -5,18 +5,26 @@ use quote::{format_ident, quote, quote_spanned};
use syn::{ext::IdentExt, spanned::Spanned, *};
#[derive(Debug, FromMeta)]
pub struct Args {}
pub struct Args {
module: Option<String>,
}
pub fn transform(_args: Args, mut item: Item) -> Result<TokenStream> {
let (name, impl_type, impl_cdef) = match item {
pub fn transform(args: Args, mut item: Item) -> Result<TokenStream> {
let (name, impl_type, impl_module, impl_cdef) = match item {
Item::Struct(ref mut str) => (
str.ident.clone(),
generate_type(&str.ident)?,
args.module
.map(|name| generate_module(&name, &str.ident))
.transpose()?,
generate_cdef_structure(str)?,
),
Item::Enum(ref mut enu) => (
enu.ident.clone(),
generate_type(&enu.ident)?,
args.module
.map(|name| generate_module(&name, &enu.ident))
.transpose()?,
generate_cdef_enum(enu)?,
),
_ => syn_error!(item, "expected struct or enum"),
@ -35,6 +43,7 @@ pub fn transform(_args: Args, mut item: Item) -> Result<TokenStream> {
mod #mod_name {
use super::*;
#impl_type
#impl_module
#impl_cdef
}
))
@ -47,9 +56,7 @@ fn generate_type(ty: &Ident) -> Result<TokenStream> {
Ok(quote_spanned!(span =>
unsafe impl #ffi::Type for #ty {
fn name() -> impl ::std::fmt::Display {
#name
}
fn name() -> impl ::std::fmt::Display { #name }
fn ty() -> #ffi::TypeType {
#ffi::TypeType::Aggregate
@ -72,6 +79,16 @@ fn generate_type(ty: &Ident) -> Result<TokenStream> {
))
}
fn generate_module(name: &str, ty: &Ident) -> Result<TokenStream> {
let ffi = ffi_crate();
Ok(quote_spanned!(ty.span() =>
impl #ffi::Module for #ty {
fn name() -> impl ::std::fmt::Display { #name }
}
))
}
fn generate_cdef_structure(str: &mut ItemStruct) -> Result<TokenStream> {
syn_assert!(
str.generics.params.is_empty(),
@ -155,13 +172,12 @@ fn parse_cfield_attrs(attrs: &mut Vec<Attribute>) -> Result<CFieldAttrs> {
let mut parsed = CFieldAttrs::default();
let mut i = 0;
while let Some(attr) = attrs.get(i) {
if let Some(name) = attr.path().get_ident() {
if name == "opaque" {
let path = attr.path();
if path.is_ident("opaque") {
parsed.opaque = true;
attrs.remove(i);
continue;
}
}
i += 1;
}

View File

@ -1,7 +1,6 @@
use darling::{FromMeta, ast::NestedMeta};
use proc_macro::TokenStream as TokenStream1;
use quote::ToTokens;
use syn::parse_macro_input;
mod cdef;
mod metatype;
@ -17,8 +16,10 @@ pub fn cdef(args: TokenStream1, input: TokenStream1) -> TokenStream1 {
}
#[proc_macro_attribute]
pub fn metatype(_args: TokenStream1, input: TokenStream1) -> TokenStream1 {
metatype::transform(parse_macro_input!(input))
pub fn metatype(args: TokenStream1, input: TokenStream1) -> TokenStream1 {
NestedMeta::parse_meta_list(args.into())
.and_then(|meta| metatype::Args::from_list(&meta).map_err(Into::into))
.and_then(|args| metatype::transform(args, syn::parse(input)?))
.unwrap_or_else(|err| err.into_compile_error().into_token_stream())
.into()
}

View File

@ -2,19 +2,23 @@ use crate::utils::{
StringLike, ffi_crate, is_optionlike, is_primitivelike, is_stringlike, is_unit, pat_ident,
syn_assert, syn_error, ty_name,
};
use darling::FromMeta;
use proc_macro2::TokenStream;
use quote::{ToTokens, format_ident, quote, quote_spanned};
use std::{collections::HashSet, fmt, iter};
use syn::{ext::IdentExt, spanned::Spanned, *};
pub fn transform(mut imp: ItemImpl) -> Result<TokenStream> {
#[derive(Debug, FromMeta)]
pub struct Args {}
pub fn transform(args: Args, mut imp: ItemImpl) -> Result<TokenStream> {
syn_assert!(
imp.generics.params.is_empty(),
imp.generics,
"cannot be generic (not yet implemented)"
);
let impls = generate_impls(&mut imp)?;
let impls = generate_impls(&args, &mut imp)?;
let mod_name = format_ident!("__{}_metatype", ty_name(&imp.self_ty)?);
Ok(quote_spanned!(imp.self_ty.span() =>
@ -46,7 +50,7 @@ impl Registry {
}
}
fn generate_impls(imp: &mut ItemImpl) -> Result<TokenStream> {
fn generate_impls(_args: &Args, imp: &mut ItemImpl) -> Result<TokenStream> {
let ffi = ffi_crate();
let ty = imp.self_ty.clone();
let ty_name = ty_name(&ty)?;
@ -54,6 +58,26 @@ fn generate_impls(imp: &mut ItemImpl) -> Result<TokenStream> {
let mut mms = HashSet::new();
let mut lua_drop = None;
// process lua includes
imp.attrs.retain(|attr| {
if attr.path().is_ident("include") {
if let Ok(path) = attr.parse_args::<LitStr>() {
registry.build.push(quote_spanned!(path.span() =>
b.include_lua(::std::include_str!(#path));
));
return false;
} else if let Ok(chunk) = attr.parse_args::<Block>() {
registry.build.push(quote_spanned!(chunk.span() =>
b.include_lua(#ffi::__internal::luaify_chunk!(#chunk));
));
return false;
}
}
true
});
// process extern "Lua-C" ffi functions
for func in get_ffi_functions(imp)? {
if let Some(mm) = func.attrs.metamethod {
syn_assert!(
@ -66,6 +90,7 @@ fn generate_impls(imp: &mut ItemImpl) -> Result<TokenStream> {
add_ffi_function(&mut registry, &func)?;
}
// process extern "Lua" lua functions
for func in get_lua_functions(imp)? {
if let Some(mm) = func.attrs.metamethod {
syn_assert!(
@ -82,10 +107,12 @@ fn generate_impls(imp: &mut ItemImpl) -> Result<TokenStream> {
}
}
// if no #[new] metamethod is defined, inject fallback new
if !mms.contains(&Metamethod::New) {
inject_fallback_new(&mut registry)?;
}
// inject __gc/drop
inject_merged_drop(&mut registry, lua_drop.as_ref())?;
let shims = &registry.shims;
@ -258,7 +285,7 @@ fn get_ffi_functions(imp: &mut ItemImpl) -> Result<Vec<FfiFunction>> {
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);
funcs.push(FfiFunction {
@ -279,7 +306,7 @@ fn parse_ffi_function_attrs(attrs: &mut Vec<Attribute>) -> Result<FfiFunctionAtt
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)
&& let Ok(method) = Metamethod::try_from(&name.unraw())
{
match method {
Metamethod::Gc => syn_error!(attr, "implement `Drop` instead"),
@ -546,6 +573,12 @@ fn get_lua_functions(imp: &mut ItemImpl) -> Result<Vec<LuaFunction>> {
"cannot be generic"
);
syn_assert!(
func.sig.constness.is_none(),
func.sig.constness,
"cannot be const"
);
let mut params: Vec<_> = func
.sig
.inputs
@ -566,7 +599,7 @@ fn get_lua_functions(imp: &mut ItemImpl) -> Result<Vec<LuaFunction>> {
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);
funcs.push(LuaFunction {
@ -629,6 +662,7 @@ fn stub_lua_type(ty: &Type) -> Result<Type> {
} else {
match ty_name(ty)?.to_string().as_str() {
"any" => quote_spanned!(span => any),
"many" => quote_spanned!(span => many),
"nil" => quote_spanned!(span => nil),
"boolean" => quote_spanned!(span => boolean),
"lightuserdata" => quote_spanned!(span => lightuserdata),
@ -652,7 +686,7 @@ fn parse_lua_function_attrs(attrs: &mut Vec<Attribute>) -> Result<LuaFunctionAtt
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)
&& let Ok(method) = Metamethod::try_from(&name.unraw())
{
match method {
Metamethod::New => syn_error!(attr, r#"cannot be applied to a lua function"#),
@ -718,11 +752,17 @@ fn inject_merged_drop(registry: &mut Registry, lua: Option<&LuaFunction>) -> Res
"finaliser must take exactly one parameter"
);
match lua.params[0] {
// should be `self: cdata` PatType
Pat::Type(ref ty) => {
syn_assert!(
pat_ident(&lua.params[0])? == "self",
pat_ident(&ty.pat)? == "self",
lua.params[0],
"finaliser parameter must be `self`"
);
}
_ => syn_error!(lua.params[0], "finaliser parameter must be `self`"),
}
let params = &lua.params;
let body = &lua.body;
@ -773,30 +813,39 @@ fn document_lua_function(func: &mut ImplItemFn) {
]));
}
fn document_async(func: &mut ImplItemFn) {
func.attrs.insert(0, parse_quote!(#[doc =
r#"<span class="stab" title="This function is asynchronous." style="float: right; background: #ebf5ff; margin-left: 3px; padding-left: 5px; padding-right: 5px;">Async</span>"#
]));
}
fn document_metamethod(func: &mut ImplItemFn, method: Metamethod) {
let s = match method {
Metamethod::Eq => "This is a metamethod which is called by the `==` operator.".into(),
Metamethod::Len => "This is a metamethod which is called by the `#` operator.".into(),
Metamethod::Lt => "This is a metamethod which is called by the `<` operator.".into(),
Metamethod::Le => "This is a metamethod which is called by the `<=` operator.".into(),
Metamethod::Concat => "This is a metamethod which is called by the `..` operator.".into(),
Metamethod::Add => "This is a metamethod which is called by the `+` operator.".into(),
Metamethod::Sub => "This is a metamethod which is called by the `-` operator.".into(),
Metamethod::Mul => "This is a metamethod which is called by the `*` operator.".into(),
Metamethod::Div => "This is a metamethod which is called by the `/` operator.".into(),
Metamethod::Mod => "This is a metamethod which is called by the `%` operator.".into(),
Metamethod::Pow => "This is a metamethod which is called by the `^` operator.".into(),
Metamethod::Unm => "This is a metamethod which is called by the `-` operator.".into(),
Metamethod::Eq => "This is a metamethod which is called by the `==` operator.",
Metamethod::Len => "This is a metamethod which is called by the `#` operator.",
Metamethod::Lt => "This is a metamethod which is called by the `<` operator.",
Metamethod::Le => "This is a metamethod which is called by the `<=` operator.",
Metamethod::Concat => "This is a metamethod which is called by the `..` operator.",
Metamethod::Call => {
"This is a metamethod which can be called by calling `(...)` on the value directly."
}
Metamethod::Add => "This is a metamethod which is called by the `+` operator.",
Metamethod::Sub => "This is a metamethod which is called by the `-` operator.",
Metamethod::Mul => "This is a metamethod which is called by the `*` operator.",
Metamethod::Div => "This is a metamethod which is called by the `/` operator.",
Metamethod::Mod => "This is a metamethod which is called by the `%` operator.",
Metamethod::Pow => "This is a metamethod which is called by the `^` operator.",
Metamethod::Unm => "This is a metamethod which is called by the `-` operator.",
Metamethod::ToString => {
"This is a metamethod which can be called by the `tostring` built-in function.".into()
"This is a metamethod which is called by the [`tostring(...)`](https://www.lua.org/manual/5.1/manual.html#pdf-tostring) built-in function."
}
Metamethod::Pairs => {
"This is a metamethod which can be called by the `pairs` built-in function.".into()
"This is a metamethod which is called by the [`pairs(...)`](https://www.lua.org/manual/5.1/manual.html#pdf-pairs) built-in function."
}
Metamethod::Ipairs => {
"This is a metamethod which can be called by the `ipairs` built-in function.".into()
"This is a metamethod which is called by the [`ipairs(...)`](https://www.lua.org/manual/5.1/manual.html#pdf-ipairs) built-in function."
}
_ => format!("This is a metamethod and cannot be called directly."),
_ => "This is a metamethod and cannot be called directly.",
};
func.attrs.insert(0, parse_quote!(#[doc =

View File

@ -1,5 +1,5 @@
use std::env;
use syn::{spanned::Spanned, *};
use syn::{ext::IdentExt, spanned::Spanned, *};
macro_rules! syn_error {
($src:expr, $($fmt:expr),+) => {{
@ -63,7 +63,7 @@ pub fn is_primitivelike(ty: &Type) -> bool {
Type::Path(path) => {
if let Some(name) = path.path.get_ident() {
return matches!(
name.to_string().as_str(),
name.unraw().to_string().as_str(),
"bool"
| "u8"
| "u16"
@ -115,31 +115,37 @@ pub fn is_stringlike(ty: &Type) -> Option<StringLike> {
&& ty.mutability.is_none()
&& ty.lifetime.is_none()
{
match *ty.elem {
Some(match *ty.elem {
Type::Slice(ref slice) => {
// match &[u8]
if let Type::Path(ref path) = *slice.elem
&& let Some(name) = path.path.get_ident()
&& name == "u8"
{
return Some(StringLike::SliceU8);
match name.unraw().to_string().as_str() {
"u8" => StringLike::SliceU8,
_ => return None,
}
} else {
return None;
}
}
Type::Path(ref path) => {
// match &str or &BStr
if let Some(name) = path.path.get_ident() {
match name.to_string().as_str() {
"str" => return Some(StringLike::Str),
"BStr" => return Some(StringLike::BStr),
_ => {}
match name.unraw().to_string().as_str() {
"str" => StringLike::Str,
"BStr" => StringLike::BStr,
_ => return None,
}
} else {
return None;
}
}
}
_ => {}
}
}
_ => return None,
})
} else {
None
}
}
pub fn is_optionlike(ty: &Type) -> Option<&Type> {

View File

@ -9,6 +9,7 @@ use std::{
ffi::{CStr, CString, NulError},
fmt,
marker::PhantomData,
mem::ManuallyDrop,
ops::{Deref, DerefMut},
os::raw::{c_char, c_int, c_void},
pin::Pin,
@ -35,6 +36,11 @@ pub fn url() -> &'static str {
LUAJIT_URL.to_str().unwrap()
}
// reexport constants
pub use luajit_sys::{
LUA_ENVIRONINDEX, LUA_GLOBALSINDEX, LUA_MULTRET, LUA_NOREF, LUA_REFNIL, LUA_REGISTRYINDEX,
};
/// Lua error.
#[derive(Debug, Error)]
#[non_exhaustive]
@ -477,6 +483,19 @@ pub struct Ref {
key: c_int,
}
impl Ref {
/// Consumes this ref and returns the original key used to create the ref.
///
/// This key can be used to index into the registry table ([`LUA_REGISTRYINDEX`]) to retrieve
/// the referenced value. The key can be converted back into a ref using
/// [`State::new_ref_unchecked`].
pub fn into_raw(self) -> c_int {
let Self { ref mut state, key } = *ManuallyDrop::new(self);
unsafe { ptr::drop_in_place(state) }
key
}
}
impl Drop for Ref {
fn drop(&mut self) {
// SAFETY: luaL_unref is guaranteed to not fail
@ -871,7 +890,7 @@ impl Stack {
/// array-part, these values are **not** cleared. The number of values popped is returned, which
/// is always equal to `n`.
///
/// This method does not invoke any metamethods.
/// This method does not invoke any metamethods. The table is not popped from the stack.
///
/// Equivalent to `table.pack(...)`.
///
@ -907,7 +926,8 @@ impl Stack {
/// pushed at the top of the stack in ascending order. If `i > j`, then nothing is pushed.
/// Otherwise, `j - i + 1` values are pushed, and the number of values pushed is returned.
///
/// This method does not invoke any metamethods.
/// This method does not invoke any metamethods. The table is not popped from the stack or
/// altered in any way.
///
/// Equivalent to `table.unpack(list, i, j)`.
///

View File

@ -10,11 +10,11 @@ pub use lb::task;
#[doc(hidden)]
pub fn open(#[allow(unused)] rt: &mut lb::runtime::Builder) {
#[cfg(feature = "task")]
rt.module::<task::lb_tasklib>("lb:task");
rt.module::<task::lb_tasklib>();
#[cfg(feature = "task")]
rt.module::<chan::lb_chanlib>("lb:channel");
rt.module::<chan::lb_chanlib>();
#[cfg(feature = "fs")]
rt.module::<fs::lb_fslib>("lb:fs");
rt.module::<fs::lb_fslib>();
#[cfg(feature = "net")]
rt.module::<net::lb_netlib>("lb:net");
rt.module::<net::lb_netlib>();
}

View File

@ -40,6 +40,22 @@ fn panic_cb(panic: &panic::PanicHookInfo) {
);
}
fn error_cb(err: &luajit::Error) {
match err.trace() {
Some(trace) => eprintln!("{}\n{trace}", err.red().bold()),
None => eprintln!("{}", err.red().bold()),
}
process::exit(1);
}
fn unwrap_exit<T, E: Display>(code: ExitCode) -> impl FnOnce(E) -> T {
move |err| {
eprintln!("{}", err.red().bold());
code.exit()
}
}
#[derive(Debug, Parser)]
struct Args {
/// Paths to scripts to execute.
@ -121,12 +137,12 @@ impl Args {
}
}
fn main() {
fn main() -> Result<(), ExitCode> {
panic::set_hook(Box::new(panic_cb));
let args = Args::parse();
if args.version {
return print_version();
return Ok(print_version());
}
init_logger(&args);
@ -153,13 +169,6 @@ fn print_version() {
);
}
fn unwrap_exit<T, E: Display>(code: ExitCode) -> impl FnOnce(E) -> T {
move |err| {
eprintln!("{}", err.red().bold());
code.exit()
}
}
fn init_logger(args: &Args) {
use tracing::level_filters::LevelFilter;
use tracing_subscriber::util::*;
@ -192,16 +201,15 @@ fn init_logger(args: &Args) {
}
fn init_tokio(args: &Args) -> tokio::runtime::Runtime {
let mut rt = match args.threads.get() {
match args.threads.get() {
1 => tokio::runtime::Builder::new_current_thread(),
n => {
let mut rt = tokio::runtime::Builder::new_multi_thread();
rt.worker_threads(n - 1);
rt
}
};
rt.enable_all()
}
.enable_all()
.thread_name("luby")
.max_blocking_threads(args.blocking_threads.get())
.build()
@ -209,37 +217,43 @@ fn init_tokio(args: &Args) -> tokio::runtime::Runtime {
}
fn init_lua(args: &Args) -> lb::runtime::Runtime {
let mut rt = {
let mut rt = lb::runtime::Builder::new();
luby::open(&mut rt);
if args.dump.iter().find(|s| *s == "cdef").is_some() {
print!("{}", rt.registry());
print!("{}", rt.registry()); // for cdef debugging
}
let mut rt = rt.build().unwrap();
rt.unhandled_error(error_cb).build().unwrap()
};
for arg in args.jit.iter() {
let mut s = rt.guard();
if let Some((cmd, flags)) = parse_jitlib_cmd(arg)
let res = if let Some((cmd, flags)) = parse_jitlib_cmd(arg)
&& let Ok(_) = s.require(format!("jit.{cmd}"), 1)
{
(s.push("start"), s.get(-2), s.push(flags));
s.call(1, 0)
s.call(1, 0) // require("jit.{cmd}").start(flags)
} else {
s.require("jit", 1).unwrap();
match arg.as_str() {
cmd @ ("on" | "off" | "flush") => {
(s.push(cmd), s.get(-2));
s.call(0, 0)
s.call(0, 0) // require("jit").[on/off/flush]()
}
arg => {
flags => {
(s.push("opt"), s.get(-2));
(s.push("start"), s.get(-2), s.push(arg));
s.call(1, 0)
(s.push("start"), s.get(-2), s.push(flags));
s.call(1, 0) // require("jit").opt.start(flags)
}
}
};
if let Err(err) = res {
drop(s);
rt.report_error(&err);
}
.unwrap_or_else(unwrap_exit(ExitCode::Usage));
}
rt
@ -254,27 +268,22 @@ fn parse_jitlib_cmd(s: &str) -> Option<(&str, &str)> {
}
}
async fn main_async(args: Args, state: &mut luajit::State) {
async fn main_async(args: Args, cx: &mut lb::runtime::Context) -> Result<(), ExitCode> {
for ref path in args.path {
let mut s = state.guard();
let chunk = match std::fs::read(path) {
Ok(chunk) => chunk,
Err(err) => {
eprintln!("{}", format_args!("{path}: {err}").red().bold());
ExitCode::NoInput.exit();
return Err(ExitCode::NoInput);
}
};
s.load(&luajit::Chunk::new(chunk).path(path))
.unwrap_or_else(unwrap_exit(ExitCode::NoInput));
if let Err(err) = s.call_async(0, 0).await {
match err.trace() {
Some(trace) => eprintln!("{}\n{trace}", err.red().bold()),
None => eprintln!("{}", err.red().bold()),
if let Err(ref err) = cx.load(&luajit::Chunk::new(chunk).path(path)) {
cx.report_error(err);
} else if let Err(ref err) = cx.call_async(0, 0).await {
cx.report_error(err);
}
}
process::exit(1);
}
}
Ok(())
}

97
tests/main.lua Normal file
View File

@ -0,0 +1,97 @@
if (...) ~= nil and (...).type == "group" then return end -- prevent recursive main call
local fs = require("lb:fs")
local global = _G
local colors = {
reset = "\x1b[0m",
pass = "\x1b[32;1m",
fail = "\x1b[31;1m",
}
local function color(name, s)
return colors[name] .. s .. colors.reset
end
local function create_test(name, f, group)
local test = { type = "test", name = name or "", group = group, state = "pending", f = f }
local fenv = setmetatable({}, { __index = global })
setfenv(f, fenv)
return test
end
local function create_group(name, f, parent)
local group = { type = "group", name = name or "", parent = parent, items = {} }
local fenv = setmetatable({
describe = function(name, f)
local item = create_group(name, f, group)
table.insert(group.items, item)
return item
end,
test = function(name, f)
local item = create_test(name, f, group)
table.insert(group.items, item)
return item
end,
}, { __index = global })
setfenv(f, fenv)
f(group)
return group
end
local function name_test(test)
local name = test.name
local group = test.group
while group ~= nil do
if group.name ~= "" then name = group.name .. " " .. name end
group = group.parent
end
return name
end
local function trace(msg)
return color("fail", msg) .. debug.traceback("", 2):sub(("\nstack traceback:"):len() + 1)
end
local function run_test(test)
local ok, res = xpcall(test.f, trace, test)
if ok then
test.state = "pass"
print("", color("pass", "PASS") .. " " .. name_test(test))
else
test.state = "fail"
print("", color("fail", "FAIL") .. " " .. name_test(test) .. "\n")
print(res .. "\n")
end
return test
end
local function start(cx, item)
if item.type == "test" then
table.insert(cx.tasks, spawn(run_test, item))
elseif item.type == "group" then
for _, item in ipairs(item.items) do
start(cx, item)
end
end
end
local function run(item)
local cx = { tasks = {} }
local pass = true
start(cx, item)
for _, task in ipairs(cx.tasks) do
if task:await().state ~= "pass" then pass = false end
end
if pass then return 0 end -- report status to cargo
return 1
end
return run(create_group("", function()
for entry in fs:glob("{tests,crates/*/tests}/**/*.lua") do
local path = entry:path():sub(3)
local f, err = loadfile(path)
if not f then error(err) end
describe(path, f)
end
end))

44
tests/main.rs Normal file
View File

@ -0,0 +1,44 @@
use luajit::Chunk;
use owo_colors::OwoColorize;
use std::{
fs,
process::{self, ExitCode},
};
fn main() -> ExitCode {
let tokio = tokio::runtime::Runtime::new().unwrap();
let lua = {
let mut rt = lb::runtime::Builder::new();
luby::open(&mut rt);
rt.unhandled_error(error_cb).build().unwrap()
};
let path = "tests/main.lua";
let main = lua.spawn(async move |s| {
if let Err(ref err) = s.load(Chunk::new(fs::read(path).unwrap()).path(path)) {
s.report_error(err);
} else if let Err(ref err) = s.call_async(0, 1).await {
s.report_error(err);
}
if s.slot(1).integer().unwrap_or(1) == 0 {
ExitCode::SUCCESS
} else {
ExitCode::FAILURE
}
});
tokio.block_on(async move {
lua.await;
main.await.unwrap()
})
}
fn error_cb(err: &luajit::Error) {
match err.trace() {
Some(trace) => eprintln!("{}\n{trace}", err.red().bold()),
None => eprintln!("{}", err.red().bold()),
}
process::exit(1);
}