Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 0 additions & 13 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,9 @@
!.gitignore
!.gitattributes

# Project files
/.cache/
/.fleet/
/.idea/
/.vscode/

# OS specific temporary files
.DS_Store
Thumbs.db

# Added by cargo
/target

# Lua compiler output
luac.out

# PUC-Rio Lua 5.1.1 reference (downloaded via AGENTS.md setup)
/lua-5.1.1/
/lua-5.1.1.tar.gz
Expand Down
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ keywords = ["lua", "interpreter", "scripting", "wow", "vm"]
[features]
default = []
dynmod = []
send = []

[[bin]]
name = "rilua"
Expand Down
11 changes: 5 additions & 6 deletions src/bin/riluac.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,10 @@
use std::env;
use std::io::Read;
use std::process;
use std::rc::Rc;

use rilua::compiler;
use rilua::vm::listing;
use rilua::vm::proto::Proto;
use rilua::vm::proto::{Proto, ProtoRef};

/// Version string matching PUC-Rio format.
const VERSION: &str = "Lua 5.1.1 Copyright (C) 1994-2006 Lua.org, PUC-Rio";
Expand Down Expand Up @@ -138,7 +137,7 @@ fn do_args() -> Args {
}

/// Compile a single source or load a binary chunk, reading from file or stdin.
fn compile_source(filename: &str) -> Rc<Proto> {
fn compile_source(filename: &str) -> ProtoRef {
let (source, name) = if filename.is_empty() {
// Read from stdin.
let mut buf = Vec::new();
Expand Down Expand Up @@ -172,7 +171,7 @@ fn compile_source(filename: &str) -> Rc<Proto> {
///
/// Matches PUC-Rio's `combine()` from `luac.c`: creates a main function
/// that calls CLOSURE+CALL for each input file's Proto.
fn combine(protos: Vec<Rc<Proto>>) -> Rc<Proto> {
fn combine(protos: Vec<ProtoRef>) -> ProtoRef {
use rilua::vm::instructions::{Instruction, OpCode};

if protos.len() == 1 {
Expand Down Expand Up @@ -204,7 +203,7 @@ fn combine(protos: Vec<Rc<Proto>>) -> Rc<Proto> {

main.protos = protos;

Rc::new(main)
ProtoRef::new(main)
}

fn main() {
Expand All @@ -219,7 +218,7 @@ fn main() {
};

// Compile all input files.
let protos: Vec<Rc<Proto>> = files.iter().map(|f| compile_source(f)).collect();
let protos: Vec<ProtoRef> = files.iter().map(|f| compile_source(f)).collect();

// Combine into a single Proto (wraps multiple files).
let proto = combine(protos);
Expand Down
15 changes: 8 additions & 7 deletions src/compiler/codegen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
//! during code generation, and `BlockContext` tracks lexical scopes.

use std::collections::HashMap;
use std::rc::Rc;

use crate::error::{LuaError, LuaResult, SyntaxError};

Expand All @@ -18,7 +17,9 @@ use crate::vm::instructions::{
BITRK, Instruction, LFIELDS_PER_FLUSH, LUAI_MAXUPVALUES, LUAI_MAXVARS, MAXARG_BX, MAXARG_C,
MAXINDEXRK, MAXSTACK, NO_JUMP, NO_REG, OpCode, is_k,
};
use crate::vm::proto::{LocalVar, Proto, VARARG_HASARG, VARARG_ISVARARG, VARARG_NEEDSARG};
use crate::vm::proto::{
LocalVar, Proto, ProtoRef, VARARG_HASARG, VARARG_ISVARARG, VARARG_NEEDSARG,
};
use crate::vm::value::Val;

// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -1719,7 +1720,7 @@ impl Compiler {
}

/// Compiles a Lua source string into a Proto (function prototype).
pub fn compile(source: &[u8], name: &str) -> LuaResult<Rc<Proto>> {
pub fn compile(source: &[u8], name: &str) -> LuaResult<ProtoRef> {
let block = parser::parse(source, name)?;
let mut compiler = Compiler::new(name);

Expand All @@ -1730,14 +1731,14 @@ pub fn compile(source: &[u8], name: &str) -> LuaResult<Rc<Proto>> {
compile_block(&mut compiler, &block)?;

let proto = compiler.finish_main();
Ok(Rc::new(proto))
Ok(ProtoRef::new(proto))
}

/// Compiles Lua source from a reader-based lexer into a Proto.
///
/// The lexer pulls data on demand from its reader function, matching
/// PUC-Rio's ZIO model where the lexer drives I/O.
pub fn compile_with_lexer(lexer: Lexer<'_>, name: &str) -> LuaResult<Rc<Proto>> {
pub fn compile_with_lexer(lexer: Lexer<'_>, name: &str) -> LuaResult<ProtoRef> {
let block = parser::parse_with_lexer(lexer)?;
let mut compiler = Compiler::new(name);

Expand All @@ -1748,7 +1749,7 @@ pub fn compile_with_lexer(lexer: Lexer<'_>, name: &str) -> LuaResult<Rc<Proto>>
compile_block(&mut compiler, &block)?;

let proto = compiler.finish_main();
Ok(Rc::new(proto))
Ok(ProtoRef::new(proto))
}

/// Compiles a block of statements inside a new scope.
Expand Down Expand Up @@ -2529,7 +2530,7 @@ fn compile_funcbody(
// Add proto as a child of the current function
let parent_fs = compiler.fs_mut();
let proto_idx = parent_fs.proto.protos.len();
parent_fs.proto.protos.push(Rc::new(proto));
parent_fs.proto.protos.push(ProtoRef::new(proto));

// Emit CLOSURE instruction
#[allow(clippy::cast_possible_truncation)]
Expand Down
25 changes: 25 additions & 0 deletions src/handles.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,15 @@ use crate::vm::value::{Userdata, Val};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Table(pub(crate) GcRef<crate::vm::table::Table>);

impl Table {
/// Creates a `Table` handle from a raw `GcRef<Table>`.
///
/// Used when converting `Val::Table` to a typed handle.
pub fn from_gc_ref(r: GcRef<crate::vm::table::Table>) -> Self {
Self(r)
}
}

/// Handle to a Lua function (closure) in the GC arena.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Function(pub(crate) GcRef<Closure>);
Expand All @@ -33,6 +42,15 @@ pub struct Thread(pub(crate) GcRef<LuaThread>);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AnyUserData(pub(crate) GcRef<Userdata>);

impl AnyUserData {
/// Creates an `AnyUserData` handle from a raw `GcRef<Userdata>`.
///
/// Used when converting `Val::Userdata` to a typed handle.
pub fn from_gc_ref(r: GcRef<Userdata>) -> Self {
Self(r)
}
}

impl Table {
/// Gets a value by key without metamethod dispatch.
pub fn raw_get(&self, state: &LuaState, key: Val) -> LuaResult<Val> {
Expand Down Expand Up @@ -87,6 +105,13 @@ impl Table {
}

impl Function {
/// Creates a `Function` handle from a raw `GcRef<Closure>`.
///
/// Used when converting `Val::Function` to a typed handle.
pub fn from_gc_ref(r: GcRef<Closure>) -> Self {
Self(r)
}

/// Returns the underlying `GcRef` for internal use.
pub fn gc_ref(self) -> GcRef<Closure> {
self.0
Expand Down
105 changes: 97 additions & 8 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,14 @@ pub mod stdlib;
pub mod vm;

use std::any::Any;
use std::rc::Rc;

// Re-exports for public API.
pub use conversion::{FromLua, FromLuaMulti, IntoLua, IntoLuaMulti};
pub use error::{LuaError, LuaResult, RuntimeError, runtime_error};
pub use handles::{AnyUserData, Function, Table, Thread};
pub use stdlib::StdLib;
pub use vm::closure::RustFn;
pub use vm::proto::ProtoRef;
pub use vm::state::ThreadStatus;
pub use vm::value::Val;

Expand Down Expand Up @@ -107,6 +107,22 @@ pub struct Lua {
state: LuaState,
}

// SAFETY: With the `send` feature enabled, all fields of `LuaState` are
// `Send`-safe:
// - `ProtoRef` is `Arc<Proto>` (Send + Sync)
// - `UserDataBox` is `Box<dyn Any + Send>` (Send)
// - `GcRef<T>` is two `u32` indices + PhantomData (Send)
// - `Gc` contains `Arena<T>` (Vec of owned data) and `StringTable` (HashMap)
// - `IoFile` has `unsafe impl Send` (FILE* can be transferred between threads)
// - All other fields are primitives, `Vec<Val>`, `Vec<CallInfo>`, etc.
//
// `Lua` requires `&mut self` for all operations, preventing concurrent access.
// This impl enables embedding rilua in frameworks (e.g., bevy_mod_scripting)
// that store script contexts behind a Mutex.
#[cfg(feature = "send")]
#[allow(unsafe_code)]
unsafe impl Send for Lua {}

impl Lua {
/// Creates a new Lua state with all standard libraries loaded.
pub fn new() -> LuaResult<Self> {
Expand Down Expand Up @@ -188,9 +204,9 @@ impl Lua {
/// Compiles Lua source bytes (or loads a binary chunk) and returns a function handle.
pub fn load_bytes(&mut self, source: &[u8], name: &str) -> LuaResult<Function> {
let proto = compile_or_undump(source, name)?;
let mut proto = Rc::try_unwrap(proto).unwrap_or_else(|rc| (*rc).clone());
let mut proto = ProtoRef::try_unwrap(proto).unwrap_or_else(|rc| (*rc).clone());
patch_string_constants(&mut proto, &mut self.state.gc);
let proto = Rc::new(proto);
let proto = ProtoRef::new(proto);

let num_upvalues = proto.num_upvalues as usize;
let mut lua_cl = LuaClosure::new(proto, self.state.global);
Expand Down Expand Up @@ -241,18 +257,33 @@ impl Lua {
/// The returned `AnyUserData` handle can be passed to Lua code or
/// stored as a global. Use [`AnyUserData::set_metatable`] to attach
/// methods.
///
/// With the `send` feature enabled, `T` must also implement `Send`.
#[cfg(not(feature = "send"))]
pub fn create_userdata<T: Any>(&mut self, data: T) -> AnyUserData {
let ud = vm::value::Userdata::new(Box::new(data));
let r = self.state.gc.alloc_userdata(ud);
AnyUserData(r)
}

/// Creates a new userdata containing `data` with no metatable.
///
/// With the `send` feature, `T` must implement `Send` so the `Lua`
/// instance remains thread-safe.
#[cfg(feature = "send")]
pub fn create_userdata<T: Any + Send>(&mut self, data: T) -> AnyUserData {
let ud = vm::value::Userdata::new(Box::new(data));
let r = self.state.gc.alloc_userdata(ud);
AnyUserData(r)
}

/// Creates a new userdata with a named, registry-cached metatable.
///
/// The metatable is stored in the registry under `type_name`. If a
/// metatable for that name already exists, it is reused. This is the
/// same pattern used by PUC-Rio's `luaL_newmetatable` for C userdata
/// types.
#[cfg(not(feature = "send"))]
pub fn create_typed_userdata<T: Any>(
&mut self,
data: T,
Expand All @@ -264,6 +295,20 @@ impl Lua {
Ok(AnyUserData(r))
}

/// Creates a new userdata with a named, registry-cached metatable
/// (thread-safe variant).
#[cfg(feature = "send")]
pub fn create_typed_userdata<T: Any + Send>(
&mut self,
data: T,
type_name: &str,
) -> LuaResult<AnyUserData> {
let mt = stdlib::new_metatable(&mut self.state, type_name)?;
let ud = vm::value::Userdata::with_metatable(Box::new(data), mt);
let r = self.state.gc.alloc_userdata(ud);
Ok(AnyUserData(r))
}

/// Creates or retrieves a named metatable for a userdata type.
///
/// The metatable is cached in the registry by `type_name`. Methods
Expand Down Expand Up @@ -520,6 +565,41 @@ impl Lua {
table.raw_len(&self.state)
}

/// Returns the next key-value pair after `key` in the table.
///
/// Pass `Val::Nil` to start iteration. Returns `None` when the
/// table is exhausted. This mirrors PUC-Rio's `lua_next`.
pub fn table_next(&self, table: &Table, key: Val) -> LuaResult<Option<(Val, Val)>> {
let t = self.state.gc.tables.get(table.0).ok_or_else(|| {
LuaError::Runtime(RuntimeError {
message: "table has been collected".into(),
level: 0,
traceback: vec![],
})
})?;
t.next(key, &self.state.gc.string_arena)
}

/// Returns the byte content of a `Val::Str`.
///
/// Returns `None` if the value is not a string or the string has
/// been collected.
pub fn val_as_bytes(&self, val: Val) -> Option<&[u8]> {
if let Val::Str(str_ref) = val {
self.state.gc.string_arena.get(str_ref).map(|s| s.data())
} else {
None
}
}

/// Borrows a typed value from a userdata handle.
///
/// Returns `None` if the stored type is not `T` or the userdata has
/// been collected.
pub fn borrow_userdata<T: std::any::Any>(&self, ud: &AnyUserData) -> Option<&T> {
ud.borrow::<T>(&self.state)
}

/// Sets a named Rust function on a table.
///
/// Creates a closure wrapping `func` and stores it as `table[name]`.
Expand Down Expand Up @@ -550,10 +630,10 @@ impl Lua {
// -----------------------------------------------------------------------

/// Patches string constants, creates a main closure, and executes it.
fn run_proto(&mut self, proto: Rc<Proto>) -> LuaResult<()> {
let mut proto = Rc::try_unwrap(proto).unwrap_or_else(|rc| (*rc).clone());
fn run_proto(&mut self, proto: ProtoRef) -> LuaResult<()> {
let mut proto = ProtoRef::try_unwrap(proto).unwrap_or_else(|rc| (*rc).clone());
patch_string_constants(&mut proto, &mut self.state.gc);
let proto = Rc::new(proto);
let proto = ProtoRef::new(proto);

let num_upvalues = proto.num_upvalues as usize;
let mut lua_cl = LuaClosure::new(proto, self.state.global);
Expand Down Expand Up @@ -641,7 +721,7 @@ pub fn exec_with_name(source: &[u8], name: &str) -> LuaResult<()> {
/// Detects the `\x1bLua` signature and dispatches to `undump` for binary
/// chunks or `compile` for source text. Both return an unpatched Proto
/// (strings in `string_pool`, `Val::Nil` placeholders).
pub(crate) fn compile_or_undump(source: &[u8], name: &str) -> LuaResult<Rc<Proto>> {
pub(crate) fn compile_or_undump(source: &[u8], name: &str) -> LuaResult<ProtoRef> {
// Skip shebang line before checking for binary signature.
// PUC-Rio's luaL_loadfile reads past the leading '#' line, then
// checks if the remaining content starts with LUA_SIGNATURE.
Expand Down Expand Up @@ -672,7 +752,7 @@ pub(crate) fn patch_string_constants(proto: &mut Proto, gc: &mut Gc) {
proto.constants[idx as usize] = Val::Str(str_ref);
}
for child in &mut proto.protos {
patch_string_constants(Rc::make_mut(child), gc);
patch_string_constants(ProtoRef::make_mut(child), gc);
}
}

Expand Down Expand Up @@ -969,4 +1049,13 @@ mod tests {
clear_interrupted();
assert!(!check_interrupted(), "flag should be false after clear");
}

/// Compile-time assertion that `Lua` is `Send` when the `send`
/// feature is enabled.
#[cfg(feature = "send")]
#[test]
fn lua_is_send() {
fn assert_send<T: Send>() {}
assert_send::<Lua>();
}
}
7 changes: 7 additions & 0 deletions src/platform.rs
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,13 @@ pub(crate) mod dynlib {
path: String,
}

// DynLib's handle is a dlopen/LoadLibrary pointer. It can be safely
// transferred between threads; only concurrent use is unsafe (prevented
// by &mut Lua).
#[cfg(feature = "send")]
#[allow(unsafe_code)]
unsafe impl Send for DynLib {}

impl DynLib {
/// Opens a shared library at `path`.
pub(crate) fn open(path: &str) -> Result<Self, String> {
Expand Down
Loading