Repository: Uni-Gal/Ayaka (language: rust)

Issue:
format 插件重用宿主的 rt-format
rt-format 太大了，拖慢加载。


Discussion / hints:

Fix patch:
diff --git a/plugins/format/Cargo.toml b/plugins/format/Cargo.toml
index 8681ce6d..914c8c04 100644
--- a/plugins/format/Cargo.toml
+++ b/plugins/format/Cargo.toml
@@ -10,6 +10,4 @@ forced-target = "wasm32-unknown-unknown"
 crate-type = ["cdylib"]
 
 [dependencies]
-ayaka-script-types = { path = "../../utils/ayaka-script-types", features = ["rt-format"] }
 ayaka-bindings = { path = "../../utils/ayaka-bindings" }
-rt-format = "0.3"
diff --git a/plugins/format/src/lib.rs b/plugins/format/src/lib.rs
index d79e5fc7..0cb88580 100644
--- a/plugins/format/src/lib.rs
+++ b/plugins/format/src/lib.rs
@@ -1,28 +1,16 @@
 use ayaka_bindings::*;
-use log::warn;
-use rt_format::*;
-use std::collections::HashMap;
 
 #[export]
 fn plugin_type() -> PluginType {
     PluginType::default()
 }
 
+#[import("format")]
+extern "C" {
+    fn __format(args: Vec<RawValue>) -> RawValue;
+}
+
 #[export]
 fn fmt(args: Vec<RawValue>) -> RawValue {
-    if args.is_empty() {
-        warn!("Format args is empty.");
-        RawValue::Unit
-    } else {
-        ParsedFormat::parse(
-            &args[0].get_str(),
-            &args[1..],
-            &HashMap::<String, RawValue>::new(),
-        )
-        .map(|r| RawValue::Str(r.to_string()))
-        .unwrap_or_else(|i| {
-            warn!("Format failed, stopped at {}.", i);
-            Default::default()
-        })
-    }
+    __format(args)
 }
diff --git a/plugins/live2d/src/lib.rs b/plugins/live2d/src/lib.rs
index 20ea5b1c..b896efee 100644
--- a/plugins/live2d/src/lib.rs
+++ b/plugins/live2d/src/lib.rs
@@ -21,16 +21,13 @@ fn find_model(
     root_path: impl AsRef<Path>,
     game_props: &HashMap<String, String>,
 ) -> Option<PathBuf> {
-    let base_dir = root_path.as_ref().join(
-        game_props
-            .get("ch_models")
-            .map(|s| s.as_str())
-            .unwrap_or(""),
-    );
-    ["model.json", "model3.json"]
-        .iter()
-        .map(|ex| base_dir.join(ch).join(ch).with_extension(ex))
-        .find(|p| p.exists())
+    game_props.get("ch_models").and_then(|ch_models| {
+        let base_dir = root_path.as_ref().join(ch_models);
+        ["model.json", "model3.json"]
+            .iter()
+            .map(|ex| base_dir.join(ch).join(ch).with_extension(ex))
+            .find(|p| p.exists())
+    })
 }
 
 #[export]
diff --git a/plugins/media/src/lib.rs b/plugins/media/src/lib.rs
index 8535da72..fc94653b 100644
--- a/plugins/media/src/lib.rs
+++ b/plugins/media/src/lib.rs
@@ -11,25 +11,20 @@ fn plugin_type() -> PluginType {
         .build()
 }
 
-fn find_exists(name: &str, base_dir: impl AsRef<Path>, exs: &[&str]) -> Option<PathBuf> {
-    let base_dir = base_dir.as_ref();
-    exs.iter()
-        .map(|ex| base_dir.join(name).with_extension(ex))
-        .find(|p| p.exists())
+fn find_exists(name: &str, base_dir: Option<&Path>, exs: &[&str]) -> Option<PathBuf> {
+    base_dir.and_then(|base_dir| {
+        exs.iter()
+            .map(|ex| base_dir.join(name).with_extension(ex))
+            .find(|p| p.exists())
+    })
 }
 
-fn file(
-    args: Vec<String>,
-    base_dir: impl AsRef<Path>,
-    prop: &str,
-    exs: &[&str],
-) -> TextProcessResult {
+fn file(args: Vec<String>, base_dir: Option<&Path>, prop: &str, exs: &[&str]) -> TextProcessResult {
     assert_eq!(args.len(), 1);
-    let base_dir = base_dir.as_ref();
     log::debug!(
         "File {:?}, {:?}, {}, {:?}",
         args,
-        base_dir.display(),
+        base_dir.map(|p| p.display()),
         prop,
         exs
     );
@@ -50,12 +45,10 @@ fn file_ctx(
 ) -> TextProcessResult {
     file(
         args,
-        ctx.root_path.join(
-            ctx.game_props
-                .get(game_prop)
-                .map(|s| s.as_str())
-                .unwrap_or(""),
-        ),
+        ctx.game_props
+            .get(game_prop)
+            .map(|game_prop| ctx.root_path.join(game_prop))
+            .as_deref(),
         prop,
         exs,
     )
@@ -96,14 +89,10 @@ fn process_action(mut ctx: ActionProcessContext) -> Action {
     let voice_id = ctx.action.ctx.cur_act.to_string();
     let res = file(
         vec![voice_id],
-        ctx.root_path
-            .join(
-                ctx.game_props
-                    .get("voices")
-                    .map(|s| s.as_str())
-                    .unwrap_or(""),
-            )
-            .join(&ctx.action.ctx.cur_para),
+        ctx.game_props
+            .get("voices")
+            .map(|p| ctx.root_path.join(p).join(&ctx.action.ctx.cur_para))
+            .as_deref(),
         "voice",
         &["mp3"],
     );
@@ -115,11 +104,9 @@ fn process_action(mut ctx: ActionProcessContext) -> Action {
 
 #[export]
 fn process_game(mut ctx: GameProcessContext) -> GameProcessResult {
-    let base_dir = ctx
-        .root_path
-        .join(ctx.props.get("bgs").map(|s| s.as_str()).unwrap_or(""));
+    let base_dir = ctx.props.get("bgs").map(|p| ctx.root_path.join(p));
     if let Some(bg) = ctx.props.get_mut("bg") {
-        if let Some(path) = find_exists(bg, &base_dir, &["png", "jpg", "gif"]) {
+        if let Some(path) = find_exists(bg, base_dir.as_deref(), &["png", "jpg", "gif"]) {
             *bg = path.to_string_lossy().into_owned();
         }
     }
diff --git a/utils/ayaka-bindings-impl/src/lib.rs b/utils/ayaka-bindings-impl/src/lib.rs
index d7ad8fcc..5bc002b5 100644
--- a/utils/ayaka-bindings-impl/src/lib.rs
+++ b/utils/ayaka-bindings-impl/src/lib.rs
@@ -1,6 +1,9 @@
 use proc_macro::TokenStream;
-use quote::quote;
-use syn::{parse_macro_input, parse_str, Ident, ItemFn};
+use quote::{quote, TokenStreamExt};
+use syn::{
+    parse_macro_input, parse_str, AttributeArgs, FnArg, ForeignItem, Ident, ItemFn, ItemForeignMod,
+    Lit, NestedMeta,
+};
 
 #[proc_macro_attribute]
 pub fn export(_attr: TokenStream, input: TokenStream) -> TokenStream {
@@ -8,7 +11,7 @@ pub fn export(_attr: TokenStream, input: TokenStream) -> TokenStream {
     let func = parse_macro_input!(func as ItemFn);
     let name = func.sig.ident;
     let name_str = name.to_string();
-    let expname = parse_str::<Ident>(&format!("__{}", name_str)).unwrap();
+    let expname = parse_str::<Ident>(&format!("__export_{}", name_str)).unwrap();
     let input = proc_macro2::TokenStream::from(input);
     let export_func = quote! {
         #[doc(hidden)]
@@ -21,3 +24,54 @@ pub fn export(_attr: TokenStream, input: TokenStream) -> TokenStream {
     };
     TokenStream::from(export_func)
 }
+
+#[proc_macro_attribute]
+pub fn import(attr: TokenStream, input: TokenStream) -> TokenStream {
+    let attr = parse_macro_input!(attr as AttributeArgs);
+    let mut module = String::new();
+    for a in attr {
+        match a {
+            NestedMeta::Lit(Lit::Str(s)) => module = s.value(),
+            _ => unimplemented!(),
+        }
+    }
+    let input = parse_macro_input!(input as ItemForeignMod);
+    let mut imports = quote! {};
+    for func in input.items {
+        match func {
+            ForeignItem::Fn(func) => {
+                let attrs = func.attrs;
+                let vis = func.vis;
+                let sig = func.sig;
+
+                let params = sig.inputs.clone();
+                let params = params
+                    .into_iter()
+                    .map(|arg| match arg {
+                        FnArg::Typed(p) => p.pat,
+                        _ => unimplemented!(),
+                    })
+                    .collect::<Vec<_>>();
+
+                let name = sig.ident.clone();
+                let name_str = name.to_string();
+                let impname = parse_str::<Ident>(&format!("__import_{}", name_str)).unwrap();
+                let import_func = quote! {
+                    #[doc(hidden)]
+                    #[link(wasm_import_module = #module)]
+                    extern "C" {
+                        #[link_name = #name_str]
+                        fn #impname(len: usize, data: *const u8) -> u64;
+                    }
+                    #[allow(unsafe_code)]
+                    #(#attrs)* #vis #sig {
+                        unsafe { ::ayaka_bindings::__import(#impname, (#(#params,)*)) }
+                    }
+                };
+                imports.append_all(import_func);
+            }
+            _ => unimplemented!(),
+        }
+    }
+    TokenStream::from(imports)
+}
diff --git a/utils/ayaka-bindings/Cargo.toml b/utils/ayaka-bindings/Cargo.toml
index a0ff94a7..d788954d 100644
--- a/utils/ayaka-bindings/Cargo.toml
+++ b/utils/ayaka-bindings/Cargo.toml
@@ -11,3 +11,4 @@ ayaka-bindings-impl = { path = "../ayaka-bindings-impl" }
 serde = "1.0"
 rmp-serde = "1.1"
 log = "0.4"
+scopeguard = "1.1"
diff --git a/utils/ayaka-bindings/src/lib.rs b/utils/ayaka-bindings/src/lib.rs
index efd22626..ccb45a1a 100644
--- a/utils/ayaka-bindings/src/lib.rs
+++ b/utils/ayaka-bindings/src/lib.rs
@@ -10,6 +10,7 @@ pub use log;
 
 mod logger;
 
+use scopeguard::defer;
 use serde::{de::DeserializeOwned, Serialize};
 use std::alloc::{self, Layout};
 
@@ -59,4 +60,18 @@ pub unsafe fn __export<Params: DeserializeOwned, Res: Serialize>(
     ((len as u64) << 32) | (ptr as u64)
 }
 
-pub use ayaka_bindings_impl::export;
+#[doc(hidden)]
+pub unsafe fn __import<Params: Serialize, Res: DeserializeOwned>(
+    f: unsafe extern "C" fn(len: usize, data: *const u8) -> u64,
+    args: Params,
+) -> Res {
+    let data = rmp_serde::to_vec(&args).unwrap();
+    let res = f(data.len(), data.as_ptr());
+    let (len, res) = ((res >> 32) as usize, (res & 0xFFFFFFFF) as *mut u8);
+    defer! { __abi_free(res, len) };
+    let data = std::slice::from_raw_parts_mut(res, len);
+    let data = rmp_serde::from_slice(data).unwrap();
+    data
+}
+
+pub use ayaka_bindings_impl::{export, import};
diff --git a/utils/ayaka-bindings/src/logger.rs b/utils/ayaka-bindings/src/logger.rs
index 5442d421..1a491522 100644
--- a/utils/ayaka-bindings/src/logger.rs
+++ b/utils/ayaka-bindings/src/logger.rs
@@ -1,18 +1,12 @@
+use crate::__import;
 use log::Log;
 
-#[cfg(target_arch = "wasm32")]
 #[link(wasm_import_module = "log")]
 extern "C" {
-    fn __log(len: usize, data: *const u8);
+    fn __log(len: usize, data: *const u8) -> u64;
     fn __log_flush();
 }
 
-#[cfg(not(target_arch = "wasm32"))]
-unsafe fn __log(_len: usize, _data: *const u8) {}
-
-#[cfg(not(target_arch = "wasm32"))]
-unsafe fn __log_flush() {}
-
 pub struct PluginLogger;
 
 impl PluginLogger {
@@ -37,8 +31,7 @@ impl Log for PluginLogger {
 
     fn log(&self, record: &log::Record) {
         let record: ayaka_bindings_types::Record = record.into();
-        let data = rmp_serde::to_vec(&record).unwrap();
-        unsafe { __log(data.len(), data.as_ptr()) }
+        unsafe { __import::<_, ()>(__log, (record,)) };
     }
 
     fn flush(&self) {
diff --git a/utils/ayaka-runtime/src/context.rs b/utils/ayaka-runtime/src/context.rs
index 5a2a6804..40a0e68a 100644
--- a/utils/ayaka-runtime/src/context.rs
+++ b/utils/ayaka-runtime/src/context.rs
@@ -99,7 +99,7 @@ impl Context {
             let res_path = root_path.join(res_path);
             for rl in std::fs::read_dir(res_path)? {
                 let p = rl?.path();
-                if p.is_file() && p.extension().map(|ex| ex == "yaml").unwrap_or_default() {
+                if p.is_file() && p.extension().unwrap_or_default() == "yaml" {
                     let loc = p
                         .file_stem()
                         .and_then(|s| s.to_string_lossy().parse::<Locale>().ok())
@@ -124,11 +124,12 @@ impl Context {
                 let mut paras_map = HashMap::new();
                 for p in std::fs::read_dir(p)? {
                     let p = p?.path();
-                    if p.is_file() && p.extension().map(|ex| ex == "yaml").unwrap_or_default() {
+                    if p.is_file() && p.extension().unwrap_or_default() == "yaml" {
                         let key = p
                             .file_stem()
-                            .map(|s| s.to_string_lossy().into_owned())
-                            .unwrap_or_default();
+                            .unwrap_or_default()
+                            .to_string_lossy()
+                            .into_owned();
                         let para = std::fs::read(p)?;
                         let para = serde_yaml::from_slice(&para)?;
                         paras_map.insert(key, para);
@@ -487,10 +488,12 @@ impl Context {
                 }
                 (false, _) => {
                     if self.ctx.cur_base_para == self.ctx.cur_para {
-                        error!(
-                            "Cannot find paragraph \"{}\"",
-                            self.ctx.cur_para.escape_default()
-                        );
+                        if !self.ctx.cur_para.is_empty() {
+                            error!(
+                                "Cannot find paragraph \"{}\"",
+                                self.ctx.cur_para.escape_default()
+                            );
+                        }
                         return None;
                     } else {
                         self.ctx.cur_base_para = self.ctx.cur_para.clone();
diff --git a/utils/ayaka-runtime/src/lib.rs b/utils/ayaka-runtime/src/lib.rs
index 01e59cba..68cbf126 100644
--- a/utils/ayaka-runtime/src/lib.rs
+++ b/utils/ayaka-runtime/src/lib.rs
@@ -7,9 +7,11 @@
 #![warn(missing_docs)]
 #![deny(unsafe_code)]
 #![feature(absolute_path)]
+#![feature(fn_traits)]
 #![feature(generators)]
 #![feature(once_cell)]
 #![feature(round_char_boundary)]
+#![feature(unboxed_closures)]
 
 mod config;
 mod context;
diff --git a/utils/ayaka-runtime/src/plugin.rs b/utils/ayaka-runtime/src/plugin.rs
index 71994ab4..5691e056 100644
--- a/utils/ayaka-runtime/src/plugin.rs
+++ b/utils/ayaka-runtime/src/plugin.rs
@@ -7,6 +7,7 @@ use crate::*;
 use anyhow::Result;
 use ayaka_bindings_types::*;
 use log::warn;
+use rt_format::ParsedFormat;
 use scopeguard::defer;
 use serde::{de::DeserializeOwned, Serialize};
 use std::{collections::HashMap, path::Path};
@@ -17,9 +18,10 @@ use wasmer_wasi::*;
 
 /// An instance of a WASM plugin module.
 pub struct Host {
+    instance: Instance,
+    memory: Memory,
     abi_free: NativeFunc<(i32, i32), ()>,
     abi_alloc: NativeFunc<i32, i32>,
-    instance: Instance,
 }
 
 unsafe fn mem_slice(memory: &Memory, start: i32, len: i32) -> &[u8] {
@@ -40,12 +42,14 @@ impl Host {
     /// Loads the WASM [`Module`], with some imports.
     pub fn new(module: &Module, resolver: &(dyn Resolver + Send + Sync)) -> Result<Self> {
         let instance = Instance::new(module, resolver)?;
+        let memory = instance.exports.get_memory("memory")?.clone();
         let abi_free = instance.exports.get_native_function("__abi_free")?;
         let abi_alloc = instance.exports.get_native_function("__abi_alloc")?;
         Ok(Self {
+            instance,
+            memory,
             abi_free,
             abi_alloc,
-            instance,
         })
     }
 
@@ -57,7 +61,7 @@ impl Host {
         name: &str,
         args: Params,
     ) -> Result<Res> {
-        let memory = self.instance.exports.get_memory("memory")?;
+        let memory = &self.memory;
         let func = self
             .instance
             .exports
@@ -139,6 +143,27 @@ pub enum LoadStatus {
 struct RuntimeInstanceData {
     #[wasmer(export)]
     memory: LazyInit<Memory>,
+    #[wasmer(export(name = "__abi_alloc"))]
+    alloc: LazyInit<NativeFunc<i32, i32>>,
+}
+
+impl RuntimeInstanceData {
+    pub unsafe fn import<Params: DeserializeOwned, Res: Serialize>(
+        &self,
+        len: i32,
+        data: i32,
+        f: impl FnOnce<Params, Output = Res>,
+    ) -> u64 {
+        let memory = self.memory.get_unchecked();
+        let data = mem_slice(memory, data, len);
+        let data = rmp_serde::from_slice(data).unwrap();
+        let res = f.call_once(data);
+        let data = rmp_serde::to_vec(&res).unwrap();
+        let alloc = self.alloc.get_unchecked();
+        let ptr = alloc.call(data.len() as _).unwrap();
+        mem_slice_mut(memory, ptr, data.len() as _).copy_from_slice(&data);
+        ((data.len() as u64) << 32) | (ptr as u64)
+    }
 }
 
 impl Runtime {
@@ -146,27 +171,54 @@ impl Runtime {
         let log_func = Function::new_native_with_env(
             store,
             RuntimeInstanceData::default(),
-            |env_data: &RuntimeInstanceData, len: i32, data: i32| {
-                let memory = unsafe { env_data.memory.get_unchecked() };
-                let data = unsafe { mem_slice(memory, data, len) };
-                let data: Record = rmp_serde::from_slice(data).unwrap();
-                log::logger().log(
-                    &log::Record::builder()
-                        .level(data.level)
-                        .target(&data.target)
-                        .args(format_args!("{}", data.msg))
-                        .module_path(data.module_path.as_deref())
-                        .file(data.file.as_deref())
-                        .line(data.line)
-                        .build(),
-                );
+            |env_data: &RuntimeInstanceData, len: i32, data: i32| unsafe {
+                env_data.import(len, data, |data: Record| {
+                    log::logger().log(
+                        &log::Record::builder()
+                            .level(data.level)
+                            .target(&data.target)
+                            .args(format_args!("{}", data.msg))
+                            .module_path(data.module_path.as_deref())
+                            .file(data.file.as_deref())
+                            .line(data.line)
+                            .build(),
+                    );
+                })
             },
         );
         let log_flush_func = Function::new_native(store, || log::logger().flush());
+
+        let format_func = Function::new_native_with_env(
+            store,
+            RuntimeInstanceData::default(),
+            |env_data: &RuntimeInstanceData, len: i32, data: i32| unsafe {
+                env_data.import(len, data, |args: Vec<RawValue>| {
+                    if args.is_empty() {
+                        warn!("Format args is empty.");
+                        RawValue::Unit
+                    } else {
+                        ParsedFormat::parse(
+                            &args[0].get_str(),
+                            &args[1..],
+                            &HashMap::<String, RawValue>::new(),
+                        )
+                        .map(|r| RawValue::Str(r.to_string()))
+                        .unwrap_or_else(|i| {
+                            warn!("Format failed, stopped at {}.", i);
+                            Default::default()
+                        })
+                    }
+                })
+            },
+        );
+
         let import_object = imports! {
             "log" => {
                 "__log" => log_func,
                 "__log_flush" => log_flush_func,
+            },
+            "format" => {
+                "__format" => format_func,
             }
         };
         let wasi_env = WasiState::new("ayaka-runtime")
@@ -199,11 +251,11 @@ impl Runtime {
             std::fs::read_dir(path)?
                 .try_filter_map(|f| {
                     let p = f.path();
-                    if p.is_file() && p.extension().map(|s| s == "wasm").unwrap_or_default() {
+                    if p.is_file() && p.extension().unwrap_or_default() == "wasm" {
                         let name = p
                             .file_stem()
-                            .map(|s| s.to_string_lossy())
                             .unwrap_or_default()
+                            .to_string_lossy()
                             .into_owned();
                         Ok(Some((name, p)))
                     } else {
diff --git a/utils/ayaka-runtime/src/settings.rs b/utils/ayaka-runtime/src/settings.rs
index d4869cb5..e9f8b1b0 100644
--- a/utils/ayaka-runtime/src/settings.rs
+++ b/utils/ayaka-runtime/src/settings.rs
@@ -119,11 +119,7 @@ pub fn load_records(ident: &str, game: &str) -> Result<Vec<ActionRecord>> {
         .map_err(anyhow::Error::from)
         .try_filter_map(|entry| {
             let p = entry.path();
-            if p.is_file()
-                && p.file_name()
-                    .map(|s| s == "global.json")
-                    .unwrap_or_default()
-            {
+            if p.is_file() && p.file_name().unwrap_or_default() == "global.json" {
                 Ok(Some(load_file(&p)?))
             } else {
                 Ok(None)
diff --git a/utils/ayaka-script/src/text.rs b/utils/ayaka-script/src/text.rs
index 1d1de14f..0077f97f 100644
--- a/utils/ayaka-script/src/text.rs
+++ b/utils/ayaka-script/src/text.rs
@@ -555,7 +555,10 @@ impl<'a> TextParser<'a> {
                 Command::Character(
                     Self::concat_params(&params[0])?,
                     Self::concat_params(
-                        params.get(1).map(|slice| slice.as_slice()).unwrap_or(&[]),
+                        params
+                            .get(1)
+                            .map(|slice| slice.as_slice())
+                            .unwrap_or_default(),
                     )?,
                 )
             }


Test patch: