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

Issue:
支持多个 WASM 运行时
现在纯用 Rust 创作并支持 WASI 的 WASM 运行时有三个：

* Wasmtime
* Wasmer
* Wasmi

此外还有一些其它语言创作，但是提供 Rust 接口的 WASM 运行时：

* WasmEdge

考虑抽象出来一个 crate，并且至少提供上面三个的实现。

我的仓库里面几个分支已经成功实现了 Wasmtime 以及 Wasmi（Windows 下需要别的 crate 的补丁）的支持。这之中 Wasmi 的跨平台支持是非常诱人的，因为它是个解释器。


Discussion / hints:

Fix patch:
diff --git a/bins/Makefile b/bins/Makefile
index 126df22e..f574e65f 100644
--- a/bins/Makefile
+++ b/bins/Makefile
@@ -15,8 +15,8 @@ release-cross: dist
 
 .PHONY: run run-gui run-latex
 run:
-	RUST_LOG=info cargo run --package ayaka-check -- $(FILE) --auto
+	cargo run --package ayaka-check -- $(FILE) --auto
 run-gui:
 	cd ayaka-gui && $(MAKE) run FILE=$(FILE)
 run-latex:
-	RUST_LOG=info cargo run --package ayaka-latex -- $(FILE) -o $(basename $(FILE)).tex
+	cargo run --package ayaka-latex -- $(FILE) -o $(basename $(FILE)).tex
diff --git a/bins/ayaka-check/Cargo.toml b/bins/ayaka-check/Cargo.toml
index 0dd8e75b..eab453b0 100644
--- a/bins/ayaka-check/Cargo.toml
+++ b/bins/ayaka-check/Cargo.toml
@@ -7,4 +7,4 @@ edition = "2021"
 ayaka-runtime = { path = "../../utils/ayaka-runtime" }
 tokio = { version = "1", features = ["macros", "rt"] }
 clap = { version = "4.0", features = ["derive"] }
-env_logger = "0.9"
+flexi_logger = { version = "0.24", default-features = false, features = ["colors"] }
diff --git a/bins/ayaka-check/src/main.rs b/bins/ayaka-check/src/main.rs
index 5a475312..0a5eb6b1 100644
--- a/bins/ayaka-check/src/main.rs
+++ b/bins/ayaka-check/src/main.rs
@@ -1,5 +1,6 @@
-use ayaka_runtime::{anyhow::Result, log::LevelFilter, *};
+use ayaka_runtime::{anyhow::Result, *};
 use clap::Parser;
+use flexi_logger::{LogSpecification, Logger};
 use std::{
     ffi::OsString,
     io::{stdin, stdout, Write},
@@ -34,9 +35,12 @@ fn pause(auto: bool) -> Result<()> {
 #[tokio::main(flavor = "current_thread")]
 async fn main() -> Result<()> {
     let opts = Options::parse();
-    env_logger::Builder::from_default_env()
-        .filter_module("wasmer", LevelFilter::Warn)
-        .try_init()?;
+    let spec = LogSpecification::parse("warn,ayaka=debug")?;
+    let _log_handle = Logger::with(spec)
+        .log_to_stdout()
+        .set_palette("b1;3;2;4;6".to_string())
+        .use_utc()
+        .start()?;
     let context = Context::open(&opts.input, FrontendType::Text);
     pin_mut!(context);
     while let Some(status) = context.next().await {
diff --git a/bins/ayaka-gui/src-tauri/src/main.rs b/bins/ayaka-gui/src-tauri/src/main.rs
index 56ca38c6..3bee64c0 100644
--- a/bins/ayaka-gui/src-tauri/src/main.rs
+++ b/bins/ayaka-gui/src-tauri/src/main.rs
@@ -2,6 +2,7 @@
     all(not(debug_assertions), target_os = "windows"),
     windows_subsystem = "windows"
 )]
+#![feature(absolute_path)]
 
 use ayaka_runtime::{
     anyhow::{self, anyhow, Result},
@@ -13,6 +14,7 @@ use serde::{Deserialize, Serialize};
 use std::{
     collections::{HashMap, HashSet},
     fmt::Display,
+    path::PathBuf,
 };
 use tauri::{async_runtime::Mutex, command, AppHandle, Manager, State};
 use trylog::TryLog;
@@ -68,6 +70,7 @@ impl OpenGameStatus {
 struct Storage {
     ident: String,
     config: String,
+    root_path: PathBuf,
     records: Mutex<Vec<ActionRecord>>,
     context: Mutex<Option<Context>>,
     current: Mutex<Option<RawContext>>,
@@ -77,9 +80,16 @@ struct Storage {
 
 impl Storage {
     pub fn new(ident: impl Into<String>, config: impl Into<String>) -> Self {
+        let config = config.into();
+        let root_path = std::path::absolute(&config)
+            .unwrap()
+            .parent()
+            .unwrap()
+            .to_path_buf();
         Self {
             ident: ident.into(),
-            config: config.into(),
+            config,
+            root_path,
             ..Default::default()
         }
     }
@@ -102,6 +112,11 @@ impl GameInfo {
     }
 }
 
+#[command]
+fn absolute_path(storage: State<'_, Storage>, path: String) -> CommandResult<String> {
+    Ok(storage.root_path.join(path).to_string_lossy().into_owned())
+}
+
 #[command]
 async fn open_game(handle: AppHandle, storage: State<'_, Storage>) -> CommandResult<()> {
     let config = &storage.config;
@@ -415,16 +430,15 @@ fn main() -> Result<()> {
         .plugin(tauri_plugin_localhost::Builder::new(port).build())
         .setup(|app| {
             let ident = app.config().tauri.bundle.identifier.clone();
+            let spec = LogSpecification::parse("warn,ayaka=debug")?;
             let log_handle = if cfg!(debug_assertions) {
-                Logger::with(LogSpecification::parse(
-                    "debug,wasmer=warn,regalloc=info,cranelift=info",
-                )?)
-                .log_to_stdout()
-                .set_palette("b1;3;2;4;6".to_string())
-                .use_utc()
-                .start()?
+                Logger::with(spec)
+                    .log_to_stdout()
+                    .set_palette("b1;3;2;4;6".to_string())
+                    .use_utc()
+                    .start()?
             } else {
-                Logger::with(LogSpecification::parse("info,wasmer=warn")?)
+                Logger::with(spec)
                     .log_to_file(
                         FileSpec::default()
                             .directory(app.path_resolver().app_log_dir().unwrap())
@@ -458,6 +472,7 @@ fn main() -> Result<()> {
         })
         .invoke_handler(tauri::generate_handler![
             ayaka_version,
+            absolute_path,
             open_game,
             get_settings,
             set_settings,
diff --git a/bins/ayaka-gui/src/components/Live2D.vue b/bins/ayaka-gui/src/components/Live2D.vue
index 912323a6..9f50850b 100644
--- a/bins/ayaka-gui/src/components/Live2D.vue
+++ b/bins/ayaka-gui/src/components/Live2D.vue
@@ -35,7 +35,7 @@ export default {
                     for (const name of this.names) {
                         let m = this.models.get(name)
                         if (!m) {
-                            const path = conv_src((this.game.props as any)["ch_" + name + "_model"]) ?? ""
+                            const path = await conv_src((this.game.props as any)["ch_" + name + "_model"]) ?? ""
                             m = await Live2DModel.from(path)
                             m.name = name
                             console.log("Loaded Live2D model: %O", m);
diff --git a/bins/ayaka-gui/src/interop/index.ts b/bins/ayaka-gui/src/interop/index.ts
index b5ae1916..77b87268 100644
--- a/bins/ayaka-gui/src/interop/index.ts
+++ b/bins/ayaka-gui/src/interop/index.ts
@@ -1,9 +1,9 @@
 import { convertFileSrc, invoke } from "@tauri-apps/api/tauri"
 import { Locale } from 'vue-i18n'
 
-export function conv_src(path?: string): string | undefined {
+export async function conv_src(path?: string): Promise<string | undefined> {
     if (path) {
-        return decodeURIComponent(convertFileSrc(path))
+        return decodeURIComponent(convertFileSrc(await invoke("absolute_path", { path: path })))
     }
     return undefined
 }
diff --git a/bins/ayaka-gui/src/views/GameView.vue b/bins/ayaka-gui/src/views/GameView.vue
index 1aa5921a..7a4d7860 100644
--- a/bins/ayaka-gui/src/views/GameView.vue
+++ b/bins/ayaka-gui/src/views/GameView.vue
@@ -44,6 +44,10 @@ export default {
             type_text_buffer: [] as ActionLine[],
             type_sub_text: "",
             type_sub_text_buffer: [] as ActionLine[],
+            bg: undefined as string | undefined,
+            bgm: undefined as string | undefined,
+            voice: undefined as string | undefined,
+            video: undefined as string | undefined,
             play_state: PlayState.Manual,
             mutex: new Mutex(),
         }
@@ -67,6 +71,8 @@ export default {
         // Should be called in mutex
         async fetch_current_run() {
             const ctx = await current_run()
+            this.bg = await conv_src(ctx?.locals.bg)
+            this.bgm = await conv_src(ctx?.locals.bgm)
             const actions = await current_action()
             this.title = await current_title() ?? ""
             console.info(actions)
@@ -103,6 +109,8 @@ export default {
                         }
                         break
                 }
+                this.voice = await conv_src(this.action.vars.voice)
+                this.video = await conv_src(this.vars.video)
             }
         },
         // Should be called in mutex
@@ -245,9 +253,9 @@ export default {
 </script>
 
 <template>
-    <audio ref="bgm" :src="conv_src(raw_ctx.locals.bgm)" type="audio/mpeg" autoplay hidden loop></audio>
-    <audio ref="voice" :src="conv_src(action.vars.voice)" type="audio/mpeg" autoplay hidden></audio>
-    <img class="background" :src="conv_src(raw_ctx.locals.bg)">
+    <audio ref="bgm" :src="bgm" type="audio/mpeg" autoplay hidden loop></audio>
+    <audio ref="voice" :src="voice" type="audio/mpeg" autoplay hidden></audio>
+    <img class="background" :src="bg">
     <Live2D :names="live2d_names(raw_ctx.locals)"></Live2D>
     <div class="card-lines">
         <ActionCard :ch="action.character" :line="type_text" :sub_line="type_sub_text"></ActionCard>
@@ -259,8 +267,7 @@ export default {
         <span>Powered by Ayaka.</span>
     </div>
     <div class="content-full bg-body" :hidden="!vars.video">
-        <video ref="video" class="background" @ended="onvideoended" :src="conv_src(vars.video)" type="video/mp4"
-            autoplay></video>
+        <video ref="video" class="background" @ended="onvideoended" :src="video" type="video/mp4" autoplay></video>
     </div>
     <div class="backboard" @click="next"></div>
     <div class="commands">
diff --git a/bins/ayaka-gui/src/views/HomeView.vue b/bins/ayaka-gui/src/views/HomeView.vue
index 88b5ae79..64025e17 100644
--- a/bins/ayaka-gui/src/views/HomeView.vue
+++ b/bins/ayaka-gui/src/views/HomeView.vue
@@ -14,7 +14,7 @@ export default {
     async created() {
         const res = await info()
         this.title = res.title
-        this.bg = res.props.bg
+        this.bg = await conv_src(res.props.bg)
     },
     methods: {
         async new_game() {
@@ -28,7 +28,7 @@ export default {
 </script>
 
 <template>
-    <img class="background" :src="conv_src(bg)">
+    <img class="background" :src="bg">
     <div class="content-full bg-body backboard-bg"></div>
     <div class="content">
         <div class="d-grid gap-4 col-4 mx-auto">
diff --git a/bins/ayaka-gui/vite.config.ts b/bins/ayaka-gui/vite.config.ts
index 2aa6d6ef..e2b60226 100644
--- a/bins/ayaka-gui/vite.config.ts
+++ b/bins/ayaka-gui/vite.config.ts
@@ -6,7 +6,7 @@ import vue from '@vitejs/plugin-vue'
 export default defineConfig({
   plugins: [vue()],
   clearScreen: false,
-  envPrefix: ['VITE_', 'TAURI_', 'RUST_'],
+  envPrefix: ['VITE_', 'TAURI_'],
   build: {
     target: ['es2021', 'chrome97', 'safari13'],
     minify: !env.TAURI_DEBUG ? 'esbuild' : false,
diff --git a/bins/ayaka-latex/Cargo.toml b/bins/ayaka-latex/Cargo.toml
index bdea313f..115303fa 100644
--- a/bins/ayaka-latex/Cargo.toml
+++ b/bins/ayaka-latex/Cargo.toml
@@ -7,4 +7,4 @@ edition = "2021"
 ayaka-runtime = { path = "../../utils/ayaka-runtime" }
 tokio = { version = "1", features = ["fs", "io-util", "macros", "rt"] }
 clap = { version = "4.0", features = ["derive"] }
-env_logger = "0.9"
+flexi_logger = { version = "0.24", default-features = false, features = ["colors"] }
diff --git a/bins/ayaka-latex/src/main.rs b/bins/ayaka-latex/src/main.rs
index 1e463812..4deab863 100644
--- a/bins/ayaka-latex/src/main.rs
+++ b/bins/ayaka-latex/src/main.rs
@@ -1,7 +1,8 @@
 mod writer;
 
-use ayaka_runtime::{anyhow::Result, log::LevelFilter, *};
+use ayaka_runtime::{anyhow::Result, *};
 use clap::Parser;
+use flexi_logger::{LogSpecification, Logger};
 use std::ffi::OsString;
 use writer::LaTeXWriter;
 
@@ -18,9 +19,12 @@ pub struct Options {
 #[tokio::main(flavor = "current_thread")]
 async fn main() -> Result<()> {
     let opts = Options::parse();
-    env_logger::Builder::from_default_env()
-        .filter_module("wasmer", LevelFilter::Warn)
-        .try_init()?;
+    let spec = LogSpecification::parse("warn")?;
+    let _log_handle = Logger::with(spec)
+        .log_to_stdout()
+        .set_palette("b1;3;2;4;6".to_string())
+        .use_utc()
+        .start()?;
     let context = Context::open(&opts.input, FrontendType::Latex);
     let mut ctx = context.await?;
 
diff --git a/book/src/platforms.md b/book/src/platforms.md
index 2a2f4686..e08f9e29 100644
--- a/book/src/platforms.md
+++ b/book/src/platforms.md
@@ -11,6 +11,9 @@ Ensured to work well with these triples:
 ## Tier 2
 Should work well with these triples, but not tested:
 
+* i686-pc-windows-msvc
+* aarch64-pc-windows-msvc
+* i686-pc-windows-gnu
 * x86_64-pc-windows-gnu
 * aarch64-unknown-linux-gnu
 * aarch64-apple-darwin
@@ -18,6 +21,4 @@ Should work well with these triples, but not tested:
 ## Tier 3
 May not build or run because of dependencies:
 
-* i686-pc-windows-msvc
-* i686-pc-windows-gnu
-* aarch64-pc-windows-msvc
+* s390x-unknown-linux-gnu
diff --git a/book/src/plugin/summary.md b/book/src/plugin/summary.md
index 46e99b14..bf23784a 100644
--- a/book/src/plugin/summary.md
+++ b/book/src/plugin/summary.md
@@ -24,6 +24,12 @@ plugins:
 ```
 You don't need to specify the extension.
 
+## WASM directory mappings
+The parent directory of the config file (aka. the root directory) is mapped to `/` in the plugins.
+Some plugins, e.g. media, need to determine if the resource files exist.
+Therefore, the files should be placed under the root directory.
+Symbolic links may not work if they point to directories outside the root directory.
+
 ## The text processing workflow
 ``` dot process
 digraph {
diff --git a/plugins/live2d/src/lib.rs b/plugins/live2d/src/lib.rs
index 5dfb6451..2396e591 100644
--- a/plugins/live2d/src/lib.rs
+++ b/plugins/live2d/src/lib.rs
@@ -76,7 +76,7 @@ fn hide(ctx: LineProcessContext) -> LineProcessResult {
 fn process_game(mut ctx: GameProcessContext) -> GameProcessResult {
     if let Some(names) = ctx.props.remove("ch_names") {
         for name in names.split(',') {
-            if let Some(path) = find_model(name, &ctx.root_path, &ctx.props) {
+            if let Some(path) = find_model(name, "/", &ctx.props) {
                 ctx.props.insert(
                     format!("ch_{}_model", name),
                     path.to_string_lossy().into_owned(),
diff --git a/plugins/media/src/lib.rs b/plugins/media/src/lib.rs
index cc70ec47..db70e6ca 100644
--- a/plugins/media/src/lib.rs
+++ b/plugins/media/src/lib.rs
@@ -52,10 +52,7 @@ fn file_ctx(
 ) -> LineProcessResult {
     file(
         &ctx.props[prop].get_str(),
-        ctx.game_props
-            .get(game_prop)
-            .map(|game_prop| ctx.root_path.join(game_prop))
-            .as_deref(),
+        ctx.game_props.get(game_prop).map(PathBuf::from).as_deref(),
         prop,
         exs,
         temp,
@@ -84,7 +81,7 @@ fn process_action(mut ctx: ActionProcessContext) -> ActionProcessResult {
         &voice_id,
         ctx.game_props
             .get("voices")
-            .map(|p| ctx.root_path.join(p).join(&ctx.ctx.cur_para))
+            .map(|p| PathBuf::from(p).join(&ctx.ctx.cur_para))
             .as_deref(),
         "voice",
         &["mp3"],
@@ -96,7 +93,7 @@ fn process_action(mut ctx: ActionProcessContext) -> ActionProcessResult {
 
 #[export]
 fn process_game(mut ctx: GameProcessContext) -> GameProcessResult {
-    let base_dir = ctx.props.get("bgs").map(|p| ctx.root_path.join(p));
+    let base_dir = ctx.props.get("bgs").map(PathBuf::from);
     if let Some(bg) = ctx.props.get_mut("bg") {
         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/Cargo.toml b/utils/Cargo.toml
index d0b9cb10..4be945f8 100644
--- a/utils/Cargo.toml
+++ b/utils/Cargo.toml
@@ -2,6 +2,11 @@
 members = [
     "ayaka-bindings-types",
     "ayaka-script",
+    "ayaka-plugin",
+    "ayaka-plugin-nop",
+    "ayaka-plugin-wasmer",
+    "ayaka-plugin-wasmtime",
+    "ayaka-plugin-wasmi",
     "ayaka-runtime",
 ]
 
diff --git a/utils/Makefile b/utils/Makefile
index 7708e85c..9501a2aa 100644
--- a/utils/Makefile
+++ b/utils/Makefile
@@ -1,8 +1,8 @@
 .PHONY: test nextest clean update doc
 test:
-	cargo test
+	cargo test --all-features
 nextest:
-	cargo nextest run
+	cargo nextest run --all-features
 clean:
 	cargo clean
 update:
diff --git a/utils/ayaka-bindings-types/Cargo.toml b/utils/ayaka-bindings-types/Cargo.toml
index c0e837b9..0b8d75d4 100644
--- a/utils/ayaka-bindings-types/Cargo.toml
+++ b/utils/ayaka-bindings-types/Cargo.toml
@@ -9,4 +9,3 @@ ayaka-script = { path = "../ayaka-script" }
 fallback = "0.1"
 serde = { version = "1.0", features = ["derive"] }
 log = { version = "0.4", features = ["serde"] }
-bitflags = "1.3"
diff --git a/utils/ayaka-bindings-types/src/plugin.rs b/utils/ayaka-bindings-types/src/plugin.rs
index f8d3b31c..5d5b41d8 100644
--- a/utils/ayaka-bindings-types/src/plugin.rs
+++ b/utils/ayaka-bindings-types/src/plugin.rs
@@ -1,9 +1,6 @@
 use crate::*;
 use serde::{Deserialize, Serialize};
-use std::{
-    collections::HashMap,
-    path::{Path, PathBuf},
-};
+use std::collections::HashMap;
 
 /// The bit flags to describe plugin type.
 ///
@@ -104,8 +101,6 @@ pub enum FrontendType {
 /// ```
 #[derive(Debug, Serialize, Deserialize)]
 pub struct ActionProcessContext {
-    /// The root path of the game profile.
-    pub root_path: PathBuf,
     /// The global properties of the game profile.
     pub game_props: HashMap<String, String>,
     /// The frontend type.
@@ -119,7 +114,6 @@ pub struct ActionProcessContext {
 #[derive(Debug, Serialize)]
 #[doc(hidden)]
 pub struct ActionProcessContextRef<'a> {
-    pub root_path: &'a Path,
     pub game_props: &'a HashMap<String, String>,
     pub frontend: FrontendType,
     pub ctx: &'a RawContext,
@@ -153,8 +147,6 @@ pub struct ActionProcessResult {
 /// ```
 #[derive(Debug, Serialize, Deserialize)]
 pub struct TextProcessContext {
-    /// The root path of the game profile.
-    pub root_path: PathBuf,
     /// The global properties of the game profile.
     pub game_props: HashMap<String, String>,
     /// The frontend type.
@@ -164,7 +156,6 @@ pub struct TextProcessContext {
 #[derive(Debug, Serialize)]
 #[doc(hidden)]
 pub struct TextProcessContextRef<'a> {
-    pub root_path: &'a Path,
     pub game_props: &'a HashMap<String, String>,
     pub frontend: FrontendType,
 }
@@ -195,8 +186,6 @@ pub struct GameProcessContext {
     pub title: String,
     /// The author of the game.
     pub author: String,
-    /// The root path of the game profile.
-    pub root_path: PathBuf,
     /// The global properties of the game.
     pub props: HashMap<String, String>,
 }
@@ -206,7 +195,6 @@ pub struct GameProcessContext {
 pub struct GameProcessContextRef<'a> {
     pub title: &'a str,
     pub author: &'a str,
-    pub root_path: &'a Path,
     pub props: &'a HashMap<String, String>,
 }
 
@@ -237,8 +225,6 @@ pub struct GameProcessResult {
 /// ```
 #[derive(Debug, Serialize, Deserialize)]
 pub struct LineProcessContext {
-    /// The root path of the game profile.
-    pub root_path: PathBuf,
     /// The global properties of the game profile.
     pub game_props: HashMap<String, String>,
     /// The frontend type.
@@ -252,7 +238,6 @@ pub struct LineProcessContext {
 #[derive(Debug, Serialize)]
 #[doc(hidden)]
 pub struct LineProcessContextRef<'a> {
-    pub root_path: &'a Path,
     pub game_props: &'a HashMap<String, String>,
     pub frontend: FrontendType,
     pub ctx: &'a RawContext,
diff --git a/utils/ayaka-bindings/src/logger.rs b/utils/ayaka-bindings/src/logger.rs
index 1a491522..93c9a6a9 100644
--- a/utils/ayaka-bindings/src/logger.rs
+++ b/utils/ayaka-bindings/src/logger.rs
@@ -1,9 +1,8 @@
-use crate::__import;
 use log::Log;
 
 #[link(wasm_import_module = "log")]
 extern "C" {
-    fn __log(len: usize, data: *const u8) -> u64;
+    fn __log(len: usize, data: *const u8);
     fn __log_flush();
 }
 
@@ -31,7 +30,9 @@ impl Log for PluginLogger {
 
     fn log(&self, record: &log::Record) {
         let record: ayaka_bindings_types::Record = record.into();
-        unsafe { __import::<_, ()>(__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()) };
     }
 
     fn flush(&self) {
diff --git a/utils/ayaka-plugin-nop/Cargo.toml b/utils/ayaka-plugin-nop/Cargo.toml
new file mode 100644
index 00000000..40935011
--- /dev/null
+++ b/utils/ayaka-plugin-nop/Cargo.toml
@@ -0,0 +1,8 @@
+[package]
+name = "ayaka-plugin-nop"
+version = "0.1.0"
+edition = "2021"
+
+[dependencies]
+ayaka-plugin = { path = "../ayaka-plugin" }
+anyhow = "1.0"
diff --git a/utils/ayaka-plugin-nop/src/lib.rs b/utils/ayaka-plugin-nop/src/lib.rs
new file mode 100644
index 00000000..ebd1edf4
--- /dev/null
+++ b/utils/ayaka-plugin-nop/src/lib.rs
@@ -0,0 +1,43 @@
+//! A no-operation plugin backend.
+
+#![warn(missing_docs)]
+
+use anyhow::bail;
+use ayaka_plugin::*;
+use std::{collections::HashMap, path::Path};
+
+/// A no-operation WASM module.
+///
+/// Any call will return an error.
+pub struct NopModule;
+
+impl RawModule for NopModule {
+    type Linker = NopStoreLinker;
+
+    type Func = ();
+
+    fn call<T>(&self, name: &str, _args: &[u8], _f: impl FnOnce(&[u8]) -> Result<T>) -> Result<T> {
+        bail!("Trying to call {name}.")
+    }
+}
+
+/// A no-operation store & linker.
+pub struct NopStoreLinker;
+
+impl StoreLinker<NopModule> for NopStoreLinker {
+    fn new(_root_path: impl AsRef<Path>) -> Result<Self> {
+        Ok(Self)
+    }
+
+    fn create(&self, _binary: &[u8]) -> Result<NopModule> {
+        Ok(NopModule)
+    }
+
+    fn import(&mut self, _ns: impl Into<String>, _funcs: HashMap<String, ()>) -> Result<()> {
+        Ok(())
+    }
+
+    fn wrap(&self, _f: impl Fn() + Send + Sync + 'static) {}
+
+    fn wrap_with_args_raw(&self, _f: impl (Fn(&[u8]) -> Result<()>) + Send + Sync + 'static) {}
+}
diff --git a/utils/ayaka-plugin-wasmer/Cargo.toml b/utils/ayaka-plugin-wasmer/Cargo.toml
new file mode 100644
index 00000000..b93550bc
--- /dev/null
+++ b/utils/ayaka-plugin-wasmer/Cargo.toml
@@ -0,0 +1,10 @@
+[package]
+name = "ayaka-plugin-wasmer"
+version = "0.1.0"
+edition = "2021"
+
+[dependencies]
+ayaka-plugin = { path = "../ayaka-plugin" }
+wasmer = "3.0"
+wasmer-wasi = "3.0"
+scopeguard = "1.1"
diff --git a/utils/ayaka-plugin-wasmer/src/lib.rs b/utils/ayaka-plugin-wasmer/src/lib.rs
new file mode 100644
index 00000000..00781658
--- /dev/null
+++ b/utils/ayaka-plugin-wasmer/src/lib.rs
@@ -0,0 +1,297 @@
+//! Wasmer-based plugin backend.
+
+#![allow(clippy::mut_from_ref)]
+#![warn(missing_docs)]
+
+use ayaka_plugin::*;
+use scopeguard::defer;
+use std::{
+    collections::HashMap,
+    path::Path,
+    sync::{Arc, Mutex},
+};
+use wasmer::*;
+use wasmer_wasi::*;
+
+unsafe fn mem_slice<R>(
+    store: &impl AsStoreRef,
+    memory: &Memory,
+    start: i32,
+    len: i32,
+    f: impl FnOnce(&[u8]) -> R,
+) -> R {
+    f(memory
+        .view(store)
+        .data_unchecked()
+        .get_unchecked(start as usize..)
+        .get_unchecked(..len as usize))
+}
+
+unsafe fn mem_slice_mut<R>(
+    store: &impl AsStoreRef,
+    memory: &Memory,
+    start: i32,
+    len: i32,
+    f: impl FnOnce(&mut [u8]) -> R,
+) -> R {
+    f(memory
+        .view(store)
+        .data_unchecked_mut()
+        .get_unchecked_mut(start as usize..)
+        .get_unchecked_mut(..len as usize))
+}
+
+type HostStore = Arc<Mutex<Store>>;
+
+struct HostInstance {
+    store: HostStore,
+    instance: Instance,
+}
+
+impl HostInstance {
+    pub fn new(store: HostStore, instance: Instance) -> Self {
+        Self { store, instance }
+    }
+
+    pub fn get_memory(&self) -> Result<HostMemory, ExportError> {
+        self.instance
+            .exports
+            .get_memory(MEMORY_NAME)
+            .map(|mem| HostMemory::new(self.store.clone(), mem.clone()))
+    }
+
+    pub fn get_func(&self, name: &str) -> Result<HostFunction, ExportError> {
+        self.instance
+            .exports
+            .get_function(name)
+            .map(|func| HostFunction::new(self.store.clone(), func.clone()))
+    }
+}
+
+struct HostMemory {
+    store: HostStore,
+    memory: Memory,
+}
+
+impl HostMemory {
+    pub fn new(store: HostStore, memory: Memory) -> Self {
+        Self { store, memory }
+    }
+
+    pub unsafe fn slice<R>(&self, start: i32, len: i32, f: impl FnOnce(&[u8]) -> R) -> R {
+        let store = self.store.lock().unwrap();
+        mem_slice(&store.as_store_ref(), &self.memory, start, len, f)
+    }
+
+    pub unsafe fn slice_mut<R>(&self, start: i32, len: i32, f: impl FnOnce(&mut [u8]) -> R) -> R {
+        let store = self.store.lock().unwrap();
+        mem_slice_mut(&store.as_store_ref(), &self.memory, start, len, f)
+    }
+}
+
+struct HostFunction {
+    store: HostStore,
+    func: Function,
+}
+
+impl HostFunction {
+    pub fn new(store: HostStore, func: Function) -> Self {
+        Self { store, func }
+    }
+
+    pub fn call(&self, params: &[Value]) -> Result<Box<[Value]>, RuntimeError> {
+        self.func
+            .call(&mut self.store.lock().unwrap().as_store_mut(), params)
+    }
+}
+
+/// A Wasmer [`Instance`].
+pub struct WasmerModule {
+    instance: HostInstance,
+    memory: HostMemory,
+    abi_free: HostFunction,
+    abi_alloc: HostFunction,
+}
+
+impl WasmerModule {
+    /// Loads the WASM [`Module`], with some imports.
+    pub(crate) fn new(store: HostStore, instance: Instance) -> Result<Self> {
+        let instance = HostInstance::new(store, instance);
+        let memory = instance.get_memory()?;
+        let abi_free = instance.get_func(ABI_FREE_NAME)?;
+        let abi_alloc = instance.get_func(ABI_ALLOC_NAME)?;
+        Ok(Self {
+            instance,
+            memory,
+            abi_free,
+            abi_alloc,
+        })
+    }
+}
+
+impl RawModule for WasmerModule {
+    type Linker = WasmerStoreLinker;
+
+    type Func = WasmerFunction;
+
+    fn call<T>(&self, name: &str, data: &[u8], f: impl FnOnce(&[u8]) -> Result<T>) -> Result<T> {
+        let memory = &self.memory;
+        let func = self.instance.get_func(name)?;
+
+        let ptr = self.abi_alloc.call(&[Value::I32(data.len() as i32)])?[0].unwrap_i32();
+        defer! { self.abi_free.call(&[Value::I32(ptr), Value::I32(data.len() as i32)]).unwrap(); }
+        unsafe { memory.slice_mut(ptr, data.len() as i32, |s| s.copy_from_slice(data)) };
+
+        let res = func.call(&[Value::I32(data.len() as i32), Value::I32(ptr)])?[0].unwrap_i64();
+        let (len, res) = ((res >> 32) as i32, (res & 0xFFFFFFFF) as i32);
+        defer! { self.abi_free.call(&[Value::I32(res), Value::I32(len)]).unwrap(); }
+
+        let res_data = unsafe { memory.slice(res, len, |s| f(s)) }?;
+        Ok(res_data)
+    }
+}
+
+#[doc(hidden)]
+#[derive(Clone)]
+pub struct RuntimeInstanceData {
+    memory: Option<Memory>,
+    #[allow(clippy::type_complexity)]
+    func: Arc<dyn (Fn(&[u8]) -> Result<()>) + Send + Sync + 'static>,
+}
+
+impl RuntimeInstanceData {
+    pub fn new(func: impl (Fn(&[u8]) -> Result<()>) + Send + Sync + 'static) -> Self {
+        Self {
+            memory: None,
+            func: Arc::new(func),
+        }
+    }
+
+    pub fn set_memory(&mut self, memory: Memory) {
+        self.memory = Some(memory);
+    }
+
+    pub fn memory(&self) -> &Memory {
+        self.memory.as_ref().unwrap()
+    }
+
+    pub fn call(this: FunctionEnvMut<Self>, len: i32, ptr: i32) {
+        let memory = this.data().memory();
+        unsafe {
+            mem_slice(&this, memory, ptr, len, |slice| {
+                (this.data().func)(slice).unwrap();
+            })
+        };
+    }
+}
+
+/// A Wasmer [`Store`] with some [`ImportObject`]s.
+pub struct WasmerStoreLinker {
+    store: HostStore,
+    wasi_env: WasiEnv,
+    imports: HashMap<String, HashMap<String, WasmerFunction>>,
+}
+
+impl WasmerStoreLinker {
+    fn wrap_impl(
+        store: &mut impl AsStoreMut,
+        func: WasmerFunction,
+    ) -> (Function, Option<FunctionEnv<RuntimeInstanceData>>) {
+        match func {
+            WasmerFunction::Function(func) => (func, None),
+            WasmerFunction::WithEnv(env_data) => {
+                let env = FunctionEnv::new(store, env_data);
+                let func = Function::new_typed_with_env(store, &env, RuntimeInstanceData::call);
+                (func, Some(env))
+            }
+        }
+    }
+}
+
+impl StoreLinker<WasmerModule> for WasmerStoreLinker {
+    fn new(root_path: impl AsRef<Path>) -> Result<Self> {
+        let store = Store::default();
+        let wasi_state = WasiState::new("ayaka-plugin-wasmer")
+            .preopen(|p| p.directory(root_path.as_ref()).alias("/").read(true))?
+            .build()?;
+        let wasi_env = WasiEnv::new(wasi_state);
+        Ok(Self {
+            store: Arc::new(Mutex::new(store)),
+            wasi_env,
+            imports: HashMap::default(),
+        })
+    }
+
+    fn create(&self, binary: &[u8]) -> Result<WasmerModule> {
+        let instance = {
+            let mut store = self.store.lock().unwrap();
+            let module = Module::from_binary(&store.as_store_ref(), binary)?;
+            let wasi_env = self.wasi_env.clone();
+            let mut wasi_env = WasiFunctionEnv::new(&mut store.as_store_mut(), wasi_env);
+            let mut wasi_import = generate_import_object_from_env(
+                &mut store.as_store_mut(),
+                &wasi_env.env,
+                WasiVersion::Latest,
+            );
+            let mut envs = vec![];
+            for (ns, funcs) in &self.imports {
+                wasi_import.register_namespace(
+                    ns,
+                    funcs.iter().map(|(name, func)| {
+                        (
+                            name.clone(),
+                            Extern::Function({
+                                let (func, env) =
+                                    Self::wrap_impl(&mut store.as_store_mut(), func.clone());
+                                if let Some(env) = env {
+                                    envs.push(env)
+                                }
+                                func
+                            }),
+                        )
+                    }),
+                );
+            }
+            let instance = Instance::new(&mut store.as_store_mut(), &module, &wasi_import)?;
+            wasi_env.initialize(&mut store.as_store_mut(), &instance)?;
+            let memory = instance.exports.get_memory(MEMORY_NAME)?;
+            for env in &envs {
+                env.as_mut(&mut store.as_store_mut())
+                    .set_memory(memory.clone());
+            }
+            instance
+        };
+        let host = WasmerModule::new(self.store.clone(), instance)?;
+        Ok(host)
+    }
+
+    fn import(
+        &mut self,
+        ns: impl Into<String>,
+        funcs: HashMap<String, WasmerFunction>,
+    ) -> Result<()> {
+        self.imports.insert(ns.into(), funcs);
+        Ok(())
+    }
+
+    fn wrap(&self, f: impl Fn() + Send + Sync + 'static) -> WasmerFunction {
+        WasmerFunction::Function(Function::new_typed(
+            &mut self.store.lock().unwrap().as_store_mut(),
+            f,
+        ))
+    }
+
+    fn wrap_with_args_raw(
+        &self,
+        f: impl (Fn(&[u8]) -> Result<()>) + Send + Sync + 'static,
+    ) -> WasmerFunction {
+        WasmerFunction::WithEnv(RuntimeInstanceData::new(f))
+    }
+}
+
+#[doc(hidden)]
+#[derive(Clone)]
+pub enum WasmerFunction {
+    Function(Function),
+    WithEnv(RuntimeInstanceData),
+}
diff --git a/utils/ayaka-plugin-wasmi/Cargo.toml b/utils/ayaka-plugin-wasmi/Cargo.toml
new file mode 100644
index 00000000..075ab448
--- /dev/null
+++ b/utils/ayaka-plugin-wasmi/Cargo.toml
@@ -0,0 +1,11 @@
+[package]
+name = "ayaka-plugin-wasmi"
+version = "0.1.0"
+edition = "2021"
+
+[dependencies]
+ayaka-plugin = { path = "../ayaka-plugin" }
+wasmi = { git = "https://github.com/paritytech/wasmi.git" }
+wasmi_wasi = { git = "https://github.com/paritytech/wasmi.git" }
+cap-std = "0.26"
+scopeguard = "1.1"
diff --git a/utils/ayaka-plugin-wasmi/src/lib.rs b/utils/ayaka-plugin-wasmi/src/lib.rs
new file mode 100644
index 00000000..0745521a
--- /dev/null
+++ b/utils/ayaka-plugin-wasmi/src/lib.rs
@@ -0,0 +1,239 @@
+//! Wasmi-based plugin backend.
+
+#![warn(missing_docs)]
+
+use ayaka_plugin::*;
+use scopeguard::defer;
+use std::{
+    collections::HashMap,
+    path::Path,
+    sync::{Arc, Mutex},
+};
+use wasmi::{core::Trap, *};
+use wasmi_wasi::*;
+
+unsafe fn mem_slice<'a, T: 'a>(
+    store: impl Into<StoreContext<'a, T>>,
+    memory: &Memory,
+    start: i32,
+    len: i32,
+) -> &'a [u8] {
+    memory
+        .data(store)
+        .get_unchecked(start as usize..)
+        .get_unchecked(..len as usize)
+}
+
+unsafe fn mem_slice_mut<'a, T: 'a>(
+    store: impl Into<StoreContextMut<'a, T>>,
+    memory: &Memory,
+    start: i32,
+    len: i32,
+) -> &'a mut [u8] {
+    memory
+        .data_mut(store)
+        .get_unchecked_mut(start as usize..)
+        .get_unchecked_mut(..len as usize)
+}
+
+type HostStore = Arc<Mutex<Store<WasiCtx>>>;
+
+struct HostInstance {
+    store: HostStore,
+    instance: Instance,
+}
+
+impl HostInstance {
+    pub fn new(store: HostStore, instance: Instance) -> Self {
+        Self { store, instance }
+    }
+
+    pub fn get_memory(&self) -> Option<HostMemory> {
+        self.instance
+            .get_export(self.store.lock().unwrap().as_context_mut(), MEMORY_NAME)
+            .map(|mem| HostMemory::new(self.store.clone(), mem.into_memory().unwrap()))
+    }
+
+    pub fn get_typed_func<Params: WasmParams, Results: WasmResults>(
+        &self,
+        name: &str,
+    ) -> Result<HostTypedFunc<Params, Results>, wasmi::Error> {
+        let func = {
+            let mut store = self.store.lock().unwrap();
+            self.instance
+                .get_export(store.as_context_mut(), name)
+                .unwrap()
+                .into_func()
+                .unwrap()
+                .typed(store.as_context())?
+        };
+        Ok(HostTypedFunc::new(self.store.clone(), func))
+    }
+}
+
+struct HostMemory {
+    store: HostStore,
+    memory: Memory,
+}
+
+impl HostMemory {
+    pub fn new(store: HostStore, memory: Memory) -> Self {
+        Self { store, memory }
+    }
+
+    pub unsafe fn slice<R>(&self, start: i32, len: i32, f: impl FnOnce(&[u8]) -> R) -> R {
+        let store = self.store.lock().unwrap();
+        let data = mem_slice(store.as_context(), &self.memory, start, len);
+        f(data)
+    }
+
+    pub unsafe fn slice_mut<R>(&self, start: i32, len: i32, f: impl FnOnce(&mut [u8]) -> R) -> R {
+        let mut store = self.store.lock().unwrap();
+        let data = mem_slice_mut(store.as_context_mut(), &self.memory, start, len);
+        f(data)
+    }
+}
+
+struct HostTypedFunc<Params, Results> {
+    store: HostStore,
+    func: TypedFunc<Params, Results>,
+}
+
+impl<Params: WasmParams, Results: WasmResults> HostTypedFunc<Params, Results> {
+    pub fn new(store: HostStore, func: TypedFunc<Params, Results>) -> Self {
+        Self { store, func }
+    }
+
+    pub fn call(&self, params: Params) -> Result<Results, Trap> {
+        self.func
+            .call(self.store.lock().unwrap().as_context_mut(), params)
+    }
+}
+
+/// A Wasmi [`Instance`].
+pub struct WasmiModule {
+    instance: HostInstance,
+    memory: HostMemory,
+    abi_free: HostTypedFunc<(i32, i32), ()>,
+    abi_alloc: HostTypedFunc<i32, i32>,
+}
+
+impl WasmiModule {
+    pub(crate) fn new(store: HostStore, module: &Module, linker: &Linker<WasiCtx>) -> Result<Self> {
+        let instance = {
+            let mut store = store.lock().unwrap();
+            linker
+                .instantiate(store.as_context_mut(), module)?
+                .start(store.as_context_mut())?
+        };
+        let instance = HostInstance::new(store, instance);
+        let memory = instance.get_memory().unwrap();
+        let abi_free = instance.get_typed_func(ABI_FREE_NAME)?;
+        let abi_alloc = instance.get_typed_func(ABI_ALLOC_NAME)?;
+        Ok(Self {
+            instance,
+            memory,
+            abi_free,
+            abi_alloc,
+        })
+    }
+}
+
+impl RawModule for WasmiModule {
+    type Linker = WasmiStoreLinker;
+
+    type Func = Func;
+
+    fn call<T>(&self, name: &str, data: &[u8], f: impl FnOnce(&[u8]) -> Result<T>) -> Result<T> {
+        let memory = &self.memory;
+        let func = self.instance.get_typed_func::<(i32, i32), u64>(name)?;
+
+        let ptr = self.abi_alloc.call(data.len() as i32)?;
+        defer! { self.abi_free.call((ptr, data.len() as i32)).unwrap(); }
+        unsafe { memory.slice_mut(ptr, data.len() as i32, |s| s.copy_from_slice(data)) };
+
+        let res = func.call((data.len() as i32, ptr))?;
+        let (len, res) = ((res >> 32) as i32, (res & 0xFFFFFFFF) as i32);
+        defer! { self.abi_free.call((res, len)).unwrap(); }
+
+        let res_data = unsafe { memory.slice(res, len, |s| f(s)) }?;
+        Ok(res_data)
+    }
+}
+
+/// A Wasmi [`Store`] with [`Linker`].
+pub struct WasmiStoreLinker {
+    engine: Engine,
+    store: HostStore,
+    linker: Linker<WasiCtx>,
+}
+
+impl WasmiStoreLinker {
+    fn preopen_root(root_path: impl AsRef<Path>) -> Result<cap_std::fs::Dir> {
+        let mut options = std::fs::OpenOptions::new();
+        options.read(true);
+        #[cfg(windows)]
+        {
+            use std::os::windows::fs::OpenOptionsExt;
+            options.share_mode(3); // remove FILE_SHARE_DELETE
+            options.custom_flags(0x02000000); // open dir with FILE_FLAG_BACKUP_SEMANTICS
+        }
+        let root = options.open(root_path)?;
+        Ok(cap_std::fs::Dir::from_std_file(root))
+    }
+}
+
+impl StoreLinker<WasmiModule> for WasmiStoreLinker {
+    fn new(root_path: impl AsRef<Path>) -> Result<Self> {
+        let engine = Engine::default();
+        let wasi = WasiCtxBuilder::new()
+            .inherit_stdio()
+            .preopened_dir(Self::preopen_root(root_path)?, "/")?
+            .build();
+        let mut store = Store::new(&engine, wasi);
+        let mut linker = Linker::new();
+        define_wasi(&mut linker, &mut store, |ctx| ctx)?;
+        Ok(Self {
+            engine,
+            store: Arc::new(Mutex::new(store)),
+            linker,
+        })
+    }
+
+    fn create(&self, binary: &[u8]) -> Result<WasmiModule> {
+        let module = Module::new(&self.engine, binary)?;
+        let host = WasmiModule::new(self.store.clone(), &module, &self.linker)?;
+        Ok(host)
+    }
+
+    fn import(&mut self, ns: impl Into<String>, funcs: HashMap<String, Func>) -> Result<()> {
+        let ns = ns.into();
+        for (name, func) in funcs {
+            self.linker.define(&ns, &name, func)?;
+        }
+        Ok(())
+    }
+
+    fn wrap(&self, f: impl Fn() + Send + Sync + 'static) -> Func {
+        Func::wrap(self.store.lock().unwrap().as_context_mut(), f)
+    }
+
+    fn wrap_with_args_raw(
+        &self,
+        f: impl (Fn(&[u8]) -> Result<()>) + Send + Sync + 'static,
+    ) -> Func {
+        Func::wrap(
+            self.store.lock().unwrap().as_context_mut(),
+            move |store: Caller<WasiCtx>, len: i32, data: i32| unsafe {
+                let memory = store
+                    .get_export(MEMORY_NAME)
+                    .unwrap()
+                    .into_memory()
+                    .unwrap();
+                let data = mem_slice(store.as_context(), &memory, data, len);
+                f(data).map_err(|e| Trap::new(e.to_string()))?;
+                Ok(())
+            },
+        )
+    }
+}
diff --git a/utils/ayaka-plugin-wasmtime/Cargo.toml b/utils/ayaka-plugin-wasmtime/Cargo.toml
new file mode 100644
index 00000000..97632aa4
--- /dev/null
+++ b/utils/ayaka-plugin-wasmtime/Cargo.toml
@@ -0,0 +1,10 @@
+[package]
+name = "ayaka-plugin-wasmtime"
+version = "0.1.0"
+edition = "2021"
+
+[dependencies]
+ayaka-plugin = { path = "../ayaka-plugin" }
+wasmtime = "2.0"
+wasmtime-wasi = "2.0"
+scopeguard = "1.1"
diff --git a/utils/ayaka-plugin-wasmtime/src/lib.rs b/utils/ayaka-plugin-wasmtime/src/lib.rs
new file mode 100644
index 00000000..f023bc53
--- /dev/null
+++ b/utils/ayaka-plugin-wasmtime/src/lib.rs
@@ -0,0 +1,227 @@
+//! Wasmtime-based plugin backend.
+
+#![warn(missing_docs)]
+
+use ayaka_plugin::*;
+use scopeguard::defer;
+use std::{
+    collections::HashMap,
+    path::Path,
+    sync::{Arc, Mutex},
+};
+use wasmtime::*;
+use wasmtime_wasi::*;
+
+unsafe fn mem_slice<'a, T: 'a>(
+    store: impl Into<StoreContext<'a, T>>,
+    memory: &Memory,
+    start: i32,
+    len: i32,
+) -> &'a [u8] {
+    memory
+        .data(store)
+        .get_unchecked(start as usize..)
+        .get_unchecked(..len as usize)
+}
+
+unsafe fn mem_slice_mut<'a, T: 'a>(
+    store: impl Into<StoreContextMut<'a, T>>,
+    memory: &Memory,
+    start: i32,
+    len: i32,
+) -> &'a mut [u8] {
+    memory
+        .data_mut(store)
+        .get_unchecked_mut(start as usize..)
+        .get_unchecked_mut(..len as usize)
+}
+
+type HostStore = Arc<Mutex<Store<WasiCtx>>>;
+
+struct HostInstance {
+    store: HostStore,
+    instance: Instance,
+}
+
+impl HostInstance {
+    pub fn new(store: HostStore, instance: Instance) -> Self {
+        Self { store, instance }
+    }
+
+    pub fn get_memory(&self) -> Option<HostMemory> {
+        self.instance
+            .get_memory(self.store.lock().unwrap().as_context_mut(), MEMORY_NAME)
+            .map(|mem| HostMemory::new(self.store.clone(), mem))
+    }
+
+    pub fn get_typed_func<Params: WasmParams, Results: WasmResults>(
+        &self,
+        name: &str,
+    ) -> Result<HostTypedFunc<Params, Results>> {
+        self.instance
+            .get_typed_func(self.store.lock().unwrap().as_context_mut(), name)
+            .map(|func| HostTypedFunc::new(self.store.clone(), func))
+    }
+}
+
+struct HostMemory {
+    store: HostStore,
+    memory: Memory,
+}
+
+impl HostMemory {
+    pub fn new(store: HostStore, memory: Memory) -> Self {
+        Self { store, memory }
+    }
+
+    pub unsafe fn slice<R>(&self, start: i32, len: i32, f: impl FnOnce(&[u8]) -> R) -> R {
+        let store = self.store.lock().unwrap();
+        let data = mem_slice(store.as_context(), &self.memory, start, len);
+        f(data)
+    }
+
+    pub unsafe fn slice_mut<R>(&self, start: i32, len: i32, f: impl FnOnce(&mut [u8]) -> R) -> R {
+        let mut store = self.store.lock().unwrap();
+        let data = mem_slice_mut(store.as_context_mut(), &self.memory, start, len);
+        f(data)
+    }
+}
+
+struct HostTypedFunc<Params, Results> {
+    store: HostStore,
+    func: TypedFunc<Params, Results>,
+}
+
+impl<Params: WasmParams, Results: WasmResults> HostTypedFunc<Params, Results> {
+    pub fn new(store: HostStore, func: TypedFunc<Params, Results>) -> Self {
+        Self { store, func }
+    }
+
+    pub fn call(&self, params: Params) -> Result<Results, Trap> {
+        self.func
+            .call(self.store.lock().unwrap().as_context_mut(), params)
+    }
+}
+
+/// A Wasmtime [`Instance`].
+pub struct WasmtimeModule {
+    instance: HostInstance,
+    memory: HostMemory,
+    abi_free: HostTypedFunc<(i32, i32), ()>,
+    abi_alloc: HostTypedFunc<i32, i32>,
+}
+
+impl WasmtimeModule {
+    pub(crate) fn new(store: HostStore, module: &Module, linker: &Linker<WasiCtx>) -> Result<Self> {
+        let instance = linker.instantiate(store.lock().unwrap().as_context_mut(), module)?;
+        let instance = HostInstance::new(store, instance);
+        let memory = instance.get_memory().unwrap();
+        let abi_free = instance.get_typed_func(ABI_FREE_NAME)?;
+        let abi_alloc = instance.get_typed_func(ABI_ALLOC_NAME)?;
+        Ok(Self {
+            instance,
+            memory,
+            abi_free,
+            abi_alloc,
+        })
+    }
+}
+
+impl RawModule for WasmtimeModule {
+    type Linker = WasmtimeStoreLinker;
+
+    type Func = Func;
+
+    fn call<T>(&self, name: &str, data: &[u8], f: impl FnOnce(&[u8]) -> Result<T>) -> Result<T> {
+        let memory = &self.memory;
+        let func = self.instance.get_typed_func::<(i32, i32), u64>(name)?;
+
+        let ptr = self.abi_alloc.call(data.len() as i32)?;
+        defer! { self.abi_free.call((ptr, data.len() as i32)).unwrap(); }
+        unsafe { memory.slice_mut(ptr, data.len() as i32, |s| s.copy_from_slice(data)) };
+
+        let res = func.call((data.len() as i32, ptr))?;
+        let (len, res) = ((res >> 32) as i32, (res & 0xFFFFFFFF) as i32);
+        defer! { self.abi_free.call((res, len)).unwrap(); }
+
+        let res_data = unsafe { memory.slice(res, len, |s| f(s)) }?;
+        Ok(res_data)
+    }
+}
+
+/// A Wasmtime [`Store`] with [`Linker`].
+pub struct WasmtimeStoreLinker {
+    engine: Engine,
+    store: HostStore,
+    linker: Linker<WasiCtx>,
+}
+
+impl WasmtimeStoreLinker {
+    fn preopen_root(root_path: impl AsRef<Path>) -> Result<Dir> {
+        let mut options = std::fs::OpenOptions::new();
+        options.read(true);
+        #[cfg(windows)]
+        {
+            use std::os::windows::fs::OpenOptionsExt;
+            options.share_mode(3); // remove FILE_SHARE_DELETE
+            options.custom_flags(0x02000000); // open dir with FILE_FLAG_BACKUP_SEMANTICS
+        }
+        let root = options.open(root_path)?;
+        Ok(Dir::from_std_file(root))
+    }
+}
+
+impl StoreLinker<WasmtimeModule> for WasmtimeStoreLinker {
+    fn new(root_path: impl AsRef<Path>) -> Result<Self> {
+        let engine = Engine::default();
+        let wasi = WasiCtxBuilder::new()
+            .inherit_stdio()
+            .preopened_dir(Self::preopen_root(root_path)?, "/")?
+            .build();
+        let store = Store::new(&engine, wasi);
+        let mut linker = Linker::new(&engine);
+        add_to_linker(&mut linker, |ctx| ctx)?;
+        Ok(Self {
+            engine,
+            store: Arc::new(Mutex::new(store)),
+            linker,
+        })
+    }
+
+    fn create(&self, binary: &[u8]) -> Result<WasmtimeModule> {
+        let module = Module::new(&self.engine, binary)?;
+        let host = WasmtimeModule::new(self.store.clone(), &module, &self.linker)?;
+        Ok(host)
+    }
+
+    fn import(&mut self, ns: impl Into<String>, funcs: HashMap<String, Func>) -> Result<()> {
+        let ns = ns.into();
+        for (name, func) in funcs {
+            self.linker.define(&ns, &name, func)?;
+        }
+        Ok(())
+    }
+
+    fn wrap(&self, f: impl Fn() + Send + Sync + 'static) -> Func {
+        Func::wrap(self.store.lock().unwrap().as_context_mut(), f)
+    }
+
+    fn wrap_with_args_raw(
+        &self,
+        f: impl (Fn(&[u8]) -> Result<()>) + Send + Sync + 'static,
+    ) -> Func {
+        Func::wrap(
+            self.store.lock().unwrap().as_context_mut(),
+            move |mut store: Caller<WasiCtx>, len: i32, data: i32| unsafe {
+                let memory = store
+                    .get_export(MEMORY_NAME)
+                    .unwrap()
+                    .into_memory()
+                    .unwrap();
+                let data = mem_slice(store.as_context(), &memory, data, len);
+                f(data).map_err(|e| Trap::new(e.to_string()))?;
+                Ok(())
+            },
+        )
+    }
+}
diff --git a/utils/ayaka-plugin/Cargo.toml b/utils/ayaka-plugin/Cargo.toml
new file mode 100644
index 00000000..6f41f402
--- /dev/null
+++ b/utils/ayaka-plugin/Cargo.toml
@@ -0,0 +1,9 @@
+[package]
+name = "ayaka-plugin"
+version = "0.1.0"
+edition = "2021"
+
+[dependencies]
+serde = "1.0"
+anyhow = "1.0"
+rmp-serde = "1.1"
diff --git a/utils/ayaka-plugin/src/lib.rs b/utils/ayaka-plugin/src/lib.rs
new file mode 100644
index 00000000..2e76fc55
--- /dev/null
+++ b/utils/ayaka-plugin/src/lib.rs
@@ -0,0 +1,95 @@
+//! Base crate for plugin runtimes.
+//!
+//! This crate provides abstract types and traits
+//! for different plugin backends.
+
+#![feature(fn_traits)]
+#![feature(tuple_trait)]
+#![feature(unboxed_closures)]
+#![warn(missing_docs)]
+#![deny(unsafe_code)]
+
+pub use anyhow::Result;
+
+use serde::{de::DeserializeOwned, Serialize};
+use std::{collections::HashMap, marker::Tuple, path::Path};
+
+/// The exported method `__abi_alloc`.
+pub const ABI_ALLOC_NAME: &str = "__abi_alloc";
+/// The exported method `__abi_free`.
+pub const ABI_FREE_NAME: &str = "__abi_free";
+/// The default exported memory name.
+pub const MEMORY_NAME: &str = "memory";
+
+/// Represents a raw plugin module.
+pub trait RawModule: Sized {
+    /// The linker type that can create raw module.
+    type Linker: StoreLinker<Self>;
+
+    /// The import function type.
+    type Func;
+
+    /// Calls a method by name.
+    ///
+    /// The args and returns are bytes.
+    fn call<T>(&self, name: &str, args: &[u8], f: impl FnOnce(&[u8]) -> Result<T>) -> Result<T>;
+}
+
+/// High-level wrapper for plugin module.
+pub struct PluginModule<M: RawModule> {
+    module: M,
+}
+
+impl<M: RawModule> PluginModule<M> {
+    /// Creates a wrapper on raw module.
+    pub fn new(module: M) -> Self {
+        Self { module }
+    }
+
+    /// Call a method by name.
+    ///
+    /// The args and returns are passed by MessagePack with [`rmp_serde`].
+    pub fn call<P: Serialize, R: DeserializeOwned>(&self, name: &str, args: P) -> Result<R> {
+        let data = rmp_serde::to_vec(&args)?;
+        self.module.call(name, &data, |res| {
+            let res = rmp_serde::from_slice(res)?;
+            Ok(res)
+        })
+    }
+}
+
+/// Represents the store & linker of plugin modules.
+pub trait StoreLinker<M: RawModule>: Sized {
+    /// Creates a new instance of [`StoreLinker`].
+    ///
+    /// The `root_path` is used to preopen the root dir,
+    /// and mapped to `/`.
+    fn new(root_path: impl AsRef<Path>) -> Result<Self>;
+
+    /// Create a raw module from binary.
+    fn create(&self, binary: &[u8]) -> Result<M>;
+
+    /// Import functions by namespace and names.
+    fn import(&mut self, ns: impl Into<String>, funcs: HashMap<String, M::Func>) -> Result<()>;
+
+    /// Wrap a simple function.
+    fn wrap(&self, f: impl Fn() + Send + Sync + 'static) -> M::Func;
+
+    /// Wrap a function with args in bytes.
+    fn wrap_with_args_raw(
+        &self,
+        f: impl (Fn(&[u8]) -> Result<()>) + Send + Sync + 'static,
+    ) -> M::Func;
+
+    /// Wrap a function with args.
+    fn wrap_with_args<Params: DeserializeOwned + Tuple>(
+        &self,
+        f: impl Fn<Params, Output = ()> + Send + Sync + 'static,
+    ) -> M::Func {
+        self.wrap_with_args_raw(move |data| {
+            let data = rmp_serde::from_slice(data)?;
+            f.call(data);
+            Ok(())
+        })
+    }
+}
diff --git a/utils/ayaka-runtime/Cargo.toml b/utils/ayaka-runtime/Cargo.toml
index 58c43974..5a252f7e 100644
--- a/utils/ayaka-runtime/Cargo.toml
+++ b/utils/ayaka-runtime/Cargo.toml
@@ -6,6 +6,7 @@ edition = "2021"
 [dependencies]
 ayaka-bindings-types = { path = "../ayaka-bindings-types" }
 ayaka-script = { path = "../ayaka-script" }
+ayaka-plugin = { path = "../ayaka-plugin" }
 fallback = "0.1"
 language-matcher = "0.1"
 icu_locid = { version = "1.0.0", features = ["std"] }
@@ -13,17 +14,30 @@ sys-locale = "0.2"
 serde = { version = "1.0", features = ["derive"] }
 serde_yaml = "0.9"
 serde_json = "1.0"
-rmp-serde = "1.1"
 anyhow = "1.0"
-wasmer = "2.3"
-wasmer-wasi = "2.3"
-tokio = { version = "1", features = ["macros", "sync"] }
 stream-future = "0.3"
 futures-util = "0.3"
 tryiterator = { git = "https://github.com/Pernosco/tryiterator.git" }
 dirs = "4.0"
-scopeguard = "1.1"
 trylog = "0.2"
+cfg-if = "1.0"
+
+ayaka-plugin-nop = { path = "../ayaka-plugin-nop" }
+ayaka-plugin-wasmi = { path = "../ayaka-plugin-wasmi", optional = true }
+ayaka-plugin-wasmtime = { path = "../ayaka-plugin-wasmtime", optional = true }
+ayaka-plugin-wasmer = { path = "../ayaka-plugin-wasmer", optional = true }
 
 [dev-dependencies]
-tokio = { version = "1", features = ["parking_lot", "rt"] }
+tokio = { version = "1", features = ["macros", "parking_lot", "rt"] }
+generic-tests = "0.1"
+
+[features]
+default = ["wasmi"]
+wasmi = ["ayaka-plugin-wasmi"]
+wasmtime = ["ayaka-plugin-wasmtime"]
+wasmer = ["ayaka-plugin-wasmer"]
+
+[[test]]
+name = "runtime"
+path = "tests/runtime.rs"
+required-features = ["wasmi", "wasmtime", "wasmer"]
diff --git a/utils/ayaka-runtime/src/context.rs b/utils/ayaka-runtime/src/context.rs
index 28d974f2..04daff3f 100644
--- a/utils/ayaka-runtime/src/context.rs
+++ b/utils/ayaka-runtime/src/context.rs
@@ -8,10 +8,7 @@ use ayaka_script::{Command, Line, Text};
 use fallback::Fallback;
 use log::error;
 use script::*;
-use std::{
-    collections::HashMap,
-    path::{Path, PathBuf},
-};
+use std::{collections::HashMap, path::Path};
 use stream_future::stream;
 use trylog::TryLog;
 
@@ -20,7 +17,6 @@ pub struct Context {
     /// The inner [`Game`] object.
     pub game: Game,
     frontend: FrontendType,
-    root_path: PathBuf,
     runtime: Runtime,
     /// The inner raw context.
     pub ctx: RawContext,
@@ -58,9 +54,8 @@ impl Context {
             .as_ref()
             .parent()
             .ok_or_else(|| anyhow!("Cannot get parent from input path."))?;
-        let root_path = std::path::absolute(root_path)?;
         let runtime = {
-            let runtime = Runtime::load(&config.plugins.dir, &root_path, &config.plugins.modules);
+            let runtime = Runtime::load(&config.plugins.dir, root_path, &config.plugins.modules);
             pin_mut!(runtime);
             while let Some(load_status) = runtime.next().await {
                 match load_status {
@@ -74,12 +69,10 @@ impl Context {
         };
 
         yield OpenStatus::GamePlugin;
-        for m in &runtime.game_modules {
-            let module = &runtime.modules[m];
+        for module in runtime.game_modules() {
             let ctx = GameProcessContextRef {
                 title: &config.title,
                 author: &config.author,
-                root_path: &root_path,
                 props: &config.props,
             };
             let res = module.process_game(ctx)?;
@@ -138,7 +131,6 @@ impl Context {
         Ok(Self {
             game: Game { config, paras, res },
             frontend,
-            root_path,
             runtime,
             ctx: RawContext::default(),
             record: ActionRecord::default(),
@@ -227,13 +219,12 @@ impl Context {
                         }
                     }
                     Command::Other(cmd, args) => {
-                        if let Some(module) = self.runtime.text_modules.get(cmd) {
+                        if let Some(module) = self.runtime.text_module(cmd) {
                             let ctx = TextProcessContextRef {
-                                root_path: &self.root_path,
                                 game_props: &self.game.config.props,
                                 frontend: self.frontend,
                             };
-                            let res = self.runtime.modules[module].dispatch_text(cmd, args, ctx)?;
+                            let res = module.dispatch_text(cmd, args, ctx)?;
                             action.text.extend(res.text.text);
                             action.vars.extend(res.text.vars);
                         }
@@ -275,11 +266,8 @@ impl Context {
                 self.vars.clear();
                 let cmd = props.iter().next().map(|(key, _)| key);
                 if let Some(cmd) = cmd {
-                    let module = self.runtime.line_modules.get(cmd);
-                    if let Some(module) = module {
-                        let module = &self.runtime.modules[module];
+                    if let Some(module) = self.runtime.line_module(cmd) {
                         let ctx = LineProcessContextRef {
-                            root_path: &self.root_path,
                             game_props: &self.game.config.props,
                             frontend: self.frontend,
                             ctx: &self.ctx,
@@ -328,10 +316,8 @@ impl Context {
     }
 
     fn process_action_text(&self, ctx: &RawContext, action: &mut ActionText) -> Result<()> {
-        for action_module in &self.runtime.action_modules {
-            let module = &self.runtime.modules[action_module];
+        for module in self.runtime.action_modules() {
             let ctx = ActionProcessContextRef {
-                root_path: &self.root_path,
                 game_props: &self.game.config.props,
                 frontend: self.frontend,
                 ctx,
diff --git a/utils/ayaka-runtime/src/lib.rs b/utils/ayaka-runtime/src/lib.rs
index 37dc5aed..aca7aa3a 100644
--- a/utils/ayaka-runtime/src/lib.rs
+++ b/utils/ayaka-runtime/src/lib.rs
@@ -6,11 +6,10 @@
 
 #![warn(missing_docs)]
 #![deny(unsafe_code)]
-#![feature(absolute_path)]
 #![feature(fn_traits)]
 #![feature(generators)]
 #![feature(once_cell)]
-#![feature(tuple_trait)]
+#![feature(type_alias_impl_trait)]
 #![feature(unboxed_closures)]
 
 mod config;
diff --git a/utils/ayaka-runtime/src/plugin.rs b/utils/ayaka-runtime/src/plugin.rs
index 7ea16d1b..ba418e70 100644
--- a/utils/ayaka-runtime/src/plugin.rs
+++ b/utils/ayaka-runtime/src/plugin.rs
@@ -1,99 +1,38 @@
 //! The plugin utilities.
 
-#![allow(unsafe_code)]
-#![allow(clippy::mut_from_ref)]
-
 use crate::*;
 use anyhow::Result;
-use ayaka_bindings_types::*;
-use log::warn;
-use scopeguard::defer;
-use serde::{de::DeserializeOwned, Serialize};
-use std::{collections::HashMap, marker::Tuple, path::Path};
+use ayaka_plugin::*;
+use std::{collections::HashMap, path::Path};
 use stream_future::stream;
 use tryiterator::TryIteratorExt;
-use wasmer::*;
-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>,
-}
+use trylog::TryLog;
 
-unsafe fn mem_slice(memory: &Memory, start: i32, len: i32) -> &[u8] {
-    memory
-        .data_unchecked()
-        .get_unchecked(start as usize..)
-        .get_unchecked(..len as usize)
+/// The plugin module with high-level interfaces.
+pub struct Module<M: RawModule = BackendModule> {
+    module: PluginModule<M>,
 }
 
-unsafe fn mem_slice_mut(memory: &Memory, start: i32, len: i32) -> &mut [u8] {
-    memory
-        .data_unchecked_mut()
-        .get_unchecked_mut(start as usize..)
-        .get_unchecked_mut(..len as usize)
-}
-
-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,
-        })
-    }
-
-    /// Calls a method by name.
-    ///
-    /// The args and returns are passed by MessagePack with [`rmp_serde`].
-    pub fn call<Params: Serialize, Res: DeserializeOwned>(
-        &self,
-        name: &str,
-        args: Params,
-    ) -> Result<Res> {
-        let memory = &self.memory;
-        let func = self
-            .instance
-            .exports
-            .get_native_function::<(i32, i32), u64>(name)?;
-
-        let data = rmp_serde::to_vec(&args)?;
-
-        let ptr = self.abi_alloc.call(data.len() as i32)?;
-        defer! { self.abi_free.call(ptr, data.len() as i32).unwrap(); }
-        unsafe { mem_slice_mut(memory, ptr, data.len() as i32) }.copy_from_slice(&data);
-
-        let res = func.call(data.len() as i32, ptr)?;
-        let (len, res) = ((res >> 32) as i32, (res & 0xFFFFFFFF) as i32);
-        defer! { self.abi_free.call(res, len).unwrap(); }
-
-        let res_data = unsafe { mem_slice(memory, res, len) };
-        let res_data = rmp_serde::from_slice(res_data)?;
-        Ok(res_data)
+impl<M: RawModule> Module<M> {
+    fn new(module: M) -> Self {
+        Self {
+            module: PluginModule::new(module),
+        }
     }
 
     /// Calls a script plugin method by name.
     pub fn dispatch_method(&self, name: &str, args: &[RawValue]) -> Result<RawValue> {
-        self.call(name, (args,))
+        self.module.call(name, (args,))
     }
 
     /// Gets the [`PluginType`].
     pub fn plugin_type(&self) -> Result<PluginType> {
-        self.call("plugin_type", ())
+        self.module.call("plugin_type", ())
     }
 
     /// Processes [`Action`] in action plugin.
     pub fn process_action(&self, ctx: ActionProcessContextRef) -> Result<ActionProcessResult> {
-        self.call("process_action", (ctx,))
+        self.module.call("process_action", (ctx,))
     }
 
     /// Calls a custom command in the text plugin.
@@ -103,7 +42,7 @@ impl Host {
         args: &[String],
         ctx: TextProcessContextRef,
     ) -> Result<TextProcessResult> {
-        self.call(name, (args, ctx))
+        self.module.call(name, (args, ctx))
     }
 
     /// Calls a custom command in the line plugin.
@@ -112,27 +51,22 @@ impl Host {
         name: &str,
         ctx: LineProcessContextRef,
     ) -> Result<LineProcessResult> {
-        self.call(name, (ctx,))
+        self.module.call(name, (ctx,))
     }
 
     /// Processes [`Game`] when opening the config file.
     pub fn process_game(&self, ctx: GameProcessContextRef) -> Result<GameProcessResult> {
-        self.call("process_game", (ctx,))
+        self.module.call("process_game", (ctx,))
     }
 }
 
 /// The plugin runtime.
-pub struct Runtime {
-    /// The plugins map by name.
-    pub modules: HashMap<String, Host>,
-    /// The action plugins.
-    pub action_modules: Vec<String>,
-    /// The text plugins by command name.
-    pub text_modules: HashMap<String, String>,
-    /// The line plugins by command name.
-    pub line_modules: HashMap<String, String>,
-    /// The game plugins.
-    pub game_modules: Vec<String>,
+pub struct Runtime<M: RawModule = BackendModule> {
+    modules: HashMap<String, Module<M>>,
+    action_modules: Vec<String>,
+    text_modules: HashMap<String, String>,
+    line_modules: HashMap<String, String>,
+    game_modules: Vec<String>,
 }
 
 /// The load status of [`Runtime`].
@@ -144,66 +78,36 @@ pub enum LoadStatus {
     LoadPlugin(String, usize, usize),
 }
 
-#[derive(Default, Clone, WasmerEnv)]
-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 + Tuple, Res: Serialize>(
-        &self,
-        len: i32,
-        data: i32,
-        f: impl FnOnce<Params, Output = Res>,
-    ) -> std::result::Result<u64, RuntimeError> {
-        let memory = self.memory.get_unchecked();
-        let data = mem_slice(memory, data, len);
-        let data = rmp_serde::from_slice(data).map_err(|e| RuntimeError::new(e.to_string()))?;
-        let res = f.call_once(data);
-        let data = rmp_serde::to_vec(&res).map_err(|e| RuntimeError::new(e.to_string()))?;
-        let alloc = self.alloc.get_unchecked();
-        let ptr = alloc.call(data.len() as _)?;
-        mem_slice_mut(memory, ptr, data.len() as _).copy_from_slice(&data);
-        Ok(((data.len() as u64) << 32) | (ptr as u64))
-    }
-}
-
-impl Runtime {
-    fn imports(store: &Store) -> Result<Box<dyn NamedResolver + Send + Sync>> {
-        let log_func = Function::new_native_with_env(
-            store,
-            RuntimeInstanceData::default(),
-            |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 import_object = imports! {
-            "log" => {
-                "__log" => log_func,
-                "__log_flush" => log_flush_func,
-            }
-        };
-        let wasi_env = WasiState::new("ayaka-runtime")
-            .preopen_dir("/")?
-            .finalize()?;
-        let wasi_import = generate_import_object_from_env(store, wasi_env, WasiVersion::Latest);
-        Ok(Box::new(import_object.chain_front(wasi_import)))
+impl<M: RawModule> Runtime<M> {
+    fn new_linker(root_path: impl AsRef<Path>) -> Result<M::Linker> {
+        let mut store = M::Linker::new(root_path)?;
+        let log_func = store.wrap_with_args(|data: Record| {
+            let module_path = format!(
+                "{}::<plugin>::{}",
+                module_path!(),
+                data.module_path.unwrap_or_default()
+            );
+            let target = format!("{}::<plugin>::{}", module_path!(), data.target);
+            log::logger().log(
+                &log::Record::builder()
+                    .level(data.level)
+                    .target(&target)
+                    .args(format_args!("{}", data.msg))
+                    .module_path(Some(&module_path))
+                    .file(data.file.as_deref())
+                    .line(data.line)
+                    .build(),
+            )
+        });
+        let log_flush_func = store.wrap(|| log::logger().flush());
+        store.import(
+            "log",
+            HashMap::from([
+                ("__log".to_string(), log_func),
+                ("__log_flush".to_string(), log_flush_func),
+            ]),
+        )?;
+        Ok(store)
     }
 
     /// Load plugins from specific directory and plugin names.
@@ -217,15 +121,11 @@ impl Runtime {
         rel_to: impl AsRef<Path> + 'a,
         names: &'a [impl AsRef<str>],
     ) -> Result<Self> {
-        let path = rel_to.as_ref().join(dir);
+        let root_path = rel_to.as_ref();
+        let path = root_path.join(dir);
         yield LoadStatus::CreateEngine;
-        let store = Store::default();
-        let import_object = Self::imports(&store)?;
-        let mut modules = HashMap::new();
-        let mut action_modules = vec![];
-        let mut text_modules = HashMap::new();
-        let mut line_modules = HashMap::new();
-        let mut game_modules = vec![];
+        let store = Self::new_linker(root_path)?;
+        let mut runtime = Self::new();
         let paths = if names.is_empty() {
             std::fs::read_dir(path)?
                 .try_filter_map(|f| {
@@ -260,41 +160,102 @@ impl Runtime {
         for (i, (name, p)) in paths.into_iter().enumerate() {
             yield LoadStatus::LoadPlugin(name.clone(), i, total_len);
             let buf = std::fs::read(p)?;
-            let module = Module::from_binary(&store, &buf)?;
-            let runtime = Host::new(&module, &import_object)?;
-            let plugin_type = runtime.plugin_type()?;
-            if plugin_type.action {
-                action_modules.push(name.clone());
-            }
-            for cmd in plugin_type.text {
-                let res = text_modules.insert(cmd.clone(), name.clone());
-                if let Some(old_module) = res {
-                    warn!(
-                        "Command `{}` is overrided by \"{}\" over \"{}\"",
-                        cmd, name, old_module
-                    );
-                }
-            }
-            for cmd in plugin_type.line {
-                let res = line_modules.insert(cmd.clone(), name.clone());
-                if let Some(old_module) = res {
-                    warn!(
-                        "Command `{}` is overrided by \"{}\" over \"{}\"",
-                        cmd, name, old_module
-                    );
-                }
+            let module = Module::new(store.create(&buf)?);
+            runtime.insert_module(name, module)?;
+        }
+        Ok(runtime)
+    }
+
+    fn new() -> Self {
+        Self {
+            modules: HashMap::default(),
+            action_modules: vec![],
+            text_modules: HashMap::default(),
+            line_modules: HashMap::default(),
+            game_modules: vec![],
+        }
+    }
+
+    fn insert_module(&mut self, name: String, module: Module<M>) -> Result<()> {
+        let plugin_type = module
+            .plugin_type()
+            .unwrap_or_default_log("Cannot determine module type");
+        if plugin_type.action {
+            self.action_modules.push(name.clone());
+        }
+        for cmd in plugin_type.text {
+            let res = self.text_modules.insert(cmd.clone(), name.clone());
+            if let Some(old_module) = res {
+                log::warn!(
+                    "Text command `{}` is overrided by \"{}\" over \"{}\"",
+                    cmd,
+                    name,
+                    old_module
+                );
             }
-            if plugin_type.game {
-                game_modules.push(name.clone());
+        }
+        for cmd in plugin_type.line {
+            let res = self.line_modules.insert(cmd.clone(), name.clone());
+            if let Some(old_module) = res {
+                log::warn!(
+                    "Line command `{}` is overrided by \"{}\" over \"{}\"",
+                    cmd,
+                    name,
+                    old_module
+                );
             }
-            modules.insert(name, runtime);
         }
-        Ok(Self {
-            modules,
-            action_modules,
-            text_modules,
-            line_modules,
-            game_modules,
-        })
+        if plugin_type.game {
+            self.game_modules.push(name.clone());
+        }
+        self.modules.insert(name, module);
+        Ok(())
+    }
+
+    /// Gets module from name.
+    pub fn module(&self, key: &str) -> Option<&Module<M>> {
+        self.modules.get(key)
+    }
+
+    /// Iterates action modules.
+    pub fn action_modules(&self) -> impl Iterator<Item = &Module<M>> {
+        self.action_modules
+            .iter()
+            .map(|key| self.module(key).unwrap())
+    }
+
+    /// Gets text module from command.
+    pub fn text_module(&self, cmd: &str) -> Option<&Module<M>> {
+        self.text_modules.get(cmd).and_then(|key| self.module(key))
+    }
+
+    /// Gets line module from command.
+    pub fn line_module(&self, cmd: &str) -> Option<&Module<M>> {
+        self.line_modules.get(cmd).and_then(|key| self.module(key))
+    }
+
+    /// Iterates game modules.
+    pub fn game_modules(&self) -> impl Iterator<Item = &Module<M>> {
+        self.game_modules
+            .iter()
+            .map(|key| self.module(key).unwrap())
+    }
+}
+
+#[doc(hidden)]
+pub use backend::BackendModule;
+
+#[doc(hidden)]
+mod backend {
+    cfg_if::cfg_if! {
+        if #[cfg(feature = "wasmi")] {
+            pub use ayaka_plugin_wasmi::WasmiModule as BackendModule;
+        } else if #[cfg(feature = "wasmtime")] {
+            pub use ayaka_plugin_wasmtime::WasmtimeModule as BackendModule;
+        } else if #[cfg(feature = "wasmer")] {
+            pub use ayaka_plugin_wasmer::WasmerModule as BackendModule;
+        } else {
+            pub use ayaka_plugin_nop::NopModule as BackendModule;
+        }
     }
 }
diff --git a/utils/ayaka-runtime/src/script.rs b/utils/ayaka-runtime/src/script.rs
index 7976c681..15ae30d7 100644
--- a/utils/ayaka-runtime/src/script.rs
+++ b/utils/ayaka-runtime/src/script.rs
@@ -1,24 +1,25 @@
 //! The script interpreter.
 
-use crate::plugin::Runtime;
+use crate::plugin::{BackendModule, Runtime};
 use ayaka_bindings_types::VarMap;
+use ayaka_plugin::RawModule;
 use ayaka_script::*;
 use log::warn;
 use trylog::TryLog;
 
 /// The variable table in scripts.
-pub struct VarTable<'a> {
+pub struct VarTable<'a, M: RawModule = BackendModule> {
     /// The plugin runtime.
-    pub runtime: &'a Runtime,
+    pub runtime: &'a Runtime<M>,
     /// The context variables.
     pub locals: &'a mut VarMap,
     /// The locale variables.
     pub vars: VarMap,
 }
 
-impl<'a> VarTable<'a> {
+impl<'a, M: RawModule> VarTable<'a, M> {
     /// Creates a new [`VarTable`].
-    pub fn new(runtime: &'a Runtime, locals: &'a mut VarMap) -> Self {
+    pub fn new(runtime: &'a Runtime<M>, locals: &'a mut VarMap) -> Self {
         Self {
             runtime,
             locals,
@@ -35,17 +36,17 @@ impl<'a> VarTable<'a> {
 /// Represents a callable part of a script.
 pub trait Callable {
     /// Calls the part with the [`VarTable`].
-    fn call(&self, ctx: &mut VarTable) -> RawValue;
+    fn call<M: RawModule>(&self, ctx: &mut VarTable<M>) -> RawValue;
 }
 
 impl<T: Callable> Callable for &T {
-    fn call(&self, ctx: &mut VarTable) -> RawValue {
+    fn call<M: RawModule>(&self, ctx: &mut VarTable<M>) -> RawValue {
         (*self).call(ctx)
     }
 }
 
 impl<T: Callable> Callable for Option<T> {
-    fn call(&self, ctx: &mut VarTable) -> RawValue {
+    fn call<M: RawModule>(&self, ctx: &mut VarTable<M>) -> RawValue {
         match self {
             Some(c) => c.call(ctx),
             None => RawValue::Unit,
@@ -54,7 +55,7 @@ impl<T: Callable> Callable for Option<T> {
 }
 
 impl Callable for Program {
-    fn call(&self, ctx: &mut VarTable) -> RawValue {
+    fn call<M: RawModule>(&self, ctx: &mut VarTable<M>) -> RawValue {
         ctx.vars.clear();
         let mut res = RawValue::Unit;
         for expr in &self.0 {
@@ -65,7 +66,7 @@ impl Callable for Program {
 }
 
 impl Callable for Expr {
-    fn call(&self, ctx: &mut VarTable) -> RawValue {
+    fn call<M: RawModule>(&self, ctx: &mut VarTable<M>) -> RawValue {
         match self {
             Self::Ref(r) => r.call(ctx),
             Self::Const(c) => c.clone(),
@@ -96,7 +97,12 @@ impl Callable for Expr {
     }
 }
 
-fn bin_val(ctx: &mut VarTable, lhs: &Expr, op: &ValBinaryOp, rhs: &Expr) -> RawValue {
+fn bin_val<M: RawModule>(
+    ctx: &mut VarTable<M>,
+    lhs: &Expr,
+    op: &ValBinaryOp,
+    rhs: &Expr,
+) -> RawValue {
     let lhs = lhs.call(ctx);
     let rhs = rhs.call(ctx);
     let t = lhs.get_type().max(rhs.get_type());
@@ -154,7 +160,12 @@ fn bin_str_val(lhs: RawValue, op: &ValBinaryOp, rhs: RawValue) -> RawValue {
     }
 }
 
-fn bin_logic(ctx: &mut VarTable, lhs: &Expr, op: &LogicBinaryOp, rhs: &Expr) -> RawValue {
+fn bin_logic<M: RawModule>(
+    ctx: &mut VarTable<M>,
+    lhs: &Expr,
+    op: &LogicBinaryOp,
+    rhs: &Expr,
+) -> RawValue {
     let res = match op {
         LogicBinaryOp::And => lhs.call(ctx).get_bool() && rhs.call(ctx).get_bool(),
         LogicBinaryOp::Or => lhs.call(ctx).get_bool() || rhs.call(ctx).get_bool(),
@@ -185,7 +196,7 @@ fn bin_ord_logic<T: Ord>(lhs: &T, op: &LogicBinaryOp, rhs: &T) -> bool {
     }
 }
 
-fn assign(ctx: &mut VarTable, e: &Expr, val: RawValue) -> RawValue {
+fn assign<M: RawModule>(ctx: &mut VarTable<M>, e: &Expr, val: RawValue) -> RawValue {
     match e {
         Expr::Ref(r) => match r {
             Ref::Var(n) => ctx.vars.insert(n.into(), val),
@@ -196,7 +207,7 @@ fn assign(ctx: &mut VarTable, e: &Expr, val: RawValue) -> RawValue {
     RawValue::Unit
 }
 
-fn call(ctx: &mut VarTable, ns: &str, name: &str, args: &[Expr]) -> RawValue {
+fn call<M: RawModule>(ctx: &mut VarTable<M>, ns: &str, name: &str, args: &[Expr]) -> RawValue {
     if ns.is_empty() {
         match name {
             "if" => if args.get(0).call(ctx).get_bool() {
@@ -210,8 +221,7 @@ fn call(ctx: &mut VarTable, ns: &str, name: &str, args: &[Expr]) -> RawValue {
     } else {
         let args = args.iter().map(|e| e.call(ctx)).collect::<Vec<_>>();
         ctx.runtime
-            .modules
-            .get(ns)
+            .module(ns)
             .map(|runtime| {
                 runtime
                     .dispatch_method(name, &args)
@@ -222,7 +232,7 @@ fn call(ctx: &mut VarTable, ns: &str, name: &str, args: &[Expr]) -> RawValue {
 }
 
 impl Callable for Ref {
-    fn call(&self, ctx: &mut VarTable) -> RawValue {
+    fn call<M: RawModule>(&self, ctx: &mut VarTable<M>) -> RawValue {
         match self {
             Self::Var(n) => ctx
                 .vars
@@ -239,7 +249,7 @@ impl Callable for Ref {
 }
 
 impl Callable for Text {
-    fn call(&self, ctx: &mut VarTable) -> RawValue {
+    fn call<M: RawModule>(&self, ctx: &mut VarTable<M>) -> RawValue {
         let mut str = String::new();
         for line in &self.0 {
             match line {
@@ -264,122 +274,3 @@ impl Callable for Text {
         RawValue::Str(str.trim().to_string())
     }
 }
-
-#[cfg(test)]
-mod test {
-    use crate::{plugin::Runtime, script::*};
-    use tokio::sync::OnceCell;
-
-    static RUNTIME: OnceCell<Runtime> = OnceCell::const_new();
-
-    async fn with_ctx(f: impl FnOnce(&mut VarTable)) {
-        let runtime = RUNTIME
-            .get_or_init(|| async {
-                let runtime = Runtime::load(
-                    "../../examples/plugins",
-                    env!("CARGO_MANIFEST_DIR"),
-                    &["random"],
-                );
-                runtime.await.unwrap()
-            })
-            .await;
-        let mut locals = VarMap::default();
-        let mut ctx = VarTable::new(runtime, &mut locals);
-        f(&mut ctx);
-    }
-
-    #[tokio::test]
-    async fn vars() {
-        with_ctx(|ctx| {
-            assert_eq!(
-                ProgramParser::new()
-                    .parse(
-                        "
-                            a = 0;
-                            a += 1;
-                            a += a;
-                            a
-                        "
-                    )
-                    .ok()
-                    .call(ctx),
-                RawValue::Num(2)
-            );
-
-            assert_eq!(
-                ProgramParser::new().parse("a").ok().call(ctx),
-                RawValue::Unit
-            );
-
-            assert_eq!(
-                ProgramParser::new()
-                    .parse(
-                        "
-                            $a = 0;
-                            $a += 1;
-                            $a += a;
-                            $a
-                        "
-                    )
-                    .ok()
-                    .call(ctx),
-                RawValue::Num(1)
-            );
-
-            assert_eq!(
-                ProgramParser::new().parse("$a").ok().call(ctx),
-                RawValue::Num(1)
-            );
-        })
-        .await;
-    }
-
-    #[tokio::test]
-    async fn if_test() {
-        with_ctx(|ctx| {
-            assert_eq!(
-                ProgramParser::new()
-                    .parse(
-                        r##"
-                            if(1 + 1 + 4 + 5 + 1 + 4 == 16, "sodayo", ~)
-                        "##
-                    )
-                    .ok()
-                    .call(ctx)
-                    .get_num(),
-                6
-            );
-            assert_eq!(
-                ProgramParser::new()
-                    .parse(
-                        r##"
-                            if(true, "sodayo")
-                        "##
-                    )
-                    .ok()
-                    .call(ctx)
-                    .get_str(),
-                "sodayo"
-            );
-        })
-        .await;
-    }
-
-    #[tokio::test]
-    async fn random() {
-        with_ctx(|ctx| {
-            assert!((0..10).contains(
-                &ProgramParser::new()
-                    .parse(
-                        r##"
-                            random.rnd(10)
-                        "##
-                    )
-                    .ok()
-                    .call(ctx)
-                    .get_num()
-            ))
-        })
-        .await;
-    }
-}
diff --git a/utils/ayaka-script/src/line.rs b/utils/ayaka-script/src/line.rs
index ac9de954..acb9bd3f 100644
--- a/utils/ayaka-script/src/line.rs
+++ b/utils/ayaka-script/src/line.rs
@@ -127,11 +127,7 @@ mod test {
         );
         assert_eq!(
             lines[3],
-            Line::Custom({
-                let mut map = HashMap::new();
-                map.insert("video".to_string(), RawValue::Num(0));
-                map
-            })
+            Line::Custom(HashMap::from([("video".to_string(), RawValue::Num(0))]))
         );
         assert_eq!(lines[4], Line::Empty);
     }


Test patch:
diff --git a/utils/ayaka-runtime/tests/runtime.rs b/utils/ayaka-runtime/tests/runtime.rs
new file mode 100644
index 00000000..d6c5e985
--- /dev/null
+++ b/utils/ayaka-runtime/tests/runtime.rs
@@ -0,0 +1,127 @@
+use ayaka_plugin::RawModule;
+use ayaka_plugin_nop::NopModule;
+use ayaka_runtime::{plugin::Runtime, script::*, *};
+
+async fn with_ctx<M: RawModule>(f: impl FnOnce(&mut VarTable<M>)) {
+    let runtime = Runtime::load(
+        "../../examples/plugins",
+        env!("CARGO_MANIFEST_DIR"),
+        &["random"],
+    )
+    .await
+    .unwrap();
+    let mut locals = VarMap::default();
+    let mut ctx = VarTable::new(&runtime, &mut locals);
+    f(&mut ctx);
+}
+
+#[tokio::test]
+async fn vars() {
+    with_ctx::<NopModule>(|ctx| {
+        assert_eq!(
+            ProgramParser::new()
+                .parse(
+                    "
+                        a = 0;
+                        a += 1;
+                        a += a;
+                        a
+                    "
+                )
+                .ok()
+                .call(ctx),
+            RawValue::Num(2)
+        );
+
+        assert_eq!(
+            ProgramParser::new().parse("a").ok().call(ctx),
+            RawValue::Unit
+        );
+
+        assert_eq!(
+            ProgramParser::new()
+                .parse(
+                    "
+                        $a = 0;
+                        $a += 1;
+                        $a += a;
+                        $a
+                    "
+                )
+                .ok()
+                .call(ctx),
+            RawValue::Num(1)
+        );
+
+        assert_eq!(
+            ProgramParser::new().parse("$a").ok().call(ctx),
+            RawValue::Num(1)
+        );
+    })
+    .await;
+}
+
+#[tokio::test]
+async fn if_test() {
+    with_ctx::<NopModule>(|ctx| {
+        assert_eq!(
+            ProgramParser::new()
+                .parse(
+                    r##"
+                        if(1 + 1 + 4 + 5 + 1 + 4 == 16, "sodayo", ~)
+                    "##
+                )
+                .ok()
+                .call(ctx)
+                .get_num(),
+            6
+        );
+        assert_eq!(
+            ProgramParser::new()
+                .parse(
+                    r##"
+                        if(true, "sodayo")
+                    "##
+                )
+                .ok()
+                .call(ctx)
+                .get_str(),
+            "sodayo"
+        );
+    })
+    .await;
+}
+
+#[generic_tests::define(attrs(tokio::test))]
+mod runtime_tests {
+    use super::*;
+    use ayaka_plugin::RawModule;
+    use ayaka_plugin_wasmer::WasmerModule;
+    use ayaka_plugin_wasmi::WasmiModule;
+    use ayaka_plugin_wasmtime::WasmtimeModule;
+
+    #[tokio::test]
+    async fn random<M: RawModule>() {
+        with_ctx::<M>(|ctx| {
+            assert!((0..10).contains(
+                &ProgramParser::new()
+                    .parse(
+                        r##"
+                            random.rnd(10)
+                        "##
+                    )
+                    .ok()
+                    .call(ctx)
+                    .get_num()
+            ))
+        })
+        .await;
+    }
+
+    #[instantiate_tests(<WasmiModule>)]
+    mod inst_wasmi {}
+    #[instantiate_tests(<WasmtimeModule>)]
+    mod inst_wasmtime {}
+    #[instantiate_tests(<WasmerModule>)]
+    mod inst_wasmer {}
+}