diff --git a/Cargo.toml b/Cargo.toml index 7935265..e568cbd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,11 +23,12 @@ strip = true [features] default = [ - "std", "cli", "native-tls", ] -std = [ +std = [] +std-system = [ + "std", "tokio", "futures", "async-channel", @@ -40,7 +41,7 @@ std = [ "time/std", ] cli = [ - "std", + "std-system", "actix-web", "actix-cors", "clap", diff --git a/README.md b/README.md index 423d3b0..02e5a2f 100644 --- a/README.md +++ b/README.md @@ -10,8 +10,9 @@ | name | default | description | | ---- | ------- | ----------- | -| `std` | on | Enables the `std` crate dependency and access to the default [`StdSystem`](crate::std_system::StdSystem) implementation of [`System`](crate::runtime::System) | -| `cli` | on | Enables the `std` feature flag and additionally gives access to the [`cli`](crate::cli) submodule, which gives API access to the standard CLI (needed for syscall extensions) rather than having to write a CLI from scratch | +| `std` | on | Enables the `std` crate dependency and access to a number of helper types that depend on the standard library | +| `std-system` | on | Enables the `std` feature flag and also the [`StdSystem`](crate::std_system::StdSystem) implementation of [`System`](crate::runtime::System) | +| `cli` | on | Enables the `std-system` feature flag and additionally gives access to the [`cli`](crate::cli) submodule, which gives API access to the standard CLI rather than having to write a CLI from scratch | | `serde` | on | Enables serialization of some types | | `native-tls` | on | Enables the `native-tls` feature for TLS-capable dependencies (only used if `std` is also enabled) | | `native-tls-vendored` | off | Enables the `native-tls-vendored` feature for TLS-capable dependencies (only used if `std` is also enabled) | diff --git a/examples/basic.rs b/examples/basic.rs index c7bd568..d722441 100644 --- a/examples/basic.rs +++ b/examples/basic.rs @@ -1,5 +1,6 @@ use netsblox_vm::real_time::*; use netsblox_vm::std_system::*; +use netsblox_vm::std_util::*; use netsblox_vm::bytecode::*; use netsblox_vm::process::*; use netsblox_vm::runtime::*; diff --git a/src/bytecode.rs b/src/bytecode.rs index c5c3ef0..580f25c 100644 --- a/src/bytecode.rs +++ b/src/bytecode.rs @@ -11,9 +11,6 @@ use alloc::rc::Rc; use core::mem; -#[cfg(feature = "std")] -use std::io::{self, Write}; - #[cfg(feature = "serde")] use serde::{Serialize, Deserialize}; @@ -2574,7 +2571,7 @@ impl ByteCode { } /// Generates a hex dump of the stored code, including instructions and addresses. #[cfg(feature = "std")] - pub fn dump_code(&self, f: &mut dyn Write) -> io::Result<()> { + pub fn dump_code(&self, f: &mut dyn std::io::Write) -> std::io::Result<()> { let mut pos = 0; while pos < self.code.len() { let (ins, aft) = Instruction::read(&self.code, &self.data, pos); @@ -2603,7 +2600,7 @@ impl ByteCode { } /// Generate a hex dump of the stored program data, including string literals and meta values. #[cfg(feature = "std")] - pub fn dump_data(&self, f: &mut dyn Write) -> io::Result<()> { + pub fn dump_data(&self, f: &mut dyn std::io::Write) -> std::io::Result<()> { for (i, bytes) in self.data.chunks(BYTES_PER_LINE).enumerate() { write!(f, "{:08} ", i * BYTES_PER_LINE)?; for &b in bytes { diff --git a/src/cli.rs b/src/cli.rs index c550521..ed720b3 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -40,6 +40,7 @@ use crate::gc::*; use crate::json::*; use crate::real_time::*; use crate::std_system::*; +use crate::std_util::*; use crate::bytecode::*; use crate::runtime::*; use crate::process::*; diff --git a/src/lib.rs b/src/lib.rs index cb6fc04..7286d76 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -42,7 +42,8 @@ mod meta { include!(concat!(env!("OUT_DIR"), "/meta.rs")); } -#[cfg(feature = "std")] pub mod std_system; +#[cfg(feature = "std")] pub mod std_util; +#[cfg(feature = "std-system")] pub mod std_system; #[cfg(feature = "cli")] pub mod cli; #[cfg(test)] mod test; diff --git a/src/runtime.rs b/src/runtime.rs index d262807..34ad1ff 100644 --- a/src/runtime.rs +++ b/src/runtime.rs @@ -1781,6 +1781,18 @@ pub enum Precision { High, } +/// A key type used to await a reply message from an external source. +#[derive(Debug, Clone, PartialOrd, Ord, PartialEq, Eq)] +pub struct ExternReplyKey { + request_id: String, +} +/// A key type required for this client to send a reply message. +#[derive(Debug, Clone)] +pub struct InternReplyKey { + src_id: String, + request_id: String, +} + /// Represents all the features of an implementing system. /// /// This type encodes any features that cannot be performed without platform-specific resources. diff --git a/src/std_system.rs b/src/std_system.rs index 265a54b..875b7f6 100644 --- a/src/std_system.rs +++ b/src/std_system.rs @@ -30,22 +30,11 @@ use crate::runtime::*; use crate::process::*; use crate::json::*; use crate::gc::*; +use crate::std_util::*; use crate::*; const MESSAGE_REPLY_TIMEOUT: Duration = Duration::from_millis(1500); -/// A [`StdSystem`] key type used to await a reply message from an external source. -#[derive(Debug, Clone, PartialOrd, Ord, PartialEq, Eq)] -pub struct ExternReplyKey { - request_id: String, -} -/// A [`StdSystem`] key type required for this client to send a reply message. -#[derive(Debug, Clone)] -pub struct InternReplyKey { - src_id: String, - request_id: String, -} - struct Context { base_url: String, client_id: String, @@ -56,89 +45,20 @@ struct Context { role_name: String, role_id: String, } -struct RpcRequest>> { +struct RpcRequest, S: System> { service: String, rpc: String, args: Vec<(String, Json)>, - key: RequestKey, + key: RequestKey, } struct ReplyEntry { expiry: OffsetDateTime, value: Option, } -/// A [`StdSystem`] key type for an asynchronous request. -pub struct RequestKey>>(Arc>>>); -impl>> RequestKey { - fn poll(&self) -> AsyncResult> { self.0.lock().unwrap().poll() } -} -impl>> Key> for RequestKey { - /// Completes the request with the given result. - /// A value of [`Ok`] denotes a successful request, whose value will be returned to the system - /// after conversion under [`CustomTypes::from_intermediate`]. - /// A value of [`Err`] denotes a failed request, which will be returned as an error to the runtime, - /// subject to the caller's [`ErrorScheme`] setting. - fn complete(self, value: Result) { - assert!(self.0.lock().unwrap().complete(value).is_ok()) - } -} - -/// A [`StdSystem`] key type for an asynchronous command. -pub struct CommandKey(Arc>>>); -impl CommandKey { - fn poll(&self) -> AsyncResult> { self.0.lock().unwrap().poll() } -} -impl Key> for CommandKey { - /// Completes the command. - /// A value of [`Ok`] denotes a successful command. - /// A value of [`Err`] denotes a failed command, which will be returned as an error to the runtime, - /// subject to the caller's [`ErrorScheme`] setting. - fn complete(self, value: Result<(), String>) { - assert!(self.0.lock().unwrap().complete(value).is_ok()) - } -} - -struct ClockCache { - value: Mutex, - precision: Precision, -} - -/// A clock with optional coarse granularity. -pub struct Clock { - utc_offset: UtcOffset, - cache: Option, -} -impl Clock { - /// Creates a new [`Clock`] with the specified [`UtcOffset`] and (optional) cache [`Precision`] (see [`Clock::read`]). - pub fn new(utc_offset: UtcOffset, cache_precision: Option) -> Self { - let mut res = Self { utc_offset, cache: None }; - if let Some(precision) = cache_precision { - res.cache = Some(ClockCache { value: Mutex::new(res.update()), precision }); - } - res - } - /// Reads the current time with the specified level of precision. - /// If caching was enabled by [`Clock::new`], requests at or below the cache precision level will use the cached timestamp. - /// In any other case, the real current time is fetched by [`Clock::update`] and the result is stored in the cache if caching is enabled. - pub fn read(&self, precision: Precision) -> OffsetDateTime { - match &self.cache { - Some(cache) if precision <= cache.precision => *cache.value.lock().unwrap(), - _ => self.update(), - } - } - /// Reads the real world time and stores the result in the cache if caching was enabled by [`Clock::new`]. - pub fn update(&self) -> OffsetDateTime { - let t = OffsetDateTime::now_utc().to_offset(self.utc_offset); - if let Some(cache) = &self.cache { - *cache.value.lock().unwrap() = t; - } - t - } -} - type MessageReplies = BTreeMap; -async fn call_rpc_async>>(context: &Context, client: &reqwest::Client, service: &str, rpc: &str, args: &[(&str, &Json)]) -> Result { +async fn call_rpc_async, S: System>(context: &Context, client: &reqwest::Client, service: &str, rpc: &str, args: &[(&str, &Json)]) -> Result { let time = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_millis(); let url = format!("{services_url}/{service}/{rpc}?clientId={client_id}&t={time}", services_url = context.services_url, client_id = context.client_id); @@ -182,7 +102,7 @@ pub struct StdSystem>> { rng: Mutex, clock: Arc, - rpc_request_pipe: Sender>, + rpc_request_pipe: Sender>, message_replies: Arc>, message_sender: Sender>, @@ -339,11 +259,11 @@ impl>> StdSystem { let (sender, receiver) = channel(); #[tokio::main(flavor = "multi_thread", worker_threads = 1)] - async fn handler>>(client: Arc, context: Arc, receiver: Receiver>) { + async fn handler>>(client: Arc, context: Arc, receiver: Receiver>>) { while let Ok(request) = receiver.recv() { let (client, context) = (client.clone(), context.clone()); tokio::spawn(async move { - let res = call_rpc_async::(&context, &client, &request.service, &request.rpc, &request.args.iter().map(|x| (x.0.as_str(), &x.1)).collect::>()).await; + let res = call_rpc_async::>(&context, &client, &request.service, &request.rpc, &request.args.iter().map(|x| (x.0.as_str(), &x.1)).collect::>()).await; request.key.complete(res.map(Into::into)); }); } @@ -390,7 +310,7 @@ impl>> StdSystem { /// Asynchronously calls an RPC and returns the result. /// This function directly makes requests to NetsBlox, bypassing any RPC hook defined by [`Config`]. pub async fn call_rpc_async(&self, service: &str, rpc: &str, args: &[(&str, &Json)]) -> Result { - call_rpc_async::(&self.context, &self.client, service, rpc, args).await + call_rpc_async::(&self.context, &self.client, service, rpc, args).await } /// Gets the public id of the running system that can be used to send messages to this client. @@ -434,7 +354,7 @@ impl>> StdSystem { } } impl>> System for StdSystem { - type RequestKey = RequestKey; + type RequestKey = RequestKey; type CommandKey = CommandKey; type ExternReplyKey = ExternReplyKey; @@ -454,8 +374,8 @@ impl>> System for StdSystem { Ok(match self.config.request.as_ref() { Some(handler) => { - let key = RequestKey(Arc::new(Mutex::new(AsyncResult::new()))); - match handler(mc, RequestKey(key.0.clone()), request, proc) { + let key = RequestKey::new(); + match handler(mc, key.clone(), request, proc) { RequestStatus::Handled => key, RequestStatus::UseDefault { key: _, request } => return Err(ErrorCause::NotSupported { feature: request.feature() }), } @@ -481,8 +401,8 @@ impl>> System for StdSystem { Ok(match self.config.command.as_ref() { Some(handler) => { - let key = CommandKey(Arc::new(Mutex::new(AsyncResult::new()))); - match handler(mc, CommandKey(key.0.clone()), command, proc) { + let key = CommandKey::new(); + match handler(mc, key.clone(), command, proc) { CommandStatus::Handled => key, CommandStatus::UseDefault { key: _, command } => return Err(ErrorCause::NotSupported { feature: command.feature() }), } diff --git a/src/std_util.rs b/src/std_util.rs new file mode 100644 index 0000000..e4a1e2c --- /dev/null +++ b/src/std_util.rs @@ -0,0 +1,89 @@ +use alloc::string::String; + +use std::sync::{Arc, Mutex}; + +use crate::real_time::*; +use crate::runtime::*; +use crate::*; + +struct ClockCache { + value: Mutex, + precision: Precision, +} + +/// A clock with optional coarse granularity. +pub struct Clock { + utc_offset: UtcOffset, + cache: Option, +} +impl Clock { + /// Creates a new [`Clock`] with the specified [`UtcOffset`] and (optional) cache [`Precision`] (see [`Clock::read`]). + pub fn new(utc_offset: UtcOffset, cache_precision: Option) -> Self { + let mut res = Self { utc_offset, cache: None }; + if let Some(precision) = cache_precision { + res.cache = Some(ClockCache { value: Mutex::new(res.update()), precision }); + } + res + } + /// Reads the current time with the specified level of precision. + /// If caching was enabled by [`Clock::new`], requests at or below the cache precision level will use the cached timestamp. + /// In any other case, the real current time is fetched by [`Clock::update`] and the result is stored in the cache if caching is enabled. + pub fn read(&self, precision: Precision) -> OffsetDateTime { + match &self.cache { + Some(cache) if precision <= cache.precision => *cache.value.lock().unwrap(), + _ => self.update(), + } + } + /// Reads the real world time and stores the result in the cache if caching was enabled by [`Clock::new`]. + pub fn update(&self) -> OffsetDateTime { + let t = OffsetDateTime::now_utc().to_offset(self.utc_offset); + if let Some(cache) = &self.cache { + *cache.value.lock().unwrap() = t; + } + t + } +} + +/// A shared [`Key`] type for an asynchronous request. +#[derive(Educe)] +#[educe(Clone)] +pub struct RequestKey, S: System>(Arc>>>); +impl, S: System> RequestKey { + pub fn new() -> Self { + Self(Arc::new(Mutex::new(AsyncResult::new()))) + } + pub fn poll(&self) -> AsyncResult> { + self.0.lock().unwrap().poll() + } +} +impl, S: System> Key> for RequestKey { + /// Completes the request with the given result. + /// A value of [`Ok`] denotes a successful request, whose value will be returned to the system + /// after conversion under [`CustomTypes::from_intermediate`]. + /// A value of [`Err`] denotes a failed request, which will be returned as an error to the runtime, + /// subject to the caller's [`ErrorScheme`] setting. + fn complete(self, value: Result) { + assert!(self.0.lock().unwrap().complete(value).is_ok()) + } +} + +/// A shared [`Key`] type for an asynchronous command. +#[derive(Clone)] +pub struct CommandKey(Arc>>>); +impl CommandKey { + pub fn new() -> Self { + Self(Arc::new(Mutex::new(AsyncResult::new()))) + } + pub fn poll(&self) -> AsyncResult> { + self.0.lock().unwrap().poll() + } +} +impl Key> for CommandKey { + /// Completes the command. + /// A value of [`Ok`] denotes a successful command. + /// A value of [`Err`] denotes a failed command, which will be returned as an error to the runtime, + /// subject to the caller's [`ErrorScheme`] setting. + fn complete(self, value: Result<(), String>) { + assert!(self.0.lock().unwrap().complete(value).is_ok()) + } +} \ No newline at end of file diff --git a/src/test/mod.rs b/src/test/mod.rs index f01ef8b..a7c0d8c 100644 --- a/src/test/mod.rs +++ b/src/test/mod.rs @@ -6,6 +6,7 @@ use core::iter; use crate::runtime::*; use crate::process::*; use crate::std_system::*; +use crate::std_util::*; use crate::gc::*; mod process;