Skip to main content

retex_lua/
lib.rs

1//! Embedded Lua 5.3 and the LuaTeX Lua API.
2//!
3//! [`LuaTexHost`] implements the engine's [`LuaHost`] trait. During a
4//! `\directlua` call the engine is lent to Lua as scoped userdata stored in
5//! the registry under [`ENGINE_KEY`]. Library functions borrow it from
6//! there, so no Lua object can outlive the borrow. Nested calls
7//! (Lua → TeX → Lua) install a fresh scoped reference and restore the
8//! previous one afterwards.
9//!
10//! The libraries follow the LuaTeX reference manual (chapter 10, "The TeX
11//! related libraries") and are checked against the behaviour of the
12//! reference `luatex` binary.
13
14use std::rc::Rc;
15
16use mlua::{AnyUserData, Lua, Value};
17use retex_engine::input::LuaItem;
18use retex_engine::{Engine, LuaHost};
19
20mod callback;
21mod compat;
22mod extras;
23mod fontlib;
24mod fontloaderlib;
25mod fonttable;
26mod kpselib;
27mod langlib;
28mod lfslib;
29mod lualib;
30pub mod luaotfload;
31pub mod nodelib;
32mod otfaux;
33mod otfnodes;
34mod pdflib;
35mod siolib;
36mod statuslib;
37mod texiolib;
38mod texlib;
39mod tokenlib;
40
41/// Registry key of the scoped engine reference.
42pub const ENGINE_KEY: &str = "retex.engine";
43
44/// Text printed by `tex.print` and friends during the current calls, one
45/// buffer per nesting level.
46#[derive(Default)]
47pub(crate) struct PrintStack(pub Vec<Vec<LuaItem>>);
48
49/// Runs `f` with the engine currently lent to Lua.
50pub(crate) fn with_engine<R>(lua: &Lua, f: impl FnOnce(&mut Engine) -> R) -> mlua::Result<R> {
51    let ud: AnyUserData = lua
52        .named_registry_value(ENGINE_KEY)
53        .map_err(|e| mlua::Error::runtime(format!("no engine: {e}")))?;
54    ud.borrow_mut_scoped::<Engine, R>(f)
55        .map_err(|e| mlua::Error::runtime(format!("engine borrow: {e}")))
56}
57
58/// Registry key of the factory used by [`create_function_plain_errors`].
59const PLAIN_ERRORS_KEY: &str = "retex.plainerrors";
60
61/// Like `Lua::create_function`, but errors reach Lua code as plain
62/// strings, as `luaL_error` raises them in LuaTeX's C libraries
63/// (`pcall(pdf.obj)` returns `false, "pdf.obj() needs at least one
64/// argument"`), instead of mlua's error userdata with a traceback.
65pub(crate) fn create_function_plain_errors<A, R, F>(lua: &Lua, f: F) -> mlua::Result<mlua::Function>
66where
67    A: mlua::FromLuaMulti,
68    R: mlua::IntoLuaMulti,
69    F: Fn(&Lua, A) -> mlua::Result<R> + 'static,
70{
71    let inner = lua.create_function(move |lua, args: A| {
72        let mut out = match f(lua, args) {
73            Ok(r) => r.into_lua_multi(lua)?,
74            Err(e) => {
75                let msg = lua.create_string(lua_error_message(&e))?;
76                return Ok(mlua::MultiValue::from_vec(vec![
77                    Value::Boolean(false),
78                    Value::String(msg),
79                ]));
80            }
81        };
82        out.push_front(Value::Boolean(true));
83        Ok(out)
84    })?;
85    let factory: mlua::Function = match lua.named_registry_value::<Value>(PLAIN_ERRORS_KEY)? {
86        Value::Function(f) => f,
87        _ => {
88            let f = lua
89                .load(
90                    "local f = ...
91                     local function finish(ok, ...)
92                       if ok then return ... end
93                       error((...), 0)
94                     end
95                     return function(...) return finish(f(...)) end",
96                )
97                .set_name("=[retex]")
98                .into_function()?;
99            lua.set_named_registry_value(PLAIN_ERRORS_KEY, &f)?;
100            f
101        }
102    };
103    factory.call(inner)
104}
105
106/// Appends an item to the current print buffer.
107pub(crate) fn push_print(lua: &Lua, item: LuaItem) {
108    if let Some(mut ps) = lua.app_data_mut::<PrintStack>()
109        && let Some(top) = ps.0.last_mut()
110    {
111        top.push(item);
112    }
113}
114
115/// The Lua interpreter with LuaTeX's libraries.
116pub struct LuaTexHost {
117    lua: Lua,
118}
119
120/// A sampling profiler for performance work (`RETEX_LUA_PROFILE=1`):
121/// every 1000 Lua VM instructions, the running function is charged one
122/// sample; the busiest functions are printed to stderr at exit.
123const PROFILER_START: &str = r#"
124local samples, getinfo = {}, debug.getinfo
125debug.sethook(function()
126  local i = getinfo(2, "S")
127  if i then
128    local k = i.short_src .. ":" .. i.linedefined
129    samples[k] = (samples[k] or 0) + 1
130  end
131end, "", 1000)
132__retex_profile = samples
133"#;
134
135const PROFILER_REPORT: &str = r#"
136local s = __retex_profile
137if not s then return end
138debug.sethook()
139local t, total = {}, 0
140for k, n in pairs(s) do t[#t + 1] = {k, n}; total = total + n end
141table.sort(t, function(a, b) return a[2] > b[2] end)
142io.stderr:write(string.format("[lua profile] %d samples (1 per 1000 instructions)\n", total))
143for i = 1, math.min(25, #t) do
144  io.stderr:write(string.format("[lua profile] %6d  %5.1f%%  %s\n", t[i][2], 100 * t[i][2] / total, t[i][1]))
145end
146"#;
147
148impl Drop for LuaTexHost {
149    fn drop(&mut self) {
150        if std::env::var_os("RETEX_LUA_PROFILE").is_some() {
151            let _ = self.lua.load(PROFILER_REPORT).exec();
152        }
153    }
154}
155
156impl LuaTexHost {
157    /// Creates the interpreter and installs the libraries.
158    pub fn new() -> mlua::Result<Rc<Self>> {
159        // SAFETY: LuaTeX documents expect the full standard library,
160        // including `debug`. This is the crate's single audited unsafe site:
161        // mlua requires `unsafe` to load the debug library, which can break
162        // Lua's own invariants but not Rust's memory safety outside Lua.
163        #[allow(unsafe_code)]
164        let lua = unsafe { Lua::unsafe_new_with(mlua::StdLib::ALL, mlua::LuaOptions::new()) };
165        lua.set_app_data(PrintStack::default());
166        retex_engine::timing::mark("create: Lua state");
167        compat::install(&lua)?;
168        texlib::install(&lua)?;
169        texiolib::install(&lua)?;
170        lualib::install(&lua)?;
171        callback::install(&lua)?;
172        kpselib::install(&lua)?;
173        statuslib::install(&lua)?;
174        lfslib::install(&lua)?;
175        nodelib::install(&lua)?;
176        tokenlib::install(&lua)?;
177        extras::install(&lua)?;
178        luaotfload::install(&lua)?;
179        langlib::install(&lua)?;
180        fontlib::install(&lua)?;
181        fontloaderlib::install(&lua)?;
182        pdflib::install(&lua)?;
183        retex_engine::timing::mark("create: Lua libraries");
184        retex_lpeg::install(&lua)?;
185        retex_engine::timing::mark("create: LPeg");
186        siolib::install(&lua)?;
187        // The command line, as LuaTeX's `arg`: the program name as invoked
188        // at index 0, then the arguments verbatim (hyperxmp reads it).
189        let arg = lua.create_table()?;
190        for (i, a) in std::env::args_os().enumerate() {
191            arg.raw_set(i as i64, lua.create_string(a.as_encoded_bytes())?)?;
192        }
193        lua.globals().set("arg", arg)?;
194        // The job's output directory, for the commands it runs.
195        let output_directory = lua.create_function(|lua, ()| {
196            Ok(crate::with_engine(lua, |e| {
197                e.files
198                    .output_dir
199                    .as_ref()
200                    .map(|d| d.to_string_lossy().into_owned())
201            })
202            .ok()
203            .flatten())
204        })?;
205        lua.load(include_str!("shell.lua"))
206            .set_name("=shell")
207            .call::<()>(output_directory)?;
208        if std::env::var_os("RETEX_LUA_PROFILE").is_some() {
209            lua.load(PROFILER_START).set_name("=profiler").exec()?;
210        }
211        Ok(Rc::new(LuaTexHost { lua }))
212    }
213
214    /// The underlying Lua state.
215    pub fn lua(&self) -> &Lua {
216        &self.lua
217    }
218
219    /// Installs a host into an engine.
220    pub fn attach(engine: &mut Engine) -> mlua::Result<Rc<Self>> {
221        let host = LuaTexHost::new()?;
222        engine.lua = Some(host.clone() as Rc<dyn LuaHost>);
223        Ok(host)
224    }
225}
226
227impl LuaTexHost {
228    /// Lends the engine to Lua while `f` runs (see the module docs).
229    fn lend<R>(
230        &self,
231        engine: &mut Engine,
232        f: impl FnOnce(&Lua) -> mlua::Result<R>,
233    ) -> mlua::Result<R> {
234        let lua = &self.lua;
235        lua.scope(|scope| {
236            let ud = scope.create_any_userdata_ref_mut(&mut *engine)?;
237            let prev: Value = lua.named_registry_value(ENGINE_KEY)?;
238            lua.set_named_registry_value(ENGINE_KEY, ud)?;
239            let r = f(lua);
240            lua.set_named_registry_value(ENGINE_KEY, prev)?;
241            r
242        })
243    }
244}
245
246impl LuaHost for LuaTexHost {
247    fn run(&self, engine: &mut Engine, name: &str, code: &[u8]) -> Result<(), String> {
248        let lua = &self.lua;
249        if let Some(mut ps) = lua.app_data_mut::<PrintStack>() {
250            ps.0.push(Vec::new());
251        }
252        let chunk_name = format!("=[{name}]");
253        let result = lua.scope(|scope| {
254            let ud = scope.create_any_userdata_ref_mut(&mut *engine)?;
255            let prev: Value = lua.named_registry_value(ENGINE_KEY)?;
256            lua.set_named_registry_value(ENGINE_KEY, ud)?;
257            let r = lua.load(code).set_name(chunk_name.as_str()).exec();
258            lua.set_named_registry_value(ENGINE_KEY, prev)?;
259            r
260        });
261        let items = lua
262            .app_data_mut::<PrintStack>()
263            .and_then(|mut ps| ps.0.pop())
264            .unwrap_or_default();
265        engine.push_lua_output(items);
266        result.map_err(|e| lua_error_message(&e))
267    }
268
269    fn call_simple(&self, engine: &mut Engine, name: &str) -> Result<(), String> {
270        let Some(f) = callback::get(&self.lua, name) else {
271            return Ok(());
272        };
273        self.lend(engine, |_lua| f.call::<()>(()))
274            .map_err(|e| lua_error_message(&e))
275    }
276
277    fn has_callback(&self, name: &str) -> bool {
278        callback::get(&self.lua, name).is_some()
279    }
280
281    fn define_font(&self, engine: &mut Engine, name: &[u8], size: i32) -> Option<u32> {
282        let f = callback::get(&self.lua, "define_font")?;
283        let next_id = engine.fonts.fonts.len() as i64;
284        let name = self.lua.create_string(name).ok()?;
285        let r = self.lend(engine, |_lua| {
286            f.call::<Value>((name, i64::from(size), next_id))
287        });
288        match r {
289            Ok(Value::Integer(n)) if n > 0 => Some(n as u32),
290            Ok(Value::Number(n)) if n > 0.0 => Some(n as u32),
291            Ok(Value::Table(t)) => {
292                let info = fonttable::font_from_table(&t).ok()?;
293                Some(engine.register_font(info))
294            }
295            Ok(_) => None,
296            Err(e) => {
297                engine.lua_error(&lua_error_message(&e));
298                None
299            }
300        }
301    }
302
303    fn list_filter(
304        &self,
305        engine: &mut Engine,
306        callback_name: &str,
307        head: retex_engine::nodes::NodeRef,
308        group: &str,
309    ) -> retex_engine::ListFilter {
310        use retex_engine::ListFilter;
311        let Some(f) = callback::get(&self.lua, callback_name) else {
312            return ListFilter::Keep(head);
313        };
314        let r = self.lend(engine, |lua| {
315            let h = if head.is_null() {
316                Value::Nil
317            } else {
318                Value::UserData(lua.create_userdata(nodelib::LNode(head.0))?)
319            };
320            f.call::<Value>((h, group))
321        });
322        match r {
323            Ok(Value::Boolean(false)) => ListFilter::Flush,
324            Ok(Value::UserData(u)) => match u.borrow::<nodelib::LNode>() {
325                Ok(n) => ListFilter::Keep(retex_engine::nodes::NodeRef(n.0)),
326                Err(_) => ListFilter::Keep(head),
327            },
328            Ok(Value::Integer(n)) => ListFilter::Keep(retex_engine::nodes::NodeRef(n as u32)),
329            Ok(_) => ListFilter::Keep(head),
330            Err(e) => {
331                engine.lua_error(&lua_error_message(&e));
332                ListFilter::Keep(head)
333            }
334        }
335    }
336
337    fn glyph_info(&self, engine: &mut Engine, p: retex_engine::nodes::NodeRef) -> Option<Vec<u8>> {
338        let f = callback::get(&self.lua, "glyph_info")?;
339        let r = self.lend(engine, |lua| {
340            let n = lua.create_userdata(nodelib::LNode(p.0))?;
341            f.call::<Value>(n)
342        });
343        match r {
344            Ok(Value::String(s)) => Some(s.as_bytes().to_vec()),
345            Ok(_) => None,
346            Err(e) => {
347                engine.lua_error(&lua_error_message(&e));
348                None
349            }
350        }
351    }
352
353    fn dump(&self, _engine: &mut Engine) -> Vec<u8> {
354        // Store the functions in `lua.bytecode` as (index, bytecode) pairs.
355        let mut out = retex_engine::format::Writer::default();
356        let mut entries: Vec<(i64, Vec<u8>)> = Vec::new();
357        if let Ok(lt) = self.lua.globals().get::<mlua::Table>("lua")
358            && let Ok(bc) = lt.get::<mlua::Table>("bytecode")
359        {
360            for (k, v) in bc.pairs::<i64, mlua::Function>().flatten() {
361                entries.push((k, v.dump(false)));
362            }
363        }
364        entries.sort_by_key(|(k, _)| *k);
365        out.u(entries.len() as u64);
366        for (k, b) in entries {
367            out.i(k);
368            out.bytes(&b);
369        }
370        out.buf
371    }
372
373    fn undump(&self, _engine: &mut Engine, data: &[u8]) -> Result<(), String> {
374        if data.is_empty() {
375            return Ok(());
376        }
377        let mut r = retex_engine::format::Reader::new(data);
378        let n = r.u().map_err(|e| e.to_string())?;
379        let lt: mlua::Table = self.lua.globals().get("lua").map_err(|e| e.to_string())?;
380        let bc: mlua::Table = lt.get("bytecode").map_err(|e| e.to_string())?;
381        for _ in 0..n {
382            let k = r.i().map_err(|e| e.to_string())?;
383            let b = r.bytes().map_err(|e| e.to_string())?;
384            let f = self
385                .lua
386                .load(b)
387                .set_mode(mlua::ChunkMode::Binary)
388                .into_function()
389                .map_err(|e| e.to_string())?;
390            bc.set(k, f).map_err(|e| e.to_string())?;
391        }
392        Ok(())
393    }
394}
395
396/// Formats a Lua error the way LuaTeX reports it: the message without
397/// mlua's decorations.
398pub fn lua_error_message(e: &mlua::Error) -> String {
399    match e {
400        mlua::Error::RuntimeError(s) | mlua::Error::SyntaxError { message: s, .. } => s.clone(),
401        mlua::Error::CallbackError { cause, .. } => lua_error_message(cause),
402        other => other.to_string(),
403    }
404}
405
406#[cfg(test)]
407mod tests {
408    use super::*;
409
410    #[test]
411    fn arg_holds_the_command_line() {
412        let host = LuaTexHost::new().expect("host");
413        let (n, first): (i64, String) = host.lua().load("return #arg, arg[0]").eval().expect("arg");
414        assert_eq!(n as usize, std::env::args().count() - 1);
415        assert_eq!(first, std::env::args().next().unwrap());
416    }
417
418    #[test]
419    fn lpeg_is_installed() {
420        let host = LuaTexHost::new().expect("host");
421        let r: (i64, bool, String, bool) = host
422            .lua()
423            .load(
424                r#"return lpeg.match(lpeg.R"az"^1, "abc1"), package.loaded.lpeg == lpeg,
425                   lpeg.version(), package.loaded.re == nil"#,
426            )
427            .eval()
428            .expect("lpeg");
429        assert_eq!(r, (4, true, "1.0.1".to_string(), true));
430    }
431}