| name | lua-to-rust |
| description | Use when migrating Lua codebases to Rust — covers table to struct/enum, metatable OOP to traits, coroutine to async/await, OpenResty to Axum, LuaRocks to Cargo, and incremental migration via mlua embedding. Includes canonical code patterns, common mistakes, and reference implementations. |
| updated | 2026-07-30T00:00:00.000Z |
Lua to Rust Migration
Architecture Mapping
Lua's register-based VM (PUC-Rio Lua or LuaJIT's tracing JIT) executes bytecode in a single-threaded event loop, with all state held in the global _G table and a C API for native extensions. Rust replaces the interpreter entirely — producing a statically linked native binary with no runtime overhead. Where Lua relies on garbage collection (incremental mark-sweep or generational in LuaJIT) and the coroutine module for cooperative multitasking, Rust provides ownership-based memory management with deterministic cleanup and a rich async runtime via tokio.
The critical architectural shift: Lua's "table as universal data structure" becomes Rust's typed struct and enum; Lua's metatable-based OOP becomes Rust's trait system; and Lua's C-module extension mechanism (require "module" loading .so files) becomes Cargo's compile-time dependency resolution. For large Lua codebases (OpenResty/Nginx, Redis scripts, game engine scripting), the recommended strategy embeds Lua in Rust via mlua and incrementally rewrites modules, preserving the scripting flexibility at the edges where it matters.
| Lua Concept | Rust Equivalent |
|---|
| Lua VM (lua/luajit) | rustc + LLVM (AOT compilation) |
| LuaRocks | Cargo + crates.io |
| C API / LuaJIT FFI | bindgen + cc crate / FFI declarations |
| require / package.path | mod / use / Cargo.toml [dependencies] |
Global table _G | No global mutable state — prefer dependency injection or once_cell::sync::Lazy |
| Embedded scripting | mlua / rlua crate (embed Lua in Rust) or rewrite natively |
| Nginx + OpenResty | Custom Rust HTTP service (actix-web / axum) |
Lua excels at glue code and DSLs. When migrating to Rust, preserve the scripting flexibility either through mlua embedding or by defining a clean trait-based plugin system.
Type System Mapping
Lua has exactly 8 types, all dynamic. Rust has a rich static type system. The migration requires deciding which concrete Rust type replaces each polymorphic Lua table.
| Lua Type | Rust Type | Notes |
|---|
nil | Option<T> | Every nullable value becomes Option |
boolean | bool | Direct mapping |
number | f64 or i64 | Lua 5.3+ has integer subtype; LuaJIT uses f64. Choose based on domain. |
string | String / &str | Lua strings are immutable byte buffers; Rust strings are UTF-8. Use Vec<u8> for binary data. |
table (array) | Vec<T> | Numeric keys 1..n in Lua map to 0-indexed Vec in Rust |
table (hash) | HashMap<K, V> / BTreeMap<K, V> | String-keyed tables; use BTreeMap when ordering matters |
table (mixed) | struct with named fields | When keys are fixed and known at compile time |
table (object) | struct + impl | Metatable-based OOP becomes trait implementations |
function | fn(...) -> ... / Fn / FnMut / FnOnce | Function pointers or closure traits |
thread (coroutine) | async fn / Future | Lua coroutines map to async/await |
userdata | Box<dyn Any> / opaque struct | C-protected userdata becomes Rust struct with private fields |
Dynamic Dispatch Pattern
Lua tables that hold mixed types require an enum for static typing:
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
enum LuaValue {
Nil,
Bool(bool),
Number(f64),
String(String),
Table(HashMap<String, LuaValue>),
Array(Vec<LuaValue>),
}
#[derive(Debug, Clone)]
struct Player {
name: String,
age: u32,
tags: Vec<String>,
}
Memory & Ownership Model
Lua uses a tracing garbage collector (incremental mark-sweep in PUC Lua, generational in LuaJIT). Rust ownership eliminates GC entirely.
| Lua Pattern | Rust Translation |
|---|
| GC-managed objects | Ownership system — values dropped at end of scope |
| Shared references (table aliases) | Rc<T> for shared ownership, Arc<T> for thread-safe sharing |
| Mutable shared state | RefCell<T> (single-threaded) or RwLock<T> / Mutex<T> (multi-threaded) |
| Weak references | Weak<T> — prevents reference cycles |
__gc metamethod | Drop trait implementation |
| Circular references | Weak<T> or arena-based allocation with indices instead of references |
Reference Cycle Example
use std::rc::{Rc, Weak};
use std::cell::RefCell;
struct Node {
parent: RefCell<Weak<Node>>,
child: RefCell<Option<Rc<Node>>>,
}
impl Drop for Node {
fn drop(&mut self) {
tracing::debug!("Node dropped");
}
}
Concurrency / Async Translation
Lua has no native threading (only coroutines). LuaJIT has basic FFI-based threads but no memory safety guarantees. Rust provides both sync and async concurrency with compile-time safety.
| Lua Concurrency | Rust Equivalent |
|---|
coroutine.create(f) | tokio::spawn(async move { ... }) |
coroutine.resume(co) | .await on a Future |
coroutine.yield(val) | pending!() / async streams / yield_now().await |
coroutine.status(co) | Future state is implicit; use futures::future::poll_fn |
coroutine.wrap(f) | Closure returning a Future |
| No parallelism (single OS thread) | rayon::spawn / tokio::task::spawn_blocking |
lua_lock (GIL) | No GIL — concurrent access via Arc<RwLock<T>> |
Coroutine to Async Stream
use futures::stream::{self, Stream};
fn range_gen(n: u64) -> impl Stream<Item = u64> {
stream::iter(1..=n)
}
Error-Safe Coroutine Resume
fn safe_call<F, T, E>(f: F) -> Result<T, Box<dyn std::error::Error>>
where
F: FnOnce() -> Result<T, E>,
E: std::error::Error + 'static,
{
f().map_err(|e| Box::new(e) as Box<dyn std::error::Error>)
}
fn safe_panic<F, T>(f: F) -> Result<T, Box<dyn std::any::Any + Send>>
where
F: FnOnce() -> T + std::panic::UnwindSafe,
{
std::panic::catch_unwind(f)
}
Build System & Dependencies
| Lua | Rust |
|---|
| LuaRocks (.rockspec) | Cargo.toml |
require "module" | mod module; / use crate::module; |
package.path | Module search via filesystem hierarchy under src/ |
package.cpath (C modules) | [dependencies] with -sys crates or build.rs + cc crate |
luarocks install | cargo add <crate> |
| luarocks tree (local) | target/ directory |
| Lua 5.1 / 5.2 / 5.3 / 5.4 / LuaJIT | rustc edition 2018/2021/2024 + target triple |
Cargo.toml for a Migrated Lua Project
[package]
name = "my-app"
version = "0.1.0"
edition = "2021"
[dependencies]
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
regex = "1"
mlua = { version = "0.10", features = ["lua54", "vendored"] }
once_cell = "1"
tracing = "0.1"
thiserror = "2"
[dev-dependencies]
rstest = "0.22"
Standard Library & Ecosystem Mapping
| Lua Library / Function | Rust Equivalent |
|---|
table.insert(t, v) | vec.push(v) |
table.remove(t, pos) | vec.remove(pos) |
table.concat(t, sep) | vec.join(sep) (via itertools) 或 vec.iter().join(sep) |
table.sort(t, cmp) | vec.sort_by(cmp) / vec.sort() |
table.pack(...) | 元组 (a, b, c) 或 vec![a, b, c] |
table.unpack(t) | 解构语法或索引访问 |
string.sub(s, i, j) | &s[i-1..j] (注意边界检查) |
string.find(s, pat) | s.find(pat) / regex::Regex::find |
string.gsub(s, pat, repl) | regex::Regex::replace_all |
string.format(...) | format!(...) |
string.len(s) | s.len() (字节长度) 或 s.chars().count() (字符数) |
string.match(s, pat) | regex::Regex::captures |
string.gmatch(s, pat) | regex::Regex::captures_iter |
string.byte(s, i) | s.as_bytes()[i-1] |
string.char(...) | char::from_u32(...) / std::str::from_utf8 |
io.open(path, mode) | std::fs::File::open(path) / std::fs::File::create(path) |
io.read("*all") | std::fs::read_to_string(path) |
|
OpenResty / Nginx → Rust HTTP Services
OpenResty (Nginx + LuaJIT) is one of the most common Lua deployment targets. Its non-blocking I/O model maps naturally to Rust's async ecosystem.
| OpenResty / Nginx Concept | Rust Equivalent | Notes |
|---|
nginx.conf location / {} | axum::Router::route() | Route definitions in Rust, not config files |
ngx.req.get_uri_args() | axum::extract::Query<T> | Type-safe query extraction via serde |
ngx.req.get_post_args() | axum::extract::Form<T> / Json<T> | Typed body extraction |
ngx.req.get_headers() | axum::http::HeaderMap | Header extraction |
ngx.say() / ngx.print() | axum::response::Html / Json | Typed response types |
ngx.exit(code) | Return StatusCode enum variant | Map HTTP status directly |
ngx.sleep(seconds) | tokio::time::sleep(Duration::from_secs(n)) | Async sleep, no blocking |
ngx.timer.at(delay, callback) | tokio::time::interval + tokio::spawn | Scheduled async tasks |
ngx.location.capture() | reqwest::Client internal subrequest | HTTP client for internal calls |
ngx.shared.DICT (shared memory) | dashmap::DashMap / moka::Cache | In-process concurrent cache |
lua-resty-core | Axum + tower ecosystem | Standard library of middleware |
lua-resty-redis / redis-lua | redis crate (async) | Async Redis with connection pooling |
lua-resty-mysql / pgmoon | sqlx | Async, compile-time checked SQL |
Nginx config → Cargo.toml + main.rs:
# Lua/OpenResty: routing in nginx.conf
location /api/v1/users {
content_by_lua_block {
local users = require("handlers.users")
users.handle_get()
}
}
use axum::{routing::get, Router};
async fn handle_get_users(
State(state): State<Arc<AppState>>,
Query(params): Query<UserParams>,
) -> Result<Json<Vec<User>>, AppError> {
let users = state.db.get_users(¶ms).await?;
Ok(Json(users))
}
let app = Router::new()
.route("/api/v1/users", get(handle_get_users))
.with_state(app_state);
String Pattern to Regex Translation
use regex::Regex;
fn lua_style_match(input: &str) -> Option<String> {
let re = Regex::new(r"(\d+)").unwrap();
re.captures(input)
.and_then(|caps| caps.get(1))
.map(|m| m.as_str().to_string())
}
fn lua_style_gsub(input: &str) -> String {
let re = Regex::new(r"-").unwrap();
re.replace_all(input, "_").to_string()
}
Canonical Patterns
Pattern 1: Module Definition
pub fn hello(name: &str) -> String {
format!("Hello {name}")
}
Pattern 2: Multi-Return Values
fn find_item(items: &[String], target: &str) -> (bool, Option<usize>) {
items.iter()
.position(|s| s == target)
.map_or((false, None), |i| (true, Some(i)))
}
Pattern 3: Varargs to Generic Slices
fn sum(args: &[i64]) -> i64 {
args.iter().sum()
}
macro_rules! sum {
($($x:expr),*) => {
{
let mut total = 0i64;
$(total += $x;)*
total
}
};
}
Pattern 4: Metatable-Based OOP
struct Animal {
name: String,
}
impl Animal {
fn new(name: impl Into<String>) -> Self {
Self { name: name.into() }
}
fn speak(&self) -> String {
format!("{} makes a sound", self.name)
}
}
struct Dog {
animal: Animal,
breed: String,
}
impl std::ops::Deref for Dog {
type Target = Animal;
fn deref(&self) -> &Animal { &self.animal }
}
Pattern 5: pcall / xpcall Error Handling
fn risky_function() -> Result<String, MyError> {
Ok("result".into())
}
match risky_function() {
Ok(result) => { }
Err(e) => tracing::error!("Operation failed: {e}"),
}
let outcome = risky_function()
.and_then(|val| another_op(&val))
.map(|final_val| format!("processed: {final_val}"));
Pattern 6: Closures Over Upvalues
fn counter() -> impl FnMut() -> i32 {
let mut count = 0;
move || {
count += 1;
count
}
}
Pattern 7: Iterator Generators
struct RangeIter {
current: i32,
end: i32,
}
impl Iterator for RangeIter {
type Item = i32;
fn next(&mut self) -> Option<Self::Item> {
if self.current > self.end {
None
} else {
let val = self.current;
self.current += 1;
Some(val)
}
}
}
FFI & Incremental Migration
The most practical approach for migrating a large Lua codebase is to embed Lua in Rust via mlua, then incrementally rewrite modules to pure Rust.
| Strategy | Tool | When to Use |
|---|
| Embed Lua in Rust | mlua / rlua | Large existing Lua code; call Lua from Rust |
| Call Rust from Lua | mlua create_function / UserData | Replace hot paths first; keep glue code in Lua |
| Replace Lua C modules | cc crate + bindgen | Lua C library dependencies; wrap with safe Rust |
| Full rewrite | Pure Rust | Small codebase or clear performance motivation |
| LuaJIT FFI replacement | Rust FFI (extern "C") | Direct C library calls; safer bindings |
mlua Embedding Example
use mlua::{Lua, Function, Table, UserData, UserDataMethods};
struct Config { max_retries: u32, timeout_ms: u64 }
impl UserData for Config {
fn add_methods<'lua, M: UserDataMethods<'lua, Self>>(methods: &mut M) {
methods.add_method("get_timeout", |_, cfg, ()| Ok(cfg.timeout_ms));
}
}
fn run_legacy_script() -> mlua::Result<()> {
let lua = Lua::new();
let config = Config { max_retries: 3, timeout_ms: 5000 };
lua.globals().set("config", config)?;
let log_fn = lua.create_function(|_, msg: String| {
tracing::info!("[lua] {msg}");
Ok(())
})?;
lua.globals().set("log_info", log_fn)?;
lua.load(r#"
log_info("Starting with timeout: " .. config:get_timeout() .. "ms")
"#).()?;
(())
}
Building an Incremental Replacement Pipeline
- Wrap the entire Lua application in mlua with Rust
main() as entry point.
- Profile and identify hot Lua functions. Rewrite them as Rust
create_function callbacks.
- Move business logic from Lua tables into Rust
UserData structs.
- Gradually replace
require calls with Rust module imports.
- Once all logic is in Rust, remove the mlua dependency (or keep it for plugin scripting).
Common Mistakes
Mistake 1: 1-Based to 0-Based Index Confusion
fn get_first<T>(items: &[T]) -> &T {
&items[1]
}
fn get_first<T>(items: &[T]) -> Option<&T> {
items.first()
}
Mistake 2: Overusing Rc<RefCell> — Lua GC Emulation
type Globals = Rc<RefCell<HashMap<String, Rc<RefCell<LuaValue>>>>>;
#[derive(Debug)]
struct AppState {
config: Config,
players: HashMap<String, Player>,
}
Mistake 3: String Concatenation in Hot Loops
let mut result = String::new();
for item in &items {
result = result + &format!("{item},");
}
let result = items.iter()
.map(|s| s.as_str())
.collect::<Vec<_>>()
.join(",");
Mistake 4: Using panic! Instead of Result for Recoverable Errors
fn load_config(path: &str) -> Config {
let content = std::fs::read_to_string(path).unwrap();
serde_json::from_str(&content).unwrap()
}
fn load_config(path: &str) -> Result<Config, Box<dyn std::error::Error>> {
let content = std::fs::read_to_string(path)?;
let config = serde_json::from_str(&content)?;
Ok(config)
}
Mistake 5: Assuming Nil/None Semantics in Collections
let mut items: Vec<Option<String>> = vec![None, None];
items[5] = Some("hello".into());
let mut items: HashMap<usize, String> = HashMap::new();
items.insert(5, "hello".into());
Reference Implementations
| Project | Description | Migration Strategy |
|---|
| StyLua | Lua formatter written in Rust | Full Rust implementation of a Lua tool; uses full-moon for parsing |
| mlua | High-level Lua bindings for Rust | Embedding pattern — call Lua from Rust; UserData trait maps to Lua objects |
| rlua | Safe high-level Lua bindings (predecessor to mlua) | Similar embedding approach |
| full-moon | Lossless Lua parser in Rust | Parse Lua AST; useful for migrating Lua DSLs to Rust-native parsers |
| LuaJIT-remake | LuaJIT concepts implemented in Rust | Reference for understanding LuaJIT internals in Rust |
| Roblox-ts | TypeScript-to-Luau compiler | Type-first approach to Lua; similar migration philosophy |
Cross-Reference
- c-to-rust — For migrating Lua C modules and FFI patterns
- nodejs-to-rust — For event-loop and async patterns common to both Lua and JS
- go-to-rust — For concurrent goroutine-to-tokio patterns similar to coroutine migration
- zig-to-rust — For low-level memory patterns relevant to LuaJIT FFI replacement