📜 ⬆️ ⬇️

Asynchronous user scripts on pure Rust without frameworks and SMS

Hi, Habr!

Sometimes when developing network services and user interfaces, one has to deal with rather complex interaction scenarios containing branches and loops. Such scenarios do not fit into a simple state machine - it is not enough to store all the data in the session object, it is also advisable to track the system’s route into a particular state, and in some cases be able to go back a few steps, repeat the dialogue in a loop, and t .d Previously, for this purpose, you had to develop your own data structures that simulate a stacking machine, or even use third-party scripting languages. With the advent of asynchronous capabilities in almost all programming languages, it became possible to write scripts in the same language in which the service is written. The script, with its own stack and local variables, is actually a user session, that is, it stores both the data and the route. For example, a gorutina with a blocking reading from a channel easily solves this problem, but firstly, the green thread is not free fun, and secondly, we write to Rust, where there are no green threads, but there are generators and async / await .

For example, let's write a simple http-bot, which displays the html-form in the browser, asking questions to the user until he answers that he feels good. The program is the simplest single-stream http-server, the bot script is written in the form of the Rust generator. Let me remind you that JavaScript generators allow two-way data exchange, that is, a generator can be sent to the generator, next (my_question) , and a return my_response can be returned from it. In Rust, the transfer of values ​​into the generator is not yet implemented (but promised), so we will exchange data through a shared cell, in which lies the structure with the data received and sent. The script of our bot is created by the function create_scenario () , which returns an instance of the generator, essentially a closure to which the parameter is moved - a pointer to a cell with udata data. For each user session, we store our own data cell and our own generator instance, with our own stack state and local variable values.
')
#[derive(Default, Clone)] struct UserData { sid: String, msg_in: String, msg_out: String, script: String, } type UserDataCell = Rc<RefCell<UserData>>; struct UserSession { udata: UserDataCell, scenario: Pin<Box<dyn Generator<Yield = (), Return = ()>>>, } type UserSessions = HashMap<String, UserSession>; fn create_scenario(udata: UserDataCell) -> impl Generator<Yield = (), Return = ()> { move || { let uname; let mut umood; udata.borrow_mut().msg_out = format!("Hi, what is you name ?"); yield (); uname = udata.borrow().msg_in.clone(); udata.borrow_mut().msg_out = format!("{}, how are you feeling ?", uname); yield (); 'not_ok: loop { umood = udata.borrow().msg_in.clone(); if umood.to_lowercase() == "ok" { break 'not_ok; } udata.borrow_mut().msg_out = format!("{}, think carefully, maybe you're ok ?", uname); yield (); umood = udata.borrow().msg_in.clone(); if umood.to_lowercase() == "ok" { break 'not_ok; } udata.borrow_mut().msg_out = format!("{}, millions of people are starving, maybe you're ok ?", uname); yield (); } udata.borrow_mut().msg_out = format!("{}, good bye !", uname); return (); } } 

Each step of the script consists of simple actions — get a link to the cell contents, save user input in local variables, set the answer text and give control out, by means of yield . As can be seen from the code, our generator returns an empty tuple (), and all data is transmitted through a common cell with a reference counter Ref <Cell <... >> . Inside the generator, you need to ensure that borrowing the contents of the borrow () cell does not pass through the yield point, otherwise it will be impossible to update the data from outside the generator - therefore, unfortunately, you cannot write once at the beginning of the algorithm let udata_mut = udata.borrow_mut () , but have to borrow value after each yield.

We implement our own event loop (read from socket), and for each incoming request either create a new user session, or find an existing sid, updating the data in it:

 let mut udata: UserData = read_udata(&mut stream); let mut sid = udata.sid.clone(); let session; if sid == "" { //new session sid = rnd.gen::<u64>().to_string(); udata.sid = sid.clone(); let udata_cell = Rc::new(RefCell::new(udata)); sessions.insert( sid.clone(), UserSession { udata: udata_cell.clone(), scenario: Box::pin(create_scenario(udata_cell)), } ); session = sessions.get_mut(&sid).unwrap(); } else { match sessions.get_mut(&sid) { Some(s) => { session = s; session.udata.replace(udata); } None => { println!("unvalid sid: {}", &sid); continue; } } } 

Next, we transfer control inside the corresponding generator, and the updated data is output back to the socket. At the last step, when the entire script is passed - we delete the session from the hashmap and hide the input field from the html page using the js script.

 udata = match session.scenario.as_mut().resume() { GeneratorState::Yielded(_) => session.udata.borrow().clone(), GeneratorState::Complete(_) => { let mut ud = sessions.remove(&sid).unwrap().udata.borrow().clone(); ud.script = format!("document.getElementById('form').style.display = 'none'"); ud } }; write_udata(&udata, &mut stream); 

Full working code here:
github.com/epishman/habr_samples/blob/master/chatbot/main.rs

I apologize for the "collective farm" parsing http, which does not even support Cyrillic input, but everything is done using standard language tools, without frameworks, libraries, and sms. I don’t really like the cloning of strings, and the script itself doesn’t look quite compact due to the abundant use of borrow_mut () and clone () . Probably, experienced rastas will be able to simplify it. The main thing is that the problem is solved with minimal means, and I hope that we will soon get a complete set of asynchronous tools in a stable release.

PS
Compiling requires nightly build:
 rustup default nightly rustup update 

My companions from the English Stack Overflow helped me deal with the generators:
stackoverflow.com/questions/56460206/how-can-i-transfer-some-values-into-a-rust-generator-at-each-step

Source: https://habr.com/ru/post/455010/


All Articles