#[cfg(feature = "native")]
pub mod operator;
mod run;
pub mod runtime;
#[cfg(feature = "studio-bridge")]
pub mod studio;
#[cfg(feature = "tui")]
pub mod tui;
#[cfg(feature = "native")]
pub use run::{run, run_with, run_with_frontend, run_with_hal};
pub use runtime::RuntimeError;
pub use arora_behavior_tree::behavior::BehaviorTreeInterpreter;
pub use arora_behavior_tree::ModuleFunction;
use crate::runtime::EndpointInbound;
use anyhow::Result;
use arora_behavior::{interpreter_module, BehaviorInterpreter};
pub use arora_bridge::Caller;
use arora_bridge::{Bridge, BridgeCommand, BridgeError, BridgeOp, Inbound};
use arora_engine::engine::{EngineBuilder, PinnedEngine};
#[cfg(feature = "native")]
use arora_engine::executor::{native::NativeExecutor, wasm::WebAssemblyExecutor};
use arora_engine::load::load_module_from_parts;
use arora_engine::module::ModuleBuilder;
use arora_hal::{FakeHal, Hal, UpdatesStream};
use arora_simple_data_store::SimpleDataStore;
use arora_types::call::{Call, CallBridge, CallError, CallResult};
use arora_types::data::{DataStore, Subscription};
use arora_types::module::low::Header;
use futures::channel::{mpsc, oneshot};
use futures::stream::{self, Fuse, SelectAll};
use futures::StreamExt;
use runtime::{Clock, Pending};
use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;
use uuid::Uuid;
pub struct Arora {
pub(crate) store: Box<dyn DataStore>,
pub(crate) engine: PinnedEngine,
pub(crate) function_index: Rc<HashMap<Uuid, ModuleFunction>>,
pub(crate) interpreter: runtime::InterpreterCell,
pub(crate) hal: Box<dyn Hal>,
pub(crate) bridges: Vec<Box<dyn Bridge>>,
pub(crate) hal_feed: Fuse<UpdatesStream>,
pub(crate) inbound: SelectAll<EndpointInbound>,
pub(crate) pending: Pending,
pub(crate) data_requested: Vec<bool>,
pub(crate) caller_tx: mpsc::UnboundedSender<Inbound>,
pub(crate) store_changes: Subscription,
pub(crate) clock: Clock,
}
impl Arora {
pub fn builder() -> AroraBuilder {
AroraBuilder::default()
}
pub fn store(&self) -> &dyn DataStore {
&*self.store
}
pub fn call(&mut self, call: Call) -> Result<CallResult, CallError> {
self.engine.arora_call(call)
}
pub fn engine(&mut self) -> &mut dyn CallBridge {
&mut self.engine
}
pub fn caller(&self) -> LocalCaller {
LocalCaller {
tx: self.caller_tx.clone(),
}
}
}
#[derive(Clone)]
pub struct LocalCaller {
tx: mpsc::UnboundedSender<Inbound>,
}
impl Caller for LocalCaller {
fn call(&self, call: Call) -> arora_bridge::CallFuture<'_> {
Box::pin(async move {
let (tx, rx) = oneshot::channel();
self.tx
.unbounded_send(Inbound::Command(BridgeCommand::new(
BridgeOp::Call(call),
tx,
)))
.map_err(|_| CallError::Generic {
message: "the device is gone".to_string(),
})?;
match rx.await {
Ok(Ok(result)) => Ok(result),
Ok(Err(message)) => Err(CallError::Generic { message }),
Err(_) => Err(CallError::Generic {
message: "the device dropped the call".to_string(),
}),
}
})
}
}
#[derive(Default)]
pub struct AroraBuilder {
store: Option<Box<dyn DataStore>>,
hal: Option<Box<dyn Hal>>,
bridges: Vec<Box<dyn Bridge>>,
interpreter: Option<Box<dyn BehaviorInterpreter>>,
functions: HashMap<Uuid, ModuleFunction>,
modules: Vec<(Header, Box<[u8]>)>,
}
impl AroraBuilder {
pub fn with_data_store(mut self, store: Box<dyn DataStore>) -> Self {
self.store = Some(store);
self
}
pub fn with_hal(mut self, hal: Box<dyn Hal>) -> Self {
self.hal = Some(hal);
self
}
pub fn with_bridge(mut self, bridge: Box<dyn Bridge>) -> Self {
self.bridges.push(bridge);
self
}
pub fn with_behavior_interpreter(mut self, interpreter: Box<dyn BehaviorInterpreter>) -> Self {
self.interpreter = Some(interpreter);
self
}
pub fn with_module(mut self, header: Header, executable: impl Into<Box<[u8]>>) -> Self {
self.modules.push((header, executable.into()));
self
}
pub fn with_host_module(mut self, functions: impl IntoIterator<Item = ModuleFunction>) -> Self {
for function in functions {
self.functions.insert(function.function_id, function);
}
self
}
#[cfg(feature = "native")]
pub async fn run(mut self) -> Result<()> {
let frontend = run::select_frontend();
if self.bridges.is_empty() {
#[cfg(feature = "studio-bridge")]
{
self = self.with_bridge(studio::default_bridge(&frontend).await?);
}
#[cfg(not(feature = "studio-bridge"))]
{
self = self.with_bridge(run::local_ws_bridge().await?);
}
}
run::run_builder_with_frontend(self, frontend).await
}
pub fn build(self) -> Result<Arora> {
let mut engine = build_engine()?;
for (header, executable) in self.modules {
load_module_from_parts(&mut engine, header, executable)
.map_err(|e| anyhow::anyhow!("failed to load module: {e}"))?;
}
let store = self
.store
.unwrap_or_else(|| Box::new(SimpleDataStore::new()));
let hal: Box<dyn Hal> = self.hal.unwrap_or_else(|| Box::new(FakeHal::new()));
let store_changes = store.subscribe();
let hal_feed = hal.updates().fuse();
let mut bridges = self.bridges;
let mut inbound = SelectAll::new();
for (endpoint, bridge) in bridges.iter_mut().enumerate() {
let disconnected = stream::once(async {
Inbound::DeviceInfo(Err(BridgeError::Disconnected(
"the endpoint's inbound stream ended".into(),
)))
});
inbound.push(
bridge
.take_inbound()
.chain(disconnected)
.map(move |event| (Some(endpoint), event))
.boxed(),
);
}
let (caller_tx, caller_rx) = mpsc::unbounded();
inbound.push(caller_rx.map(|event| (None, event)).boxed());
let endpoints = bridges.len();
let function_index = Rc::new(self.functions);
let interpreter = self
.interpreter
.unwrap_or_else(|| Box::new(BehaviorTreeInterpreter::new(function_index.clone())));
let interpreter: runtime::InterpreterCell = Rc::new(RefCell::new(Some(interpreter)));
let module = ModuleBuilder::new(interpreter_module::ID)
.function(interpreter_module::LOAD, {
let cell = interpreter.clone();
move |call| {
let graph = interpreter_module::decode_load(&call)
.map_err(|message| CallError::Guest { message })?;
runtime::with_interpreter(&cell, |interpreter| interpreter.load(graph))
}
})
.function(interpreter_module::EDIT, {
let cell = interpreter.clone();
move |call| {
let diff = interpreter_module::decode_edit(&call)
.map_err(|message| CallError::Guest { message })?;
runtime::with_interpreter(&cell, |interpreter| interpreter.apply(diff))
}
})
.build();
engine.register_module(module.id(), Box::new(module));
Ok(Arora {
store,
engine,
function_index,
interpreter,
hal,
bridges,
hal_feed,
inbound,
pending: Pending::default(),
data_requested: vec![false; endpoints],
caller_tx,
store_changes,
clock: Clock::default(),
})
}
}
#[cfg(feature = "native")]
fn build_engine() -> Result<PinnedEngine> {
Ok(EngineBuilder::new()
.add_executor(
WebAssemblyExecutor::new()
.map_err(|e| anyhow::anyhow!("failed to create wasm executor: {e}"))?,
)
.add_executor(NativeExecutor::new())
.build())
}
#[cfg(not(feature = "native"))]
fn build_engine() -> Result<PinnedEngine> {
use arora_engine::executor::browser::BrowserExecutor;
Ok(EngineBuilder::new()
.add_executor(BrowserExecutor::new())
.build())
}
#[cfg(all(test, feature = "native"))]
mod module_loading_tests {
use super::*;
use arora_types::call::{Call, CallBridge};
use arora_types::value::Value;
const HEADER_YAML: &str = include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/../../modules/test-rust-wasm/src/arora_generated/module.yaml"
));
const WASM: &[u8] = include_bytes!(env!("CARGO_CDYLIB_FILE_TEST_RUST_WASM_test_rust_wasm"));
const SUCCEED: &str = "00cd31a8-2cf4-48e6-a957-69a55de90424";
fn test_module_header() -> Header {
serde_yaml::from_str(HEADER_YAML).expect("parse test-rust-wasm header yaml")
}
#[test]
fn with_module_loads_a_wasm_module_reachable_through_call() {
let header = test_module_header();
let module_id = header.id;
let mut arora = Arora::builder()
.with_module(header, WASM.to_vec())
.build()
.expect("build a device with a loaded wasm module");
let result = arora
.call(Call {
module_id: Some(module_id),
id: Uuid::parse_str(SUCCEED).expect("valid uuid"),
args: Vec::new(),
})
.expect("call succeed() on the loaded module");
assert_eq!(result.ret, Value::Boolean(true));
}
#[test]
fn a_call_naming_no_module_is_refused() {
let mut arora = Arora::builder().build().expect("build the default device");
let err = arora
.call(Call {
module_id: None,
id: Uuid::parse_str(SUCCEED).expect("valid uuid"),
args: Vec::new(),
})
.expect_err("a module-less call is refused");
assert!(err.to_string().contains("module id"), "{err}");
}
#[test]
fn call_loads_a_behavior_with_no_bridge() {
let mut arora = Arora::builder().build().expect("build the default device");
let result = arora
.call(interpreter_module::encode_load(
&arora_behavior::Graph::empty(),
))
.expect("the load call succeeds");
assert_eq!(result.ret, arora_types::value::Value::Unit);
}
#[tokio::test]
async fn a_caller_call_lands_on_the_next_step() {
let mut arora = Arora::builder().build().expect("build the default device");
let caller = arora.caller();
let mut call = Box::pin(caller.call(interpreter_module::encode_load(
&arora_behavior::Graph::empty(),
)));
assert!(futures::poll!(call.as_mut()).is_pending());
arora
.step(std::time::Duration::from_millis(10))
.expect("step");
let result = call.await.expect("the load call succeeds");
assert_eq!(result.ret, arora_types::value::Value::Unit);
}
#[tokio::test]
async fn a_caller_reaches_a_running_device() {
use futures::FutureExt;
let mut arora = Arora::builder().build().expect("build the default device");
let caller = arora.caller();
let run = arora.run(std::time::Duration::from_millis(5));
let call = caller.call(interpreter_module::encode_load(
&arora_behavior::Graph::empty(),
));
futures::pin_mut!(run, call);
let outcome = tokio::time::timeout(std::time::Duration::from_secs(2), async {
futures::select! {
result = call.fuse() => result,
_ = run.fuse() => panic!("run ended before the call resolved"),
}
})
.await
.expect("the running device answers promptly");
assert_eq!(
outcome.expect("the load call succeeds").ret,
arora_types::value::Value::Unit
);
}
#[test]
fn engine_registers_and_dispatches_an_in_process_callable() {
use arora_types::call::Callable;
struct Answer;
impl Callable for Answer {
fn call(&self, _caller: &mut dyn CallBridge) -> Result<Value, CallError> {
Ok(Value::I32(42))
}
}
let mut arora = Arora::builder().build().expect("build the default device");
let id = arora.engine().arora_register_callable(Rc::new(Answer));
let result = arora
.engine()
.arora_call_indirect(&id)
.expect("the registered callable dispatches");
assert!(matches!(result, Value::I32(42)));
}
#[test]
fn builds_without_any_module() {
Arora::builder()
.build()
.expect("the default device builds with no modules loaded");
}
#[test]
fn a_module_that_fails_to_load_fails_the_build() {
let header = test_module_header();
let result = Arora::builder()
.with_module(header, vec![0xDE, 0xAD, 0xBE, 0xEF]) .build();
assert!(
result.is_err(),
"build must fail when a module's executable cannot load"
);
}
}