1use 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
41pub const ENGINE_KEY: &str = "retex.engine";
43
44#[derive(Default)]
47pub(crate) struct PrintStack(pub Vec<Vec<LuaItem>>);
48
49pub(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
58const PLAIN_ERRORS_KEY: &str = "retex.plainerrors";
60
61pub(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
106pub(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
115pub struct LuaTexHost {
117 lua: Lua,
118}
119
120const 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 pub fn new() -> mlua::Result<Rc<Self>> {
159 #[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 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 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 pub fn lua(&self) -> &Lua {
216 &self.lua
217 }
218
219 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 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 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
396pub 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}