Rust guide

Types

Tuples

In rust, you can create a tuple by let tuple = (x0, x1, x2) and access them by tuple.0, tuple.1, and tuple.2. For example:

let tuple = (1, "hello");
assert_eq!(tuple.0, 1);
assert_eq!(tuple.1, "hello");

Is equivalent to the following Python code:

tuple = (1, "hello")
assert tuple[0] == 1
assert tuple[1] == "hello"

Tuple Structs

A tuple struct is similar to a tuple but the type itself is named:

struct T(i32, String);
let t = T(42, "hello".to_string());
assert_eq!(t.0, 42);
assert_eq!(t.1, "hello");

Enums

enum let you express a type that has several variants:

enum Color {
    Red,
    Green,
    Blue,
}

This is similar to enum in C++, the difference is that Rust allows the variants to contain fields, which make it an Algebraic Data Type:

enum T {
    A(usize),
    B(i32, String),
    C { field: String },
}

Polymorphic Types

This is similar to templates in C++. In Rust:

struct MyType<T> {
    vec: Vec<T>
}

impl<T> MyType<T> { // notice the location of the type variable
    fn new() -> Self {
        Self {
            vec: vec![],
        }
    }
}

In C++:

template<typename T>
struct MyType {
    vector<T> vec;

    static MyType new() {
        MyType ret;
        ret.vec = vector<T>();
        return ret;
    }
}

Pattern matching

match

Use match to match an enum with different variants. The pattern _ will match anything:

match future.poll() {
    Poll::Ready(Some(x)) if x % 2 == 1 => {
        f(x);
    }
    Poll::Ready(_) => return Poll::Pending,
    _ => println!("default match"),
}

if let, while let

Use if let to go into a branch if the expression matches:

if let Poll::Ready(x) = future.poll() {
    return x;
}

There is also while let:

while let Poll::Ready(Some(x)) = stream.poll() {
    f(x);
}
// equivalent to
loop {
    if let Poll::Ready(Some(x)) = stream.poll() {
        f(x);
    } else {
        break;
    }
}

Option

Option<T> is a type that has two variants:

pub enum Option<T> {
    None,
    Some(T),
}

This is similar to nullable types in other languages (e.g., None in Python or null in Java). The difference is that in Rust, you must explicitly wrap a type inside Option so you can set it to be None.

Some important functions you will use in this project:

def unwrap(option):
    assert option is not None
    return option
match x {
    Some(_) => true,
    None => false,
}
resource = self.resource
self.resource = None
do_something(resource)

In rust, you will do

// make self.resource a type of Option<T>
let resource = self.resource.take()
do_something(resource)

Result

Result<T, E> is a type that represents a result that could fail. It is a replacement of error handling using catch and throw in other language.

match do_something() {
    Ok(result) => return f(result),
    Err(err) => {
        println!("err = {}", err),
    }
}

Which does similar things to the following Python code:

try:
    do_something()
except Exception as e:
    print('err = {}'.format(e))

Some handy functions:

Operator ?

You can write result? if result is of type Result. The expression is roughly equivalent to

{
    match result {
        Ok(v) => v,
        Err(err) => return v,
    }
}

This is useful so you can propagate error by appending results with ?:

let file = open("file")?;
let but = file.read_to_string()?;

Lifetime

The lifetime is where a variable lives or a borrow is valid. In rust, lifetime are represent with 'a, 'b, etc. For example:

{
   let x = vec![1, 2, 3];
   {
       let y = &mut x;
       f(y);
       // The borrow of y ends here.
   }
   // x is dropped here.
}

After rust added non-lexical lifetime, the compiler will tweak the lifetime of borrows for you, which makes your life easier, but the lifetime of an object always ends at the end of the scope.

'static

A static lifetime means that the variable can live for the running program’s entire lifetime. It is represented as 'static

Smart Pointers

Box

Box is a pointer to an allocation on the heap. It is equivalent to unique_ptr in C++:
In Rust:

let x = Box::new(1);
let y = x; // x is moved and cannot be used anymore.

In C++:

std::unique_ptr<int> x = std::make_unique<int>(1);
std::unique_ptr<int> y = std::move(x);

Rc

Rc is a reference-counting pointer. It is equivalent to shared_ptr in C++:

let x = Rc::new(1);
let y = Rc::clone(&x); // x is moved and cannot be used anymore.

In C++:

std::shared_ptr<int> x = std::make_shared<int>(1);
std::shared_ptr<int> y = x;

It is useful when multiple ownership to an object is needed.Rc will drop the object when the last reference goes out of scope. Use Rc::clone to get a shallow copy of the object (it only copies the address and increases the reference by one).

Rc only allows you to dereference into the inner type immutably. Thus, you will typically use Rc<RefCell<T>> so that you can modify T, see the section for RefCell.

For x: Rc<T>, if T has a method f, you can simply call x.f(). Rust will dereference the pointer for you (this works for most smart pointers, including Box, etc.).

Arc

Arc has the same interface as Rc, the only difference is that Arc is thread-safe and has more overhead. In this project, you only need Rc most of the time because we are running the whole program in a single thread. The only exception is when you are implementing ArcWake, you need to use Arc because Rust imposes a rule that Waker must be thread safe.

Interior Mutability

Interior mutability allows you to turn an immutable reference into mutable ones, usually in runtime. This is useful when it is hard or impossible to prove that two mutable borrow do not intersect.

RefCell

Refcell let you turn an immutable borrow to a mutable one:

fn f(v: &RefCell<Vec<i32>>) { // Notice that v is an immutable borrow.
   let &mut vec = v.get_mut(); // This will compile.
   vec.push(0);
}

Some important functions for RefCell<T>:

Notice that you must ensure that either there is only one mutable borrow or there are only immutable borrows at any time. RefCell track the number of borrows, so if the property is violated, it will panic.

Cell

Cell<T> is a special version mainly used for type T that is Copy.

Pin

Pin<T> is a type that guaranteed the underlying object cannot be moved, where T is a pointer type (&mut, Box, etc.). We need this guarantee to ensure memory safety because a future can have self-references to itself, which make it unsafe to be moved.

Some useful functions:

Other useful functions:

pin-project

One key operation we need is the ability to project a Pin into fields:

struct Wrap<F: Future> {
    future: F,
}
fn f(pin: Pin<&mut Wrap>) {
    // We need pin.future to be Pin<&mut F> in order to poll the future.
}

To get rid of the need of writing unsafe code ourselves, we use an external library called pin-project. We mark the type using #[pin_project] and its fields as #[pin], and the library will generate extra codes for use (you can imagine that pin_project is a complicated macro). Then, we can use project() to project pin onto the fields:

#[pin-project]
struct Wrap<F: Future> {
    #[pin] future: F,
    unpin: usize,
}
fn f(pin: Pin<&mut Wrap>) {
    let this = pin.project();
    // You can imagine this is
    // WrapPinned {
    //     future: Pin<&mut F>,
    //     unpin: &mut usize,
    // }
    this.future.poll();
}

Trait

Trait is how you do polymorphism in rust. Rust does not have other construct such as inheritance.
You define a trait like:

trait MyTrait {
    fn implemented(&self) -> u32;
    fn provided(&self) -> String {
        implemented(self).to_string()
    }
}

When other people implement MyTrait for their type T, they will need to implement the function named implemented, and they get a default implementation of provided for free (they can also override provided if they want.). The following is an example of implementing MyTrait for Vec<i32>

impl MyTrait for Vec<i32> {
    fn implemented(&self) -> u32 {
        self.len() as u32
    }
}

This is similar to an interface in Java:

public interface MyTrait {
    int implemented();
    default String provided() {
        return implemented().to_string()
    }
}

public class IntVec implements MyTrait {
    public int implemented() {
        return len();
    }
}

Self

In the definition of a trait, Self refers to the type that is implementing this trait:

trait MyTrait {
    fn f(&self) -> Self;
}
impl MyTrait for Vec<i32> {
    fn f(&self) -> Self {
        self.clone()
    }
}

Associated type

An associated type connects a type placeholder with a trait. The implementer must decide the type:

trait MyTrait {
    type Output;
    fn get(&self) -> Self::Output;
}
impl MyTrait for Vec<i32> {
    type Output = i32;
    fn get(&self) -> Self::Output {
        self[0]
    }
}

Bounds

You can put trait bounds on type variables:

fn f<T: MyTrait>(t: T) {
    // Can be called only when T implements MyTrait.
}
fn g<T>(t: T)
where
    T: MyTrait
{
    // The same as above.
}
struct U<T>(T)
where
     T: MyTrait + OtherTrait,  // Use + to indicate intersection.
     T::Output: Clone; // You can also put restrictions on associated types.

Drop

The Drop trait is defined as

trait Drop {
    fn drop(&mut self);
}

drop will be called when the variable goes out of scope, which is equivalent to destructors in C++. You can implement this trait to customize behaviors when dropped.

Clone, Copy

Clone represents types that can be cloned. Copy represents types that can be trivially copied, which usually means a memcpy operation. Most primitive types (e.g., u32, bool, etc.) implements Copy.

Deref, DerefMut

Deref and DerefMut represent types that can be dereference by the operator *. For example, Rc<T> implements Deref<T>, so that if rc: Rc<T>, *rcwill be T. The only difference between Deref and DerefMut is you get an immutable or mutable ones.
When you are calling a function, for example, rc.method(), rust will try to find method in Rc first, if not found, it will try to dereference into inner T and find the method, and so on.

FnOnce, FnMut

In rust, FnOnce represents function types that can be called once, and FnMut represents function that can be called multiple times.

Unpin

Unpin are types that can be moved safely.

PhantomData

When defining a generic type, (e.g., T<A, B>), rust requires that all the type variables are used at least once. Thus, you will see we placed PhantomData inside structs. This type does not take any memory space and creates no overhead.

Hints

General

  1. What is the recommended development environment on Myth?

    1. Install Visual Studio Code and set up remote development extension here
    2. Install rust-analyzer here.
    3. Install rust plugin for Visual Studio Code here.
    4. Run cargo doc --document-private-items on the project root.
    5. Run cd target/doc && python3 -m http.server [port] to open an HTTP server for serving docs.

    You can replace Visual Studio Code with any IDE you like, such as vim, emacs, … The key is to get language client support, for example, vim-lsp, etc.

  2. When installing Rust on Myth, it shows:

    warning: If you are sure that you want both rustup and your 	already installed Rust warning: then please reply y
    

    What should I do?
    Just reply y.

  3. I am getting rustup: command not found.
    Remember to source your ~/.bashrc or re-login.

3.1 Executor

  1. We have released a suggested structure for the executor.
  2. How do I get a Context?
    You create your waker (WakerInner) that implements ArcWake. Then, you call create_waker to get a Waker type. finally, you call Context::from_wakerto create a Context. The whole thing look something like:
let waker_inner = WakerInner { ... };
let waker = create_waker(waker_inner);
let mut context = Context::from_waker(&waker);
  1. If I implement a busy waiting executor, which tests will fail?
    These tests will fail:
    executor::executor::itest_executor_do_not_poll_suspended
    executor::io::itest_executor_io_accept_no_busy_wait
    executor::io::itest_executor_io_read_no_busy_wait
    executor::io::itest_executor_io_write_no_busy_wait
    executor::queue::itest_queue_pop_no_busy_wait
    executor::queue::itest_queue_push_no_busy_wait
    executor::timer::itest_edge_only_most_recent_waker_should_wake
    executor::timer::itest_executor_timer_no_busy_wait
    
    Also, depending on your implementation, some queue tests might fail.
  2. How do I turn a busy waiting executor into a non-busy waiting one?
    1. Change your executor so that it will not push the future that returns pending back to the queue.
    2. Implement ArcWake so that your waker will put the future back to the executor queue when wake_by_ref is called.
    3. Change all your futures so that when they return Pending, they also register the waker (you get that from Context). For example, you will need to call register_fd_events in AsyncRead, etc.
  3. I am getting the following error:
error[E0308]: mismatched types
= note: expected struct `Rc<RefCell<Pin<Box<(dyn Future<Output = ()> + 'static)>>>>`    
           found struct `Rc<RefCell<Pin<Box<F>>>>`

Rust somehow is not able to deduce the type correctly if you let a variable to be

let future = Rc::new(RefCell::new(Box::pin(future)));

put it into the queue directly:

self.queue.borrow_mut().push_back(Rc::new(RefCell::new(Box::pin(future))));
  1. How to take out the future from WakerInner?
    Use Option::take.
  2. Why Handle does not implement Clone?
    Yes, that was a design mistake. You can implement Clone for Handle, but not required.
  3. In the recommended implementation scaffold, won’t the Waker keep holding the future even if the future has completed?
    Yes, in theory, the shared future should be defined as Rc<RefCell<Option<BoxFuture>>>, or you need to destroy all wakers once the future completes. We simplify this part and will not test it.

3.2 Async IO Futures

Networking IO

  1. When should those future return Pending?
    When you get WouldBlock error from the underlying IO operations.
  2. In AsyncAccept, how can I access the TcpListener?
    You access it by self.listener.0.
  3. In AsyncAccept, how can I turn a TcpStream into an AsyncStream?
    Use AsyncStream::from_tcp_stream. Do not construct the stream by yourself or else you might forget to set it to non-blocking mode.
  4. Why isn’t bind and connect asynchronous?
    To implement that you will need to play with Linux system calls, which we leave that for next time.
  5. For read_exact and write_all, just use the read and write async functions you just implemented. You will do something like
    pub async fn read_exact<'a>(&'a mut self, buf: &'a mut [u8]) -> io::Result<()> {
        // ...
        let num_bytes_read = self.read(&mut buf[bytes_read..]).await?;
        // ...
    }
    
    read writes the data to the buffer in-place.
  6. How do we know that we reached EOF?
    In this case, TcpStream::read (and so does your AsyncStream::read) will return 0 (zero byte is read).
  7. How do I use register_fd_events?
    You will pass either &TcpStream or &TcpListener as object, and you will need to specify whether it is a read or write interest. You also need to pass a clone of the waker into it.
    Remember to store the Token returned by register_fd_events. If you drop the token,
    it will deregister the event and your future will not wake up.
  8. Which interest should we register?
    For read and accept, it should Read. For write it should be Write.
  9. My future is not getting waken, why is that?
    There may be various bugs causing this problem, check that:
    1. You called wait_fd_events to let epoll return ready events.
    2. You store the token from register_fd_events to prevent it being drop. When the token is dropped, it will do clean-up and deregister the event.
    3. Your wake does put the future back to your event queue.

Timer

  1. How precise should our timer be?
    Not so much. We allow a margin of [-10ms, 100ms]. You can simply wake up every, say 50ms, to check if any timer expires. Or you can use fancier data structures like std::collections::BinaryHeap.
  2. Do we need to incorporate Token in our design?
    You may omit it. Token is there to ensure the events are cleared after a future is dropped. This ensures that file descriptors are released if the future is dropped before receiving the IO event. We won’t be so strict on checking if you have done the clean-up.
  3. What exactly should I do here?
    You would probably need to extend the reactor so that you can register timer events. For each
    timer event, you will need to store a timestamp and a waker so you can wake up a future after a
    certain amount of time passes. Then, each time you return from wait_fd_events, check if any timer expires

Putting it All Together

  1. You can make those function (send, recv, …) async. That is,
pub async fn send(self, val: T) -> io::Result<Channel</* ... */>>
pub async fn recv(self) -> (io::Result<Channel</* ... */>>, T)
  1. Do the error messages of those compile fail test need to match that of the reference solution?
    No, the only requirement is that they do not compile.
  2. How should we send the data in send, choose_left, and choose_right?
    Follow the guideline in the comment. For example, choose_left should send a single byte 0 and choose_right should send a byte 1.
  3. How do we return a new channel with a different session type?
    Session types are just a marker, and you can simply return a new channel by creating one:
    Channel {
        stream, // equivalent to `stream: stream`,
        phantom: PhantomData
    }
    
  4. What should step in Channel<Var<N>, Env> do?
    You need to figure out the correct return type for step. You will need to restore the type and the Env just after step from the corresponding Rec type is called. For example, If Env = (S0, (S1, (S2, ()))) and we have Var<Succ<Zero>>, then you need to retrieve S1 and restore Env = (S1, (S2, ())). You should see now how you will use Pop<N>. Notice that you will not need separate cases for Var<Zero> and Var<Succ<Zero>>.
  5. I am getting error: SessionType is not implemented for Pop<N>::Head
    You can add bounds to the type, e.g:
    impl<...> Channel<...>
    where
        <Env as Pop<N>>::Head: SessionType
    
  6. Should I add any type bound to the session type list (i.e., Env and Env::List)?
    No, you are not required to place any type bound on the session type list. You are welcome to come up with your own design so that it is ensured that all the types on the list implements SessionType, but all the correct usage still need to compile.
  7. In Recv, how do we create a buffer with dynamic length len?
    You should create a vector instead. Use let buf = vec![0; len].