This is a tutorial for using the Scribble-Java toolchain (for Scribble version 0.4).
Scribble-Java is a toolchain for programming distributed applications in Java based on the theory of multiparty session types.
The main input and output of the toolchain are:
|
|
(Draft) revision for Scribble 0.4. |
This quick start will run through the specification of a simple application protocol in Scribble, to Java implementations of client and server endpoints using the Scribble-generated APIs.
We illustrate a client-server protocol for a network service that adds two numbers.
An outline of the protocol:
Below we give a Scribble specification of the Adder application protocol.
This code is available from the Scribble GitHub repository:
|
We use the above example to give a brief overiew of Scribble syntax. The links give further details of each element.
type <java> "java.lang.Integer" from "rt.jar" as Int;declares java.lang.Integer as a Scribble payload type, Int, for use in this Scribble module.
global protocol Adder(role C, role S) { ... }
choice at C { ... } or { ... }
states that the protocol proceeds according to one of the or-separated blocks.
Add(Int, Int)specifies:
Add(Int, Int) from C to S;
do Adder(C, S);allows a protocol to be defined in terms of other (sub)protocols. It is also used for recursive protocol definitions, as for Adder in this example.
The command given below assumes:
scribble-demos/scrib/tutorial/src/tutorial/adder/Adder.scr(As found in the Scribble-Java GitHub repository.)
In a Linux, Cygwin/Windows, or Mac OS terminal, the command is:
./scribblec.sh scribble-demos/scrib/tutorial/src/tutorial/adder/Adder.scr -api Adder C -d scribble-demos/scrib/tutorial/src/
The above command performs the following tasks.
See here for more information on using the command line tool.
We use the Endpoint API generated in the previous step to summarise the key elements behind Scribble Endpoint APIs.
We then give an example implementation using the above Endpoint API.
Below is the endpoint finite state machine (FSM) for the C role in Adder.
|
So that they may be used as types in Java, Scribble generates the various role and operator names in the source protocol as singleton types. For the Adder protocol, Scribble generates the following singleton types as API constants:
| Roles | C, S |
| Operators | Add, Res, Bye |
The API generation renders each state in the Endpoint FSM as a Java class for a state-specific communication channel (which we call a state channel for short).
See here for an example Javadoc generated (by the standard javadoc tool) from the Scribble-generated API for C in Adder.
The following summarises the key state channel classes and their I/O methods.
| Type | Method |
| Adder_C_2 | send(S role, Add op, Integer arg0, Integer arg1) |
| EndSocket | send(S role, Bye op) |
| Type | Method |
| Adder_C_1 | receive(S role, Res op, Buf<? super Integer> arg0) |
We explain the Endpoint API for S later. It features the API generation for non-unary receive (i.e., branch) states, which is not demonstrated by the API for C.
Usage contract of Scribble-generated Endpoint APIs
Starting from an instance of the initial state channel, an endpoint implemention should proceed by calling one method on the current channel to obtain the next, up to the end of the protocol (if any).
Below we give an example implementation of a C endpoint in an application of Adder. We use the Scribble-generated API following the above usage contract. This client uses the Adder service to calculate the n-th Fibonacci number.
The full code is available from the Scribble GitHub repository:
|
We summarise the key elements in the above code.
The main method contains a typical preamble for an implemention of a client endpoint using an Endpoint API.
Adder adder = new Adder();The Session Class in this example, Adder, is part of the generated Endpoint API.
try (MPSTEndpoint<Adder, C> client =
new MPSTEndpoint<>(adder, C, new ObjectStreamFormatter())) { ... }
client.connect(S, SocketChannelEndpoint::new, "localhost", 8888);connect is the client-side operation for requesting a connection.
new Adder_C_1(client)
The fibo method contains the main body of the protocol implementation using the Endpoint API explained earlier. It takes the initial state channel, Adder_C_1, and follows the protocol through to the terminal state channel, EndSocket, returned by c1.send(S, Bye).
We give an example implementation of an S server for Adder. We highlight the features that are different or not shown in the example client implementation.
The full code is available from the Scribble GitHub repository:
|
server.accept(ss, C);
Adder_S_1 is the initial state channel of the Endpoint API generated for S. This state is a non-unary receive state, or branch state.
The Adder_S_1 class has an instance method:
Adder_S_1_Cases branch(C role)
This method blocks until a message is received. It returns a new instance of Adder_S_1_Cases that has a field op of the generated type:
enum Branch_S_C_Add_Int_Int__C_Bye_Enum implements OpEnum {
Add, Bye
}
The name of the enum is a mechanically derived name that does not need to be exposed in endpoint implementation code in typical usage. The API sets the value of the op field of the returned Adder_S_1_Cases according to the operator of the received message.
Adder_S_1_Cases has the instance methods:
Adder_S_2 receive(C role, Add op, Buf<? super Integer> arg1, Buf<? super Integer> arg2)
EndSocket receive(C role, Bye op)
In each case of the switch(cases.op) statement, the corresponding method is used as a "cast" to obtain the appropriate state channel from the Cases object. The above methods are generated to throw a ScribbleRuntimeException if the wrong method is used.
This section describes the syntax of Scribble modules and global protocols in more detail. It describes the syntactic constraints imposed by Scribble on global protocol definitions.
A Scribble module is a file with the following elements, in order.
module mypackage.MyModule;
import somepackage.subpackage.SomeModule;
type <java> "java.lang.Integer" from "rt.jar" as Int;
Message signature name declarations are explained later in this tutorial.
The notion of well-formed module is discussed here.
A message signature specifies the key elements of a message.
op ( pay1, ..., payn )
A Scribble global protocol describes the interactions between the participants of a communication session from the global (i.e., neutral) perspective. A Scribble protocol abstracts session participants as named roles. Sessions are sometimes referred to as conversations.
Scribble protocols are strictly explicit about session interactions -- the only application-level messages to be communicated in a Scribble session are those that are explicitly specified in the protocol.
global protocol MyProto(role r1, ..., role rn) {
...
}
A message interaction is a role-to-role pairing of a send action and a receive action of a message.
The message may be specified as a message signature, i.e., an operator and a payload:
op(pay) from r1 to r2;
or as a message signature name:
Msg from r1 to r2;
In both cases, r1 is the sender role and r2 is the receiver role.
Send actions are considered output actions, and receive actions are considered input actions.
The choice statement designates a branch point in the protocol. The protocol proceeds according to one of the or-separated cases.
choice at r { ... } or ... or { ... }
A choice statement is subject to the following syntactic constraints.
The do statement instructs the specified roles to perform the interactions of the specified protocol inline with the current session.
do MyProto(r1, ..., rn);
In other words, each role in the role argument list plays the corresponding role in the role declaration list of the target protocol declaration.
A do statement may be used to define (mutually) recursive protocols.
In addition to the previously stated constraints on do statements, Scribble global protocol definitions must be tail recursive per role. This means:
Message signature name declarations may occur alongside payload type declarations.
sig <java> "javapackage.JavaClass" from "myjar.jar" as MyMessage;
In message interactions that specify message signature names, the message signature name itself is used to identify the message (cf. the operator in a message signature).
A global protocol declaration may have the modifier explicit.
explicit global protocol MyProto(...) { ... }
The communication model of a non-explicit global protocol starts with all roles pairwise connected by binary channels.
By contrast, the communication model of an explicit global protocol starts with all roles unconnected. A pair of roles are connected by:
connect r1 to r2;
A connection interaction is a role-to-role pairing of a connection request action and a connection accept action. Role r1 is the client role, and r2 is the server role.
A connection interaction is a synchronous interaction between the client and server, i.e., the first role to perform either connection action is blocked until the other role performs the counterpart action. The connection interaction establishes a bidirectional binary (i.e., two-party) channel with the characteristics of the Scribble communication model, i.e. message delivery is reliable and ordered in each direction.
A pair of roles are disconnected by:
disconnect r1 and r2;
Each of the two disconnect actions in the above pairing is a local action, i.e. it does not directly pertain to any interaction between the two roles. A disconnect action closes the input queue at the local endpoint of the binary channel between these two roles.
In the context of Scribble, request actions are considered output actions, and accept actions are considered input actions.
An explicit global protocol is subject to the following syntactic constraints.
This means there is at most one connection between any two roles.
This section describes the notion of whether a global protocol is valid in Scribble.
Scribble validates that a global protocol satisfies a set of desirable properties. These include safety properties, such as no reception errors (a role never receives an unexpected message), and liveness properties, such as eventual reception (a sent message can always eventually be received).
A Scribble global protocol is validated by two steps.
Specifically, the second step is conducted on a 1-bounded model of the protocol. That is, the model given by the exhaustive execution of the system of Endpoint FSMs where each communication channel is limited to a maximum capacity of one message in each direction.
The Scribble validation of global protocols is sound. This means a valid protocol satisfies the safety and liveness properties in the general setting, i.e., the system of Endpoint FSMs with unbounded channels. Soundness is due to the characteristics of the protocol (i.e., the restrictions on endpoint behaviours) imposed by syntactic well-formedness.
A module is well-formed if every global protocol in the module is valid.
The Endpoint FSM for any role of a syntactically well-formed Scribble global protocol has the following characteristics.
We first introduce some terminology.
A valid Scribble global protocol satisfies the following safety properties.
A valid Scribble global protocol satisfies the following liveness properties.
Scribble supports an option to consider unfair endpoint implementations of the choice between actions at output states. Here, "unfair" means that the implementation of the choice may never perform a particular action, even if the choice may be repeated infinitely often (i.e., in a recursive protocol).
For this option, Scribble adopts a "worst case" view where endpoint implementations exercise a minimal number of output choice options during session execution. This is affected by a model transformation that commits a role to perform again the same action whenever an output state is revisited.
See here for the instructions on enabling or disabling this option in the command line tool.
This section describes the generation of protocol-specific Java Endpoint APIs by Scribble in more detail.
An Endpoint API for a role in a global protocol has two parts.
For a global protocol, e.g. Adder, in the Scribble module tutorial.adder.Adder, Scribble generates the Session Class as follows.
public Adder()
An Endpoint API uses classes from the base Scribble API, which will be referred to in the following sections.
public MPSTEndpoint(P sess, R self, ScribMessageFormatter f)
A global protocol may feature several kinds of names: roles, message operators and message signature names.
Scribble generates a Java singleton type for each of these names. The following illustrates the code generated for the C role in Adder.
package tutorial.adder.Adder.Adder.roles;
public final class C extends org.scribble.sesstype.name.Role {
private static final long serialVersionUID = -6429099329627603991L;
public static final C C = new C();
private C() {
super("C");
}
}
The generation for message operators and message signature names is similar.
For convenience, the Session Class is also generated with field constants referring to the singleton type instances. E.g., the Adder Session Class has the following fields:
public static final C C = tutorial.adder.Adder.Adder.roles.C.C;
public static final S S = tutorial.adder.Adder.Adder.roles.S.S;
public static final Add Add = tutorial.adder.Adder.Adder.ops.Add.Add;
public static final Bye Bye = tutorial.adder.Adder.Adder.ops.Bye.Bye;
public static final Res Res = tutorial.adder.Adder.Adder.ops.Res.Res;
The generation of a State Channel API for a role in a global protocol is based on the Endpoint FSM of that role.
Assume a valid global protocol G in a well-formed module mypack.MyMod. Let s be a state in the Endpoint FSM of G for role r.
If s is a terminal state in the Endpoint FSM, the Scribble generates:
N.B. Henceforth, references to a class G_r_s where s is a terminal state should be considered as EndSocket.
Otherwise (i.e., if s is not a terminal state):
public G_r_s(MPSTEndpointOtherwise G_r_s has no public constructor.ep) throws ScribbleRuntimeException
Let:
For each send action ai to receiver role r_i for message m:
public G_r_si send(ri role, Op op, Pay1 arg1, ..., Payn argn) throws ScribbleRuntimeException, IOException
public G_r_si send(ri role, M msg) throws ScribbleRuntimeException, IOException
The send methods are generated to return without blocking.
For each connect action ai to receiver role r_i, Scribble generates an instance method in class G_r_s:
public G_r_si connect(ri role, Callable<? extends BinaryChannelEndpoint> cons, String host, int port)
throws ScribbleRuntimeException, IOException
The connect method is generated to block until the connection is established or an exception is thrown.
Let:
If s is a unary receive state, i.e., n = 1, and a1 specifies message m, then:
public G_r_s1 receive(r' role, Op op, Buf<? super Pay1> arg1, ..., Buf<? super Payn> argn)
throws ScribbleRuntimeException, IOException, ClassNotFoundException
public G_r_s1 receive(r' role, Buf<? super M arg1)
throws ScribbleRuntimeException, IOException, ClassNotFoundException
The receive methods are generated to block until a message is received.
Otherwise, i.e., n > 1, Scribble generates:
public G_r_si_Cases branch(r' role) throws ScribbleRuntimeException, IOException
The branch method is generated to block until a message is received.
For each (receive) action ai from sender r' for message mi:
public G_r_si receive(r' role, Op op, Buf<? super Pay1> arg1, ..., Buf<? super Payn> argn)
throws ScribbleRuntimeException, IOException, ClassNotFoundException
public G_r_si receive(r' role, Buf<? super M arg1)
throws ScribbleRuntimeException, IOException, ClassNotFoundException
org.scribble.net.Buf<T> is a utility class in the Scribble API.
The receive methods in both of the above cases are generated to write the each of the received payload values or message object to the val field of the corresponding Buf argument before returning.
Let:
Scribble generates an instance method in class G_r_s:
public G_r_s1 accept(ri role, ScribServerSocket ss) throws ScribbleRuntimeException, IOException
The accept method is generated to block until the connection is established or an exception is thrown.
If s is a disconnect state in the Endpoint FSM of G for role r:
public G_r_s' disconnect(r' role) throws ScribbleRuntimeException, IOExceptionwhere r' is the object role of the disconnect action of the transition from s, and s' is the successor state.
Scribble enforces a linear usage discipline on state channel instances, by generating run-time checks into the Endpoint API. This means an endpoint implementation should invoke one I/O method on every instance of a state channel, apart from EndSocket. The following situations throw a ScribbleRuntimeException.
This section describes additional details of endpoint implementation using Scribble-generated APIs and the associated safety properties.
The following is from the example implementation of a C client in Adder.
Adder adder = new Adder();
try (MPSTEndpoint<Adder, C> client =
new MPSTEndpoint<>(adder, C, new ObjectStreamFormatter())) {
client.connect(S, SocketChannelEndpoint::new, "localhost", 8888);
/* Body of endpoint implementation, starting from new Adder_C_1(client)*/
}
Client-side connection requests are peformed by the connect method of MPSTEndpoint.
client.connect(S, SocketChannelEndpoint::new, "localhost", 8888);
The above method has the same signature as the State Channel API connect method generated for explicit connect actions.
The following is from the example implementation of a S server in Adder.
try (ScribServerSocket ss = new SocketChannelServer(8888)) {
while (true) {
Adder adder = new Adder();
try (MPSTEndpoint<Adder, S> server
= new MPSTEndpoint<>(adder, S, new ObjectStreamFormatter())) {
server.accept(ss, C);
new AdderS().run(new Adder_S_1(server));
} catch (ScribbleRuntimeException | IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
}
Server-side connections are accepted via an instance of org.scrible.net.scribsock.ScribServerSocket.
try (ScribServerSocket ss = new SocketChannelServer(8888)) { ... }
A connection is accepted by the accept method of MPSTEndpoint.
server.accept(ss, C);
The actions performed in a non-explicit connection phase are not verified in any way. This means it is up to the programmer to avoid errors such as the following.
The following is the API usage contract for endpoint implementations using Scribble-generated Endpoint APIs.
Starting from an instance of the initial state channel, an endpoint implemention should proceed by calling one method on the current channel to obtain the next, up to the end of the protocol (if any).
An Endpoint API is generated to throw a ScribbleRuntimeException at run-time if the above rule, i.e. linear usage of state channels, is violated. The I/O action of the offending method invocation will not be performed.
Note that the only way that an endpoint implementation using an Endpoint API may attempt to violate the protocol is by violating state channel linearity. This confers the following form of safety for Scribble endpoint implementations.
The source code of the master version of Scribble-Java is available from GitHub.
https://github.com/scribble/scribble-java.git
We give basic instructions for two ways of building the Scribble-Java tool from source. Both ways will create the file
dist/target/scribble-dist-0.4-SNAPSHOT.zip
under the root directory of the scribble-java project.
Building Scribble-Java using command line Maven
These instructions have been tested on Ubuntu 16.04.2 using:
From the root directory of the scribble-java project:
Building Scribble-Java using Eclipse
These instructions have been tested for:
N.B. Eclipse must be configured to use a JDK 8 (Preferences → Java → Installed JREs).
Steps:
The scribble-dist-0.4-SNAPSHOT.zip built by the above instructions contains:
The script is immediately executable after extracting the zip contents.
Alternatively, the script is executable from the scribble-java root directory, without lib, by:
Assuming the scribblec.sh script is installed at the current directory, various command line options are shown by:
./scribblec.sh --help
The following summarises a few options mentioned in this tutorial:
| -ip import_path | Sets the import path for module imports. |
| -fair | Disables the model transformation and checking for "unfair" endpoint implementations. |
A command for validating the example Adder protocol and generating the API for the C role was given here.
Further examples of using the scribblec.sh script can be found in this README.