basic/
basic.rs

1use netsblox_vm::real_time::*;
2use netsblox_vm::std_system::*;
3use netsblox_vm::std_util::*;
4use netsblox_vm::bytecode::*;
5use netsblox_vm::process::*;
6use netsblox_vm::runtime::*;
7use netsblox_vm::project::*;
8use netsblox_vm::gc::*;
9use netsblox_vm::ast;
10use netsblox_vm::compact_str::CompactString;
11
12use std::io::Read;
13use std::time::Duration;
14use std::sync::Arc;
15use std::rc::Rc;
16
17// -----------------------------------------------------------------
18
19const BASE_URL: &'static str = "https://cloud.netsblox.org";
20
21const CLOCK_INTERVAL: Duration = Duration::from_millis(10);
22const COLLECT_INTERVAL: Duration = Duration::from_secs(60);
23
24const YIELDS_BEFORE_SLEEP: usize = 64;
25const IDLE_SLEEP_TIME: Duration = Duration::from_millis(1);
26
27// -----------------------------------------------------------------
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30enum NativeType {} // type enum for a NativeValue - we don't have any native values we want to expose, so just use an empty enum
31
32#[derive(Debug)]
33enum NativeValue {} // enum for native values that are exposed to the vm - we don't have any we want to expose, so just use an empty enum
34impl GetType for NativeValue {
35    type Output = NativeType;
36    fn get_type(&self) -> Self::Output {
37        unreachable!() // because we don't have any native values to get the type of
38    }
39}
40
41struct EntityState; // a type to hold custom entity (sprite or stage) state - we don't have any, so just use a unit struct
42impl From<EntityKind<'_, '_, C, StdSystem<C>>> for EntityState {
43    fn from(_: EntityKind<'_, '_, C, StdSystem<C>>) -> Self {
44        EntityState
45    }
46}
47
48struct ProcessState; // a type to hold custom process (script) state - we don't have any, so just use a unit struct
49impl From<ProcessKind<'_, '_, C, StdSystem<C>>> for ProcessState {
50    fn from(_: ProcessKind<'_, '_, C, StdSystem<C>>) -> Self {
51        ProcessState
52    }
53}
54impl Unwindable for ProcessState {
55    type UnwindPoint = (); // a type to represent process (script) state unwind points - we don't have any process state, so just use a unit struct
56    fn get_unwind_point(&self) -> Self::UnwindPoint { }
57    fn unwind_to(&mut self, _: &Self::UnwindPoint) { }
58}
59
60struct C; // a type to hold all of our custom type definitions for the vm to use
61impl CustomTypes<StdSystem<C>> for C {
62    type NativeValue = NativeValue; // a type to hold any native rust values exposed to the vm
63    type Intermediate = SimpleValue; // a Send type that serves as an intermediate between vm gc values and normal rust
64
65    type EntityState = EntityState; // a type to hold the custom state for an entity (sprite or stage)
66    type ProcessState = ProcessState; // a type to hold the custom state for a process (script)
67
68    // a function to convert intermediate values into native vm values
69    fn from_intermediate<'gc>(mc: &Mutation<'gc>, value: Self::Intermediate) -> Value<'gc, C, StdSystem<C>> {
70        Value::from_simple(mc, value)
71    }
72}
73
74// our top-level gc arena - this will hold our gc-allocated project and everything it contains
75#[derive(Collect)]
76#[collect(no_drop)]
77struct Env<'gc> {
78                               proj: Gc<'gc, RefLock<Project<'gc, C, StdSystem<C>>>>,
79    #[collect(require_static)] locs: Locations, // bytecode locations info for generating error traces
80}
81type EnvArena = Arena<Rootable![Env<'_>]>;
82
83// converts a netsblox xml project containing a single role into a new gc environment object containing a running project
84fn get_running_project(xml: &str, system: Rc<StdSystem<C>>) -> EnvArena {
85    EnvArena::new(|mc| {
86        let parser = ast::Parser::default();
87        let ast = parser.parse(xml).unwrap();
88        assert_eq!(ast.roles.len(), 1); // this should be handled more elegantly in practice - for the sake of this example, we only allow one role
89
90        let (bytecode, init_info, locs, _) = ByteCode::compile(&ast.roles[0]).unwrap();
91
92        let mut proj = Project::from_init(mc, &init_info, Rc::new(bytecode), Settings::default(), system);
93        proj.input(mc, Input::Start); // this is equivalent to clicking the green flag button
94
95        Env { proj: Gc::new(mc, RefLock::new(proj)), locs }
96    })
97}
98
99fn main() {
100    // read in an xml file whose path is given as a command line argument
101    let args = std::env::args().collect::<Vec<_>>();
102    if args.len() != 2 {
103        panic!("usage: {} [xml file]", &args[0]);
104    }
105    let mut xml = String::new();
106    std::fs::File::open(&args[1]).expect("failed to open file").read_to_string(&mut xml).expect("failed to read file");
107
108    // create a new shared clock and start a thread that updates it at our desired interval
109    let clock = Arc::new(Clock::new(UtcOffset::UTC, Some(Precision::Medium)));
110    let clock_clone = clock.clone();
111    std::thread::spawn(move || loop {
112        std::thread::sleep(CLOCK_INTERVAL);
113        clock_clone.update();
114    });
115
116    // create a custom config for the system - in this simple example we just implement the say/think blocks to print to stdout
117    let config = Config::<C, StdSystem<C>> {
118        request: None,
119        command: Some(Rc::new(|_mc, key, command, _proc| match command {
120            Command::Print { style: _, value } => {
121                if let Some(value) = value {
122                    println!("{value:?}");
123                }
124                key.complete(Ok(())); // any request that you handle must be completed - otherwise the calling process will hang forever
125                CommandStatus::Handled
126            }
127            _ => CommandStatus::UseDefault { key, command }, // anything you don't handle should return the key and command to invoke the default behavior instead
128        })),
129    };
130
131    // initialize our system with all the info we've put together
132    let system = Rc::new(StdSystem::new_sync(CompactString::new(BASE_URL), None, config, clock.clone()));
133    let mut env = get_running_project(&xml, system);
134
135    // begin running the code - these are some helpers to make things more efficient in terms of memory and cpu resources
136    let mut idle_sleeper = IdleAction::new(YIELDS_BEFORE_SLEEP, Box::new(|| std::thread::sleep(IDLE_SLEEP_TIME)));
137    let mut next_collect = clock.read(Precision::Medium) + COLLECT_INTERVAL;
138    loop {
139        env.mutate(|mc, env| {
140            let mut proj = env.proj.borrow_mut(mc);
141            for _ in 0..1024 {
142                // step the virtual machine forward by one bytecode instruction
143                let res = proj.step(mc);
144                if let ProjectStep::Error { error, proc } = &res {
145                    // if we get an error, we can generate an error summary including a stack trace - here we just print out the result
146                    let trace = ErrorSummary::extract(error, proc, &env.locs);
147                    println!("error: {error:?}\ntrace: {trace:?}");
148                }
149                // this takes care of performing thread sleep if we get a bunch of no-ops from proj.step back to back
150                idle_sleeper.consume(&res);
151            }
152        });
153        // if it's time for us to do garbage collection, do it and reset the next collection time
154        if clock.read(Precision::Low) >= next_collect {
155            env.collect_all();
156            next_collect = clock.read(Precision::Medium) + COLLECT_INTERVAL;
157        }
158    }
159}