diff --git a/testdata/examples/04_native_components.pdl b/testdata/examples/04_native_components.pdl index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..3f8a22ea785a4645acfcac6733efb31b416f15b2 100644 --- a/testdata/examples/04_native_components.pdl +++ b/testdata/examples/04_native_components.pdl @@ -0,0 +1,173 @@ +// We'll start by important the standard library that defines the builtin +// components that support a TCP listener and a TCP client. + +import std.internet::*; + +// We'll define a little utility used through this document that is called to +// retrieve the port we're going to listen on. + +func listen_port() -> u16 { + return 2392; +} + +// Next we define our server. The server accepts (for the case of this example) +// a number of connections until it will stop listening. At that point it will +// wait until it receives a signal that allows it to shut down. + +comp server(u32 num_connections, in<()> shutdown) { + // Here we set up the channels for commands, going to the listener + // component, and the channel that sends new connections back to us. + channel listen_cmd_tx -> listen_cmd_rx; + channel listen_conn_tx -> listen_conn_rx; + + // And we create the tcp_listener, imported from the standard library, here. + new tcp_listener({}, listen_port(), listen_cmd_rx, listen_conn_tx); + + // Here we set up a variable that will hold our received connections + channel client_cmd_tx -> unused_client_cmd_rx; + channel unused_client_data_tx -> client_data_rx; + auto new_connection = TcpConnection{ + tx: client_cmd_tx, + rx: client_data_rx, + }; + + auto connection_counter = 0; + while (connection_counter < num_connections) { + // We wait until we receive a new connection + print("server: waiting for an accepted connection"); + sync { + // The way the standard library is currently written, we need to + // send the `tcp_listener` component the command that it should + // listen to for the next connection. This is only one way in which + // the standard library could be written. We could also write it + // such a way such that a separate component buffers new incoming + // connections, such that we only have to `get` from that separate + // component. + // + // Note that when we get such a new connection, (see the + // TcpConnection struct in the standard library), the peers of the + // two ports are already hooked up to a `tcp_client` component, also + // defined in the standard library. + put(listen_cmd_tx, ListenerCmd::Accept); + new_connection = get(listen_conn_rx); + } + + // In any case, now that the code is here, the synchronous round that + // governed receiving the new connection has completed. And so we send + // that connection off to a handler component. In this case we have the + // `echo_machine` component, defined in this file as well. + print("server: spawning an echo'ing component"); + new echo_machine(new_connection); + connection_counter += 1; + } + + // When all of the desired connections have been handled, we first await a + // shutdown signal from another component. + print("server: awaiting shutdown signal"); + sync auto v = get(shutdown); + + // And once we have received that signal, we'll instruct the listener + // component to shut down. + print("server: shutting down listener"); + sync put(listen_cmd_tx, ListenerCmd::Shutdown); +} + +// This is the component that is spawned by the server component to handle new +// connections. All it does is wait for a single incoming TCP packet, where it +// expects a single byte of data, and then echo that back to the peer. + +comp echo_machine(TcpConnection conn) { + auto data_to_echo = {}; + + // Here is where we receive a message from a peer ... + sync { + print("echo: receiving data"); + put(conn.tx, ClientCmd::Receive); + data_to_echo = get(conn.rx); + put(conn.tx, ClientCmd::Finish); + } + + // ... and send it right back to our peer. + print("echo: sending back data"); + sync put(conn.tx, ClientCmd::Send(data_to_echo)); + + // And we ask the `tcp_client` to shut down neatly. + print("echo: shutting down"); + sync put(conn.tx, ClientCmd::Shutdown); +} + +// Here is the component that we will instantiate to connect to the `server` +// component above (more specifically, to the `tcp_listener` component +// instantiated by the `server`). This is the component that will ask the +// `echo_machine` component to echo a byte of data. + +comp echo_requester(u8 byte_to_send, out<()> done) { + // We instantiate the `tcp_client` from the standard library. This will + // perform the "connect" call to the `tcp_listener`. + channel cmd_tx -> cmd_rx; + channel data_tx -> data_rx; + new tcp_client({127, 0, 0, 1}, listen_port(), cmd_rx, data_tx); + + // And once we are connected, we send the single byte to the other side. + print("requester: sending bytes"); + sync put(cmd_tx, ClientCmd::Send({ byte_to_send })); + + // This sent byte will arrive at the `echo_machine`, which will send it + // right back to us. So here is where we wait for that byte to arrive. + auto received_byte = byte_to_send + 1; + sync { + print("requester: receiving echo response"); + put(cmd_tx, ClientCmd::Receive); + received_byte = get(data_rx)[0]; + put(cmd_tx, ClientCmd::Finish); + } + + // We make sure that we got back what we sent + while (byte_to_send != received_byte) { + print("requester: Oh no! we got back a byte different from the one we sent"); + } + + // And we shut down the TCP connection + print("requester: shutting down TCP component"); + sync put(cmd_tx, ClientCmd::Shutdown); + + // And finally we send a signal to another component (the `main` component) + // to let it know we have finished our little protocol. + sync put(done, ()); +} + +// And here the entry point for our program + +comp main() { + // Some settings for the example + auto num_connections = 12; + + // We create a new channel that allows us to shut down our server component. + // That channel being created, we can instantiate the server component. + channel shutdown_listener_tx -> shutdown_listener_rx; + new server(num_connections, shutdown_listener_rx); + + // Here we create all the requesters that will ask their peer to echo back + // a particular byte. + auto connection_index = 0; + auto all_done = {}; + while (connection_index < num_connections) { + channel done_tx -> done_rx; + new echo_requester(cast(connection_index), done_tx); + connection_index += 1; + all_done @= {done_rx}; + } + + // Here our program starts to shut down. First we'll wait until all of our + // requesting components have gotten back the byte they're expecting. + auto counter = 0; + while (counter < length(all_done)) { + print("constructor: waiting for requester to exit"); + sync auto v = get(all_done[counter]); + counter += 1; + } + + // And we shut down our server. + print("constructor: instructing listener to exit"); + sync put(shutdown_listener_tx, ()); +} \ No newline at end of file