* zellij-tile: Move `data` to zellij-utils The rationale behind this is that all components of zellij access the data structures defined in this module, as they define some of the most basic types in the application. However, so far zellij-tile is treated like a separate crate from the rest of the program in that it is the only one that doesn't have access to `zellij-utils`, which contains a lot of other data structures used throughout zellij. This poses issues as discussed in https://github.com/zellij-org/zellij/pull/1242 and is one of the reasons why the keybindings in the status bar default plugin can't be updated dynamically. It is also the main reason for why the keybindings are currently passed to the plugin as strings: The plugins only have access to `zellij-tile`, but since this is a dependency of `zellij-utils`, it can't import `zellij-utils` to access the keybindings. Other weird side-effect are that in some places `server` and `client` have to access the `zellij-tile` contents "through" `zellij-utils`, as in `use zellij_utils::zellij_tile::prelude::*`. By moving these central data structures to one common shared crate (`zellij-utils`), `zellij-tile` will be enabled to import `zellij-utils` like `screen` and `client` already do. This will, next to other things, allow dropping a lot of `std::fmt::Fmt` impls needed to convert core data structures into strings and as a consequence, a lot of string parsing in the first place. * utils: Integrate new `data` module, bump rust ver Integrates the `data` module that was previously part of `zellij-tile` to allow sharing the contained data structures between all components of zellij. This allows `zellij-tile` to use `utils` as a dependency. However, since `tile` is build against the wasm target, it cannot include all of `zellij-utils`, since a lot of dependencies there cannot compile with `wasm` as target (Examples include: termwiz, log4rs, async-std). Thus we make all the dependencies that cannot compile against `wasm` optional and introduce a new feature `full` that will compile the crate with all dependencies. Along with this, modify `lib.rs` to include most of the data structures only when compiling against the `full` feature. This makes the compiles of `zellij-tile` lighter, as it doesn't include all of `utils`. As a side effect, due to the dependency notation for the optional dependencies (See https://doc.rust-lang.org/cargo/reference/features.html#optional-dependencies), we bump the rust toolchain version to 1.60.0. * tile: Import `data` from zellij-utils Add `zellij-utils` as a dependency to `zellij-tile` and allow us access to the `data` module defined there. Update the re-export in the `prelude` such that from all of the plugins points of view *absolutely nothing changes*. * utils: Fix `data` module dependency Since the `data` module has been migrated from `zellij-tile` to `zellij-utils`, we import it from `zellij-utils` directly now. Also unify the imports for the `data` module members: We import all of the through `data::` now, not through a mixture of `data::` and `prelude::`. * client: Fix `data` module dependency Since the `data` module has been migrated from `zellij-tile` to `zellij-utils`, we import it from `zellij-utils` directly now. Also unify the imports for the `data` module members: We import all of the through `data::` now, not through a mixture of `data::` and `prelude::`. Add the "full" feature flag to the `zellij-utils` dependency so it includes all the components we need. * server: Fix `data` module dependency Since the `data` module has been migrated from `zellij-tile` to `zellij-utils`, we import it from `zellij-utils` directly now. Also unify the imports for the `data` module members: We import all of the through `data::` now, not through a mixture of `data::` and `prelude::`. Add the "full" feature flag to the `zellij-utils` dependency so it includes all the components we need. * tests: Fix `data` module dependency Since the `data` module has been migrated from `zellij-tile` to `zellij-utils`, we import it from `zellij-utils` directly now. * utils: Remove "full" feature in favor of conditional compilation using `target_family`. Replace the rust 1.60 method of specifying optional dependencies based on features and optionally include the dependencies only when not building for wasm instead. (I.e. `cfg(not(target_family = "wasm"))`) * cargo: Update module dependencies since `client`, `server` and `tile` now all depend on `utils` only.
484 lines
18 KiB
Rust
484 lines
18 KiB
Rust
use highway::{HighwayHash, PortableHash};
|
|
use log::{debug, info, warn};
|
|
use serde::{de::DeserializeOwned, Serialize};
|
|
use std::{
|
|
collections::{HashMap, HashSet},
|
|
fs,
|
|
path::{Path, PathBuf},
|
|
process,
|
|
str::FromStr,
|
|
sync::{mpsc::Sender, Arc, Mutex},
|
|
thread,
|
|
time::{Duration, Instant},
|
|
};
|
|
use url::Url;
|
|
use wasmer::{
|
|
imports, ChainableNamedResolver, Function, ImportObject, Instance, Module, Store, Value,
|
|
WasmerEnv,
|
|
};
|
|
use wasmer_wasi::{Pipe, WasiEnv, WasiState};
|
|
|
|
use crate::{
|
|
logging_pipe::LoggingPipe,
|
|
panes::PaneId,
|
|
pty::{ClientOrTabIndex, PtyInstruction},
|
|
screen::ScreenInstruction,
|
|
thread_bus::{Bus, ThreadSenders},
|
|
ClientId,
|
|
};
|
|
|
|
use zellij_utils::{
|
|
consts::{VERSION, ZELLIJ_CACHE_DIR, ZELLIJ_PROJ_DIR, ZELLIJ_TMP_DIR},
|
|
data::{Event, EventType, PluginIds},
|
|
errors::{ContextType, PluginContext},
|
|
};
|
|
use zellij_utils::{
|
|
input::command::TerminalAction,
|
|
input::layout::RunPlugin,
|
|
input::plugins::{PluginConfig, PluginType, PluginsConfig},
|
|
serde,
|
|
};
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub(crate) enum PluginInstruction {
|
|
Load(Sender<u32>, RunPlugin, usize, ClientId), // tx_pid, plugin metadata, tab_index, client_ids
|
|
Update(Option<u32>, Option<ClientId>, Event), // Focused plugin / broadcast, client_id, event data
|
|
Render(Sender<String>, u32, ClientId, usize, usize), // String buffer, plugin id, client_id, rows, cols
|
|
Unload(u32), // plugin_id
|
|
AddClient(ClientId),
|
|
RemoveClient(ClientId),
|
|
Exit,
|
|
}
|
|
|
|
impl From<&PluginInstruction> for PluginContext {
|
|
fn from(plugin_instruction: &PluginInstruction) -> Self {
|
|
match *plugin_instruction {
|
|
PluginInstruction::Load(..) => PluginContext::Load,
|
|
PluginInstruction::Update(..) => PluginContext::Update,
|
|
PluginInstruction::Render(..) => PluginContext::Render,
|
|
PluginInstruction::Unload(..) => PluginContext::Unload,
|
|
PluginInstruction::Exit => PluginContext::Exit,
|
|
PluginInstruction::AddClient(_) => PluginContext::AddClient,
|
|
PluginInstruction::RemoveClient(_) => PluginContext::RemoveClient,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(WasmerEnv, Clone)]
|
|
pub(crate) struct PluginEnv {
|
|
pub plugin_id: u32,
|
|
pub plugin: PluginConfig,
|
|
pub senders: ThreadSenders,
|
|
pub wasi_env: WasiEnv,
|
|
pub subscriptions: Arc<Mutex<HashSet<EventType>>>,
|
|
pub tab_index: usize,
|
|
pub client_id: ClientId,
|
|
#[allow(dead_code)]
|
|
plugin_own_data_dir: PathBuf,
|
|
}
|
|
|
|
// Thread main --------------------------------------------------------------------------------------------------------
|
|
pub(crate) fn wasm_thread_main(
|
|
bus: Bus<PluginInstruction>,
|
|
store: Store,
|
|
data_dir: PathBuf,
|
|
plugins: PluginsConfig,
|
|
) {
|
|
info!("Wasm main thread starts");
|
|
|
|
let mut plugin_id = 0;
|
|
let mut headless_plugins = HashMap::new();
|
|
let mut plugin_map: HashMap<(u32, ClientId), (Instance, PluginEnv)> = HashMap::new(); // u32 => pid
|
|
let mut connected_clients: Vec<ClientId> = vec![];
|
|
let plugin_dir = data_dir.join("plugins/");
|
|
let plugin_global_data_dir = plugin_dir.join("data");
|
|
|
|
#[cfg(not(feature = "disable_automatic_asset_installation"))]
|
|
fs::create_dir_all(&plugin_global_data_dir).unwrap_or_else(|e| log::error!("{:?}", e));
|
|
|
|
loop {
|
|
let (event, mut err_ctx) = bus.recv().expect("failed to receive event on channel");
|
|
err_ctx.add_call(ContextType::Plugin((&event).into()));
|
|
match event {
|
|
PluginInstruction::Load(pid_tx, run, tab_index, client_id) => {
|
|
let plugin = plugins
|
|
.get(&run)
|
|
.unwrap_or_else(|| panic!("Plugin {:?} could not be resolved", run));
|
|
|
|
let (instance, plugin_env) = start_plugin(
|
|
plugin_id, client_id, &plugin, tab_index, &bus, &store, &data_dir,
|
|
);
|
|
|
|
let mut main_user_instance = instance.clone();
|
|
let main_user_env = plugin_env.clone();
|
|
load_plugin(&mut main_user_instance);
|
|
|
|
plugin_map.insert((plugin_id, client_id), (main_user_instance, main_user_env));
|
|
|
|
// clone plugins for the rest of the client ids if they exist
|
|
for client_id in connected_clients.iter() {
|
|
let mut new_plugin_env = plugin_env.clone();
|
|
new_plugin_env.client_id = *client_id;
|
|
let module = instance.module().clone();
|
|
let wasi = new_plugin_env.wasi_env.import_object(&module).unwrap();
|
|
let zellij = zellij_exports(&store, &new_plugin_env);
|
|
let mut instance = Instance::new(&module, &zellij.chain_back(wasi)).unwrap();
|
|
load_plugin(&mut instance);
|
|
plugin_map.insert((plugin_id, *client_id), (instance, new_plugin_env));
|
|
}
|
|
pid_tx.send(plugin_id).unwrap();
|
|
plugin_id += 1;
|
|
},
|
|
PluginInstruction::Update(pid, cid, event) => {
|
|
for (&(plugin_id, client_id), (instance, plugin_env)) in &plugin_map {
|
|
let subs = plugin_env.subscriptions.lock().unwrap();
|
|
// FIXME: This is very janky... Maybe I should write my own macro for Event -> EventType?
|
|
let event_type = EventType::from_str(&event.to_string()).unwrap();
|
|
if subs.contains(&event_type)
|
|
&& ((pid.is_none() && cid.is_none())
|
|
|| (pid.is_none() && cid == Some(client_id))
|
|
|| (cid.is_none() && pid == Some(plugin_id))
|
|
|| (cid == Some(client_id) && pid == Some(plugin_id)))
|
|
{
|
|
let update = instance.exports.get_function("update").unwrap();
|
|
wasi_write_object(&plugin_env.wasi_env, &event);
|
|
update.call(&[]).unwrap();
|
|
}
|
|
}
|
|
drop(bus.senders.send_to_screen(ScreenInstruction::Render));
|
|
},
|
|
PluginInstruction::Render(buf_tx, pid, cid, rows, cols) => {
|
|
if rows == 0 || cols == 0 {
|
|
buf_tx.send(String::new()).unwrap();
|
|
} else {
|
|
let (instance, plugin_env) = plugin_map.get(&(pid, cid)).unwrap();
|
|
let render = instance.exports.get_function("render").unwrap();
|
|
|
|
render
|
|
.call(&[Value::I32(rows as i32), Value::I32(cols as i32)])
|
|
.unwrap();
|
|
|
|
buf_tx.send(wasi_read_string(&plugin_env.wasi_env)).unwrap();
|
|
}
|
|
},
|
|
PluginInstruction::Unload(pid) => {
|
|
info!("Bye from plugin {}", &pid);
|
|
// TODO: remove plugin's own data directory
|
|
let ids_in_plugin_map: Vec<(u32, ClientId)> = plugin_map.keys().copied().collect();
|
|
for (plugin_id, client_id) in ids_in_plugin_map {
|
|
if pid == plugin_id {
|
|
drop(plugin_map.remove(&(plugin_id, client_id)));
|
|
}
|
|
}
|
|
},
|
|
PluginInstruction::AddClient(client_id) => {
|
|
connected_clients.push(client_id);
|
|
|
|
let mut seen = HashSet::new();
|
|
let mut new_plugins = HashMap::new();
|
|
for (&(plugin_id, _), (instance, plugin_env)) in &plugin_map {
|
|
if seen.contains(&plugin_id) {
|
|
continue;
|
|
}
|
|
seen.insert(plugin_id);
|
|
let mut new_plugin_env = plugin_env.clone();
|
|
|
|
new_plugin_env.client_id = client_id;
|
|
new_plugins.insert(plugin_id, (instance.module().clone(), new_plugin_env));
|
|
}
|
|
for (plugin_id, (module, mut new_plugin_env)) in new_plugins.drain() {
|
|
let wasi = new_plugin_env.wasi_env.import_object(&module).unwrap();
|
|
let zellij = zellij_exports(&store, &new_plugin_env);
|
|
let mut instance = Instance::new(&module, &zellij.chain_back(wasi)).unwrap();
|
|
load_plugin(&mut instance);
|
|
plugin_map.insert((plugin_id, client_id), (instance, new_plugin_env));
|
|
}
|
|
|
|
// load headless plugins
|
|
for plugin in plugins.iter() {
|
|
if let PluginType::Headless = plugin.run {
|
|
let (instance, plugin_env) =
|
|
start_plugin(plugin_id, client_id, plugin, 0, &bus, &store, &data_dir);
|
|
headless_plugins.insert(plugin_id, (instance, plugin_env));
|
|
plugin_id += 1;
|
|
}
|
|
}
|
|
},
|
|
PluginInstruction::RemoveClient(client_id) => {
|
|
connected_clients.retain(|c| c != &client_id);
|
|
},
|
|
PluginInstruction::Exit => break,
|
|
}
|
|
}
|
|
info!("wasm main thread exits");
|
|
fs::remove_dir_all(&plugin_global_data_dir).unwrap();
|
|
}
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
fn start_plugin(
|
|
plugin_id: u32,
|
|
client_id: ClientId,
|
|
plugin: &PluginConfig,
|
|
tab_index: usize,
|
|
bus: &Bus<PluginInstruction>,
|
|
store: &Store,
|
|
data_dir: &Path,
|
|
) -> (Instance, PluginEnv) {
|
|
if plugin._allow_exec_host_cmd {
|
|
info!(
|
|
"Plugin({:?}) is able to run any host command, this may lead to some security issues!",
|
|
plugin.path
|
|
);
|
|
}
|
|
|
|
let wasm_bytes = plugin
|
|
.resolve_wasm_bytes(&data_dir.join("plugins/"))
|
|
.unwrap_or_else(|| panic!("Cannot resolve wasm bytes for plugin {:?}", plugin));
|
|
|
|
let hash: String = PortableHash::default()
|
|
.hash256(&wasm_bytes)
|
|
.iter()
|
|
.map(ToString::to_string)
|
|
.collect();
|
|
|
|
let cached_path = ZELLIJ_PROJ_DIR.cache_dir().join(&hash);
|
|
|
|
let module = unsafe {
|
|
Module::deserialize_from_file(store, &cached_path).unwrap_or_else(|_| {
|
|
let m = Module::new(store, &wasm_bytes).unwrap();
|
|
fs::create_dir_all(ZELLIJ_PROJ_DIR.cache_dir()).unwrap();
|
|
m.serialize_to_file(&cached_path).unwrap();
|
|
m
|
|
})
|
|
};
|
|
|
|
let output = Pipe::new();
|
|
let input = Pipe::new();
|
|
let stderr = LoggingPipe::new(&plugin.location.to_string(), plugin_id);
|
|
let plugin_own_data_dir = ZELLIJ_CACHE_DIR.join(Url::from(&plugin.location).to_string());
|
|
fs::create_dir_all(&plugin_own_data_dir).unwrap_or_else(|e| {
|
|
log::error!(
|
|
"Could not create plugin_own_data_dir in {:?} \n Error: {:?}",
|
|
&plugin_own_data_dir,
|
|
e
|
|
)
|
|
});
|
|
|
|
// ensure tmp dir exists, in case it somehow was deleted (e.g systemd-tmpfiles)
|
|
fs::create_dir_all(ZELLIJ_TMP_DIR.as_path()).unwrap_or_else(|e| {
|
|
log::error!(
|
|
"Could not create ZELLIJ_TMP_DIR at {:?} \n Error: {:?}",
|
|
&ZELLIJ_TMP_DIR.as_path(),
|
|
e
|
|
)
|
|
});
|
|
|
|
let mut wasi_env = WasiState::new("Zellij")
|
|
.env("CLICOLOR_FORCE", "1")
|
|
.map_dir("/host", ".")
|
|
.unwrap()
|
|
.map_dir("/data", &plugin_own_data_dir)
|
|
.unwrap()
|
|
.map_dir("/tmp", ZELLIJ_TMP_DIR.as_path())
|
|
.unwrap()
|
|
.stdin(Box::new(input))
|
|
.stdout(Box::new(output))
|
|
.stderr(Box::new(stderr))
|
|
.finalize()
|
|
.unwrap();
|
|
|
|
let wasi = wasi_env.import_object(&module).unwrap();
|
|
let mut plugin = plugin.clone();
|
|
plugin.set_tab_index(tab_index);
|
|
|
|
let plugin_env = PluginEnv {
|
|
plugin_id,
|
|
client_id,
|
|
plugin,
|
|
senders: bus.senders.clone(),
|
|
wasi_env,
|
|
subscriptions: Arc::new(Mutex::new(HashSet::new())),
|
|
plugin_own_data_dir,
|
|
tab_index,
|
|
};
|
|
|
|
let zellij = zellij_exports(store, &plugin_env);
|
|
let instance = Instance::new(&module, &zellij.chain_back(wasi)).unwrap();
|
|
|
|
(instance, plugin_env)
|
|
}
|
|
|
|
fn load_plugin(instance: &mut Instance) {
|
|
let load_function = instance.exports.get_function("_start").unwrap();
|
|
|
|
// This eventually calls the `.load()` method
|
|
load_function.call(&[]).unwrap();
|
|
}
|
|
|
|
// Plugin API ---------------------------------------------------------------------------------------------------------
|
|
|
|
pub(crate) fn zellij_exports(store: &Store, plugin_env: &PluginEnv) -> ImportObject {
|
|
macro_rules! zellij_export {
|
|
($($host_function:ident),+ $(,)?) => {
|
|
imports! {
|
|
"zellij" => {
|
|
$(stringify!($host_function) =>
|
|
Function::new_native_with_env(store, plugin_env.clone(), $host_function),)+
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
zellij_export! {
|
|
host_subscribe,
|
|
host_unsubscribe,
|
|
host_set_selectable,
|
|
host_get_plugin_ids,
|
|
host_get_zellij_version,
|
|
host_open_file,
|
|
host_switch_tab_to,
|
|
host_set_timeout,
|
|
host_exec_cmd,
|
|
}
|
|
}
|
|
|
|
fn host_subscribe(plugin_env: &PluginEnv) {
|
|
let mut subscriptions = plugin_env.subscriptions.lock().unwrap();
|
|
let new: HashSet<EventType> = wasi_read_object(&plugin_env.wasi_env);
|
|
subscriptions.extend(new);
|
|
}
|
|
|
|
fn host_unsubscribe(plugin_env: &PluginEnv) {
|
|
let mut subscriptions = plugin_env.subscriptions.lock().unwrap();
|
|
let old: HashSet<EventType> = wasi_read_object(&plugin_env.wasi_env);
|
|
subscriptions.retain(|k| !old.contains(k));
|
|
}
|
|
|
|
fn host_set_selectable(plugin_env: &PluginEnv, selectable: i32) {
|
|
match plugin_env.plugin.run {
|
|
PluginType::Pane(Some(tab_index)) => {
|
|
let selectable = selectable != 0;
|
|
plugin_env
|
|
.senders
|
|
.send_to_screen(ScreenInstruction::SetSelectable(
|
|
PaneId::Plugin(plugin_env.plugin_id),
|
|
selectable,
|
|
tab_index,
|
|
))
|
|
.unwrap()
|
|
},
|
|
_ => {
|
|
debug!(
|
|
"{} - Calling method 'host_set_selectable' does nothing for headless plugins",
|
|
plugin_env.plugin.location
|
|
)
|
|
},
|
|
}
|
|
}
|
|
|
|
fn host_get_plugin_ids(plugin_env: &PluginEnv) {
|
|
let ids = PluginIds {
|
|
plugin_id: plugin_env.plugin_id,
|
|
zellij_pid: process::id(),
|
|
};
|
|
wasi_write_object(&plugin_env.wasi_env, &ids);
|
|
}
|
|
|
|
fn host_get_zellij_version(plugin_env: &PluginEnv) {
|
|
wasi_write_object(&plugin_env.wasi_env, VERSION);
|
|
}
|
|
|
|
fn host_open_file(plugin_env: &PluginEnv) {
|
|
let path: PathBuf = wasi_read_object(&plugin_env.wasi_env);
|
|
plugin_env
|
|
.senders
|
|
.send_to_pty(PtyInstruction::SpawnTerminal(
|
|
Some(TerminalAction::OpenFile(path, None)),
|
|
ClientOrTabIndex::TabIndex(plugin_env.tab_index),
|
|
))
|
|
.unwrap();
|
|
}
|
|
|
|
fn host_switch_tab_to(plugin_env: &PluginEnv, tab_idx: u32) {
|
|
plugin_env
|
|
.senders
|
|
.send_to_screen(ScreenInstruction::GoToTab(
|
|
tab_idx,
|
|
Some(plugin_env.client_id),
|
|
))
|
|
.unwrap();
|
|
}
|
|
|
|
fn host_set_timeout(plugin_env: &PluginEnv, secs: f64) {
|
|
// There is a fancy, high-performance way to do this with zero additional threads:
|
|
// If the plugin thread keeps a BinaryHeap of timer structs, it can manage multiple and easily `.peek()` at the
|
|
// next time to trigger in O(1) time. Once the wake-up time is known, the `wasm` thread can use `recv_timeout()`
|
|
// to wait for an event with the timeout set to be the time of the next wake up. If events come in in the meantime,
|
|
// they are handled, but if the timeout triggers, we replace the event from `recv()` with an
|
|
// `Update(pid, TimerEvent)` and pop the timer from the Heap (or reschedule it). No additional threads for as many
|
|
// timers as we'd like.
|
|
//
|
|
// But that's a lot of code, and this is a few lines:
|
|
let send_plugin_instructions = plugin_env.senders.to_plugin.clone();
|
|
let update_target = Some(plugin_env.plugin_id);
|
|
let client_id = plugin_env.client_id;
|
|
thread::spawn(move || {
|
|
let start_time = Instant::now();
|
|
thread::sleep(Duration::from_secs_f64(secs));
|
|
// FIXME: The way that elapsed time is being calculated here is not exact; it doesn't take into account the
|
|
// time it takes an event to actually reach the plugin after it's sent to the `wasm` thread.
|
|
let elapsed_time = Instant::now().duration_since(start_time).as_secs_f64();
|
|
|
|
send_plugin_instructions
|
|
.unwrap()
|
|
.send(PluginInstruction::Update(
|
|
update_target,
|
|
Some(client_id),
|
|
Event::Timer(elapsed_time),
|
|
))
|
|
.unwrap();
|
|
});
|
|
}
|
|
|
|
fn host_exec_cmd(plugin_env: &PluginEnv) {
|
|
let mut cmdline: Vec<String> = wasi_read_object(&plugin_env.wasi_env);
|
|
let command = cmdline.remove(0);
|
|
|
|
// Bail out if we're forbidden to run command
|
|
if !plugin_env.plugin._allow_exec_host_cmd {
|
|
warn!("This plugin isn't allow to run command in host side, skip running this command: '{cmd} {args}'.",
|
|
cmd = command, args = cmdline.join(" "));
|
|
return;
|
|
}
|
|
|
|
// Here, we don't wait the command to finish
|
|
process::Command::new(command)
|
|
.args(cmdline)
|
|
.spawn()
|
|
.unwrap();
|
|
}
|
|
|
|
// Helper Functions ---------------------------------------------------------------------------------------------------
|
|
|
|
pub fn wasi_read_string(wasi_env: &WasiEnv) -> String {
|
|
let mut state = wasi_env.state();
|
|
let wasi_file = state.fs.stdout_mut().unwrap().as_mut().unwrap();
|
|
let mut buf = String::new();
|
|
wasi_file.read_to_string(&mut buf).unwrap();
|
|
buf
|
|
}
|
|
|
|
pub fn wasi_write_string(wasi_env: &WasiEnv, buf: &str) {
|
|
let mut state = wasi_env.state();
|
|
let wasi_file = state.fs.stdin_mut().unwrap().as_mut().unwrap();
|
|
writeln!(wasi_file, "{}\r", buf).unwrap();
|
|
}
|
|
|
|
pub fn wasi_write_object(wasi_env: &WasiEnv, object: &(impl Serialize + ?Sized)) {
|
|
wasi_write_string(wasi_env, &serde_json::to_string(&object).unwrap());
|
|
}
|
|
|
|
pub fn wasi_read_object<T: DeserializeOwned>(wasi_env: &WasiEnv) -> T {
|
|
let json = wasi_read_string(wasi_env);
|
|
serde_json::from_str(&json).unwrap()
|
|
}
|