-
Notifications
You must be signed in to change notification settings - Fork 2
Ping Pong Example
In this example, we alternate execution between two sides of a connection, incrementing a number each time.
Below is the overall Waldo file. Don't worry, we'll step through each line in a second. At this point, we're just showing the full file to give you a sense of the overall structure of a Waldo file and to give you a feel for the Waldo basic syntax.
PingPong
Endpoint Ping;
Endpoint Pong;
Sequences {
PingPongSeq: Ping.start_ping -> Pong.perform_pong -> Ping.next_ping -> Pong.final_pong;
}
Sequence PingPongSeq (Number start_num) returns Number final_num {
Ping.start_ping {
start_num += 1;
}
Pong.perform_pong {
start_num += 1;
}
Ping.next_ping {
start_num += 1;
}
Pong.final_pong {
start_num += 1;
final_num = start_num;
}
}
Ping
{
Public Function ping_seq(Number to_ping_with) returns Number
{
return PingPongSeq(to_ping_with);
}
}
Pong
{}
The first few lines of any Waldo file specify the name of the protocol that you are writing as well as the name of the endpoints in the file.
PingPong
Endpoint Ping;
Endpoint Pong;
In this example, our protocol, named PingPong
, is composed of two endpoints, Ping
and Pong
. An endpoint is one side of a protocol, encapsulating all of that side's logic and state. A Waldo file can have 1 or 2 Endpoints specified. But cannot have more or less. Later parts of the protocol file actually define the logic and state that each endpoint hold as well as how they interact to perform actions.
Sequences in Waldo are linearly ordered blocks that switch execution between each endpoint in a protocol. The sequences declaration immediately follows the file header:
Sequences {
PingPongSeq: Ping.start_ping -> Pong.perform_pong -> Ping.next_ping -> Pong.final_pong;
}
In this case, we've declared that our protocol will have a single sequence, PingPongSeq
. When the seuqence is invoked (more later), PingPongSeq
starts by executing the sequence block start_ping
on endpoint Ping
. Following the execution of this block, PingPongSeq
will automatically execute perform_pong
on endpoint Pong
, then next_ping
on Ping
, and, finally, final_pong
on Pong
. Sequence declarations must be linearly ordered and must alternate between endpoints. If you wanted to have additional sequences in your protocol, you would declare them in a similar way after the first. For instance, to define a PongPingSeq
sequence in addition to the PingPongSeq
:
Sequences {
PingPongSeq: Ping.start_ping -> Pong.perform_pong -> Ping.next_ping -> Pong.final_pong;
PongPingSeq: Pong.start_pong -> Ping.perform_ping -> Pong.next_pong -> Pong.final_ping;
}
Following sequence declarations, comes definitions of each sequence. In our example, we only have a single sequence, PingPongSeq
, which is defined by:
Sequence PingPongSeq (Number start_num) returns Number final_num {
Ping.start_ping {
start_num += 1;
}
Pong.perform_ping {
start_num += 1;
}
Ping.next_ping {
start_num += 1;
}
Pong.final_pong {
start_num += 1;
final_num = start_num;
}
}
Notice that the sequence takes in a single argument, start_num
, with type Number
and returns a final_num
, also with type Number
. (For a list of Waldo's types, see the type system page.)
A sequence definition is decomposed into multiple sequence blocks, in this example, Ping.start_ping
, Pong.perform_ping
, Ping.next_ping
, and Pong.final_pong
. Notice that both the order and names of these blocks mirror the sequence declaration. Sequence blocks specify where code is run. For instance, the block Ping.start_ping
runs on the host that the endpoint Ping
is instantiated on. Conversely, the block Pong.perform_ping
runs on the host that the endpoint Pong
is instantiated on. Notice that in this example, both endpoints have access to the variable start_num
(as well as final_num
). Arguments passed into functions as well as return types are sequence global, meaning that either endpoint in a connection can write and read to them. Using sequence global variables, each endpoint can exchange information.
Recall that the top of the protocol file declared two endpoints, Ping
and Pong
. Sequences specify endpoint logic applied when communicating between endpoints. All other endpoint logic and data are specified in each endpoint's definition. The following code defines each endpoints' additional methods and data:
Ping
{
Public Function ping_seq(Number to_ping_with) returns Number
{
return PingPongSeq(to_ping_with);
}
}
Pong
{}
As you can see, Pong
has no additional methods or data. However, Ping
, supports one additional method, ping_seq
. The type signature for ping_seq
says that it takes in a single Number
and returns a Number
. All that ping_seq
does is invoke the sequence we declared above.
It may seem strange to so trivially wrap PingPongSeq
. The reason we wrap the sequence call is because non-Waldo code can only call an endpoint object's Public
methods.
Copy the code above into a single file. From the Waldo base directory, call bin/wcompile.py [filename]
on it. This should produce a file, emitted.py
, that contains definitions and declarations for both the Ping
and Pong
endpoint in it. You can use each separately.
Waldo compiles down to Python. So (at this time), any program that uses emitted Waldo code should be written in Python. Here are two example files, one to instantiate a Ping
endpoint and one to instantiate a Pong
endpoint. This example assumes that you have already compiled your PingPongProtocol to the file emitted.py.
Instantiate Pong
:
from lib import Waldo
from emitted import Pong
import time
def pong_connected(endpoint):
print '\nPong endpoint is connected!\n'
Waldo.tcp_accept(
Pong, 'localhost',6767, connected_callback=pong_connected)
time.sleep(5)
Instantiate Ping
:
from lib import Waldo
from emitted import Ping
import time
ping = Waldo.tcp_connect(Ping, 'localhost', 6767)
print ('\nThis is result of ping seq: ' + str(ping.ping_seq(0)))
Note that these examples presuppose that the lib/
folder of Waldo is on your sys path. If it is not, you may have to "point" to it by changing the line:
from lib import Waldo
Let's look more closely at the file that instantiates Pong
. The line
Waldo.tcp_accept(
Pong, 'localhost',6767, connected_callback=pong_connected)
binds to TCP port 6767 listening for connections. When it receives a connection, it creates a Pong
endpoint and executes the callback function pong_connected
, passing the newly created Pong
endpoint to it.
Finally, we call time.sleep(5)
to wait five seconds for incoming connections. After this period, we end the program.
Correspondingly, the file that instantiates a Ping
endpoint connects to the TCP port 6767 and creates an endpoint. We can then call that endpoint's public methods.