1use crate::epoch::{get_current_epoch, ConfigEpoch};
2use crate::pool::{pool_get, pool_put};
3pub use crate::pool::{set_gc_on_put, set_max_age, set_max_spare, set_max_use};
4use anyhow::Context;
5use mlua::{
6 FromLua, FromLuaMulti, IntoLua, IntoLuaMulti, Lua, LuaSerdeExt, MetaMethod, RegistryKey, Table,
7 UserData, UserDataMethods, Value,
8};
9use parking_lot::FairMutex as Mutex;
10pub use pastey as paste;
11use prometheus::{CounterVec, HistogramTimer, HistogramVec};
12use serde::Serialize;
13use std::borrow::Cow;
14use std::collections::HashSet;
15use std::path::PathBuf;
16use std::sync::atomic::{AtomicBool, Ordering};
17use std::sync::{LazyLock, Once};
18use std::time::Instant;
19
20pub mod epoch;
21mod pool;
22
23static POLICY_FILE: LazyLock<Mutex<Option<PathBuf>>> = LazyLock::new(|| Mutex::new(None));
24static FUNCS: LazyLock<Mutex<Vec<RegisterFunc>>> = LazyLock::new(|| Mutex::new(vec![]));
25static LUA_LOAD_COUNT: LazyLock<metrics::Counter> = LazyLock::new(|| {
26 metrics::describe_counter!(
27 "lua_load_count",
28 "how many times the policy lua script has been \
29 loaded into a new context"
30 );
31 metrics::counter!("lua_load_count")
32});
33static LUA_COUNT: LazyLock<metrics::Gauge> = LazyLock::new(|| {
34 metrics::describe_gauge!("lua_count", "the number of lua contexts currently alive");
35 metrics::gauge!("lua_count")
36});
37static CALLBACK_ALLOWS_MULTIPLE: LazyLock<Mutex<HashSet<String>>> =
38 LazyLock::new(|| Mutex::new(HashSet::new()));
39
40pub static VALIDATE_ONLY: AtomicBool = AtomicBool::new(false);
41pub static VALIDATION_FAILED: AtomicBool = AtomicBool::new(false);
42static LATENCY_HIST: LazyLock<HistogramVec> = LazyLock::new(|| {
43 prometheus::register_histogram_vec!(
44 "lua_event_latency",
45 "how long a given lua event callback took",
46 &["event"]
47 )
48 .unwrap()
49});
50static EVENT_STARTED_COUNT: LazyLock<CounterVec> = LazyLock::new(|| {
51 prometheus::register_counter_vec!(
52 "lua_event_started",
53 "Incremented each time we start to call a lua event callback. Use lua_event_latency_count to track completed events",
54 &["event"]
55 )
56 .unwrap()
57});
58
59pub type RegisterFunc = fn(&Lua) -> anyhow::Result<()>;
60
61fn latency_timer(label: &str) -> HistogramTimer {
62 EVENT_STARTED_COUNT
63 .get_metric_with_label_values(&[label])
64 .expect("to get counter")
65 .inc();
66 LATENCY_HIST
67 .get_metric_with_label_values(&[label])
68 .expect("to get histo")
69 .start_timer()
70}
71
72#[derive(Debug)]
73struct LuaConfigInner {
74 lua: Lua,
75 created: Instant,
76 use_count: usize,
77 epoch: ConfigEpoch,
78}
79
80impl Drop for LuaConfigInner {
81 fn drop(&mut self) {
82 LUA_COUNT.decrement(1.);
83 }
84}
85
86#[derive(Debug)]
87pub struct LuaConfig {
88 inner: Option<LuaConfigInner>,
89}
90
91pub async fn set_policy_path(path: PathBuf) -> anyhow::Result<()> {
92 POLICY_FILE.lock().replace(path);
93 let config = load_config().await?;
94 config.put();
95 Ok(())
96}
97
98fn get_policy_path() -> Option<PathBuf> {
99 POLICY_FILE.lock().clone()
100}
101
102fn get_funcs() -> Vec<RegisterFunc> {
103 FUNCS.lock().clone()
104}
105pub fn is_validating() -> bool {
106 VALIDATE_ONLY.load(Ordering::Relaxed)
107}
108
109pub fn validation_failed() -> bool {
110 VALIDATION_FAILED.load(Ordering::Relaxed)
111}
112
113pub fn set_validation_failed() {
114 VALIDATION_FAILED.store(true, Ordering::Relaxed)
115}
116
117pub async fn load_config() -> anyhow::Result<LuaConfig> {
118 if let Some(pool) = pool_get() {
119 return Ok(pool);
120 }
121
122 LUA_LOAD_COUNT.increment(1);
123 let lua = Lua::new();
124 let created = Instant::now();
125 let epoch = get_current_epoch();
126
127 {
128 let globals = lua.globals();
129
130 if is_validating() {
131 globals.set("_VALIDATING_CONFIG", true)?;
132 }
133
134 let package: Table = globals.get("package")?;
135 let package_path: String = package.get("path")?;
136 let mut path_array: Vec<String> = package_path.split(";").map(|s| s.to_owned()).collect();
137
138 fn prefix_path(array: &mut Vec<String>, path: &str) {
139 array.insert(0, format!("{}/?.lua", path));
140 array.insert(1, format!("{}/?/init.lua", path));
141 }
142
143 prefix_path(&mut path_array, "/opt/kumomta/etc/policy");
144 prefix_path(&mut path_array, "/opt/kumomta/share");
145
146 #[cfg(debug_assertions)]
147 prefix_path(&mut path_array, "assets");
148
149 package.set("path", path_array.join(";"))?;
150 }
151
152 register_declared_events();
153
154 for func in get_funcs() {
155 (func)(&lua)?;
156 }
157
158 if let Some(policy) = get_policy_path() {
159 let code = tokio::fs::read_to_string(&policy)
160 .await
161 .with_context(|| format!("reading policy file {policy:?}"))?;
162
163 let func = {
164 let chunk = lua.load(&code);
165 let chunk = chunk.set_name(policy.to_string_lossy());
166 chunk.into_function()?
167 };
168
169 let _timer = latency_timer("context-creation");
170 func.call_async::<()>(()).await?;
171 }
172 LUA_COUNT.increment(1.);
173
174 Ok(LuaConfig {
175 inner: Some(LuaConfigInner {
176 lua,
177 created,
178 use_count: 1,
179 epoch,
180 }),
181 })
182}
183
184pub fn register(func: RegisterFunc) {
185 FUNCS.lock().push(func);
186}
187
188impl LuaConfig {
189 fn set_current_event(&mut self, name: &str) -> mlua::Result<()> {
190 self.inner
191 .as_mut()
192 .unwrap()
193 .lua
194 .globals()
195 .set("_KUMO_CURRENT_EVENT", name.to_string())
196 }
197
198 pub fn convert_args_to_multi<A: Serialize>(
201 &self,
202 args: &[A],
203 ) -> anyhow::Result<mlua::MultiValue> {
204 let lua = self.inner.as_ref().unwrap();
205 let mut arg_vec = vec![];
206 for a in args.iter() {
207 arg_vec.push(lua.lua.to_value(a)?);
208 }
209 Ok(mlua::MultiValue::from_vec(arg_vec))
210 }
211
212 pub async fn convert_args_and_call_callback<A: Serialize>(
214 &mut self,
215 sig: &CallbackSignature<Value, ()>,
216 args: A,
217 ) -> anyhow::Result<()> {
218 let lua = self.inner.as_mut().unwrap();
219 let args = lua.lua.to_value(&args)?;
220
221 let name = sig.name();
222 let decorated_name = sig.decorated_name();
223
224 match lua
225 .lua
226 .named_registry_value::<mlua::Function>(&decorated_name)
227 {
228 Ok(func) => {
229 let _timer = latency_timer(name);
230 Ok(func.call_async(args).await?)
231 }
232 _ => anyhow::bail!("{name} has not been registered"),
233 }
234 }
235
236 pub fn put(mut self) {
254 if let Some(inner) = self.inner.take() {
255 pool_put(inner);
256 }
257 }
258
259 pub async fn async_call_callback<A: IntoLuaMulti + Clone, R: FromLuaMulti + Default>(
260 &mut self,
261 sig: &CallbackSignature<A, R>,
262 args: A,
263 ) -> anyhow::Result<R> {
264 let name = sig.name();
265 self.set_current_event(name)?;
266 let lua = self.inner.as_mut().unwrap();
267 async_call_callback(&lua.lua, sig, args).await
268 }
269
270 pub async fn async_call_callback_non_default<A: IntoLuaMulti + Clone, R: FromLuaMulti>(
271 &mut self,
272 sig: &CallbackSignature<A, R>,
273 args: A,
274 ) -> anyhow::Result<R> {
275 let name = sig.name();
276 self.set_current_event(name)?;
277 let lua = self.inner.as_mut().unwrap();
278 async_call_callback_non_default(&lua.lua, sig, args).await
279 }
280
281 pub async fn async_call_callback_non_default_opt<A: IntoLuaMulti + Clone, R: FromLua>(
282 &mut self,
283 sig: &CallbackSignature<A, Option<R>>,
284 args: A,
285 ) -> anyhow::Result<Option<R>> {
286 let name = sig.name();
287 let decorated_name = sig.decorated_name();
288 self.set_current_event(name)?;
289 let lua = self.inner.as_mut().unwrap();
290
291 match lua
292 .lua
293 .named_registry_value::<mlua::Value>(&decorated_name)?
294 {
295 Value::Table(tbl) => {
296 for func in tbl.sequence_values::<mlua::Function>().collect::<Vec<_>>() {
297 let func = func?;
298 let _timer = latency_timer(name);
299 let result: mlua::MultiValue = func.call_async(args.clone()).await?;
300 if result.is_empty() {
301 continue;
303 }
304 let result = R::from_lua_multi(result, &lua.lua)?;
305 return Ok(Some(result));
306 }
307 Ok(None)
308 }
309 Value::Function(func) => {
310 sig.raise_error_if_allow_multiple()?;
311 let _timer = latency_timer(name);
312 let value: Value = func.call_async(args.clone()).await?;
313
314 match value {
315 Value::Nil => Ok(None),
316 value => {
317 let result = R::from_lua(value, &lua.lua)?;
318 Ok(Some(result))
319 }
320 }
321 }
322 _ => Ok(None),
323 }
324 }
325
326 pub fn remove_registry_value(&mut self, value: RegistryKey) -> anyhow::Result<()> {
327 Ok(self
328 .inner
329 .as_mut()
330 .unwrap()
331 .lua
332 .remove_registry_value(value)?)
333 }
334
335 pub async fn async_call_ctor<A: IntoLuaMulti + Clone>(
338 &mut self,
339 sig: &CallbackSignature<A, Value>,
340 args: A,
341 ) -> anyhow::Result<RegistryKey> {
342 let name = sig.name();
343 anyhow::ensure!(
344 !sig.allow_multiple(),
345 "ctor event signature for {name} is defined as allow_multiple, which is not supported"
346 );
347
348 let decorated_name = sig.decorated_name();
349 self.set_current_event(name)?;
350
351 let inner = self.inner.as_mut().unwrap();
352
353 let func = inner
354 .lua
355 .named_registry_value::<mlua::Function>(&decorated_name)?;
356
357 let _timer = latency_timer(name);
358 let value: Value = func.call_async(args.clone()).await?;
359 drop(func);
360
361 Ok(inner.lua.create_registry_value(value)?)
362 }
363
364 pub async fn with_registry_value<F, R, FUT>(
367 &mut self,
368 value: &RegistryKey,
369 func: F,
370 ) -> anyhow::Result<R>
371 where
372 R: FromLuaMulti,
373 F: FnOnce(Value) -> anyhow::Result<FUT>,
374 FUT: std::future::Future<Output = anyhow::Result<R>>,
375 {
376 let inner = self.inner.as_mut().unwrap();
377 let value = inner.lua.registry_value(value)?;
378 let future = (func)(value)?;
379 future.await
380 }
381}
382
383pub async fn async_call_callback<A: IntoLuaMulti + Clone, R: FromLuaMulti + Default>(
384 lua: &Lua,
385 sig: &CallbackSignature<A, R>,
386 args: A,
387) -> anyhow::Result<R> {
388 let name = sig.name();
389 let decorated_name = sig.decorated_name();
390
391 match lua.named_registry_value::<mlua::Value>(&decorated_name)? {
392 Value::Table(tbl) => {
393 for func in tbl.sequence_values::<mlua::Function>().collect::<Vec<_>>() {
394 let func = func?;
395 let _timer = latency_timer(name);
396 let result: mlua::MultiValue = func.call_async(args.clone()).await?;
397 if result.is_empty() {
398 continue;
400 }
401 let result = R::from_lua_multi(result, lua)?;
402 return Ok(result);
403 }
404 Ok(R::default())
405 }
406 Value::Function(func) => {
407 sig.raise_error_if_allow_multiple()?;
408 let _timer = latency_timer(name);
409 Ok(func.call_async(args.clone()).await?)
410 }
411 _ => Ok(R::default()),
412 }
413}
414
415pub async fn async_call_callback_non_default<A: IntoLuaMulti + Clone, R: FromLuaMulti>(
416 lua: &Lua,
417 sig: &CallbackSignature<A, R>,
418 args: A,
419) -> anyhow::Result<R> {
420 let name = sig.name();
421 let decorated_name = sig.decorated_name();
422
423 match lua.named_registry_value::<mlua::Value>(&decorated_name)? {
424 Value::Table(tbl) => {
425 for func in tbl.sequence_values::<mlua::Function>().collect::<Vec<_>>() {
426 let func = func?;
427 let _timer = latency_timer(name);
428 let result: mlua::MultiValue = func.call_async(args.clone()).await?;
429 if result.is_empty() {
430 continue;
432 }
433 let result = R::from_lua_multi(result, lua)?;
434 return Ok(result);
435 }
436 anyhow::bail!("invalid return type for {name} event");
437 }
438 Value::Function(func) => {
439 sig.raise_error_if_allow_multiple()?;
440 let _timer = latency_timer(name);
441 Ok(func.call_async(args.clone()).await?)
442 }
443 _ => anyhow::bail!("Event {name} has not been registered"),
444 }
445}
446
447pub fn get_or_create_module(lua: &Lua, name: &str) -> anyhow::Result<mlua::Table> {
448 let globals = lua.globals();
449 let package: Table = globals.get("package")?;
450 let loaded: Table = package.get("loaded")?;
451
452 let module = loaded.get(name)?;
453 match module {
454 Value::Nil => {
455 let module = lua.create_table()?;
456 loaded.set(name, module.clone())?;
457 Ok(module)
458 }
459 Value::Table(table) => Ok(table),
460 wat => anyhow::bail!(
461 "cannot register module {} as package.loaded.{} is already set to a value of type {}",
462 name,
463 name,
464 wat.type_name()
465 ),
466 }
467}
468
469pub fn get_or_create_sub_module(lua: &Lua, name_path: &str) -> anyhow::Result<mlua::Table> {
474 let mut parent = get_or_create_module(lua, "kumo")?;
475 let mut path_so_far = String::new();
476
477 for name in name_path.split('.') {
478 if !path_so_far.is_empty() {
479 path_so_far.push('.');
480 }
481 path_so_far.push_str(name);
482
483 let sub = parent.get(name)?;
484 match sub {
485 Value::Nil => {
486 let sub = lua.create_table()?;
487 parent.set(name, sub.clone())?;
488 parent = sub;
489 }
490 Value::Table(sub) => {
491 parent = sub;
492 }
493 wat => anyhow::bail!(
494 "cannot register module kumo.{path_so_far} as it is already set to a value of type {}",
495 wat.type_name()
496 ),
497 }
498 }
499
500 Ok(parent)
501}
502
503pub fn any_err<E: std::fmt::Display>(err: E) -> mlua::Error {
505 mlua::Error::external(format!("{err:#}"))
506}
507
508pub fn impl_pairs_and_index<T, M>(methods: &mut M)
515where
516 T: UserData + Serialize,
517 M: UserDataMethods<T>,
518{
519 methods.add_meta_method(MetaMethod::Pairs, move |lua, this, _: ()| {
520 let Ok(serde_json::Value::Object(map)) = serde_json::to_value(this).map_err(any_err) else {
521 return Err(mlua::Error::external("must serialize to Map"));
522 };
523
524 let mut value_iter = map.into_iter();
525
526 let iter_func = lua.create_function_mut(
527 move |lua, (_state, _control): (Value, Value)| match value_iter.next() {
528 Some((key, value)) => {
529 let key = lua.to_value(&key)?;
530 let value = lua.to_value(&value)?;
531 Ok((key, value))
532 }
533 None => Ok((Value::Nil, Value::Nil)),
534 },
535 )?;
536
537 Ok((Value::Function(iter_func), Value::Nil, Value::Nil))
538 });
539
540 methods.add_meta_method(MetaMethod::Index, move |lua, this, field: Value| {
541 let value = lua.to_value(this)?;
542 match value {
543 Value::Table(t) => t.get(field),
544 _ => Ok(Value::Nil),
545 }
546 });
547
548 methods.add_meta_method(MetaMethod::Len, move |lua, this, _: ()| {
549 let value = lua.to_value(this)?;
550 match value {
551 Value::Table(v) => v.len(),
552 Value::String(v) => Ok(v.as_bytes().len() as i64),
553 _ => Ok(0),
554 }
555 });
556}
557
558pub fn materialize_to_lua_value(lua: &Lua, value: mlua::Value) -> mlua::Result<mlua::Value> {
566 match value {
567 mlua::Value::UserData(ud) => {
568 let mt = ud.metatable()?;
569 let Ok(pairs) = mt.get::<mlua::Function>("__pairs") else {
570 let value = ud.into_lua(lua)?;
571 return Err(mlua::Error::external(format!(
572 "cannot materialize_to_lua_value {value:?} \
573 because it has no __pairs metamethod"
574 )));
575 };
576 let tbl = lua.create_table()?;
577 let (iter_func, state, mut control): (mlua::Function, mlua::Value, mlua::Value) =
578 pairs.call(mlua::Value::UserData(ud.clone()))?;
579
580 loop {
581 let (k, v): (mlua::Value, mlua::Value) =
582 iter_func.call((state.clone(), control))?;
583 if k.is_nil() {
584 break;
585 }
586
587 tbl.set(k.clone(), materialize_to_lua_value(lua, v)?)?;
588 control = k;
589 }
590
591 Ok(mlua::Value::Table(tbl))
592 }
593 mlua::Value::Table(t) => {
594 let tbl = lua.create_table()?;
595 for pair in t.pairs::<mlua::Value, mlua::Value>() {
596 let (k, v) = pair?;
597 tbl.set(k.clone(), materialize_to_lua_value(lua, v)?)?;
598 }
599 Ok(mlua::Value::Table(tbl))
600 }
601 value => Ok(value),
602 }
603}
604
605pub struct SerdeWrappedValue<T>(pub T);
607
608impl<T: serde::Serialize> SerdeWrappedValue<T> {
609 pub fn to_lua_value(&self, lua: &Lua) -> mlua::Result<mlua::Value> {
610 lua.to_value_with(&self.0, serialize_options())
611 }
612}
613
614impl<T: serde::Serialize> IntoLua for SerdeWrappedValue<T> {
615 fn into_lua(self, lua: &Lua) -> mlua::Result<mlua::Value> {
616 lua.to_value_with(&self.0, serialize_options())
617 }
618}
619
620impl<T: serde::de::DeserializeOwned> FromLua for SerdeWrappedValue<T> {
621 fn from_lua(value: mlua::Value, lua: &Lua) -> mlua::Result<SerdeWrappedValue<T>> {
622 let inner: T = from_lua_value(lua, value)?;
623 Ok(SerdeWrappedValue(inner))
624 }
625}
626
627impl<T> std::ops::Deref for SerdeWrappedValue<T> {
628 type Target = T;
629 fn deref(&self) -> &T {
630 &self.0
631 }
632}
633
634impl<T> std::ops::DerefMut for SerdeWrappedValue<T> {
635 fn deref_mut(&mut self) -> &mut T {
636 &mut self.0
637 }
638}
639
640pub fn from_lua_value<R>(lua: &Lua, value: mlua::Value) -> mlua::Result<R>
645where
646 R: serde::de::DeserializeOwned,
647{
648 let value_cloned = value.clone();
649 match lua.from_value(value) {
650 Ok(r) => Ok(r),
651 Err(err) => match materialize_to_lua_value(lua, value_cloned.clone()) {
652 Ok(materialized) => match lua.from_value(materialized.clone()) {
653 Ok(r) => Ok(r),
654 Err(err) => {
655 let mut serializer = serde_json::Serializer::new(Vec::new());
656 let serialized = match materialized.serialize(&mut serializer) {
657 Ok(_) => String::from_utf8_lossy(&serializer.into_inner()).to_string(),
658 Err(err) => format!("<unable to encode as json: {err:#}>"),
659 };
660 Err(mlua::Error::external(format!(
661 "{err:#}, while processing {serialized}"
662 )))
663 }
664 },
665 Err(materialize_err) => Err(mlua::Error::external(format!(
666 "{err:#}, while processing a userdata. \
667 Additionally, encountered {materialize_err:#} \
668 when trying to iterate the pairs of that userdata"
669 ))),
670 },
671 }
672}
673
674pub struct CallbackSignature<A, R>
688where
689 A: IntoLuaMulti,
690 R: FromLuaMulti,
691{
692 marker: std::marker::PhantomData<(A, R)>,
693 allow_multiple: bool,
694 name: Cow<'static, str>,
695}
696
697#[linkme::distributed_slice]
698pub static CALLBACK_SIGNATURES: [fn()];
699
700#[macro_export]
728macro_rules! declare_event {
729 ($vis:vis static $sym:ident: Multiple($name:literal $(,)? $($param_name:ident: $args:ty),* $(,)? ) -> $ret:ty;) => {
730 $vis static $sym: ::std::sync::LazyLock<
731 $crate::CallbackSignature<($($args),*), $ret>> =
732 ::std::sync::LazyLock::new(|| $crate::CallbackSignature::new_with_multiple($name));
733
734 $crate::paste::paste! {
735 #[linkme::distributed_slice($crate::CALLBACK_SIGNATURES)]
736 static [<CALLBACK_SIG_REGISTER_ $sym>]: fn() = || {
737 $sym.register();
738 };
739 }
740 };
741 ($vis:vis static $sym:ident: Single($name:literal $(,)? $($param_name:ident: $args:ty),* $(,)? ) -> $ret:ty;) => {
742 $vis static $sym: ::std::sync::LazyLock<
743 $crate::CallbackSignature<($($args),*), $ret>> =
744 ::std::sync::LazyLock::new(|| $crate::CallbackSignature::new($name));
745
746 $crate::paste::paste! {
747 #[linkme::distributed_slice($crate::CALLBACK_SIGNATURES)]
748 static [<CALLBACK_SIG_REGISTER_ $sym>]: fn() = || {
749 $sym.register();
750 };
751 }
752 };
753}
754
755fn register_declared_events() {
762 static ONCE: Once = Once::new();
763 ONCE.call_once(|| {
764 for reg_func in CALLBACK_SIGNATURES {
765 reg_func();
766 }
767 });
768}
769
770impl<A, R> CallbackSignature<A, R>
771where
772 A: IntoLuaMulti,
773 R: FromLuaMulti,
774{
775 pub fn new<S: Into<Cow<'static, str>>>(name: S) -> Self {
776 let name = name.into();
777
778 Self {
779 marker: std::marker::PhantomData,
780 allow_multiple: false,
781 name,
782 }
783 }
784
785 pub fn new_with_multiple<S: Into<Cow<'static, str>>>(name: S) -> Self {
789 let name = name.into();
790
791 Self {
792 marker: std::marker::PhantomData,
793 allow_multiple: true,
794 name,
795 }
796 }
797
798 pub fn register(&self) {
799 if self.allow_multiple {
800 CALLBACK_ALLOWS_MULTIPLE
801 .lock()
802 .insert(self.name.to_string());
803 }
804 }
805
806 pub fn raise_error_if_allow_multiple(&self) -> anyhow::Result<()> {
807 anyhow::ensure!(
808 !self.allow_multiple(),
809 "handler {} is set to allow multiple handlers \
810 but is registered with a single instance. This indicates that \
811 register() was not called on the signature when initializing \
812 the lua context. Please report this issue to the KumoMTA team!",
813 self.name
814 );
815 Ok(())
816 }
817
818 pub fn allow_multiple(&self) -> bool {
821 self.allow_multiple
822 }
823
824 pub fn name(&self) -> &str {
825 &self.name
826 }
827
828 pub fn decorated_name(&self) -> String {
829 decorate_callback_name(&self.name)
830 }
831}
832
833pub fn does_callback_allow_multiple(name: &str) -> bool {
834 CALLBACK_ALLOWS_MULTIPLE.lock().contains(name)
835}
836
837pub fn decorate_callback_name(name: &str) -> String {
838 format!("kumomta-on-{name}")
839}
840
841pub fn serialize_options() -> mlua::SerializeOptions {
842 mlua::SerializeOptions::new()
843 .serialize_none_to_null(false)
844 .serialize_unit_to_null(false)
845}