A Simple Web App in Rust, Part 3 -- Integration
Table of Contents
1 Previously
This is the third part in a series on writing a very simple web application in Rust.
So far, we have the the pieces for an MVP in separate rust files. Here, we want to put them together into a single app.
1.1 Review
We have the following two pieces to put together: the file writing/logging code, and the serving code. Lets review each of them.
First, the logging code:
extern crate chrono; use std::io::prelude::*; use std::fs::{File,OpenOptions}; use std::io; use chrono::{DateTime,Local}; fn formatted_time_entry() -> String { let local: DateTime<Local> = Local::now(); let formatted = local.format("%a, %b %d %Y %I:%M:%S %p\n").to_string(); formatted } fn record_entry_in_log(filename: &str, bytes: &[u8]) -> io::Result<()> { let mut file = try!(OpenOptions::new(). append(true). write(true). create(true). open(filename)); try!(file.write_all(bytes)); Ok(()) } fn log_time(filename: &'static str) -> io::Result<()> { let entry = formatted_time_entry(); let bytes = entry.as_bytes(); try!(record_entry_in_log(filename, &bytes)); Ok(()) } fn main() { match log_time("log.txt") { Ok(..) => println!("File created!"), Err(e) => println!("Error: {}", e) } }
Now, the serving code:
#[macro_use] extern crate nickel; use nickel::Nickel; fn say_hello() -> &'static str { "Hello dear world!" } fn main() { let mut server = Nickel::new(); server.utilize(router! { get "**" => |_req, _res| { say_hello() } }); server.listen("127.0.0.1:6767"); }
2 Combining the Code: Fisticuffing with the Type System
So, I want to combine these two programs.
First, I'll put them both into the same
file (and change the name of one of the main
functions, of course)
to see if they all compile together.
#[macro_use] extern crate nickel; extern crate chrono; use std::io::prelude::*; use std::fs::{File,OpenOptions}; use std::io; use chrono::{DateTime,Local}; use nickel::Nickel; fn formatted_time_entry() -> String { let local: DateTime<Local> = Local::now(); let formatted = local.format("%a, %b %d %Y %I:%M:%S %p\n").to_string(); formatted } fn record_entry_in_log(filename: &str, bytes: &[u8]) -> io::Result<()> { let mut file = try!(OpenOptions::new(). append(true). write(true). create(true). open(filename)); try!(file.write_all(bytes)); Ok(()) } fn log_time(filename: &'static str) -> io::Result<()> { let entry = formatted_time_entry(); let bytes = entry.as_bytes(); try!(record_entry_in_log(filename, &bytes)); Ok(()) } fn main2() { match log_time("log.txt") { Ok(..) => println!("File created!"), Err(e) => println!("Error: {}", e) } } fn say_hello() -> &'static str { "Hello dear world!" } fn main() { let mut server = Nickel::new(); server.utilize(router! { get "**" => |_req, _res| { say_hello() } }); server.listen("127.0.0.1:6767"); }
Compiling & Running:
$ cargo run src/main.rs:5:15: 5:19 warning: unused import, #[warn(unused_imports)] on by default src/main.rs:5 use std::fs::{File,OpenOptions}; ^~~~ src/main.rs:11:1: 15:2 warning: function is never used: `formatted_time_entry`, #[warn(dead_code)] o n by default src/main.rs:11 fn formatted_time_entry() -> String { src/main.rs:12 let local: DateTime<Local> = Local::now(); src/main.rs:13 let formatted = local.format("%a, %b %d %Y %I:%M:%S %p\n").to_string(); src/main.rs:14 formatted src/main.rs:15 } src/main.rs:17:1: 25:2 warning: function is never used: `record_entry_in_log`, #[warn(dead_code)] on by default src/main.rs:17 fn record_entry_in_log(filename: &str, bytes: &[u8]) -> io::Result<()> { src/main.rs:18 let mut file = try!(OpenOptions::new(). src/main.rs:19 append(true). src/main.rs:20 write(true). src/main.rs:21 create(true). src/main.rs:22 open(filename)); ... src/main.rs:27:1: 33:2 warning: function is never used: `log_time`, #[warn(dead_code)] on by default src/main.rs:27 fn log_time(filename: &'static str) -> io::Result<()> { src/main.rs:28 let entry = formatted_time_entry(); src/main.rs:29 let bytes = entry.as_bytes(); src/main.rs:30 src/main.rs:31 try!(record_entry_in_log(filename, &bytes)); src/main.rs:32 Ok(()) ... src/main.rs:35:1: 40:2 warning: function is never used: `main2`, #[warn(dead_code)] on by default src/main.rs:35 fn main2() { src/main.rs:36 match log_time("log.txt") { src/main.rs:37 Ok(..) => println!("File created!"), src/main.rs:38 Err(e) => println!("Error: {}", e) src/main.rs:39 } src/main.rs:40 } Running `target/debug/simple-log` Listening on http://127.0.0.1:6767 Ctrl-C to shutdown server
Cool. I totally expected those dead-code warning messages, and visiting
localhost:6767
in my browser still renders a "hello, world" page.
Here's an attempt to integrate them:
#[macro_use] extern crate nickel; extern crate chrono; use std::io::prelude::*; use std::fs::{File,OpenOptions}; use std::io; use chrono::{DateTime,Local}; use nickel::Nickel; fn formatted_time_entry() -> String { let local: DateTime<Local> = Local::now(); let formatted = local.format("%a, %b %d %Y %I:%M:%S %p\n").to_string(); formatted } fn record_entry_in_log(filename: &str, bytes: &[u8]) -> io::Result<()> { let mut file = try!(OpenOptions::new(). append(true). write(true). create(true). open(filename)); try!(file.write_all(bytes)); Ok(()) } fn log_time(filename: &'static str) -> io::Result<()> { let entry = formatted_time_entry(); let bytes = entry.as_bytes(); try!(record_entry_in_log(filename, &bytes)); Ok(()) } fn do_log_time() -> &'static str { match log_time("log.txt") { Ok(..) => println!("File created!"), Err(e) => println!("Error: {}", e) } } fn main() { let mut server = Nickel::new(); server.utilize(router! { get "**" => |_req, _res| { do_log_time() } }); server.listen("127.0.0.1:6767"); }
=>
$ cargo run Compiling simple-log v0.1.0 (file:///Users/joel/Projects/simple-log) src/main.rs:37:19: 37:44 error: mismatched types: expected `&'static str`, found `()` (expected &-ptr, found ()) [E0308] src/main.rs:37 Ok(..) => println!("File created!"), ^~~~~~~~~~~~~~~~~~~~~~~~~ src/main.rs:38:19: 38:43 error: mismatched types: expected `&'static str`, found `()` (expected &-ptr, found ()) [E0308] src/main.rs:38 Err(e) => println!("Error: {}", e) ^~~~~~~~~~~~~~~~~~~~~~~~ error: aborting due to 2 previous errors Could not compile `simple-log`. To learn more, run the command again with --verbose.
The macro println!
here is writing to standard out, but what I want is
something that will be returning a string. Is there a sprintln!
, or
something equivalent?
Doing a quick search, it looks like the answer is format!
:
#[macro_use] extern crate nickel; extern crate chrono; use std::io::prelude::*; use std::fs::{File,OpenOptions}; use std::io; use chrono::{DateTime,Local}; use nickel::Nickel; fn formatted_time_entry() -> String { let local: DateTime<Local> = Local::now(); let formatted = local.format("%a, %b %d %Y %I:%M:%S %p\n").to_string(); formatted } fn record_entry_in_log(filename: &str, bytes: &[u8]) -> io::Result<()> { let mut file = try!(OpenOptions::new(). append(true). write(true). create(true). open(filename)); try!(file.write_all(bytes)); Ok(()) } fn log_time(filename: &'static str) -> io::Result<()> { let entry = formatted_time_entry(); let bytes = entry.as_bytes(); try!(record_entry_in_log(filename, &bytes)); Ok(()) } fn do_log_time() -> &'static str { match log_time("log.txt") { Ok(..) => format!("File created!"), Err(e) => format!("Error: {}", e) } } fn main() { let mut server = Nickel::new(); server.utilize(router! { get "**" => |_req, _res| { do_log_time() } }); server.listen("127.0.0.1:6767"); }
=>
$ cargo run Compiling simple-log v0.1.0 (file:///Users/joel/Projects/simple-log) src/main.rs:37:19: 37:43 error: mismatched types: expected `&'static str`, found `collections::string::String` (expected &-ptr, found struct `collections::string::String`) [E0308] src/main.rs:37 Ok(..) => format!("File created!"), ^~~~~~~~~~~~~~~~~~~~~~~~ src/main.rs:38:19: 38:42 error: mismatched types: expected `&'static str`, found `collections::string::String` (expected &-ptr, found struct `collections::string::String`) [E0308] src/main.rs:38 Err(e) => format!("Error: {}", e) ^~~~~~~~~~~~~~~~~~~~~~~ error: aborting due to 2 previous errors Could not compile `simple-log`. To learn more, run the command again with --verbose.
So, I know there's a way to convert between a String
and an
&str
… hmm. I recall that I can use an &
.
fn do_log_time() -> &'static str { match log_time("log.txt") { Ok(..) => &format!("File created!"), Err(e) => &format!("Error: {}", e) } }
=>
$ cargo run Compiling simple-log v0.1.0 (file:///Users/joel/Projects/simple-log) src/main.rs:37:20: 37:44 error: borrowed value does not live long enough src/main.rs:37 Ok(..) => &format!("File created!"), ^~~~~~~~~~~~~~~~~~~~~~~~ note: reference must be valid for the static lifetime... src/main.rs:37:19: 37:44 note: ...but borrowed value is only valid for the expression at 37:18 src/main.rs:37 Ok(..) => &format!("File created!"), ^~~~~~~~~~~~~~~~~~~~~~~~~ src/main.rs:38:20: 38:43 error: borrowed value does not live long enough src/main.rs:38 Err(e) => &format!("Error: {}", e) ^~~~~~~~~~~~~~~~~~~~~~~ note: reference must be valid for the static lifetime... src/main.rs:38:19: 38:43 note: ...but borrowed value is only valid for the expression at 38:18 src/main.rs:38 Err(e) => &format!("Error: {}", e) ^~~~~~~~~~~~~~~~~~~~~~~~ error: aborting due to 2 previous errors Could not compile `simple-log`. To learn more, run the command again with --verbose.
This thing again. I think I'm going to need a block here:
fn do_log_time() -> &'static str { match log_time("log.txt") { Ok(..) => { let fmt = format!("File created!"); &fmt }, Err(e) => { let fmt = format!("Error: {}", e); &fmt } } }
=>
$ cargo run Compiling simple-log v0.1.0 (file:///Users/joel/Projects/simple-log) src/main.rs:39:14: 39:17 error: `fmt` does not live long enough src/main.rs:39 &fmt ^~~ note: reference must be valid for the static lifetime... src/main.rs:38:48: 40:10 note: ...but borrowed value is only valid for the block suffix following s tatement 0 at 38:47 src/main.rs:38 let fmt = format!("File created!"); src/main.rs:39 &fmt src/main.rs:40 }, src/main.rs:43:14: 43:17 error: `fmt` does not live long enough src/main.rs:43 &fmt ^~~ note: reference must be valid for the static lifetime... src/main.rs:42:47: 44:10 note: ...but borrowed value is only valid for the block suffix following s tatement 0 at 42:46 src/main.rs:42 let fmt = format!("Error: {}", e); src/main.rs:43 &fmt src/main.rs:44 } error: aborting due to 2 previous errors Could not compile `simple-log`. To learn more, run the command again with --verbose.
That didn't work. I think the problem is that fmt
only exists for
that new block, but the return value is used outside of it. What if I
promote fmt
to the top of the function?
fn do_log_time() -> &'static str { let mut fmt = "".to_string(); match log_time("log.txt") { Ok(..) => { fmt = format!("File created!"); &fmt }, Err(e) => { fmt = format!("Error: {}", e); &fmt } } }
=>
$ cargo run Compiling simple-log v0.1.0 (file:///Users/joel/Projects/simple-log) src/main.rs:40:14: 40:17 error: `fmt` does not live long enough src/main.rs:40 &fmt ^~~ note: reference must be valid for the static lifetime... src/main.rs:36:34: 48:2 note: ...but borrowed value is only valid for the block suffix following st atement 0 at 36:33 src/main.rs:36 let mut fmt = "".to_string(); src/main.rs:37 match log_time("log.txt") { src/main.rs:38 Ok(..) => { src/main.rs:39 fmt = format!("File created!"); src/main.rs:40 &fmt src/main.rs:41 }, ... src/main.rs:44:14: 44:17 error: `fmt` does not live long enough src/main.rs:44 &fmt ^~~ note: reference must be valid for the static lifetime... src/main.rs:36:34: 48:2 note: ...but borrowed value is only valid for the block suffix following st atement 0 at 36:33 src/main.rs:36 let mut fmt = "".to_string(); src/main.rs:37 match log_time("log.txt") { src/main.rs:38 Ok(..) => { src/main.rs:39 fmt = format!("File created!"); src/main.rs:40 &fmt src/main.rs:41 }, ... error: aborting due to 2 previous errors Could not compile `simple-log`. To learn more, run the command again with --verbose.
I don't know how to fix this. I'm going to just set this down, for now, and come back to it later.
—
I've tried a few new things, and nothing works. I think I need to learn more of how this ownership/lifetime stuff works.
I just read a bit of the Rust book, and I notice this note:
We choose the
String
type for the name, rather than&str
. Generally speaking, working with a type which owns its data is easier than working with one that uses references.
Because I'm in "do" mode and not "learn" mode, I want to try using
String
to see if that works.
Now:
fn do_log_time() -> String { match log_time("log.txt") { Ok(..) => format!("File created!"), Err(e) => format!("Error: {}", e) } }
=>
$ cargo run Compiling simple-log v0.1.0 (file:///Users/joel/Projects/simple-log) Running `target/debug/simple-log` Listening on http://127.0.0.1:6767 Ctrl-C to shutdown server
It worked. Visiting the page in a browser shows "File created!", and it also wrote an entry to the log file.
I'm not really surprised that this works – I kinda figured the
solution would be to return a String
instead of an &str
, but I
wanted to take it as a challenge to figure out.
Now that I think about it, this makes sense. I'm trying to
return a borrowed reference, but I also own it, so returning it
wouldn't make any sense. How would I return an &str
that I created
in my own function? I haven't seen anything using a plain, not-borrowed "str
"
anywhere.
I this absence of not-borrowed ~&str~s has to do with it representing being a plain c string pointer. This must have some complications that I'm not aware of, and for it to play nicely with Rust it must interface with rust the normal Rust rules sharing ownership must apply.
If some other part of the program has knowledge of an array of bytes,
and provides me with a reference to that array, what does that mean?
Are &str
types basically just so that C strings can be referenced
without some additional metadata associated with them?
The Rust book says &str
-> String
has some cost. I wonder if this
always true, or only for static program strings. Would a
heap-allocated &str
require copying for a String
? Now that I
think about it, I bet the answer is yes; if you want to convert
a borrowed value into something that is owned, the only
reasonable solution would be to copy it.
Anyway, I think I just want to move on. I think the answer is that
what I was trying to do just didn't make sense, and Rust correctly
stopped me. I do wish I understood why every str
is borrowed,
though.
I'm going to try to return the logged time string from log_time
and
have that displayed to the user. My first attempt:
fn log_time(filename: &'static str) -> io::Result<String> { let entry = formatted_time_entry(); let bytes = entry.as_bytes(); try!(record_entry_in_log(filename, &bytes)); Ok(entry) } fn do_log_time() -> String { match log_time("log.txt") { Ok(entry) => format!("Entry Logged: {}", entry), Err(e) => format!("Error: {}", e) } }
=>
$ cargo run Compiling simple-log v0.1.0 (file:///Users/joel/Projects/simple-log) src/main.rs:32:8: 32:13 error: cannot move out of `entry` because it is borrowed src/main.rs:32 Ok(entry) ^~~~~ src/main.rs:29:17: 29:22 note: borrow of `entry` occurs here src/main.rs:29 let bytes = entry.as_bytes(); ^~~~~ error: aborting due to previous error Could not compile `simple-log`. To learn more, run the command again with --verbose.
Hmm. So I guess that makes sense… bytes
"borrows" the contents of
entry
. And, since this value is still borrowed by the time
OK(entry)
is called, this causes the error.
This works:
fn log_time(filename: &'static str) -> io::Result<String> { let entry = formatted_time_entry(); { let bytes = entry.as_bytes(); try!(record_entry_in_log(filename, &bytes)); } Ok(entry) }
=>
$ cargo run & [1] 66858 $ Running `target/debug/simple-log` Listening on http://127.0.0.1:6767 Ctrl-C to shutdown server $ curl localhost:6767 Entry Logged: Tue, Jun 23 2015 12:34:19 AM
This isn't the first time I've used the "stick a new block
here" feature, but it does seem to work for this, and it seems like a
reasonably elegant way to handle this. My first thought though was
that I needed to call another function to somehow "convert" bytes back
into a String
, but then I realized that this didn't actually make
sense, and I needed to "deallocate" the borrow, somehow.
I don't understand what "move out of `entry`" means in that error message
though. I'm thinking that you can't transfer ownership of a value as
long as there is a borrowed reference to it, too. But maybe
that isn't actually true. Is sending it to Ok()
changing it? I'm
pretty confused by this, and the Rust book doesn't seem to address
this specific issue, but I think this must be it – ownership can't be
changed while a borrow exists. I think.
Its nice to see that as I've been browsing through the Rust book section on borrowing, using a block is the cited solution to this problem.
3 Fin
Integrating this was much harder than I expected. Borrowing/ownership got me a few times here, so I'm going to cut it at this point, since this has gotten pretty long.
Fortunately, I think I am slowly understanding how Rust works, and especially its borrowing functionality. This gives me hope for the future.
—
Series: A Simple Web App in Rust