Files, input and time
A program often needs text, files, the clock or the network. This chapter shows the parts of the
standard library for these jobs: std::string, std::fs, std::io, std::time and std::net.
Many of these functions take a raw pointer, such as a string literal or a buffer. The compiler
cannot check a raw pointer, so you call these functions inside an unsafe { .. } block.
Unsafe and FFI explains raw pointers.
Each complete program in this chapter is also a test of the compiler. You can find it under
tests/backend/pass/ with the name that its section gives.
Strings
String is a text that can grow. String::new() makes an empty string. push_c_str adds a
string literal to the end. len() gives the length in bytes, not in characters.
to_string() on an integer gives its decimal digits as a String. String::from_c_str copies a
string literal. == on two strings compares their bytes. parse_int reads the digits at the
start of a text as an i32 (book_io_build_a_string.vx):
import std::string;
fn main() -> i32 {
let mut greeting = String::new();
unsafe { greeting.push_c_str("hello, "); greeting.push_c_str("world"); }
let size = greeting.len();
println!("{} has {} bytes", greeting, size);
let n : i32 = 42;
let text = n.to_string();
let same = unsafe { String::from_c_str("42") };
println!("equal: {}", text == same);
let parsed = unsafe { parse_int("123 apples") };
println!("parsed {}", parsed + 1);
return 0;
}
It prints:
hello, world has 12 bytes
equal: true
parsed 124
println! moves a String argument (Vx#3235). So the
program gets the length before it prints greeting.
parse_int stops at the first byte that is not a digit. So "12abc" gives 12, and "abc"
gives 0. A - is not a sign. parse_int does not check for overflow.
Files
File::open(path, mode) opens a file. path is a text that ends with a zero byte, such as a
string literal. The mode is one of the three values of OpenMode:
OpenMode::Read()opens a file that exists, for reading.OpenMode::Write()makes the file, or makes an existing file empty, for writing.OpenMode::ReadWriteCreate()opens the file for both, and makes it if it does not exist. It keeps the bytes that the file has.
write(buffer, len) writes len bytes from a buffer and gives how many it wrote. read(buffer, len) reads up to len bytes into a buffer and gives how many it read. 0 means the end of the
file. seek(offset, whence) moves the position of the next read or write, and gives the new
position. whence is 0 for the start of the file, 1 for the current position and 2 for the end.
File implements Drop, so a file closes at the end of its scope. You do not close it by hand.
Ownership and borrowing explains Drop.
This program writes three lines to a file, and reads the file back one byte at a time. The test
gives the path of the file in the environment variable NOTES (book_io_write_and_read_a_file.vx):
import std::fs;
import std::string;
import std::vec;
extern {
fn getenv(name : *const i8) -> *const i8;
}
// Writes `text` to a new file at `path`. The file closes when `out` goes out of scope.
fn write_text(path : *const i8, text : *const i8) -> void {
let mut bytes = Vec<u8>::new();
let length = unsafe { string_length(text) };
for i in 0..length {
bytes.push(unsafe { text[i] } as u8);
}
let mut out = unsafe { File::open(path, OpenMode::Write()) };
let written = unsafe {
out.write(bytes.as_mut_ptr() as *const u8, length as u64)
};
println!("wrote {} bytes", written);
}
fn main() -> i32 {
let path = unsafe { getenv("NOTES") };
write_text(path, "one\ntwo\nthree\n");
let mut input = unsafe { File::open(path, OpenMode::Read()) };
let mut byte : u8 = 0;
let mut lines = 0;
loop {
let n = unsafe { input.read((&mut byte) as *mut u8, 1) };
if n == 0 {
break;
}
if byte == 10 {
lines = lines + 1;
}
}
println!("read {} lines", lines);
println!("the file ends at byte {}", input.seek(0, 2));
input.seek(4, 0);
unsafe { input.read((&mut byte) as *mut u8, 1) };
println!("byte 4 is {}, the letter t", byte);
return 0;
}
It prints:
wrote 14 bytes
read 3 lines
the file ends at byte 14
byte 4 is 116, the letter t
write_text copies the literal into a Vec<u8>, because write takes a pointer to u8 bytes.
out closes at the end of write_text. So main reads all 14 bytes.
A failed open gives a File that does nothing: each later read and write gives 0. A missing
file and an empty file look the same. So check the count that read and write give.
Standard input and output
std::io has three functions. stdin_read(buffer, len) reads up to len bytes of standard input
and gives the count. 0 means the end of the input. stdout_write and stderr_write write bytes to
standard output and standard error. This loop counts the lines of the input:
import std::io;
fn main() -> i32 {
let mut byte : u8 = 0;
let mut lines = 0;
loop {
let n = unsafe { stdin_read((&mut byte) as *mut u8, 1) };
if n == 0 {
break;
}
if byte == 10 {
lines = lines + 1;
}
}
println!("{} lines", lines);
return 0;
}
When vxc program.vx runs a program, the program gets no standard input
(Vx#3236). So this loop stops at once and prints
0 lines. Compile the program with -c and run the executable to give it input
(Your first program).
Time
std::time measures time. Instant::now() reads a clock that never goes back. elapsed() gives
the time since a reading, as a Duration. The difference later - earlier of two readings is a
Duration too. sleep(d) stops the thread for at least the Duration d.
Duration comes from core::time. Duration::from_secs, from_millis, from_micros and
from_nanos make one. as_secs, as_millis and as_nanos give its length, and subsec_millis
gives the part that is less than a second. + adds two durations
(book_io_sleep_and_measure.vx):
import core::time;
import std::time;
fn main() -> i32 {
let start = Instant::now();
sleep(Duration::from_millis(50));
let waited = start.elapsed();
println!("slept at least 50 ms: {}", waited.as_millis() >= 50);
let step = Duration::from_millis(1500);
println!("{} s and {} ms", step.as_secs(), step.subsec_millis());
let total = step + Duration::from_secs(2);
println!("total {} ms", total.as_millis());
return 0;
}
It prints:
slept at least 50 ms: true
1 s and 500 ms
total 3500 ms
The program does not print the time that it slept, because that time is different in each run.
Networks
std::net has three types. TcpListener waits for TCP connections. TcpStream is one TCP
connection. UdpSocket sends and receives UDP datagrams, which are single messages. Each address
is a text of the form host:port, for example "127.0.0.1:7878".
Each call that can fail gives a Result. Its Err holds a NetError, which says why the call
failed:
BadAddress: the address is nothost:port, or the host has no address.Refused: nothing listens at the address.Reset: the other end closed the connection.InUse: another socket uses the address.Cancelled: the program cancelled thetasksblock of the task while the call waited.Os(code): another failure, with the error number of the system.
This program makes two calls. The address of the first has no port. Port 0 in the second asks the
system for any free port (book_io_network_errors.vx):
import core::result;
import std::net;
// Prints why a network call did not work.
fn describe(e : NetError) -> void {
match e {
NetError::BadAddress => {
println!("bad address");
}
NetError::Refused => {
println!("refused");
}
NetError::Os(code) => {
println!("system error {}", code);
}
_ => {
println!("another error");
}
}
}
fn main() -> i32 {
// An address must be `host:port`. This one has no port.
match unsafe { TcpStream::connect("localhost") } {
Result<TcpStream, NetError>::Ok(_stream) => {
println!("connected");
}
Result<TcpStream, NetError>::Err(e) => {
describe(e);
}
}
// Port 0 asks the system for any free port.
match unsafe { UdpSocket::bind("127.0.0.1:0") } {
Result<UdpSocket, NetError>::Ok(_socket) => {
println!("bound a UDP socket");
}
Result<UdpSocket, NetError>::Err(e) => {
describe(e);
}
}
return 0;
}
It prints:
bad address
bound a UDP socket
The sockets close at the end of their scope, as a File does.
A server calls accept() on a TcpListener. accept waits until a client connects, and gives a
TcpStream. read and write on the stream work as they do on a File. This function sends one
byte back to the client. ? gives the Err to the caller:
fn echo_one_byte(listener : TcpListener) -> Result<u64, NetError> {
let mut server = listener;
let mut client = server.accept()?;
let mut byte : u8 = 0;
let got = unsafe { client.read((&mut byte) as *mut u8, 1) };
let _count = got?;
return unsafe { client.write((&byte) as *const u8, 1) };
}
TCP is a stream of bytes. A message can arrive in parts, so a read can give fewer bytes than the
other end wrote. To get a whole message, read in a loop until you have all of its bytes.