Introduction

The Network Service Definition Language is used to specify the message exchange between client / server applications. The language specifies the messages, their parameters, message direction, timeout, authorization, delivery style, custom types, constants, and documentation for the service and each message, parameter, type, and constant. A compiler can then be used to process the NSDL into meta-data and code to support implementing clients and servers in a variety of different programming languages and environments with the help of a small runtime library.

Syntax Elements

The NSDL uses syntax elements common to other programming languages:

  • Keywords are used to identify grammar elements such as service, constants, enumerations, structured types, external types, exceptions, and native types. The keywords are: boolean, byte, const, double, enum, exception, extends, extern, false, float, int, long, mixin, include, module, null, object, service, short, string, struct, throws, true, and void. Keywords may not be used as an identifier for any service defined element.
  • Boolean, numeric, and string literals are used to specify the values of constants, and as arguments to annotations. Examples include true, 49, 3.14159, and "boo". The usual hexadecimal, octal, and binary integral formats (e.g., 0x23, 023, and 0b011 respectively) are also supported. The usual string escapes as well as the Unicode escape are supported:
    \t, \n, \r, \\, \", and \u03a9
    
  • Identifiers are used to specify the name of the service, each message, parameter, custom type, and constant. An identifier is composed of ASCII characters, may start with an upper or lowercase alphabetic, and may contain upper and lower case alphabetic, numeric, or underscore characters. While not enforced (yet), the recommended style is that service, enum, struct, exception, and externnames should start with uppercase and use camel case conventions (e.g., FastCar, BlackCatWalking, etc.). Message and parameter names should start with lowercase and use camel case conventions (e.g., makeCall, sendName, etc.). Constants and enum elements should be all uppercase with underscores (e.g., FISH_SOUP, STALE_BREAD, etc.).
  • Definitions may be terminated by a semi-colon ( ";" ) but that is totally optional.
  • Comments may be the standard Java types:
    • // causes the rest of the line to be ignored.
    • /* blah */ causes blah to be ignored; may extend over multiple lines; may not be nested.
    • /** blah */ is a formal comment. Adds a description to the following definition (if allowed). Follows Javadoc style. Descriptions are copied into the generated interfaces.

Native Data Types

The native types include:

  • boolean - true or false
  • byte - signed 8-bit quantity (-2^7 to 2^7-1)
  • short - signed 16-bit quantity (-2^15 to 2^15-1)
  • int - signed 32-bit quantity (-2^31 to 2^31-1)
  • long - signed 64-bit quantity (-2^63 to 2^63-1)
  • float - single precision IEEE floating point (max 3.4028235e38)
  • double - double precision IEEE floating point (max 1.7976931348623157e308)
  • string - sequence of characters (generally, Unicode), arbitrary length

These types correspond to the architectural types found on almost all computer systems.

Extended Types

Common extended types are also included.

Datetime

Datetime represents a specific moment in time.

Examples

Datetime timeSaved;
boolean isSunday ( Datetime time )

List

A List represents an ordered collection of values.

Examples

List values;
object getLastValue ( List list )

Set

A Set represents an unordered collection of unique values.

Examples

Set values;
boolean addValueToSet ( Set set, object value )

Map

A Map represents a set of keys, each of which points to a value.

Example

boolean addPair ( Map map, object key, object value )

Object Data Type

An object is used to model a parameter or result where the actual type is determined at runtime. An object type specification may be used anywhere a native data type is allowed except when declaring constants.

Examples

struct ValueList ( string name, object[] values )
exception BadValue ( int id, string msg, object value )
object getValue ( string name )
object[] setValue ( string name, object value )

Constants

A constant declaration is used to include named values into the generated interfaces. There is no messaging component associated with them, just a handy way to define a named value which is available to each implementation in appropriate target syntax.

The value of the constant must be appropriate for the type.

Constant names are by convention all uppercase, with underscore used to separate words.

Syntax

<const_stmt> ::= "const" <ctype> <id> "=" <cvalue> (";")?
<ctype> ::= "boolean" | "byte" | "short" | "int" | "long" | "float" | "double" | "string"
<cvalue> ::= "true" | "false" | <integer> | <octal> | <hex> | <binary> | <decimal> | <string>

Examples

const int MAX_NAME_LENGTH = 31
const string USER_NAME = "Fred"
const float PI = 3.14159

Enumerations

An enumeration is used to declare a type which has a named set of values. These are most often used where the set of values is small. Some languages use integers to model enumerations while others use tokens (identifiers) and yet others use object instances.

Enumeration names are by convention capitalized. Enumeration item names are all uppercase.

Syntax

<enum_stmt> ::= "enum" <id> "(" <id> ("," <id>)* ")" (";")?

Examples

enum State ( OPEN, CLOSED )
enum RGBColor ( RED, GREEN, BLUE )

Structured Type

A structure is used to model a small set of named parameters of heterogeneous type. When a structure contains another structure as a parameter it is by reference. Thus, in Etch, a structure may contain itself (directly or indirectly) as a parameter (if the containment was by value, a structure could not contain itself because that would introduce a storage recursion).

Structure names are by convention capitalized. Parameter names are by convention lowercase.

Strangely enough, a structure might not have any parameters. This might seem silly, but the mere presence of the structure might be used like a boolean to assert a condition (that is, indicate the condition is true, whereas null indicates the condition is false), while allowing for future expansion by adding parameters to the structure.

Note that <ptype> includes the native types as well as <id>, <qid>, and <object>. The <id> or <qid> is used to indicate a custom type name. An <object> is used to model a parameter or result where the actual type is determined at runtime.

A structure may extend another structure. In this way the structure inherits the parameters and identity of the extended structure. This is functionally equivalent to class inheritance in java, csharp, ruby, etc.

There are some rules:

  1. a structure may not extend another such that an inheritance loop is created.
  2. a structure may not declare a parameter whose name is the same as any already in its inheritance.

Syntax

<struct_stmt> ::= "struct" <id> <params> ( "extends> <id> )? ( ";" )?
<params> ::= "(" ( <param> ( "," <param> )* )? ")"
<param> ::= <ptype> <dim> <id>
<ptype> ::= <ctype> | "object" | <ref>
<dim> ::= ( "[]" )*
<ref> ::= <xid>
<xid> :: = <qid> | <id>
<qid> :: = <ID> ("." <ID>)+>

Examples

struct Point ( int x, int y )
struct Path ( Point[] points )
struct AsyncAction () extends Action

Arrays

As you can see in <param>, above, a parameter in a structure may be dimensioned. The number of square bracket pairs indicates the dimensionality. None indicates a scalar parameter. There is no particular limit on the number of dimensions you may have or the size of any particular dimension. Let your common sense guide you, keeping in mind all the possible language bindings there might be.

Examples

int[] x // single dimensional array of int.
Point[][] grid // two-dimensional array of Point.

External Type

Sometimes you really need to be able to reference a target language specific type in the NSDL. When you do, you must think carefully about the applicability to other language bindings that may want or need to use your service. Many useful types have analogs in the other languages anyway, such as Java's HashMap, ArrayList, and Date types. Note that Etch cannot model abstract or generic types, and so where abstracts or generics are involved they must be made concrete. In Java, StrIntMap instead of HashMap<String, Integer>, HashMap, or even Map:

package project.util;

import java.util.HashMap;

public class StrIntMap extends HashMap<String, Integer>
{
   // nothing else needed
}

When you specify the external type, you import it into Etch by giving it a name that you will use inside Etch to reference it. Then for each language binding, you supply information about what the concrete type is and how to serialize / de-serialize it.

Note: the @Extern annotation, along with all the other annotations, is described later. It is only shown here because otherwise the example of extern would be too vague. See the appendix for an extern example.

Syntax

<extern_stmt> ::= "extern" <id> (";")?

Examples

@Extern("java", "project.util.StrIntMap", "", "project.util.StrIntMapSerializer", "")
extern StrIntMap

Exceptions

Exceptions are a special kind of struct which are an alternative response to a message execution. An exception cannot be a parameter or result of a message, nor a parameter in a structure.

When a program wants to return an exception in Java, it creates an instance of the exception and throws it (compare this with creating an instance of a normal type and returning it). The throw operation, unlike return, can be performed deep in the call chain and terminates each call up the chain until caught by a handler. Return, on the other hand, can only terminate one call up the call chain and makes its value available to the next level up.

In some programming languages (e.g., Java) exceptions are checked: a method cannot throw a checked exception unless it has declared that it might. Unchecked exceptions can always be thrown, and usually correspond to low-level conditions such as an attempt to use a null pointer, index an array element which doesn't exist, etc. In other programming languages (e.g., C#, Ruby, Python), all exceptions are unchecked. Some programming languages (e.g., C) do not have a concept of exception whatsoever.

Etch reflects this by requiring methods to declare thrown exceptions. In this way the declarations may be passed through to programming languages which require the declarations. Of course, it is good for the users of a service to know which exceptions a particular message might throw.

Note: exceptions should include at least some detail as to the cause, enough to suggest a solution of the problem. In messaging systems this is especially important, as a client often does not have enough access to the server to debug a problem from the server side. As you might expect, an exception with a simple message "parameter x (value 23) is not in the expected range (1-10)" is way more helpful than "value out of range". Furthermore, localization considerations might dictate that you declare an exception like this:

Exception IntOutOfRange( string name, int value, int min, int max )

and let the receiver format it as they wish. (But, that can get messy really fast!)

Like structures, exception may extend other exceptions. See the discussion under structures,above.

Syntax

<except_stmt> ::= "exception" <id> <params> ( "extends> <ref> )? ( ";" )?
<ref> ::= <xid>
<xid> :: = <qid> | <id>
<qid> :: = <ID> ("." <ID>)+>

Examples

exception BadArgument( string name, string reason )
exception NoAuthorization( string name, string reason )

Messages

Messages describe the actual network traffic between the clients and servers. We use function definition syntax but the elements are the same: a message type and parameters (names and types).

Message declarations actually describe two messages (if the message is two-way). The first is the request message which includes the message type and parameters with argument values. The second is the response message which includes the response message type and the result parameter and value.

Messages can terminate normally by returning a value or they can terminate abnormally by throwing an exception. Either type of termination will return a response message from the remote process (again, unless the message is declared one-way).

NOTE: The vast majority of modern programming languages support exceptions; those that do not will need to accommodate the dual nature of the response message.

NOTE: One-way messages must have a void result type and not throw any exceptions.

Message names and parameter names by convention start with a lowercase alphabetic, then use camel case.

Syntax

<message_stmt> ::= <rtype> <id> <params> (<throws>)? (";")?
<rtype> ::= ( <ptype> <dim> ) | "void"
<throws> ::= "throws" <ref> ( "," <ref> )*
<ref> ::= <xid>
<xid> :: = <qid> | <id>
<qid> :: = <ID> ("." <ID>)+>

Examples

int add( int x, int y )
Date getBirthday( string name ) throws NameNotFound
Location[] getBestPath( Location[] locations ) throws NoSuchPath
void report( Date when, EventType type, string detail )

Includes

You many add an include to to an etch servicde function.

Syntax

<inlcude> ::= "inlcude" <filename> (";")?

Example

To use include files in an etch file, use the .txt extention, not .etch for includes.
You can also include other include files in the etch include files.

module etch.bindings.java.compiler.test

@Direction(Both)
@Timeout(4000)
service TestInclude
{
	const int INT1 = 98765431;

	const boolean BOOL3 = false
	const boolean BOOL4 = true
	
	include "testincl.txt";
	
	const boolean BOOL1 = false
	const boolean BOOL2 = true
	
}
testincl.txt
 
const int INT2 = -2147483648;
include "testincl1.txt";

Mixin

A mixin is the mechanism used to incorporate the messages and types defined within another service into the current service. To the extent possible the original service retains its identity, such that with an object oriented language such as Java, C#, or Ruby, an instance of the current service could be passed where an instance of the mixed in service is expected. This facilitates the creation of reusable service libraries. You can have as many mixins as you like (up to the limit imposed by the various language bindings).

Two examples of a mixin are an authentication service and an event service. These could be standard components for a deployment of a suite of other services. You only have to define them once, then you can add them to each of your other services as you like.

Syntax

<mixin_stmt> ::= "mixin" <ref> (";")?
<ref> ::= <xid>
<xid> :: = <qid> | <id>
<qid> :: = <ID> ("." <ID>)+>

Examples

module etch.bindings.csharp.compiler.test

@Direction(Both)
service MixinTest
{
	mixin NestedMixin
	@Authorize(isLoggedIn)
	int add( int x, int y )
	
	@Authorize(isLoggedIn)
	int sub( int x, int y )
	
	void testRef(test t) throws Excp2
	
		
}
NestedMixin
module etch.bindings.csharp.compiler.test

@Direction(Both)
service NestedMixin
{
	
	struct test (	
		string name,		
		string value	
	) 
	
	@Unchecked
	exception Excp2()
	
	
	void login( string name, string pw )
	
	void logout()
	
	boolean isLoggedIn()
		
}

Important Notes

  • The mixin etch file must be present in the Include Path. In the above example, NestedMixin must be present in the Include Path. If the include path is not specified, then the Mixin etch file will only be searched in the current directory. Thus in the above example, if -I option (include option) is not specified, then NestedMixin will only be searched in the directory in which MixinTest is present.
  • When a user compiles the etch file which contains the mixin (TestMixin.etch in the above example), he can chose whether he wants the mixin artifacts to be generated or not. If he chooses not to generate the artifacts, then he must specify -n command line option If he chooses to generate the mixin artifacts then he must specify either -m or -d option (Please refer to the Etch Compiler Guide for more details on these options). Failure to specify either one of these option will lead to compilation problem.

Service

All the definitions above are packaged into a service, which provides a naming scope. The compiler uses the features available in each language binding to create the naming scope. For example, in Java, the compiler creates a number of interfaces and classes with names derived from the service name.

Syntax

<service> ::= opts "service" <id> "{" ( <stmt> )* "}"
opts ::= (opt)*
opt ::= @ id (args)?
args 	::= 	arg (, arg )*
arg 	::= 	( cvalue | xid | <NULL> )
cvalue 	::= 	( <TRUE> | <FALSE> | <INTEGER> | <OCTAL> | <HEX> | <BINARY> | <DECIMAL> | <STR> )
xid 	::= 	( qid | id )
<qid> :: = <ID> ("." <ID>)+>
<stmt> ::= <const_stmt> | <enum_stmt> | <struct_stmt> | <extern_stmt> | <except_stmt> | <message_stmt> | <mixin_stmt>

Example

service DigestAuth
{
   exception LoginFailure( string msg )
   string getSeed()
   void login( string name, string seed, string digest )
   throws LoginFailure
   void logout()
   boolean isLoggedIn()
}

Annotations

Annotations may be applied to a <stmt> to alter its behavior. Where it makes sense, you can even apply an annotation to the <service> to set the global behavior or defaults. See below for descriptions of supported annotations.

Syntax

opts ::= (opt)*
opt ::= @ id (args)?
args 	::= 	arg (, arg )*
arg 	::= 	( cvalue | xid | <NULL> )
cvalue 	::= 	( <TRUE> | <FALSE> | <INTEGER> | <OCTAL> | <HEX> | <BINARY> | <DECIMAL> | <STR> )
xid 	::= 	( qid | id )
<qid> :: = <ID> ("." <ID>)+>

Examples

@Timeout( 4000 )
@Direction( Server )
Date getBirthday( string name )

Module

The last bit of syntax is the module. This appears in the NSDL file before the service and defines the global namespace of the service. This has a very strong correlation to the Java package or the C# namespace, and a looser correlation to the Ruby module. It also contributes to the fully qualified name of the service itself, which is used to distinguish the service from other like-named services.

Syntax

<module> ::= "module" (<qid> | <id>) (";")? <service>

Examples

module project.auth

service DigestAuth

{

...

}

Supported Annotations

@AsyncReceiver

@AsyncReceiver( NONE | QUEUED | FREE )

Specifies that the processing mode used by the server for this message or the default processing mode for all the messages of the service. A specification on a message overrides the service default.

  • NONErequests the message be delivered on the message receiver thread. While this message is being processed, no other message may be read and delivered. Messages are processed in the order received.
  • QUEUEDrequests the message be queued to a thread pool for delivery. This is generally used by operations which complete relatively quickly but might want to dialog with the caller using callbacks, or perform some other operation which might briefly block.
  • FREErequests the message be delivered by a newly created free thread. This is generally used by long running operations.

Default

The service default is NONE.

May Be Applied To

<message_stmt>, <service>

Example

// run add asynchronously on a queued pool thread.
@AsyncReceiver( QUEUED )

int add( int x, int y )

@Authorize

 @Authorize[( method [, arg ...] )] 

Specifies that the message receiver should use the specified method to determine whether the message is allowed to be processed, or set the service default authorization method for all messages in the service. Method should be a service message declared the same direction as this message and returning a boolean result. An exception will be thrown by the receiver if the message is not allowed. A specification on a message overrides the service default.

The args are a sequence of named or unnamed constants, enumeration values, or message parameters or parameters of parameters, etc., corresponding in number, order, and type to the signature of method.

Method true always allows the message, while falsenever allows the message. Neither of these are allowed any args.

Default

The service default is true. If specified without an argument, true is assumed.

May Be Applied To

<message_stmt>, <service>

Example

// only allow logged in users to delete files.
@Authorize( isLoggedIn )

void deleteFile( string name )

// returns true if a user is logged in.
boolean isLoggedIn()

// only allow the admin of the specified user to grant rights.
@Authorize( isAdminOf, userName)
void grantRight( string userName, string right )

// returns true if the current user is the admin of the
// specified user. isAdminOf should test isLoggedIn first.
boolean isAdminOf( string userName )

// don't let anyone send this message for now.
@Authorize( false )

void rmDashRFSlash()

@Direction

 @Direction( SERVER | CLIENT | BOTH ) 

Specifies the direction of the message, or sets the service default direction for all messages in the service:

  • SERVER means from the client to the server.
  • CLIENT means from the server to the client (a callback).
  • BOTH means either client or server may send the message to the other.

A specification on a message overrides the service default.

Default

The service default is SERVER.

May Be Applied To

<message_stmt>, <service>

Example

// notify the client that a user's status has changed.
@Direction( CLIENT )

void statusUpdate( string userName, boolean online )

@Extern

 @Extern( language, name, nImport, serializer, sImport ) 

Declares language binding and serializer for an external type. Language is a compiler supported name for a binding (e.g., java, csharp, ruby); name is the language specific name of the type to use; nImport is a string that must be used in something like an import or requires statement to make name available; serializer is the language specific name of the type to use to import and export (this type must declare certain specific methods); sImport is a string that must be used in something like an import, using, include, or require statement to make serializer available.

Default

There is no default. If NSDL is compiled for a language which has no binding for an external type, the compilation will fail.

May Be Applied To

<extern_stmt>

Example

// for java, map Date to java.util.Date and use
// project.util.DateSerializer to serialize and
// deserialize the object.

@Extern( java, "java.util.Date", "",

"project.util.DateSerializer", "" )

// for csharp, map Date to System.DateTime, and
// use project.util.DateTimeSerializer to serialize
// and deserialize the object.

@Extern( csharp, "System.DateTime", "", "project.util.DateTimeSerializer", "" )

// for ruby, map Date to Time, and use TimeSerializer
// from project/util/time_serializer.rb to serialize
// and deserialize the object.

@Extern( ruby, "Time", "", "TimeSerializer", "project/util/time_serializer" )

extern Date

@Oneway

 @Oneway[( true | false )] 

Specifies that the sender of this message does not wait for any response, or specifies the service default for all messages. This is sometimes called a fire-and-forget message, and some other times called an event message. Such a message must be declared to have a void result, and may not be declared to throw any exceptions. A specification on a message overrides the service default.

Default

The service default is false. If specified without an argument, true is assumed.

May Be Applied To

<message_stmt>, <service>

Example

// notify the client that a user's status has changed.
// don't wait for any response.
@Direction( CLIENT ) @Oneway

void statusUpdate( string userName, boolean online )

@Timeout

 @Timeout( ms ) 

Specifies the time in milliseconds for the sender of a message to wait for a response, or specifies the service default timeout for all messages. Timeout of '0' means wait forever. A specification on a message overrides the service default.

Default

The service default is .

May Be Applied To

<message_stmt>, <service>

Example

// wait 30 seconds for mom to respond, then if
// she doesn't, decide whether to go anyway...
@Timeout( 30000 )

boolean getPermission( "Mom, can I go to the movie with Fred?" )

@ToString

 @ToString( fmt ) 

Specifies a format specifier for language specific "to string" methods. The "to string" methods are written in the appropriate way for the target languages. Why "to string" methods? They are needed for debugging and logging.

Default

The default is "param1={param1}, ..." for each parameter of the struct or exception.

May Be Applied To

<struct_stmt>, <except_stmt>

Example

@ToString( "Point({x}, {y})" )

struct Point( int x, int y )

@Unchecked

 @Unchecked[( true | false)] 

In languages with checked exceptions, specifies than an exception should be unchecked, or specifies that the service default for exceptions should be unchecked.

Default

The service default is false. If specified without an argument, true is assumed.

May Be Applied To

<except_stmt>, <service>

Example

@Unchecked
exception AuthException( string name, string reason )

Formal Comments

Formal comments are used to describe each Etch service description element. The descriptions are copied over into the generated files so that they are available to the programmers in their natural environments. Here is a simple example:

/**
* The HydroMath service uses an atomic-powered highly-advanced
* hydrostatic processor to perform astounding binary
* manipulations.
*/

service HydroMath
{

   /**
   * Exception thrown if the hydrostatic processor
   * encounters a problem.
   * @param code the problem description code.
   */
   exception OverflowException( int code )

   /**
   * Returns the sum of x and y.
   * @param x input value.
   * @param y input value.
   * @return the sum of x and y.
   * @throws OverflowException if the hydrostatic
   * processor encounters a problem.
   */
   void add( int x, int y ) throws OverflowException
}

This produces (for the java target) the following output:

/**
 * Exception thrown if the hydrostatic processor
 * encounters a problem.
 */
public class OverflowException extends Exception
{
   /**
    * Constructs the OverflowException.
    * @param code the problem description code.
    */
   public OverflowException( Integer code )
   {
      this.code = code;
   }

   /**
    * the problem description code. RTFM.
    */
   public Integer code;
}

/**
* The HydroMath service uses an atomic-powered highly-advanced
* hydrostatic processor to perform astounding binary
* manipulations.
*/
public interface HydroMathServer extends HydroMath
{
   /**
   * Returns the sum of x and y.
   * @param x input value.
   * @param y input value.
   * @return the sum of x and y.
   * @throws OverflowException if the hydrostatic
   * processor encounters a problem.
   */
   public Integer add( Integer x, Integer y ) throws OverflowException;
}

A formal comment may be attached to:

  • Service
  • Constant
  • Enumeration
  • Enumeration element
  • Structure
  • Exception
  • Message
  • Parameter of a structure, exception, or message
  • Exception thrown by a message

The formal comment is started by the /* sequence, and terminated by the */ sequence. The contents, minus internal sequences that look like <eol><whitespace><whitespace> are attached to the element in question.

A parameter of a structure, exception, or message may be commented two ways. The Javadoc style was shown in the HydroMath example above. The second style is shown here:

/** @return the sum of x and y. */
int add(

   /** input value. */
   int x,

   /** input value. */
   int y

) throws

   /** if the hydrostatic processor encounters a problem. */
   OverflowException

The body of the formal comment is taken whole for elements except for a structure, exception, or message. For those the body is scanned looking for @param tags, and for a message, @return or @throws tags. If / when one of those tags is found, the main description is terminated and text is spooled to the description of the sub-element. In all cases the descriptions may span multiple lines.

@param <name> <descr>

The name must specify the name of a structure, exception, or message.

@return <descr>

The message must be defined to return a value.

@throws <name> <descr>

The name must specify the name of an exception that the message throws.