Posted on by & filed under Content - Highlights and Reviews, Programming & Development.

codeA guest post by Jeffery Olson, a software engineer currently living and working in the Seattle area. He’s still searching for that sweet spot between pragmatism and idealism. Besides contributions within the Rust community, he is interested in building performant HTML5 applications using emerging web technologies like WebGL and WebRTC. He can be reached at @olsonjeffery.
Rust is a programming language, sponsored by Mozilla and an active community of contributors. Its central concerns are:

  • Safety: Rust’s compiler forbids many common categories of errors that arise in languages with manual memory access, such as use-after-free errors that arise from null or dangling pointers. It does this while statically guaranteeing that pointers and references remain valid. But it also provides an escape hatch to disable safety features for performance critical blocks, while allowing programmers to isolate and audit sensitive sections of code.
  • Practicality: The language seeks to make its safety guarantees with no cost at runtime; Its performance target is C++. Rust’s data structures aim to be memory layout compatible with C structs and it provides a rich set of tools for seamlessly interacting with C libraries.
  • Concurrency: Rust provides a Runtime API for concurrency at the Task level, mapping directly onto M:N or 1:1 schedulers. There is no shared memory between Tasks, preventing data races. They communicate via safe messaging. Of course, if this isn’t appropriate to a given context (e.g. kernel/driver development, embedded), a Rust program can opt-out of the Runtime.

This post will introduce Rust’s IO facilities, showing their use for basic filesystem operations. If you’re unfamiliar with the basic goals of Rust or its syntax, it’s recommended that you read the official Rust Tutorial, as this post assumes a passing familiarity with the language.

Appraising The Rust Runtime

Rust strives to provide a set of safe and robust APIs that serve tasks like network services or the need to interact with a user’s filesystem. The underlying code for this, combined with things like task-local-storage, messaging and scheduling primitives, comprise a Rust Runtime implementation. The rust codebase ships, by default, two Runtime implementations, distinguished around the issues of how IO is provided and how concurrency works. Very briefly:

  • The M:N scheduler, in libgreen, is the default Runtime in Rust (as of 0.9, this may change in the future), with IO implemented atop libuv and cooperative, lightweight green tasks running within a single Scheduler (which itself maps to a single OS thread) with all of the Schedulers in a given libgreen runtime running within a Pool of Schedulers with work stealing.
  • The 1:1 scheduler, in libnative, uses OS-provided APIs for all IO and individual tasks map to a single OS thread.

Putting this aside, all of the IO APIs as presented in libstd (and this post) are agnostic to the underlying Runtime implementation. This provides the programmer with the flexibility to choose the right Runtime for their needs, even allowing a mix of Runtimes within a single process. Library maintainers writing Rust code that relies on the libstd APIs can do so knowing that their code will work in either environment.

Loading A File

Given a simple text file located in the same directory as where the program is executing from, here is a simple Rust snippet that will read the file and print it out for the user:

The three use lines at the top are pulling in various bits of functionality needed to read from a file and parse it into a string.

Within the main() function, a Path is created pointing at the target file. It is then loaded with a call to File::open with a reference to path. Then the contents of the file are simultaneously read from the newly created hw_file with read_to_end and parsed to string format using from_utf8_owned. The result is stored in data.

Finally, the file is printed to stdout with the call to the println! macro. Since println! expects a static string, one is provided with a {:s} formatter along with the data value. More information about println! and format! can be found in the libstd docs for std::fmt.

Improvements And Variations

While useful for demonstration, it should be noted that read_to_end allocates and returns a ~[u8] byte vector. This approach is not very efficient in actual production scenarios where many reads would be happening in a main loop. More commonly, a programmer would stack-allocate a vector of empty bytes, and they would pass in a buffer slice (a reference to contiguous region of homogenous data, in-memory), which the method would put the file’s data into. This buffer, provided it remains in scope, can be reused any number of times while the program is still reading input:

This is the Rust version of the common “byte buffer” pattern in C et al for reading data. Of course, Rust’s approach has strong safety guarantees.

The hw_file representing the open file handle is explicity opened, but never closed. The simple explanation for this is that File implements the Drop trait, which is Rust’s way of providing destructor semantics. When the hw_file variable goes out of scope, the file handle underlying the File is closed out before the rest of its data is freed. When a programmer wishes to explicitly control a resource’s lifetime (for example: avoiding multiple handles to the same file simultaneously), they can be limited with scopes:

It is also possible to explicitly invoke a value’s destructor and remove it from the current scope by calling drop(value). In the above example this could be done with drop(read_only), removing the need for explicit scopes.

And although read_only was opened read-only (the defined behavior when calling File::open), it was stored in a mutable variable (as seen in let mut read_only = ...). This is because, although the file itself is read-only, its internal position counter is changed on calls to read-related functions. In this regard, File has read(2)-like behavior.


More information on the std::io::fs module can be found in the libstd API docs, which provide a good overview of the module, as well some common use cases. Much of Rust’s IO API is defined in this manner: Expose simple APIs that leverage the strengths of the language’s type system to expose simple, safe and performant APIs to the end user.

For more details about general programming languages, see the Safari Books Online resources referenced below.

Not a subscriber? Sign up for a free trial.

Safari Books Online has the content you need

Practical Foundations for Programming Languages offers a fresh perspective on the fundamentals of languages through the use of type theory. Whereas most textbooks on the subject emphasize taxonomy, Harper instead emphasizes genetics, examining the building blocks from which all programming languages are constructed. Language features are manifestations of type structure. The syntax of a language is governed by the constructs that define its types, and its semantics is determined by the interactions among those constructs. The soundness of a language design – the absence of ill-defined programs – follows naturally.

Tags: C#, concurrency, practicality, Rust, safety,

Comments are closed.