Running Sessions

When we define a Ferrite program with Rust expressions like let program: Session<...> = ..., the program is not executed until we run it at a later time. Ferrite provides two public functions, run_session and run_session_with_result to execute the Ferrite programs.

run_session

async fn run_session(session: Session<End>)

run_session is an async function that accepts a Ferrite program of type Session<End>, and blocks until the program has finished executed.

Since run_session requires the offered protocol to be End, this means that we cannot use it to execute Ferrite programs that offer other protocols, such as ReceiveValue<String, End>. Intuitively, this is because Ferrite cannot know in general how to run Ferrite programs offering any protocol. For instance, how are we supposed to finish run a Ferrite program that offers ReceiveValue<String, End>, if we do not have any string value to send to it?

apply_channel

fn apply_channel<A: Protocol, B: Protocol>(
  f: Session<ReceiveChannel<A, B>>,
  a: Session<A>,
) -> Session<B>

While we cannot execute Ferrite programs offering protocols other than End, we can link that Ferrite program with another program so that as a whole, the resulting Ferrite would offer the protocol End. We have seen one such example of linking hello_provider with hello_client using apply_channel in the previous chapter.

In general, if we have a provider that offers the protocol A, and a client that offers the protocol ReceiveChannel<A, B>, we can call apply_channel(client, provider) to get a new program that would spawn the provider, and forwards the offered channel to the client.

include_session

When we have a Ferrite program that offers protocols like ReceiveValue<i32, End>, we could also write another program that include the first program directly using include_session and interacts with it. This allows us to write new Ferrite programs that offer End if we knows how to interact with the original program.

As an example, consider a show_number program that receives an integer and then prints out the value it receives:

  let show_number: Session<ReceiveValue<i32, End>> = receive_value(|x| {
    println!("The magic number is: {}", x);
    terminate()
  });

We cannot run show_number directly, because it expects an integer to be sent to it. But we can now write a main program that includes show_number as follows:

  let main: Session<End> = include_session(show_number, |chan| {
    send_value_to(chan, 42, wait(chan, terminate()))
  });

  run_session(main).await;

We call include_session by passing it the original show_number program that we want to include. We also pass it a continuation closure, which gives us a channel variable chan that binds to the channel offered by show_number. We then sends the number 42 to chan, wait for it to terminate, and then terminate main.

Because the protocol offered by main is End, we can now run the full program using run_session.

run_session_with_result

async fn run_session_with_result<T: Send + 'static>(
  session: Session<SendValue<T, End>>
) -> T

Although in general Ferrite can only run programs offering End, there is another special case that Ferrite knows how to handle, which is programs that offer protocol in the form SendValue<T, A>. In this case, Ferrite knows to receive the value sent by the program, then wait for the program to terminate before returning the received value to the caller.

It is often more practical to use run_session_with_result instead of run_session, because we may want to write Rust programs that spawn Ferrite processes, then wait for some result to return.

We have seen an example use of run_session_with_result with the hello provider, which we get back the result "Hello World!" string sent by the Ferrite program.